Esse é um uso apropriado do método de redefinição do Mockito?

68

Eu tenho um método privado na minha classe de teste que constrói um Barobjeto comumente usado . O Barconstrutor chama o someMethod()método no meu objeto zombado:

private @Mock Foo mockedObject; // My mocked object
...

private Bar getBar() {
  Bar result = new Bar(mockedObject); // this calls mockedObject.someMethod()
}

Em alguns dos meus métodos de teste, eu quero verificar someMethodtambém foi invocado por esse teste específico. Algo como o seguinte:

@Test
public void someTest() {
  Bar bar = getBar();

  // do some things

  verify(mockedObject).someMethod(); // <--- will fail
}

Isso falha, porque o objeto simulado foi someMethodchamado duas vezes. Eu não quero que meus métodos de teste se preocupem com os efeitos colaterais do meu getBar()método, então seria razoável redefinir meu objeto simulado no final de getBar()?

private Bar getBar() {
  Bar result = new Bar(mockedObject); // this calls mockedObject.someMethod()
  reset(mockedObject); // <-- is this OK?
}

Eu pergunto, porque a documentação sugere a redefinição de objetos simulados geralmente é indicativa de testes ruins. No entanto, isso parece bom para mim.

Alternativa

A escolha alternativa parece estar chamando:

verify(mockedObject, times(2)).someMethod();

o que, na minha opinião, força cada teste a conhecer as expectativas de getBar(), sem nenhum ganho.

Duncan Jones
fonte

Respostas:

60

Eu acredito que este é um dos casos em que o uso reset()está ok. O teste que você está escrevendo está testando que "algumas coisas" acionam uma única chamada someMethod(). Escrever a verify()declaração com qualquer número diferente de invocações pode causar confusão.

  • atLeastOnce() permite falsos positivos, o que é uma coisa ruim, pois você deseja que seus testes estejam sempre corretos.
  • times(2)evita o falso positivo, mas faz parecer que você espera duas invocações em vez de dizer "eu sei que o construtor adiciona uma". Além disso, se algo mudar no construtor para adicionar uma chamada extra, o teste agora tem uma chance de um falso positivo. E remover a chamada causaria falha no teste, porque agora o teste está errado, e não o que está sendo testado.

Ao usar reset()o método auxiliar, você evita esses dois problemas. No entanto, você precisa ter cuidado para que ele também redefina qualquer stub que você tenha feito, portanto, seja avisado. O principal motivo reset()é desencorajado é evitar

bar = mock(Bar.class);
//do stuff
verify(bar).someMethod();
reset(bar);
//do other stuff
verify(bar).someMethod2();

Não é isso que o OP está tentando fazer. O OP, suponho, tem um teste que verifica a invocação no construtor. Para este teste, a redefinição permite isolar essa ação única e seu efeito. Este dos poucos casos com reset()pode ser útil como. As outras opções que não usam tudo têm contras. O fato de o OP ter feito esse post mostra que ele está pensando sobre a situação e não apenas utilizando cegamente o método de redefinição.

unholysampler
fonte
17
Desejo que a Mockito forneceu a chamada resetInteractions () para esquecer as interações passadas com o objetivo de verificar (..., horários (...)) e manter o stub. Isso tornaria as situações de teste de {setup; Aja; verifique;} muito mais fácil de lidar. Seria {setup; resetInteractions; Aja; verificar}
Arkadiy
2
Na verdade, desde o Mockito 2.1, ele fornece uma maneira de limpar as invocações sem redefinir os stubs:Mockito.clearInvocations(T... mocks)
Colin D Bennett
6

Usuários inteligentes do Mockito dificilmente usam o recurso de redefinição porque sabem que isso pode ser um sinal de testes ruins. Normalmente, você não precisa redefinir suas simulações, basta criar novas simulações para cada método de teste.

Em vez de reset()considerar, escreva métodos de teste simples, pequenos e focados em testes longos e superespecificados. O primeiro cheiro potencial de código está reset()no meio do método de teste.

Extraído dos documentos mockito .

Meu conselho é que você tente evitar o uso reset(). Na minha opinião, se você chamar duas vezes para someMethod, isso deve ser testado (talvez seja um acesso ao banco de dados ou outro processo longo que você queira cuidar).

Se você realmente não se importa com isso, você pode usar:

verify(mockedObject, atLeastOnce()).someMethod();

Observe que esse último pode causar um resultado falso, se você chamar someMethod de getBar, e não depois (esse é um comportamento errado, mas o teste não falhará).

gaze
fonte
2
Sim, eu vi essa citação exata (vinculei a ela a partir da minha pergunta). Atualmente, ainda estou para ver um argumento decente sobre por que meu exemplo acima é "ruim". Você pode fornecer um?
Duncan Jones
Se você precisar redefinir seus objetos simulados, parece que você está tentando testar muitas coisas no seu teste. Você pode dividir em dois testes, testando coisas menores. De qualquer forma, não sei por que você está verificando dentro do método getBar, é difícil rastrear o que está testando. Eu recomendo que você projete seu teste pensando no que sua classe deve fazer (se você precisar chamar o Método exatamente duas vezes, pelo menos uma vez, apenas uma vez, nunca etc.) e faça todas as verificações no mesmo local.
greuze
Editei minha pergunta para destacar que o problema persiste mesmo que eu não chame verifymeu método particular (o que eu concordo, provavelmente não pertence a ele). Congratulo-me com seus comentários sobre se sua resposta mudaria.
Duncan Jones
Existem muitas boas razões para usar a redefinição, neste caso, não prestaria muita atenção à citação mockito. Você pode fazer com que o JUnit Class Runner do Spring, ao executar um conjunto de testes, cause interações indesejadas, especialmente se você estiver realizando testes que envolvam chamadas de banco de dados simuladas ou que envolvam métodos particulares nos quais você não deseja usar a reflexão.
Sandy Simonton
Normalmente, acho isso difícil quando quero testar várias coisas, mas o JUnit simplesmente não oferece nenhuma maneira agradável (!) De parametrizar testes. Ao contrário do NUnit, por exemplo, com anotações.
precisa
3

Absolutamente não. Como costuma acontecer, a dificuldade que você está enfrentando para escrever um teste limpo é uma grande bandeira vermelha sobre o design do seu código de produção. Nesse caso, a melhor solução é refatorar seu código para que o construtor de Bar não chame nenhum método.

Construtores devem construir, não executar lógica. Pegue o valor de retorno do método e passe-o como um parâmetro construtor.

new Bar(mockedObject);

torna-se:

new Bar(mockedObject.someMethod());

Se isso resultar na duplicação dessa lógica em muitos lugares, considere criar um método de fábrica que possa ser testado independentemente do seu objeto Bar:

public Bar createBar(MockedObject mockedObject) {
    Object dependency = mockedObject.someMethod();
    // ...more logic that used to be in Bar constructor
    return new Bar(dependency);
}

Se essa refatoração for muito difícil, usar reset () é uma boa solução. Mas vamos deixar claro: isso indica que seu código foi mal projetado.

tonicsoft
fonte