Deridere sui metodi statici con Mockito


374

Ho scritto una fabbrica per produrre java.sql.Connectionoggetti:

public class MySQLDatabaseConnectionFactory implements DatabaseConnectionFactory {

    @Override public Connection getConnection() {
        try {
            return DriverManager.getConnection(...);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
}

Vorrei convalidare i parametri passati DriverManager.getConnection, ma non so come deridere un metodo statico. Sto usando JUnit 4 e Mockito per i miei casi di test. C'è un buon modo per deridere / verificare questo caso d'uso specifico?



5
Non puoi con mockito desing :)
MariuszS

25
@MariuszS Mockito (o EasyMock o jMock) non supporta i staticmetodi di derisione , ma per caso . Questa limitazione (insieme a nessun supporto per finalclassi / metodi di derisione o newoggetti -ed) è una conseguenza naturale (ma non intenzionale) dell'approccio impiegato per implementare la derisione, in cui vengono create dinamicamente nuove classi che implementano / estendono il tipo da deridere; altre librerie beffardo usano altri approcci che evitano queste limitazioni. Questo è accaduto anche nel mondo .NET.
Rogério,

2
@ Rogério Grazie per la spiegazione. github.com/mockito/mockito/wiki/FAQ Posso deridere metodi statici? No. Mockito preferisce l'orientamento agli oggetti e l'iniezione delle dipendenze rispetto al codice procedurale statico che è difficile da capire e cambiare. C'è anche un po 'di design dietro questa limitazione :)
MariuszS,

17
@MariuszS Ho letto che il tentativo di eliminare i casi d'uso legittimi invece di ammettere lo strumento ha delle limitazioni che non possono essere (facilmente) rimosse e senza fornire alcuna motivazione motivata. A proposito, ecco una tale discussione per il punto di vista opposto, con riferimenti.
Rogério,

Risposte:


350

Usa PowerMockito sopra Mockito.

Codice di esempio:

@RunWith(PowerMockRunner.class)
@PrepareForTest(DriverManager.class)
public class Mocker {

    @Test
    public void shouldVerifyParameters() throws Exception {

        //given
        PowerMockito.mockStatic(DriverManager.class);
        BDDMockito.given(DriverManager.getConnection(...)).willReturn(...);

        //when
        sut.execute(); // System Under Test (sut)

        //then
        PowerMockito.verifyStatic();
        DriverManager.getConnection(...);

    }

Maggiori informazioni:


4
Mentre questo funziona in teoria, facendo fatica in pratica ...
Naftuli Kay,

38
Sfortunatamente l'enorme svantaggio di questo è la necessità di PowerMockRunner.
Innokenty,

18
sut.execute ()? Si intende?
TejjD,

4
System Under Test, la classe che richiede simulazioni di DriverManager. kaczanowscy.pl/tomek/2011-01/testing-basics-sut-and-docs
MariuszS

8
Cordiali saluti, se stai già utilizzando JUnit4 puoi farlo @RunWith(PowerMockRunner.class)e sotto quello @PowerMockRunnerDelegate(JUnit4.class).
EM-Creations,

71

La strategia tipica per schivare i metodi statici che non si può evitare di utilizzare è quella di creare oggetti avvolti e utilizzare invece gli oggetti wrapper.

Gli oggetti wrapper diventano facciate per le classi statiche reali e non vengono testati.

Un oggetto wrapper potrebbe essere qualcosa di simile

public class Slf4jMdcWrapper {
    public static final Slf4jMdcWrapper SINGLETON = new Slf4jMdcWrapper();

    public String myApisToTheSaticMethodsInSlf4jMdcStaticUtilityClass() {
        return MDC.getWhateverIWant();
    }
}

Infine, la tua classe sotto test può usare questo oggetto singleton, ad esempio, avendo un costruttore predefinito per l'uso nella vita reale:

public class SomeClassUnderTest {
    final Slf4jMdcWrapper myMockableObject;

    /** constructor used by CDI or whatever real life use case */
    public myClassUnderTestContructor() {
        this.myMockableObject = Slf4jMdcWrapper.SINGLETON;
    }

    /** constructor used in tests*/
    myClassUnderTestContructor(Slf4jMdcWrapper myMock) {
        this.myMockableObject = myMock;
    }
}

E qui hai una classe che può essere facilmente testata, perché non usi direttamente una classe con metodi statici.

Se stai usando CDI e puoi usare l'annotazione @Inject, allora è ancora più facile. Crea il tuo bean Wrapper @ApplicationScoped, fai iniettare quella cosa come collaboratore (non hai nemmeno bisogno di costruttori disordinati per i test) e continua con il derisione.


3
Ho creato uno strumento per generare automaticamente interfacce "mixin" di Java 8 che racchiudono chiamate statiche: github.com/aro-tech/interface-it I mixin generati possono essere derisi come qualsiasi altra interfaccia, o se la tua classe sotto test "implementa" il interfaccia è possibile sovrascrivere uno qualsiasi dei suoi metodi in una sottoclasse per il test.
aro_tech,

25

Ho avuto un problema simile. La risposta accettata non ha funzionato per me fino a quando non ho apportato la modifica :,@PrepareForTest(TheClassThatContainsStaticMethod.class) secondo la documentazione di PowerMock per mockStatic .

E non devo usare BDDMockito.

La mia classe:

public class SmokeRouteBuilder {
    public static String smokeMessageId() {
        try {
            return InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException e) {
            log.error("Exception occurred while fetching localhost address", e);
            return UUID.randomUUID().toString();
        }
    }
}

La mia classe di test:

@RunWith(PowerMockRunner.class)
@PrepareForTest(SmokeRouteBuilder.class)
public class SmokeRouteBuilderTest {
    @Test
    public void testSmokeMessageId_exception() throws UnknownHostException {
        UUID id = UUID.randomUUID();

        mockStatic(InetAddress.class);
        mockStatic(UUID.class);
        when(InetAddress.getLocalHost()).thenThrow(UnknownHostException.class);
        when(UUID.randomUUID()).thenReturn(id);

        assertEquals(id.toString(), SmokeRouteBuilder.smokeMessageId());
    }
}

Non riesci a capire? .MockStatic e? .Quando attualmente con JUnit 4
Teddy

PowerMock.mockStatic & Mockito.when sembra non funzionare.
Teddy

Per chiunque lo vedesse in seguito, per me ho dovuto digitare PowerMockito.mockStatic (StaticClass.class);
thinkereer,

È necessario includere l'arteria arteriosa del powermock-api-mockito.
PeterS

23

Come accennato in precedenza, non è possibile deridere i metodi statici con mockito.

Se la modifica del framework di test non è un'opzione, è possibile effettuare le seguenti operazioni:

Creare un'interfaccia per DriverManager, deridere questa interfaccia, iniettarla tramite una sorta di iniezione di dipendenza e verificare su quella simulazione.


7

Osservazione: quando si chiama il metodo statico all'interno di un'entità statica, è necessario modificare la classe in @PrepareForTest.

Ad esempio:

securityAlgo = MessageDigest.getInstance(SECURITY_ALGORITHM);

Per il codice sopra, se è necessario deridere la classe MessageDigest, utilizzare

@PrepareForTest(MessageDigest.class)

Mentre se hai qualcosa come sotto:

public class CustomObjectRule {

    object = DatatypeConverter.printHexBinary(MessageDigest.getInstance(SECURITY_ALGORITHM)
             .digest(message.getBytes(ENCODING)));

}

quindi, dovresti preparare la classe in cui risiede questo codice.

@PrepareForTest(CustomObjectRule.class)

E quindi deridere il metodo:

PowerMockito.mockStatic(MessageDigest.class);
PowerMockito.when(MessageDigest.getInstance(Mockito.anyString()))
      .thenThrow(new RuntimeException());

Stavo sbattendo la testa contro il muro, cercando di capire perché la mia classe statica non si stesse prendendo in giro. Penseresti in tutti i tutorial sugli interwebs, ONE sarebbe andato oltre il caso d'uso bare-bones.
SoftwareSavant

6

Puoi farlo con un po 'di refactoring:

public class MySQLDatabaseConnectionFactory implements DatabaseConnectionFactory {

    @Override public Connection getConnection() {
        try {
            return _getConnection(...some params...);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    //method to forward parameters, enabling mocking, extension, etc
    Connection _getConnection(...some params...) throws SQLException {
        return DriverManager.getConnection(...some params...);
    }
}

Quindi puoi estendere la tua classe MySQLDatabaseConnectionFactoryper restituire una connessione derisa, fare affermazioni sui parametri, ecc.

La classe estesa può risiedere nel caso di test, se si trova nello stesso pacchetto (che ti incoraggio a fare)

public class MockedConnectionFactory extends MySQLDatabaseConnectionFactory {

    Connection _getConnection(...some params...) throws SQLException {
        if (some param != something) throw new InvalidParameterException();

        //consider mocking some methods with when(yourMock.something()).thenReturn(value)
        return Mockito.mock(Connection.class);
    }
}


6

Mockito non può acquisire metodi statici, ma a partire da Mockito 2.14.0 è possibile simularlo creando istanze di invocazione di metodi statici.

Esempio (estratto dai loro test ):

public class StaticMockingExperimentTest extends TestBase {

    Foo mock = Mockito.mock(Foo.class);
    MockHandler handler = Mockito.mockingDetails(mock).getMockHandler();
    Method staticMethod;
    InvocationFactory.RealMethodBehavior realMethod = new InvocationFactory.RealMethodBehavior() {
        @Override
        public Object call() throws Throwable {
            return null;
        }
    };

    @Before
    public void before() throws Throwable {
        staticMethod = Foo.class.getDeclaredMethod("staticMethod", String.class);
    }

    @Test
    public void verify_static_method() throws Throwable {
        //register staticMethod call on mock
        Invocation invocation = Mockito.framework().getInvocationFactory().createInvocation(mock, withSettings().build(Foo.class), staticMethod, realMethod,
                "some arg");
        handler.handle(invocation);

        //verify staticMethod on mock
        //Mockito cannot capture static methods so we will simulate this scenario in 3 steps:
        //1. Call standard 'verify' method. Internally, it will add verificationMode to the thread local state.
        //  Effectively, we indicate to Mockito that right now we are about to verify a method call on this mock.
        verify(mock);
        //2. Create the invocation instance using the new public API
        //  Mockito cannot capture static methods but we can create an invocation instance of that static invocation
        Invocation verification = Mockito.framework().getInvocationFactory().createInvocation(mock, withSettings().build(Foo.class), staticMethod, realMethod,
                "some arg");
        //3. Make Mockito handle the static method invocation
        //  Mockito will find verification mode in thread local state and will try verify the invocation
        handler.handle(verification);

        //verify zero times, method with different argument
        verify(mock, times(0));
        Invocation differentArg = Mockito.framework().getInvocationFactory().createInvocation(mock, withSettings().build(Foo.class), staticMethod, realMethod,
                "different arg");
        handler.handle(differentArg);
    }

    @Test
    public void stubbing_static_method() throws Throwable {
        //register staticMethod call on mock
        Invocation invocation = Mockito.framework().getInvocationFactory().createInvocation(mock, withSettings().build(Foo.class), staticMethod, realMethod,
                "foo");
        handler.handle(invocation);

        //register stubbing
        when(null).thenReturn("hey");

        //validate stubbed return value
        assertEquals("hey", handler.handle(invocation));
        assertEquals("hey", handler.handle(invocation));

        //default null value is returned if invoked with different argument
        Invocation differentArg = Mockito.framework().getInvocationFactory().createInvocation(mock, withSettings().build(Foo.class), staticMethod, realMethod,
                "different arg");
        assertEquals(null, handler.handle(differentArg));
    }

    static class Foo {

        private final String arg;

        public Foo(String arg) {
            this.arg = arg;
        }

        public static String staticMethod(String arg) {
            return "";
        }

        @Override
        public String toString() {
            return "foo:" + arg;
        }
    }
}

Il loro obiettivo non è supportare direttamente il derisione statica, ma migliorare le sue API pubbliche in modo che altre librerie, come Powermockito , non debbano fare affidamento su API interne o duplicare direttamente del codice Mockito. ( fonte )

Disclaimer: il team di Mockito pensa che la strada per l'inferno sia lastricata con metodi statici. Tuttavia, il lavoro di Mockito non è quello di proteggere il codice da metodi statici. Se non ti piace che il tuo team faccia beffe statiche, smetti di usare Powermockito nella tua organizzazione. Mockito ha bisogno di evolversi come toolkit con una visione supponente su come dovrebbero essere scritti i test Java (ad es. Non simulare le statistiche !!!). Tuttavia, Mockito non è dogmatico. Non vogliamo bloccare casi d'uso non consigliati come derisione statica. Non è solo il nostro lavoro.



1

Poiché quel metodo è statico, ha già tutto il necessario per usarlo, quindi vanifica lo scopo del deridere. Deridere sui metodi statici è considerato una cattiva pratica.

Se si tenta di farlo, significa che c'è qualcosa di sbagliato nel modo in cui si desidera eseguire i test.

Ovviamente puoi usare PowerMockito o qualsiasi altro framework in grado di farlo, ma prova a ripensare il tuo approccio.

Ad esempio: prova a deridere / fornire gli oggetti, che invece utilizza quel metodo statico.


0

Usa il framework JMockit . Ha funzionato per me. Non è necessario scrivere istruzioni per deridere il metodo DBConenction.getConnection (). È sufficiente solo il codice seguente.

@Mock di seguito è il pacchetto mockit.Mock

Connection jdbcConnection = Mockito.mock(Connection.class);

MockUp<DBConnection> mockUp = new MockUp<DBConnection>() {

            DBConnection singleton = new DBConnection();

            @Mock
            public DBConnection getInstance() { 
                return singleton;
            }

            @Mock
            public Connection getConnection() {
                return jdbcConnection;
            }
         };
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.