EasyMock vs Mockito vs JMockit
1. Introdução
1.1. Visão geral
Neste post, vamos falar sobremocking: o que é, por que usá-lo e vários exemplos de como simular o mesmo caso de teste usando algumas das bibliotecas de simulação mais usadas para Java.
Começaremos com algumas definições formais / semiformais de conceitos de mocking; em seguida, apresentaremos o caso em teste, acompanharemos com exemplos para cada biblioteca e terminaremos com algumas conclusões. As bibliotecas escolhidas sãoMockito,EasyMock eJMockit.
Se você acha que já conhece o básico da zombaria, talvez possa pular para o Ponto 2 sem ler os próximos três pontos.
1.2. Razões para usar simulações
Vamos começar supondo que você já codifica seguindo alguma metodologia de desenvolvimento direcionada centrada em testes (TDD, ATDD ou BDD). Ou simplesmente que você deseja criar um teste para uma classe existente que depende de dependências para alcançar sua funcionalidade.
Em qualquer caso, ao testar a unidade de uma classe, queremostest only its functionality and not that of its dependencies (porque confiamos em sua implementação ou porque iremos testá-la nós mesmos).
Para conseguir isso, precisamos fornecer ao objeto em teste, uma substituição que possamos controlar para essa dependência. Dessa forma, podemos forçar valores de retorno extremos, lançar exceções ou simplesmente reduzir métodos demorados para um valor de retorno fixo.
Essa substituição controlada é omock e ajudará você a simplificar a codificação do teste e reduzir o tempo de execução do teste.
1.3. Conceitos e definições simulados
Vamos ver quatro definições de umarticle escrito por Martin Fowler que resume o básico que todos devem saber sobre mocks:
-
ObjetosDummy são passados, mas nunca realmente usados. Geralmente, eles são usados apenas para preencher listas de parâmetros.
-
ObjetosFake têm implementações de trabalho, mas geralmente usam algum atalho que os torna inadequados para produção (um banco de dados na memória é um bom exemplo).
-
Stubs fornece respostas automáticas para chamadas feitas durante o teste, geralmente não respondendo a nada fora do que está programado para o teste. Os stubs também podem registrar informações sobre chamadas, como um stub de gateway de e-mail que lembra as mensagens que ele 'enviou', ou talvez apenas quantas mensagens ele 'enviou'.
-
Mocks é o que estamos falando aqui: objetos pré-programados com expectativas que formam uma especificação das chamadas que eles devem receber.
1.4 To Mock or Not to Mock: That Is the Question
Not everything must be mocked. Às vezes, é melhor fazer um teste de integração, já que simular esse método / recurso funcionaria apenas para obter poucos benefícios reais. Em nosso caso de teste (que será mostrado no próximo ponto), isso estaria testando oLoginDao.
OLoginDao usaria alguma biblioteca de terceiros para acesso ao banco de dados, e simular isso consistiria apenas em garantir que os parâmetros foram preparados para a chamada, mas ainda precisaríamos testar se a chamada retorna os dados que desejamos.
Por esse motivo, ele não será incluído neste exemplo (embora pudéssemos escrever o teste de unidade com chamadas simuladas para as chamadas da biblioteca de terceiros E um teste de integração com DBUnit para testar o desempenho real da biblioteca de terceiros).
2. Caso de teste
Com tudo na seção anterior em mente, vamos propor um caso de teste bastante típico e como iremos testá-lo usando mocks (quando fizer sentido usar mocks). Isso nos ajudará a ter um cenário comum para, posteriormente, poder comparar as diferentes bibliotecas de simulação.
2.1 Proposed Case
O caso de teste proposto será o processo de login em um aplicativo com uma arquitetura em camadas.
A solicitação de logon será tratada por um controlador, que usa um serviço, que usa um DAO (que procura credenciais de usuário em um banco de dados). Não nos aprofundaremos muito na implementação de cada camada e nos concentraremos mais nosinteractions between the components de cada camada.
Dessa forma, teremos umLoginController, umLoginServicee umLoginDAO. Vamos ver um diagrama para esclarecimento:
2.2 Implementation
Seguiremos agora com a implementação usada para o caso de teste, para que possamos entender o que está acontecendo (ou o que deve acontecer) nos testes.
Começaremos com o modelo usado para todas as operações,UserForm, que conterá apenas o nome do usuário e a senha (estamos usando modificadores de acesso público para simplificar) e um método getter para o campousername para permitir a simulação dessa propriedade:
public class UserForm {
public String password;
public String username;
public String getUsername(){
return username;
}
}
Vamos seguir comLoginDAO, que não terá funcionalidade, pois só queremos que seus métodos estejam lá para que possamos zombá-los quando necessário:
public class LoginDao {
public int login(UserForm userForm){
return 0;
}
}
LoginDao será usado porLoginService em seu métodologin. LoginService também terá um métodosetCurrentUser que retornavoid para testar essa simulação.
public class LoginService {
private LoginDao loginDao;
private String currentUser;
public boolean login(UserForm userForm) {
assert null != userForm;
int loginResults = loginDao.login(userForm);
switch (loginResults){
case 1:
return true;
default:
return false;
}
}
public void setCurrentUser(String username) {
if(null != username){
this.currentUser = username;
}
}
}
Finalmente,LoginController usaráLoginService para seu métodologin. Isso incluirá:
-
um caso em que nenhuma chamada para o serviço simulado será feita.
-
um caso em que apenas um método será chamado.
-
um caso em que todos os métodos serão chamados.
-
um caso em que o lançamento de exceção será testado.
public class LoginController {
public LoginService loginService;
public String login(UserForm userForm){
if(null == userForm){
return "ERROR";
}else{
boolean logged;
try {
logged = loginService.login(userForm);
} catch (Exception e) {
return "ERROR";
}
if(logged){
loginService.setCurrentUser(userForm.getUsername());
return "OK";
}else{
return "KO";
}
}
}
}
Agora que vimos o que estamos tentando testar, vamos ver como vamos simular com cada biblioteca.
3. Configuração de teste
3.1 Mockito
Para o Mockito, usaremos a versão2.8.9.
A maneira mais fácil de criar e usar mocks é por meio das anotações@Mocke@InjectMocks. O primeiro criará um mock para a classe usada para definir o campo e o segundo tentará injetar as simulações criadas no mock anotado.
Existem mais anotações, como@Spy, que permite criar uma simulação parcial (uma simulação que usa a implementação normal em métodos não simulados).
Dito isso, você precisa chamarMockitoAnnotations.initMocks(this) antes de executar qualquer teste que usaria os ditos mocks para que toda essa “mágica” funcione. Isso geralmente é feito em um método anotado@Before. Você também pode usar oMockitoJUnitRunner.
public class LoginControllerTest {
@Mock
private LoginDao loginDao;
@Spy
@InjectMocks
private LoginService spiedLoginService;
@Mock
private LoginService loginService;
@InjectMocks
private LoginController loginController;
@Before
public void setUp() {
loginController = new LoginController();
MockitoAnnotations.initMocks(this);
}
}
3.2 EasyMock
Para EasyMock, usaremos a versão3.4 (Javadoc). Observe que, com o EasyMock, para que os mocks comecem a “funcionar”, você deve chamarEasyMock.replay(mock) em cada método de teste ou receberá uma exceção.
Mocks e classes testadas também podem ser definidas por meio de anotações, mas, neste caso, em vez de chamar um método estático para que funcione, usaremosEasyMockRunner para a classe de teste.
Mocks são criados com a anotação@Mock e o objeto testado com@TestSubject (que terá suas dependências injetadas a partir de mocks criados). O objeto testado deve ser criado em linha.
@RunWith(EasyMockRunner.class)
public class LoginControllerTest {
@Mock
private LoginDao loginDao;
@Mock
private LoginService loginService;
@TestSubject
private LoginController loginController = new LoginController();
}
3.3. JMockit
Para o JMockit, usaremos a versão1.24 (Javadoc), pois a versão 1.25 ainda não foi lançada (pelo menos enquanto escrevemos isso).
A configuração do JMockit é tão fácil quanto com o Mockito, com a exceção de que não há nenhuma anotação específica para simulações parciais (e realmente não há necessidade) e que você deve usarJMockit como executor de teste.
Mocks são definidos usando a anotação@Injectable (que criará apenas uma instância mock) ou com a anotação@Mocked (que criará mocks para cada instância da classe do campo anotado).
A instância testada é criada (e suas dependências simuladas injetadas) usando a anotação@Tested.
@RunWith(JMockit.class)
public class LoginControllerTest {
@Injectable
private LoginDao loginDao;
@Injectable
private LoginService loginService;
@Tested
private LoginController loginController;
}
4. Verificando nenhuma chamada para simulação
4.1. Mockito
Para verificar se um mock recebeu nenhuma chamada no Mockito, você tem o métodoverifyZeroInteractions() que aceita um mock.
@Test
public void assertThatNoMethodHasBeenCalled() {
loginController.login(null);
Mockito.verifyZeroInteractions(loginService);
}
4.2. EasyMock
Para verificar se uma simulação não recebeu chamadas, você simplesmente não especifica o comportamento, reproduz a simulação e, por último, verifica.
@Test
public void assertThatNoMethodHasBeenCalled() {
EasyMock.replay(loginService);
loginController.login(null);
EasyMock.verify(loginService);
}
4.3. JMockit
Para verificar se um mock recebeu nenhuma chamada, você simplesmente não especifica as expectativas para esse mock e faz umFullVerifications(mock) para esse mock.
@Test
public void assertThatNoMethodHasBeenCalled() {
loginController.login(null);
new FullVerifications(loginService) {};
}
5. Definindo chamadas de métodos simulados e verificando chamadas para simulações
5.1. Mockito
Paramocking method calls, você pode usarMockito.when(mock.method(args)).thenReturn(value). Aqui você pode retornar valores diferentes para mais de uma chamada, apenas adicionando-os como mais parâmetros:thenReturn(value1, value2, value-n, …).
Observe que você não pode simular métodos de retorno nulo com esta sintaxe. Nesses casos, você usará uma verificação do referido método (conforme mostrado na linha 11).
Paraverifying calls para um mock, você pode usarMockito.verify(mock).method(args) e também pode verificar se nenhuma chamada foi feita para um mock usandoverifyNoMoreInteractions(mock).
Paraverifying args, você pode passar valores específicos ou usar correspondências predefinidas comoany(),anyString(),anyInt().. Existem muito mais desse tipo de correspondências e até mesmo a possibilidade de definir seus matchers, que veremos nos exemplos a seguir.
@Test
public void assertTwoMethodsHaveBeenCalled() {
UserForm userForm = new UserForm();
userForm.username = "foo";
Mockito.when(loginService.login(userForm)).thenReturn(true);
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
Mockito.verify(loginService).login(userForm);
Mockito.verify(loginService).setCurrentUser("foo");
}
@Test
public void assertOnlyOneMethodHasBeenCalled() {
UserForm userForm = new UserForm();
userForm.username = "foo";
Mockito.when(loginService.login(userForm)).thenReturn(false);
String login = loginController.login(userForm);
Assert.assertEquals("KO", login);
Mockito.verify(loginService).login(userForm);
Mockito.verifyNoMoreInteractions(loginService);
}
5.2. EasyMock
Paramocking method calls, você usaEasyMock.expect(mock.method(args)).andReturn(value).
Paraverifying calls para um mock, você pode usarEasyMock.verify(mock), mas deve chamá-lo dealways after chamandoEasyMock.replay(mock).
Paraverifying args, você pode passar valores específicos ou ter correspondências predefinidas como isA(Class.class),anyString(),anyInt() e alot more desse tipo de correspondência e novamente a possibilidade de definir seus matchers.
@Test
public void assertTwoMethodsHaveBeenCalled() {
UserForm userForm = new UserForm();
userForm.username = "foo";
EasyMock.expect(loginService.login(userForm)).andReturn(true);
loginService.setCurrentUser("foo");
EasyMock.replay(loginService);
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
EasyMock.verify(loginService);
}
@Test
public void assertOnlyOneMethodHasBeenCalled() {
UserForm userForm = new UserForm();
userForm.username = "foo";
EasyMock.expect(loginService.login(userForm)).andReturn(false);
EasyMock.replay(loginService);
String login = loginController.login(userForm);
Assert.assertEquals("KO", login);
EasyMock.verify(loginService);
}
5.3. JMockit
Com o JMockit, você definiusteps para teste: gravar, reproduzir e verificar.
Record é feito em um novo blocoExpectations()\{\{}} (no qual você pode definir ações para vários mocks),replay é feito simplesmente invocando um método da classe testada (que deve chamar alguns simulados objeto), everification é feito dentro de um novo blocoVerifications()\{\{}} (no qual você pode definir verificações para vários mocks).
Paramocking method calls, você pode usarmock.method(args); result = value; dentro de qualquer blocoExpectations. Aqui você pode retornar valores diferentes para mais de uma chamada apenas usandoreturns(value1, value2, …, valuen); em vez deresult = value;.
Paraverifying calls para uma simulação, você pode usar as novas Verificações()\{\{mock.call(value)}} ounew Verifications(mock)\{\{}} para verificar todas as chamadas esperadas definidas anteriormente.
Paraverifying args, você pode passar valores específicos, ou você tempredefined values comoany,anyString,anyLong, e muito mais desse tipo de valores especiais e novamente a possibilidade de definir seus matchers (que devem ser matchers Hamcrest).
@Test
public void assertTwoMethodsHaveBeenCalled() {
UserForm userForm = new UserForm();
userForm.username = "foo";
new Expectations() {{
loginService.login(userForm); result = true;
loginService.setCurrentUser("foo");
}};
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
new FullVerifications(loginService) {};
}
@Test
public void assertOnlyOneMethodHasBeenCalled() {
UserForm userForm = new UserForm();
userForm.username = "foo";
new Expectations() {{
loginService.login(userForm); result = false;
// no expectation for setCurrentUser
}};
String login = loginController.login(userForm);
Assert.assertEquals("KO", login);
new FullVerifications(loginService) {};
}
6. Zombar de exceções
6.1. Mockito
O lançamento de exceções pode ser simulado usando.thenThrow(ExceptionClass.class) apósMockito.when(mock.method(args)).
@Test
public void mockExceptionThrowin() {
UserForm userForm = new UserForm();
Mockito.when(loginService.login(userForm)).thenThrow(IllegalArgumentException.class);
String login = loginController.login(userForm);
Assert.assertEquals("ERROR", login);
Mockito.verify(loginService).login(userForm);
Mockito.verifyZeroInteractions(loginService);
}
6.2. EasyMock
O lançamento de exceções pode ser simulado usando.andThrow(new ExceptionClass()) após uma chamadaEasyMock.expect(…).
@Test
public void mockExceptionThrowing() {
UserForm userForm = new UserForm();
EasyMock.expect(loginService.login(userForm)).andThrow(new IllegalArgumentException());
EasyMock.replay(loginService);
String login = loginController.login(userForm);
Assert.assertEquals("ERROR", login);
EasyMock.verify(loginService);
}
6.3. JMockit
A exceção de zombaria lançada com o JMockito é especialmente fácil. Apenas retorne uma exceção como resultado de uma chamada de método simulada em vez do retorno "normal".
@Test
public void mockExceptionThrowing() {
UserForm userForm = new UserForm();
new Expectations() {{
loginService.login(userForm); result = new IllegalArgumentException();
// no expectation for setCurrentUser
}};
String login = loginController.login(userForm);
Assert.assertEquals("ERROR", login);
new FullVerifications(loginService) {};
}
7. Zombando de um objeto para passar ao redor
7.1. Mockito
Você também pode criar um mock para passar como argumento para uma chamada de método. Com o Mockito, você pode fazer isso com uma única linha.
@Test
public void mockAnObjectToPassAround() {
UserForm userForm = Mockito.when(Mockito.mock(UserForm.class).getUsername())
.thenReturn("foo").getMock();
Mockito.when(loginService.login(userForm)).thenReturn(true);
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
Mockito.verify(loginService).login(userForm);
Mockito.verify(loginService).setCurrentUser("foo");
}
7.2. EasyMock
Mocks podem ser criados em linha comEasyMock.mock(Class.class). Posteriormente, você pode usarEasyMock.expect(mock.method()) para prepará-lo para execução, sempre lembrando de chamarEasyMock.replay(mock) antes de usá-lo.
@Test
public void mockAnObjectToPassAround() {
UserForm userForm = EasyMock.mock(UserForm.class);
EasyMock.expect(userForm.getUsername()).andReturn("foo");
EasyMock.expect(loginService.login(userForm)).andReturn(true);
loginService.setCurrentUser("foo");
EasyMock.replay(userForm);
EasyMock.replay(loginService);
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
EasyMock.verify(userForm);
EasyMock.verify(loginService);
}
7.3. JMockit
Para simular um objeto para apenas um método, você pode simplesmente passá-lo como um parâmetro ao método de teste. Em seguida, você pode criar expectativas como em qualquer outra simulação.
@Test
public void mockAnObjectToPassAround(@Mocked UserForm userForm) {
new Expectations() {{
userForm.getUsername(); result = "foo";
loginService.login(userForm); result = true;
loginService.setCurrentUser("foo");
}};
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
new FullVerifications(loginService) {};
new FullVerifications(userForm) {};
}
8. Correspondência de argumento personalizado
8.1. Mockito
Às vezes, a correspondência de argumentos para chamadas simuladas precisa ser um pouco mais complexa do que apenas um valor fixo ouanyString(). Para aqueles casos com Mockito tem sua classe matcher que é usada comargThat(ArgumentMatcher<>).
@Test
public void argumentMatching() {
UserForm userForm = new UserForm();
userForm.username = "foo";
// default matcher
Mockito.when(loginService.login(Mockito.any(UserForm.class))).thenReturn(true);
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
Mockito.verify(loginService).login(userForm);
// complex matcher
Mockito.verify(loginService).setCurrentUser(ArgumentMatchers.argThat(
new ArgumentMatcher() {
@Override
public boolean matches(String argument) {
return argument.startsWith("foo");
}
}
));
}
8.2. EasyMock
A correspondência de argumento customizada é um pouco mais complicada com EasyMock, pois você precisa criar um método estático no qual você cria o matcher real e então o relata comEasyMock.reportMatcher(IArgumentMatcher).
Depois que esse método é criado, você o usa em sua expectativa simulada com uma chamada para o método (como visto no exemplo na linha).
@Test
public void argumentMatching() {
UserForm userForm = new UserForm();
userForm.username = "foo";
// default matcher
EasyMock.expect(loginService.login(EasyMock.isA(UserForm.class))).andReturn(true);
// complex matcher
loginService.setCurrentUser(specificArgumentMatching("foo"));
EasyMock.replay(loginService);
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
EasyMock.verify(loginService);
}
private static String specificArgumentMatching(String expected) {
EasyMock.reportMatcher(new IArgumentMatcher() {
@Override
public boolean matches(Object argument) {
return argument instanceof String
&& ((String) argument).startsWith(expected);
}
@Override
public void appendTo(StringBuffer buffer) {
//NOOP
}
});
return null;
}
8.3. JMockit
A correspondência de argumento personalizado com JMockit é feita com o método especialwithArgThat(Matcher) (que recebe objetosHamcrest'sMatcher).
@Test
public void argumentMatching() {
UserForm userForm = new UserForm();
userForm.username = "foo";
// default matcher
new Expectations() {{
loginService.login((UserForm) any);
result = true;
// complex matcher
loginService.setCurrentUser(withArgThat(new BaseMatcher() {
@Override
public boolean matches(Object item) {
return item instanceof String && ((String) item).startsWith("foo");
}
@Override
public void describeTo(Description description) {
//NOOP
}
}));
}};
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
new FullVerifications(loginService) {};
}
9. Zombaria parcial
9.1. Mockito
O Mockito permite zombaria parcial (uma zombaria que usa a implementação real em vez de chamadas de método zombadas em alguns de seus métodos) de duas maneiras.
Você pode usar.thenCallRealMethod() em uma definição de chamada de método simulado normal ou pode criar umspy em vez de um simulado, caso em que o comportamento padrão para isso será chamar a implementação real em todos os métodos simulados.
@Test
public void partialMocking() {
// use partial mock
loginController.loginService = spiedLoginService;
UserForm userForm = new UserForm();
userForm.username = "foo";
// let service's login use implementation so let's mock DAO call
Mockito.when(loginDao.login(userForm)).thenReturn(1);
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
// verify mocked call
Mockito.verify(spiedLoginService).setCurrentUser("foo");
}
9.2. EasyMock
A simulação parcial também se torna um pouco mais complicada com o EasyMock, pois você precisa definir quais métodos serão simulados ao criar a simulação.
Isso é feito comEasyMock.partialMockBuilder(Class.class).addMockedMethod(“methodName”).createMock(). Feito isso, você pode usar o mock como qualquer outro mock não parcial.
@Test
public void partialMocking() {
UserForm userForm = new UserForm();
userForm.username = "foo";
// use partial mock
LoginService loginServicePartial = EasyMock.partialMockBuilder(LoginService.class)
.addMockedMethod("setCurrentUser").createMock();
loginServicePartial.setCurrentUser("foo");
// let service's login use implementation so let's mock DAO call
EasyMock.expect(loginDao.login(userForm)).andReturn(1);
loginServicePartial.setLoginDao(loginDao);
loginController.loginService = loginServicePartial;
EasyMock.replay(loginDao);
EasyMock.replay(loginServicePartial);
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
// verify mocked call
EasyMock.verify(loginServicePartial);
EasyMock.verify(loginDao);
}
9.3. JMockit
A zombaria parcial com o JMockit é especialmente fácil. Cada chamada de método para a qual nenhum comportamento simulado foi definido em umExpectations()\{\{}} usa a implementação “real”.
Neste caso, como nenhuma expectativa é fornecida paraLoginService.login(UserForm), a implementação real (e a chamada paraLoginDAO.login(UserForm)) é executada.
@Test
public void partialMocking() {
// use partial mock
LoginService partialLoginService = new LoginService();
partialLoginService.setLoginDao(loginDao);
loginController.loginService = partialLoginService;
UserForm userForm = new UserForm();
userForm.username = "foo";
// let service's login use implementation so let's mock DAO call
new Expectations() {{
loginDao.login(userForm); result = 1;
// no expectation for loginService.login
partialLoginService.setCurrentUser("foo");
}};
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
// verify mocked call
new FullVerifications(partialLoginService) {};
new FullVerifications(loginDao) {};
}
10. Conclusão
Neste post, comparamos três bibliotecas simuladas Java, cada uma com seus pontos fortes e suas desvantagens.
-
Todos os três sãoeasily configured com anotações para ajudá-lo a definir simulações e o objeto em teste, com corredores para tornar a injeção de simulação o mais indolor possível.
-
Diríamos que o Mockito venceria aqui, pois tem uma anotação especial para simulações parciais, mas o JMockit nem precisa disso, então digamos que haja um empate entre os dois.
-
-
Todos os três seguem mais ou menos orecord-replay-verify pattern, mas em nossa opinião, o melhor para fazer isso é JMockit, pois força você a usá-los em blocos, então os testes ficam mais estruturados.
-
Easiness de uso é importante para que você possa trabalhar o menos possível para definir seus testes. O JMockit será a opção escolhida para sua estrutura fixa sempre a mesma.
-
Mockito é mais ou menos O MAIS conhecido entãocommunity será maior.
-
Ter que chamarreplay toda vez que você quiser usar um mock é umno-go claro, então vamos colocar um menos para EasyMock.
-
Consistency/simplicity também é importante para mim. Adoramos a maneira de retornar resultados do JMockit que são iguais para resultados "normais" e para exceções.
Com tudo isso dito, vamos escolherJMockit como uma espécie de vencedor, embora até agora tenhamos usadoMockito porque fomos cativados por sua simplicidade e estrutura fixa e tentarei usá-lo de agora em diante.
Ofull implementation deste tutorial pode ser encontrado emthe GitHub project, então fique à vontade para baixá-lo e brincar com ele.