Se moquer des méthodes statiques avec Mockito
j'ai écrit une usine pour produire java.sql.Connection
objets:
public class MySQLDatabaseConnectionFactory implements DatabaseConnectionFactory {
@Override public Connection getConnection() {
try {
return DriverManager.getConnection(...);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
j'aimerais valider les paramètres passés à DriverManager.getConnection
, mais je ne sais pas comment simuler une méthode statique. J'utilise JUnit 4 et Mockito pour mes tests. Est-il une bonne façon de se moquer/vérifier ce cas d'utilisation?
9 réponses
Utiliser PowerMockito sur le dessus de Mockito.
exemple de code:
@RunWith(PowerMockRunner.class)
@PrepareForTest(DriverManager.class)
public class Mocker {
@Test
public void testName() throws Exception {
//given
PowerMockito.mockStatic(DriverManager.class);
BDDMockito.given(DriverManager.getConnection(...)).willReturn(...);
//when
sut.execute();
//then
PowerMockito.verifyStatic();
DriverManager.getConnection(...);
}
plus d'informations:
la stratégie typique pour esquiver les méthodes statiques que vous n'avez aucun moyen d'éviter d'utiliser, est en créant des objets enveloppés et en utilisant les objets enveloppants à la place.
les objets d'emballage deviennent des façades aux classes statiques réelles, et vous ne les testez pas.
un objet d'emballage pourrait être quelque chose comme
public class Slf4jMdcWrapper {
public static final Slf4jMdcWrapper SINGLETON = new Slf4jMdcWrapper();
public String myApisToTheSaticMethodsInSlf4jMdcStaticUtilityClass() {
return MDC.getWhateverIWant();
}
}
Enfin, votre classe sous test peut utiliser cet objet singleton, par exemple, avoir un défaut constructeur pour usage réel:
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;
}
}
et ici vous avez une classe qui peut facilement être testé, parce que vous n'utilisez pas directement une classe avec des méthodes statiques.
si vous utilisez CDI et pouvez faire usage de l'annotation @Inject alors il est encore plus facile. Il suffit de faire votre Wrapper bean @ApplicationScoped, obtenir cette chose injectée comme un collaborateur (vous n'avez même pas besoin de constructeurs désordonnés pour les tests), et continuer avec la moquerie.
comme mentionné ci-dessus, vous ne pouvez pas vous moquer des méthodes statiques avec mockito.
si changer votre framework de test n'est pas une option, vous pouvez faire ce qui suit:
créer une interface pour DriverManager, mock cette interface, l'injecter via une sorte d'injection de dépendance et de vérifier sur cette mock.
j'ai eu un problème similaire. La réponse acceptée n'a pas fonctionné pour moi, jusqu'à ce que je fasse le changement: @PrepareForTest(TheClassThatContainsStaticMethod.class)
, selon Powermock's documentation for mockStatic .
Et je n'ai pas à utiliser BDDMockito
.
ma 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();
}
}
}
ma classe d'essai:
@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());
}
}
pour simuler la méthode statique, vous devriez utiliser un regard Powermock: https://github.com/powermock/powermock/wiki/MockStatic . Mockito ne fournit pas cette fonctionnalité.
vous pouvez lire nice un article à propos de mockito: http://refcardz.dzone.com/refcardz/mockito
Vous pouvez le faire avec un peu de 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...);
}
}
ensuite vous pouvez étendre votre classe MySQLDatabaseConnectionFactory
pour retourner une connexion moquée, faire des assertions sur les paramètres, etc.
la classe étendue peut résider dans le cas d'essai, si elle est située dans le même colis (ce que je vous encourage à faire)
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);
}
}
Observation: lorsque vous appelez méthode statique dans une entité statique, vous devez changer la classe dans @PrepareForTest.
Pour par exemple:
securityAlgo = MessageDigest.getInstance(SECURITY_ALGORITHM);
pour le code ci-dessus si vous devez vous moquer de la classe MessageDigest, utilisez
@PrepareForTest(MessageDigest.class)
tandis que si vous avez quelque chose comme ci-dessous :
public class CustomObjectRule {
object = DatatypeConverter.printHexBinary(MessageDigest.getInstance(SECURITY_ALGORITHM)
.digest(message.getBytes(ENCODING)));
}
alors, vous devez préparer la classe dans laquelle ce code réside.
@PrepareForTest(CustomObjectRule.class)
et ensuite moquer la méthode:
PowerMockito.mockStatic(MessageDigest.class);
PowerMockito.when(MessageDigest.getInstance(Mockito.anyString()))
.thenThrow(new RuntimeException());
j'ai aussi écrit une combinaison de Mockito et AspectJ: https://github.com/iirekm/misc/tree/master/ajmock
votre exemple devient:
when(() -> DriverManager.getConnection(...)).thenReturn(...);
Mockito ne peut pas capturer les méthodes statiques, mais depuis Mockito 2.14.0 vous pouvez le simuler en créant des instances d'invocation de méthodes statiques.
exemple (extrait de leurs épreuves ):
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;
}
}
}
leur objectif n'est pas de soutenir directement la moquerie statique, mais d'améliorer ses API publiques de sorte que les autres bibliothèques, comme Powermockito , ne doivent pas compter sur des APIs ou directement doivent dupliquer certains Mockito code. ( source )
avertissement: L'équipe de Mockito pense que la route de l'enfer est pavée avec des méthodes statiques. Cependant, le travail de Mockito n'est pas de protéger votre code des méthodes statiques. Si vous n'aimez pas que votre équipe fasse des moqueries statiques, arrêtez D'utiliser Powermockito dans votre organisation. Mockito doit évoluer comme une boîte à outils avec une vision opiniâtre sur la façon dont les tests Java devraient être écrits (par exemple, ne pas maquette statique!!!). Cependant, Mockito n'est pas dogmatique. Nous ne voulons pas bloquer les cas d'usage non recommandé comme les moqueries statiques. C'est juste pas notre travail.