Zombando de methods estáticos com Mockito

Eu escrevi uma fábrica para produzir objects java.sql.Connection :

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

Eu gostaria de validar os parâmetros passados ​​para DriverManager.getConnection , mas não sei como zombar de um método estático. Estou usando o JUnit 4 e o Mockito para meus casos de teste. Existe uma boa maneira de simular / verificar este caso de uso específico?

Use o PowerMockito no topo do Mockito.

Exemplo de código:

 @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(...); } 

Mais Informações:

  • Por que Mockito não zomba de methods estáticos?

A estratégia típica para evitar methods estáticos que você não tem como evitar usar é criar objects envolvidos e usar os objects wrapper.

Os objects do wrapper se tornam fachadas para as classs estáticas reais e você não os testa.

Um object wrapper pode ser algo como

 public class Slf4jMdcWrapper { public static final Slf4jMdcWrapper SINGLETON = new Slf4jMdcWrapper(); public String myApisToTheSaticMethodsInSlf4jMdcStaticUtilityClass() { return MDC.getWhateverIWant(); } } 

Finalmente, sua class em teste pode usar este object singleton, por exemplo, tendo um construtor padrão para uso na vida real:

 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 aqui você tem uma class que pode ser facilmente testada, porque você não usa diretamente uma class com methods estáticos.

Se você estiver usando o CDI e puder usar a anotação @Inject, será ainda mais fácil. Basta fazer o seu Wrapper bean @ApplicationScoped, pegar aquela coisa injetada como um colaborador (você nem precisa de construtores bagunçados para testes), e continuar com o mocking.

Como mencionado antes, você não pode zombar de methods estáticos com mockito.

Se alterar sua estrutura de teste não for uma opção, você poderá fazer o seguinte:

Crie uma interface para o DriverManager, zombe dessa interface, injete-a por meio de algum tipo de injeção de dependência e verifique essa simulação.

Eu tive uma questão semelhante. A resposta aceita não funcionou para mim, até que eu fiz a alteração: @PrepareForTest(TheClassThatContainsStaticMethod.class) , de acordo com a documentação do PowerMock para mockStatic .

E eu não tenho que usar o BDDMockito .

Minha class:

 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(); } } } 

Minha class de teste:

 @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()); } } 

Para simular o método estático, você deve usar um Powermock em: https://github.com/powermock/powermock/wiki/MockStatic . Mockito não oferece essa funcionalidade.

Você pode ler um artigo sobre mockito legal: http://refcardz.dzone.com/refcardz/mockito

Você pode fazer isso com um pouco de refatoração:

 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...); } } 

Em seguida, você pode estender sua class MySQLDatabaseConnectionFactory para retornar uma conexão MySQLDatabaseConnectionFactory , fazer asserções nos parâmetros, etc.

A class estendida pode residir dentro do caso de teste, se estiver localizada no mesmo pacote (o que eu incentivo você a fazer)

 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); } } 

Observação: quando você chama o método estático em uma entidade estática, é necessário alterar a class em @PrepareForTest.

Por exemplo:

 securityAlgo = MessageDigest.getInstance(SECURITY_ALGORITHM); 

Para o código acima, se você precisar zombar da class MessageDigest, use

 @PrepareForTest(MessageDigest.class) 

Enquanto você tem algo como abaixo:

 public class CustomObjectRule { object = DatatypeConverter.printHexBinary(MessageDigest.getInstance(SECURITY_ALGORITHM) .digest(message.getBytes(ENCODING))); } 

Então, você precisa preparar a turma em que esse código reside.

 @PrepareForTest(CustomObjectRule.class) 

E depois zombar do método:

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

Eu também escrevi uma combinação de Mockito e AspectJ: https://github.com/iirekm/misc/tree/master/ajmock

Seu exemplo se torna:

 when(() -> DriverManager.getConnection(...)).thenReturn(...); 

Mockito não pode capturar methods estáticos, mas desde o Mockito 2.14.0 você pode simulá-lo criando instâncias de invocação de methods estáticos.

Exemplo (extraído de seus testes ):

 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; } } } 

Seu objective não é suportar diretamente a simulação estática, mas melhorar suas APIs públicas para que outras bibliotecas, como a Powermockito , não tenham que confiar em APIs internas ou tenham que duplicar diretamente algum código Mockito. ( fonte )

Aviso: A equipe de Mockito acha que o caminho para o inferno está cheio de methods estáticos. No entanto, o trabalho de Mockito não é proteger seu código de methods estáticos. Se você não gosta de sua equipe fazendo escárnio estático, pare de usar o Powermockito em sua organização. O Mockito precisa evoluir como um kit de ferramentas com uma visão opinativa sobre como os testes Java devem ser escritos (por exemplo, não falsifique statistics !!!). No entanto, Mockito não é dogmático. Não queremos bloquear casos de uso não recomendados, como escorregões estáticos. Não é apenas o nosso trabalho.