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. Owhen
determina 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 matcherany
para dizer exatamente isso. OthenReturn
diz qual será o valor devolvido quando o método dowhen
for chamado. Você pode chamar owhen
quantas 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
when
que 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.