Usando o Mockito para testar classes abstratas


213

Eu gostaria de testar uma aula abstrata. Claro, eu posso escrever manualmente uma simulação que herda da classe.

Posso fazer isso usando uma estrutura de zombaria (estou usando o Mockito) em vez de criar manualmente minha zombaria? Quão?


2
A partir do Mockito 1.10.12 , o Mockito suporta classes abstratas de espionagem / zombaria diretamente:SomeAbstract spy = spy(SomeAbstract.class);
pesche

6
A partir de Mockito 2.7.14, você também pode zombar classess abstrato que requerem argumentos do construtor viamock(MyAbstractClass.class, withSettings().useConstructor(arg1, arg2).defaultAnswer(CALLS_REAL_METHODS))
Gediminas Rimsa

Respostas:


315

A sugestão a seguir permite testar classes abstratas sem criar uma subclasse "real" - a Mock é a subclasse.

use Mockito.mock(My.class, Mockito.CALLS_REAL_METHODS)e zombe de quaisquer métodos abstratos que são chamados.

Exemplo:

public abstract class My {
  public Result methodUnderTest() { ... }
  protected abstract void methodIDontCareAbout();
}

public class MyTest {
    @Test
    public void shouldFailOnNullIdentifiers() {
        My my = Mockito.mock(My.class, Mockito.CALLS_REAL_METHODS);
        Assert.assertSomething(my.methodUnderTest());
    }
}

Nota: A vantagem desta solução é que você não precisa implementar os métodos abstratos, desde que eles nunca sejam invocados.

Na minha opinião sincera, isso é mais legal do que usar um espião, já que um espião exige uma instância, o que significa que você deve criar uma subclasse instável da sua classe abstrata.


14
Como observado abaixo, isso não funciona quando a classe abstrata chama métodos abstratos para ser testada, o que geralmente acontece.
Richard Nichols

11
Isso realmente funciona quando a classe abstrata chama métodos abstratos. Basta usar a sintaxe doReturn ou doNothing em vez de Mockito.when para remover os métodos abstratos e se você remover alguma chamada concreta, certifique-se de remover primeiro as chamadas abstratas.
Vai I

2
Como injetar dependências nesse tipo de objeto (classe abstrata zombada chamando métodos reais)?
Samuel

2
Isso se comporta de maneiras inesperadas se a classe em questão tiver inicializadores de instância. O Mockito ignora os inicializadores para zombarias, o que significa que as variáveis ​​de instância inicializadas em linha serão inesperadamente nulas, o que pode causar NPEs.
Digitalbath #

1
E se o construtor da classe abstrata usar um ou mais parâmetros?
SD

68

Se você só precisar testar alguns dos métodos concretos sem tocar em nenhum dos resumos, poderá usá-lo CALLS_REAL_METHODS(consulte a resposta de Morten ), mas se o método concreto em teste chamar alguns dos resumos ou métodos de interface não implementados, isso não funcionará. - Mockito reclamará "Não é possível chamar o método real na interface java".

(Sim, é um design ruim, mas algumas estruturas, como o Tapeçaria 4, meio que o impõem a você.)

A solução alternativa é reverter essa abordagem - use o comportamento de simulação comum (isto é, tudo é ridicularizado / stubbed) e use doCallRealMethod()para chamar explicitamente o método concreto em teste. Por exemplo

public abstract class MyClass {
    @SomeDependencyInjectionOrSomething
    public abstract MyDependency getDependency();

    public void myMethod() {
        MyDependency dep = getDependency();
        dep.doSomething();
    }
}

public class MyClassTest {
    @Test
    public void myMethodDoesSomethingWithDependency() {
        MyDependency theDependency = mock(MyDependency.class);

        MyClass myInstance = mock(MyClass.class);

        // can't do this with CALLS_REAL_METHODS
        when(myInstance.getDependency()).thenReturn(theDependency);

        doCallRealMethod().when(myInstance).myMethod();
        myInstance.myMethod();

        verify(theDependency, times(1)).doSomething();
    }
}

Atualizado para adicionar:

Para métodos não nulos, você precisará usar thenCallRealMethod(), por exemplo:

when(myInstance.myNonVoidMethod(someArgument)).thenCallRealMethod();

Caso contrário, Mockito reclamará de "stubbing inacabado detectado".


9
Isso funcionará em alguns casos, no entanto, o Mockito não chama o construtor da classe abstrata subjacente com esse método. Isso pode causar a falha do "método real" devido à criação de um cenário inesperado. Portanto, esse método também não funcionará em todos os casos.
Richard Nichols

3
Sim, você não pode contar com o estado do objeto, apenas o código no método que está sendo chamado.
precisa

Ah, então os métodos de objetos são separados do estado, ótimo.
haelix

17

Você pode conseguir isso usando um espião (use a versão mais recente do Mockito 1.8+).

public abstract class MyAbstract {
  public String concrete() {
    return abstractMethod();
  }
  public abstract String abstractMethod();
}

public class MyAbstractImpl extends MyAbstract {
  public String abstractMethod() {
    return null;
  }
}

// your test code below

MyAbstractImpl abstractImpl = spy(new MyAbstractImpl());
doReturn("Blah").when(abstractImpl).abstractMethod();
assertTrue("Blah".equals(abstractImpl.concrete()));

14

As estruturas de simulação são projetadas para facilitar a simulação de dependências da classe que você está testando. Quando você usa uma estrutura de zombaria para zombar de uma classe, a maioria das estruturas cria dinamicamente uma subclasse e substitui a implementação do método pelo código para detectar quando um método é chamado e retornar um valor falso.

Ao testar uma classe abstrata, você deseja executar os métodos não abstratos do Subject Under Test (SUT), para que uma estrutura de simulação não seja o que você deseja.

Parte da confusão é que a resposta à pergunta a que você vinculou criou um escárnio que se estende de sua classe abstrata. Eu não chamaria essa classe de zombaria. Uma simulação é uma classe usada como substituta de uma dependência, é programada com expectativas e pode ser consultada para verificar se essas expectativas são atendidas.

Em vez disso, sugiro definir uma subclasse não abstrata de sua classe abstrata em seu teste. Se isso resultar em muito código, isso pode ser um sinal de que é difícil estender sua classe.

Uma solução alternativa seria tornar o seu caso de teste abstrato, com um método abstrato para criar o SUT (em outras palavras, o caso de teste usaria o padrão de design do Método de Modelo ).


8

Tente usar uma resposta personalizada.

Por exemplo:

import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

public class CustomAnswer implements Answer<Object> {

    public Object answer(InvocationOnMock invocation) throws Throwable {

        Answer<Object> answer = null;

        if (isAbstract(invocation.getMethod().getModifiers())) {

            answer = Mockito.RETURNS_DEFAULTS;

        } else {

            answer = Mockito.CALLS_REAL_METHODS;
        }

        return answer.answer(invocation);
    }
}

Ele retornará a simulação para métodos abstratos e chamará o método real para métodos concretos.


5

O que realmente me faz sentir mal ao zombar de classes abstratas é o fato de que nem o construtor padrão YourAbstractClass () é chamado (falta super () no mock) nem parece haver alguma maneira no Mockito de inicializar as propriedades de simulação padrão (por exemplo, propriedades da lista com ArrayList ou LinkedList vazio).

Minha classe abstrata (basicamente o código fonte da classe é gerada) NÃO fornece uma injeção de setter de dependência para os elementos da lista, nem um construtor onde inicializa os elementos da lista (que tentei adicionar manualmente).

Somente os atributos da classe usam a inicialização padrão: private List dep1 = new ArrayList; lista particular dep2 = new ArrayList

Portanto, NÃO há como zombar de uma classe abstrata sem usar uma implementação de objeto real (por exemplo, definição de classe interna na classe de teste de unidade, substituindo métodos abstratos) e espionando o objeto real (que faz a inicialização apropriada do campo).

Pena que apenas o PowerMock ajudaria aqui ainda mais.


2

Supondo que suas classes de teste estejam no mesmo pacote (em uma raiz de origem diferente) das classes em teste, você pode simplesmente criar o mock:

YourClass yourObject = mock(YourClass.class);

e chame os métodos que você deseja testar, como faria com qualquer outro método.

Você precisa fornecer expectativas para cada método chamado com a expectativa de qualquer método concreto chamado super método - não sei como você faria isso com o Mockito, mas acredito que isso é possível com o EasyMock.

Tudo o que isso está fazendo é criar uma instância concreta YouClasse poupar o esforço de fornecer implementações vazias de cada método abstrato.

Como um aparte, muitas vezes acho útil implementar a classe abstrata em meu teste, onde ela serve como exemplo de implementação que eu teste por meio de sua interface pública, embora isso dependa da funcionalidade fornecida pela classe abstrata.


3
Mas usar o mock não testará os métodos concretos do YourClass, ou estou errado? Não é isso que eu procuro.
Ripper234 6/07/09

1
Correto, o acima não funcionará se você quiser invocar os métodos concretos na classe abstrata.
Richard Nichols

Desculpas, vou editar um pouco sobre a expectativa, que é necessária para cada método que você chama, não apenas os abstratos.
21119 Nick Holt

mas você ainda está testando sua simulação, não os métodos concretos.
Jonatan Cloutier

2

Você pode estender a classe abstrata com uma classe anônima em seu teste. Por exemplo (usando o Junit 4):

private AbstractClassName classToTest;

@Before
public void preTestSetup()
{
    classToTest = new AbstractClassName() { };
}

// Test the AbstractClassName methods.

2

Mockito permite zombar de classes abstratas por meio da @Mockanotação:

public abstract class My {

    public abstract boolean myAbstractMethod();

    public void myNonAbstractMethod() {
        // ...
    }
}

@RunWith(MockitoJUnitRunner.class)
public class MyTest {

    @Mock(answer = Answers.CALLS_REAL_METHODS)
    private My my;

    @Test
    private void shouldPass() {
        BDDMockito.given(my.myAbstractMethod()).willReturn(true);
        my.myNonAbstractMethod();
        // ...
    }
}

A desvantagem é que ele não pode ser usado se você precisar de parâmetros do construtor.


0

Você pode instanciar uma classe anônima, injetar suas zombarias e testá-la.

@RunWith(MockitoJUnitRunner.class)
public class ClassUnderTest_Test {

    private ClassUnderTest classUnderTest;

    @Mock
    MyDependencyService myDependencyService;

    @Before
    public void setUp() throws Exception {
        this.classUnderTest = getInstance();
    }

    private ClassUnderTest getInstance() {
        return new ClassUnderTest() {

            private ClassUnderTest init(
                    MyDependencyService myDependencyService
            ) {
                this.myDependencyService = myDependencyService;
                return this;
            }

            @Override
            protected void myMethodToTest() {
                return super.myMethodToTest();
            }
        }.init(myDependencyService);
    }
}

Lembre-se de que a visibilidade deve ser protectedda propriedade myDependencyServiceda classe abstrata ClassUnderTest.


0

Os PowerMock Whitebox.invokeMethod(..)podem ser úteis nesse caso.

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.