Mockito zomba da classe final local, mas falha em Jenkins

11

Eu escrevi alguns testes de unidade para um método estático. O método estático usa apenas um argumento. O tipo do argumento é uma classe final. Em termos de código:

public class Utility {

   public static Optional<String> getName(Customer customer) {
       // method's body.
   }
}

public final class Customer {
   // class definition
}

Então, para a Utilityclasse, criei uma classe de teste UtilityTestsna qual escrevi testes para esse método getName,. A estrutura de teste de unidade é TestNG e a biblioteca de zombaria usada é Mockito. Portanto, um teste típico tem a seguinte estrutura:

public class UtilityTests {

   @Test
   public void getNameTest() {
     // Arrange
     Customer customerMock = Mockito.mock(Customer.class);
     Mockito.when(...).thenReturn(...);

     // Act
     Optional<String> name = Utility.getName(customerMock);

     // Assert
     Assert.assertTrue(...);
   }
}

Qual é o problema ?

Enquanto os testes são executados com sucesso localmente, dentro do IntelliJ, eles falham no Jenkins (quando pressiono meu código na ramificação remota, uma compilação é acionada e os testes de unidade são executados no final). A mensagem de erro é sth como o seguinte:

org.mockito.exceptions.base.MockitoException: Não é possível zombar / espionar a classe com.packagename.Customer O Mockito não pode zombar / espionar porque: - classe final

O que eu tentei?

Eu procurei um pouco, a fim de encontrar uma solução, mas não consegui. Eu notar aqui que estou não permitido mudar o fato de que Customeré uma última classe. Além disso, eu gostaria, se possível, de não mudar seu design (por exemplo, criar uma interface que contenha os métodos que eu quero zombar e afirmar que a classe Customer implementa essa interface, como Jose corretamente apontou em seu Comente). O que eu tentei é a segunda opção mencionada no mockito-final . Apesar de ter resolvido o problema, ele travou alguns outros testes de unidade :(, que não podem ser corrigidos de maneira aparente.

Questões

Então, aqui estão as duas perguntas que tenho:

  1. Como isso é possível em primeiro lugar? O teste não deveria falhar localmente e em Jenkins?
  2. Como isso pode ser corrigido com base nas restrições mencionadas acima?

Agradecemos antecipadamente por qualquer ajuda.

Christos
fonte
11
Meu palpite seria que a enable finalconfiguração funcione no seu espaço de trabalho, mas quando executada Jenkinsnão consegue encontrar esse arquivo. Verifique onde Jenkinsestá procurando o arquivo e se ele está realmente lá ou não.
segunda
Este outro segmento explica como ativar zombeteiro classe final em Mockito 2, adicionando um arquivo de configuração Mockito sob o diretório de recursos: stackoverflow.com/questions/14292863/...
José Tepedino
3
Seria possível, no código que você está lidando, extrair uma interface da classe Customer, digamos ICustomer, e usá-la na classe Utility? Então você pode zombar dessa interface em vez da aula final concreta #
Jose Tepedino
@JoseTepedino Este é um ponto válido. Faz todo o sentido e é definitivamente uma maneira elegante de superar esse problema. No entanto, me pergunto se existe outra maneira e, mais importante, quero entender por que a abordagem atual é bem-sucedida localmente e falha em Jenkins.
Christos
11
Tem Customeralguma lógica ou é apenas uma classe de dados idiota? Se for apenas um monte de campos com getters e setters, você poderá instanciar.
Willis Blackburn

Respostas:

2

Uma abordagem alternativa seria usar o padrão 'método para classificar'.

  1. Mova os métodos da classe de cliente para outra classe, diga CustomerSomething, por exemplo, / CustomerFinances (ou seja qual for a sua responsabilidade).
  2. Adicione um construtor ao cliente.
  3. Agora você não precisa zombar de Customer, apenas a classe CustomerSomething! Você pode não precisar zombar disso, se ele não tiver dependências externas.

Aqui está um bom blog sobre o tema: https://simpleprogrammer.com/back-to-basics-mock-eliminating-patterns/

Johnny Alpha
fonte
11
Obrigado pela sua resposta (+1). Eu encontrei uma maneira de corrigi-lo (resposta à segunda pergunta). No entanto, a razão pela qual os testes falham no IntelliJ ainda não está clara para mim. Além disso, não posso mais reproduzi-lo (a falha no IntelliJ), o que é totalmente estranho.
Christos
1

Como isso é possível em primeiro lugar? O teste não deveria falhar localmente e em Jenkins?

Obviamente, é uma espécie de ambiente específico. A única questão é - como determinar a causa da diferença.

Eu sugiro que você verifique o org.mockito.internal.util.MockUtil#typeMockabilityOfmétodo e compare, o que mockMakeré realmente usado nos dois ambientes e por quê.

Se mockMakerfor o mesmo - compare as classes carregadas IDE-Clientvs Jenkins-Client- elas têm alguma diferença no tempo de execução do teste.

Como isso pode ser corrigido com base nas restrições mencionadas acima?

O código a seguir foi escrito assumindo o OpenJDK 12 e o Mockito 2.28.2, mas acredito que você pode ajustá-lo para qualquer versão realmente usada.

public class UtilityTest {    
    @Rule
    public InlineMocksRule inlineMocksRule = new InlineMocksRule();

    @Rule
    public MockitoRule mockitoRule = MockitoJUnit.rule();

    @Test
    public void testFinalClass() {
        // Given
        String testName = "Ainz Ooal Gown";
        Client client = Mockito.mock(Client.class);
        Mockito.when(client.getName()).thenReturn(testName);

        // When
        String name = Utility.getName(client).orElseThrow();

        // Then
        assertEquals(testName, name);
    }

    static final class Client {
        final String getName() {
            return "text";
        }
    }

    static final class Utility {
        static Optional<String> getName(Client client) {
            return Optional.ofNullable(client).map(Client::getName);
        }
    }    
}

Com uma regra separada para zombarias em linha:

import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.mockito.internal.configuration.plugins.Plugins;
import org.mockito.internal.util.MockUtil;

import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

public class InlineMocksRule implements TestRule {
    private static Field MOCK_MAKER_FIELD;

    static {
        try {
            MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(Field.class, MethodHandles.lookup());
            VarHandle modifiers = lookup.findVarHandle(Field.class, "modifiers", int.class);

            MOCK_MAKER_FIELD = MockUtil.class.getDeclaredField("mockMaker");
            MOCK_MAKER_FIELD.setAccessible(true);

            int mods = MOCK_MAKER_FIELD.getModifiers();
            if (Modifier.isFinal(mods)) {
                modifiers.set(MOCK_MAKER_FIELD, mods & ~Modifier.FINAL);
            }
        } catch (IllegalAccessException | NoSuchFieldException ex) {
            throw new RuntimeException(ex);
        }
    }

    @Override
    public Statement apply(Statement base, Description description) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                Object oldMaker = MOCK_MAKER_FIELD.get(null);
                MOCK_MAKER_FIELD.set(null, Plugins.getPlugins().getInlineMockMaker());
                try {
                    base.evaluate();
                } finally {
                    MOCK_MAKER_FIELD.set(null, oldMaker);
                }
            }
        };
    }
}
ursa
fonte
Obrigado pela sua resposta (+1). Eu encontrei uma maneira de corrigi-lo (resposta à segunda pergunta). No entanto, a razão pela qual os testes falham no IntelliJ ainda não está clara para mim. Além disso, não posso mais reproduzi-lo (a falha no IntelliJ), o que é totalmente estranho.
Christos
1

Certifique-se de executar o teste com os mesmos argumentos. Verifique se suas configurações de execução intellij correspondem aos jenkins. https://www.jetbrains.com/help/idea/creating-and-editing-run-debug-configurations.html . Você pode tentar executar o teste na máquina local com os mesmos argumentos que no jenkins (do terminal), se falhar, isso significa que o problema está nos argumentos

Link182
fonte
O arquivo org.mockito.plugins.MockMakertambém existe na máquina jenkins. Eu uso a mesma JVM em máquinas de bot. Vou verificar os 3 que você apontou. Obrigado
Christos
Tentei executar o teste pelo console, usando o comando usado no Jenkins. Eles falham com a mesma mensagem de erro exata. Então, algo estranho acontece dentro do IntelliJ.
Christos
Dê uma olhada em .idea / workspace.xml na sua configração de execução, ela está dentro de uma tag <component>. Depois que você pode aprender a transformar esse XML em comando bash
Link182
Você pode mostrar o comando jenkins terminal que é usado para executar testes? Além disso, você pode me dizer qual gerenciador de pacotes você usa?
Link182
Como ferramenta de construção, eu uso o Gradle.
Christos