Zombando de métodos estáticos com o Mockito


374

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

public class MySQLDatabaseConnectionFactory implements DatabaseConnectionFactory {

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

Gostaria de validar os parâmetros passados 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 esse caso de uso específico?



5
Você não pode com Mockito por desing :)
MariuszS

25
@MariuszS Não é por design que o Mockito (ou EasyMock ou jMock) não suporta staticmétodos de simulação , mas por acidente . Essa limitação (junto com o suporte a finalclasses / métodos de simulação ou newobjetos com ou sem simulação ) é uma consequência natural (mas não intencional) da abordagem empregada para implementar a simulação, onde são criadas dinamicamente novas classes que implementam / estendem o tipo a ser ridicularizado; outras bibliotecas de simulação usam outras abordagens que evitam essas limitações. Isso aconteceu no mundo .NET também.
Rogério

2
@ Rogério Obrigado pela explicação. github.com/mockito/mockito/wiki/FAQ Posso simular métodos estáticos? Não. O Mockito prefere a orientação a objetos e a injeção de dependência a códigos estáticos e processuais difíceis de entender e alterar. Existe algum projeto por trás esta limitação também :)
MariuszS

17
@MariuszS Li que, como uma tentativa de descartar casos de uso legítimos, em vez de admitir a ferramenta, existem limitações que não podem ser (facilmente) removidas e sem fornecer qualquer justificativa fundamentada. BTW, aqui está uma discussão desse ponto de vista oposto, com referências.
Rogério

Respostas:


350

Use o PowerMockito em cima do Mockito.

Código de exemplo:

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

    }

Mais Informações:


4
Enquanto isso funciona em teoria, tendo um momento difícil na prática ...
Naftuli Kay

38
Infelizmente, a grande desvantagem disso é a necessidade do PowerMockRunner.
Innokenty 26/11

18
sut.execute ()? Significa?
TJJD 29/15 /

4
Sistema em teste, a classe que requer simulação do DriverManager. kaczanowscy.pl/tomek/2011-01/testing-basics-sut-and-docs
MariuszS

8
Para sua informação, se você já estiver usando o JUnit4, poderá fazer @RunWith(PowerMockRunner.class)isso abaixo @PowerMockRunnerDelegate(JUnit4.class).
EM-Creations

71

A estratégia típica para evitar métodos estáticos que você não tem como evitar, é criar objetos quebrados e, em vez disso, usar os objetos quebrados.

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

Um objeto wrapper pode ser algo como

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

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

Finalmente, sua classe em teste pode usar esse objeto 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 classe que pode ser facilmente testada, porque você não usa diretamente uma classe com métodos estáticos.

Se você estiver usando o CDI e puder fazer uso da anotação @Inject, será ainda mais fácil. Basta fazer o seu bean Wrapper @ApplicationScoped, injetar essa coisa como colaborador (você nem precisa de construtores confusos para teste) e continuar com a zombaria.


3
Criei uma ferramenta para gerar automaticamente interfaces "mixin" Java 8 que atendem chamadas estáticas: github.com/aro-tech/interface-it Os mixins gerados podem ser ridicularizados como qualquer outra interface ou se a sua classe em teste "implementar" o Na interface, você pode substituir qualquer um de seus métodos em uma subclasse para o teste.
Aro1986 #

25

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

E eu não preciso usar BDDMockito.

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

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

Não é possível descobrir? .MockStatic e? .When atualmente com JUnit 4
Teddy

PowerMock.mockStatic e Mockito.when parece não funcionar.
Teddy

Para quem vê isso mais tarde, para mim eu tive que digitar PowerMockito.mockStatic (StaticClass.class);
thinkereer 23/09/19

Você precisa incluir o powermock-api-mockito maven artterfact.
PeterS

23

Como mencionado anteriormente, você não pode zombar de métodos estáticos com o mockito.

Se alterar sua estrutura de teste não for uma opção, faça o seguinte:

Crie uma interface para o DriverManager, zombe dessa interface, injete-a através de algum tipo de injeção de dependência e verifique nessa zombaria.


7

Observação: quando você chama o método estático dentro de uma entidade estática, precisa alterar a classe em @PrepareForTest.

Por exemplo:

securityAlgo = MessageDigest.getInstance(SECURITY_ALGORITHM);

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

@PrepareForTest(MessageDigest.class)

Enquanto se você tem algo como abaixo:

public class CustomObjectRule {

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

}

então, você precisará preparar a classe em que esse código reside.

@PrepareForTest(CustomObjectRule.class)

E depois zombe do método:

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

Eu estava batendo a cabeça na parede tentando descobrir por que minha classe estática não estava zombando. Você pensaria em todos os tutoriais sobre as interwebs, o ONE teria entrado em mais do que o caso de uso básico.
SoftwareSavant

6

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 classe MySQLDatabaseConnectionFactorypara retornar uma conexão falsa, fazer afirmações sobre os parâmetros etc.

A classe estendida pode residir no caso de teste, se estiver localizado no mesmo pacote (o que eu encorajo 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);
    }
}


6

O Mockito não pode capturar métodos estáticos, mas desde o Mockito 2.14.0 você pode simulá-lo criando instâncias de chamada de métodos 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 objetivo não é oferecer suporte direto à zombaria estática, mas melhorar suas APIs públicas, para que outras bibliotecas, como o Powermockito , não precisem depender de APIs internas ou duplicar diretamente algum código Mockito. ( fonte )

Disclaimer: A equipe de Mockito acha que o caminho para o inferno é pavimentado com métodos estáticos. No entanto, o trabalho do Mockito não é proteger seu código de métodos estáticos. Se você não gosta que sua equipe faça zombaria estática, 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 zombe de estática !!!). No entanto, Mockito não é dogmático. Não queremos bloquear casos de uso não recomendados, como zombaria estática. Simplesmente não é o nosso trabalho.



1

Como esse método é estático, ele já possui tudo o que você precisa para usá-lo e, portanto, anula o propósito da zombaria. Zombar dos métodos estáticos é considerado uma má prática.

Se você tentar fazer isso, significa que há algo errado com a maneira como você deseja executar os testes.

Claro que você pode usar o PowerMockito ou qualquer outra estrutura capaz de fazer isso, mas tente repensar sua abordagem.

Por exemplo: tente simular / fornecer os objetos que esse método estático consome.


0

Use a estrutura JMockit . Funcionou para mim. Você não precisa escrever instruções para zombar do método DBConenction.getConnection (). Apenas o código abaixo é suficiente.

@Mock abaixo é mockit.Mock package

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;
            }
         };
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.