Mockito e mocks
Jonas Abreu em 27/08/2013TL;DR;
Você pode usar mocks assim com o Mockito e um guia rápido da api do Mockito no fim do post ;)
Um dos meus alunos me pediu para escrever um pouco sobre mocks e mockito, porque aparentemente não existe muito material sobre o assunto (em especial mockito) em português.
Sobre o que é mock
Eu não vou discutir o que é um mock, spy, stub, fake object e etc. Não gosto muito de discutir taxonomia. Nesse post, o que vou chamar de mock é bem abrangente: mock é qualquer objeto que você passe (para outro objeto) com o objetivo de observar/mudar o comportamento do objeto sendo testado.
Mockito
O mockito é uma biblioteca que ajuda na criação de mocks. Acho a sintaxe dele bem mais agradável do que outros projetos, como o jmock. Mas no fim é questão de gosto. Existem umas 6M bibliotecas de mock em java que fazem basicamente a mesma coisa. Vou usar ele nesse post simplesmente porque acho mais agradável :)
Exemplo
Vamos modelar um marceneiro. Por quê? Porque sim :)
Se estamos fazendo TDD, começariamos mais ou menos assim (código Java com jUnit 4):
// MarceneiroTest.java
@Test
public void deveSerrarUmaTabua() {
Marceneiro marceneiro = new Marceneiro();
}
Quando você escreveu isso, seu código não compila porque não existe a classe marceneiro. Você cria a classe e continua.
// MarceneiroTest.java
@Test
public void deveSerrarUmaTabua() {
Marceneiro marceneiro = new Marceneiro();
List<Tabua> tabuas = marceneiro.serra(new Tabua());
}
Novamente seu código não compila. Você cria a classe Tabua e o método serra:
// Marceneiro.java
public List<Tabua> serra(Tabua tabua) {
return new ArrayList<Tabua>();
}
Seu infinitest volta a ficar verde. Você volta pro próximo passo do teste:
// MarceneiroTest.java
@Test
public void deveSerrarUmaTabua() {
Marceneiro marceneiro = new Marceneiro();
List<Tabua> tabuas = marceneiro.serra(new Tabua());
Assert.assertEquals(2, tabuas.size());
}
Seu código parou de compilar. Vou pular a parte da solução mais trivial ir para a discussão de quem é que corta.
Quem corta a tábua?
Não é o marceneiro. Cortar com os dentes ou unhas ia ser bem difícil. Ele precisa de uma Serra. Mas qual tipo?
Serrote? Serra de arco? Tico-tico? Sei lá. E não faz diferença, desde que seja uma serra e corte tábuas.
Então você cria uma interface Serra e modifica o marceneiro para delegar o corte da tábua para ela.
public interface Serra {
public List<Tabua> corta(Tabua tabua);
}
// Marceneiro.java
private final Serra serra;
public Marceneiro(Serra serra) {
this.serra = serra;
}
public List<Tabua> serra(Tabua tabua) {
return serra.corta(tabua);
}
E com isso seu teste parou de compilar.
// MarceneiroTest.java
private Serra serra;
@Test
public void deveSerrarUmaTabua() {
// precisa passar uma serra
Marceneiro marceneiro = new Marceneiro(serra);
List<Tabua> tabuas = marceneiro.serra(new Tabua());
Assert.assertEquals(2, tabuas.size());
}
E é aqui que o mock te salva. Você não precisa criar uma serra de verdade para continuar o seu teste. Você cria um mock de Serra e passa. Você pode fazer isso manualmente. Muitas vezes vai ser interessante. Mas o mockito pode facilitar um pouco e você terá menos classes para manter (porque você não ia usar classes anônimas porque elas são muito feias em Java). Então vamos usar ele de uma vez :)
// MarceneiroTest.java
import org.mockito.Mockito.*;
@Mock
private Serra serra;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
when(serra.corta(any(Tabua.class)))
.thenReturn(Arrays.asList(new Tabua(), new Tabua());
}
@Test
public void deveSerrarUmaTabua() {
Marceneiro marceneiro = new Marceneiro(serra);
List<Tabua> tabuas = marceneiro.serra(new Tabua());
Assert.assertEquals(2, tabuas.size());
}
E seu infinitest fica verde :) Tudo está passando. Vamos entender essa zona toda do @Before e do @Mock.
Usando Mockito
MockitoAnnotations.initMocks(this): Simplesmente inicializa com mocks que ainda não sabem fazer nada todos os campos do seu objeto de teste (no caso,MarceneiroTest) anotados com@Mock.when(chamada_de_metodo).thenReturn(valor_de_retorno): Aqui você começa a ensinar ao seu objeto burro (mock) o que ele deve fazer em determinadas situações. Owhendetermina qual método você espera que seja chamado no futuro e com quais parâmetros. Se não faz diferença qual é o exato objeto passado na chamada, você pode usar o matcheranypara dizer exatamente isso. OthenReturndiz qual será o valor devolvido quando o método dowhenfor chamado. Você pode chamar owhenquantas vezes quiser.
Simples? Não. Mas as alternativas também não são muito boas. Depois de bastante desgaste e sofrimento você acaba pegando o jeito. Mas ainda é mais fácil de entender e usar do que qualquer spec de Java ;)
Como o Mockito funciona
Magia negra.
Não cabe nesse post. Se quiserem muito saber como funciona, me avisem nos comentários que eu escrevo outro post explicando (ou peço pro Lucas ou o Ferreira explicarem :D )
Dores com o Mockito (e mocks)
Tirando a sintaxe que te obriga a ler o código de outra forma, existem algumas coisas que precisamos prestar atenção:
- Quando você cria o mock (lembra do
@Mock?), eu disse que o objeto não sabe fazer nada. Isso não é exatamente verdade. O objeto é capaz de responder à todos os métodos definidos na interface pública dele. Só que ele devolve valores padrão. Se o retorno éint, ele devolve0. Se éString, ele devolve"", se for qualquer outra coisa que não seja um primitivo, ele devolvenull. E aqui complica. A maioria das interações não esperadas (as que você não colocou owhen) vão devolver null. Se esse valor não for usado, tranquilo. Mas se for usado você recebe uma belaNullPointerException. Então se você está escrevendo testes com mockito e tomou umaNPE, a maioria das vezes vai ser porque você esqueceu umwhen. - No exemplo eu usei o matcher
any(class). Mas não é obrigatório você usar um matcher. Você pode passar o valor exato que quer que receba. O problema é que o mockito não permite que você misture matchers com valores, então se precisar de um matcher, deve passar todos os parâmetros como matchers. Se quiser passar um valor, precisará usar o matchereq. - Classes final são invulneráveis ao tipo de Magia Negra que o Mockito usa, então você não consegue mocká-las. Se precisar muito disso, tente o powermock, que se integra com o Mockito.
- O pulo que eu dei de já criar uma interface não é muito saudável. Eu assumi que vai existir mais de um tipo de serra, o que eu não tenho como garantir. Ou seja, eu fiz design para coisas que podem não existir e esse é um dos caminhos que levam pro inferno.
- Mocks (não apenas o mockito) mandam pro limbo todas aquelas idéias bonitas sobre baixo acomplamento. Você está escrevendo um teste completamente acoplado à sua implementação atual do objeto. Refatore ela e os seus testes com mock podem quebrar. Pior ainda, eles podem continuar passando mas não estar testando mais nada.
- Se você tem que mockar muita coisa para testar, tem algo podre no seu design. Basicamente a regra que uso é: Comecei
a ficar sem paciência de tanto
whenque tenho que colocar, refatoro a classe sendo testada. E minha paciência não é muito grande quando estou escrevendo testes ;)
Guia rápido de API do Mockito
Fui procurar a documentação que eu usava e não encontrei. Então resolvi colocar aqui uma explicação rápida dos métodos
que encontrei referência em meus projetos Java com Mockito (grep -ri org.mockito workspace)
Matchers: Assim como o
any(class), existem diversos matchers que podem ser usados no lugar dos parâmetros. Todos eles se encontram na classeorg.mockito.Matchers. Recomendo que sempre façaimport static, porque facilita muito a leitura dos seus mocks. Alguns exemplos de matchers são:- eq(objeto): O parâmetro deve ser igual (equals) ao objeto passado.
- any(classe): O parâmetro pode ser qualquer objeto da classe passada como parâmetro
- anyString: O parâmetro pode ser qualquer String
- anyObject: O parâmetro pode ser qualquer Objeto
- anyInt: O parâmetro pode ser qualquer int
- anyBoolean: O parâmetro pode ser qualquer boolean
- anyLong: O parâmetro pode ser qualquer long
- anyListOf(classe): O parâmetro deve ser um List de objetos do mesmo tipo da classe passada
- endsWith(sufixo): O parâmetro deve ser uma String que termine com o sufixo passado
- contains(substring): O parâmetro deve ser uma String que contenha a substring
- argThat(matcher): Permite que você use os matchers do hamcrest como um matcher do Mockito.
@Mock: É a anotação que você coloca em cima dos seus fields para que o
MockitoAnnotations.initMocks(this)inicialize os seus mocks. @Mock private Listmock; @Before public void setup() { MockitoAnnotations.initMocks(this); }- Mockito.mock(classe): Se você não quiser usar a inicialização automática de mocks, você pode criá-los manualmente com esse método. Object mock = Mockito.mock(Object.class);
- Mockito.when(chamada_de_metodo): Como explicado acima, você usa ele para ensinar o seu mock o que ele deve devolver quando ocorrer uma chamada de método. Object mock = Mockito.mock(Object.class); when(mock.equals(anyObject())) .thenReturn(false);
- Mockito.doThrow(exception): Semelhante ao when, mas você usa quando quer que a invocação do método lance a exceção exception Object mock = Mockito.mock(Object.class); doThrow(new NullPointerException()) .when(mock).equals(anyObject());
- @Mock(answer = Answers.RETURNS_DEEP_STUBS): Quando você usa a anotação @Mock, você pode pedir que as chamadas de métodos sempre devolvam Deep Stubs. Isso significa que nos casos em que o Mockito devolveria null por padrão, agora ele devolve um mock que você pode usar no when. Apenas lembre-se que Deep Stubs é uma solução desesperada. Se precisou de Deep Stubs seu design está ruim. @Mock(answer = Answers.RETURNS_DEEP_STUBS) private List
Mockito.verify(mock): O verify serve para você garantir que o objeto de testes interagiu com o seu mock (lembra do que eu falei sobre acoplamento forte com a implementação, esse é o caso). Object mock = Mockito.mock(Object.class);
new HashMap<Object, Object>().put(mock, "asdf"); verify(mock).hashCode();Mockito.verify(mock, Mockito.never()): O verify pode receber um segundo parâmetro que determina o número de vezes que o método foi chamado. No caso do never(), ele vai verificar se o método nunca foi chamado. Caso tenha sido chamado, o teste falha. Object mock = Mockito.mock(Object.class);
new HashMap<Object, Object>().put(mock, "asdf"); verify(mock, never()).toString();Mockito.verify(mock, Mockito.atLeast(numero)): O atLeast faz com que o verify cheque se o método foi chamado pelo menos numero vezes. Object mock = Mockito.mock(Object.class);
new HashMap<Object, Object>().put(mock, "asdf"); verify(mock, atLeast(1)).hashCode();- Mockito.verify(mock, Mockito.atLeastOnce()): alias para atLeast(1), mas um pouco mais bonitinho.
- Mockito.verify(mock, Mockito.times(numero)): Semelhante ao atLeast, mas verifica se o número exato de chamadas aconteceu.
Mockito.verifyZeroInteractions: Semelhante ao verify, mas para quando você quer garantir que não aconteceu nenhuma interação com o mock. Object mock = Mockito.mock(Object.class);
new HashMap<Object, Object>().put(mock, "asdf"); // vai falhar porque o HashMap chama o método hashCode() do mock. verifyZeroInteractions(mock);
Nota
Nenhum dos códigos acima foi compilado. Provavelmente tem vários erros. Me avisem nos comentários que eu corrijo eles.