Atualmente podemos dizer que o Spring Framework é a escolha de praticamente todos os desenvolvedores Java que estão iniciando em um novo projeto, e não é pra menos pois esse framework conseguiu reunir em um lugar só toda a facilidade para desenvolvimento de aplicações Web com sua criação de serviços REST, acesso a dados, gerenciamento de filas e acesso a outros serviços, tornando-se um padrão entre seus desenvolvedores.
E além de tudo que o Spring oferece, algo que existe no framework desde sua criação e que ajuda e muito os desenvolvedores é a sua facilidade de trabalhar com injeção de dependências, pois ele possui um container de inversão de controle que permite facilmente injetar dependências em uma classe usando a famosa anotação mágica @Autowired
.
1
2
3
4
5
6
7
8
9
public class MyClass {
@Autowired
private MyDependency myDependency;
public void foo() {
myDependency.bar();
}
}
Porém, junto com essa facilidade vem um problema que trava muitos desenvolvedores no momento de criar testes unitários:
Como criar um mock de uma classe injetada por @Autowired.
Realmente, como é o próprio Spring quem faz a injeção, fica complicado de fazer esse mock, pois não é legal depender do Framework para fazer os testes unitários, já que em princípio um teste unitário deve depender somente da classe/arquivo que está sendo testado.
E para resolver esse problema é claro que temos alternativas, que vou apresentar a seguir usando como exemplo um projeto de teste encontrado por completo aqui.
Este projeto é uma aplicação web SpringBoot simples que a partir de seu nome e data de nascimento exibe uma mensagem dizendo quando será seu próximo aniversário. Nos testes iremos focar na classe BirthdayService
que calcula quando será a próxima data de aniversário e constrói a mensagem, e na classe LocalDateService
injetada pelo Spring e responsável por prover a data atual.
O projeto está usando o JUnit 5 que já faz parte da configuração inicial do SpringBoot.
Observação: Se você não sabe o que é um mock aconselho e ler primeiro esse meu post .
Injetando no construtor ou setter
Tudo bem, eu acredito que essa não seja a alternativa que você esteja procurando, mas ela funciona muito bem!
Eu sei que a maioria das pessoas usa o @Autowired
direto na declaração da dependência para facilitar a escrita do código, como podemos ver a seguir.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Service
public class BirthdayService {
public static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd/MM/yyyy");
@Autowired
private LocalDateService localDateService;
public String getBirthdayMessage(String name, LocalDate bornDay) {
var nextBirthDay = getNextBirthday(bornDay);
var message = new StringBuilder()
.append("Olá ")
.append(name)
.append(", seu próximo aniversário será no dia ")
.append(DATE_FORMAT.format(nextBirthDay))
.append("!");
return message.toString();
}
private LocalDate getNextBirthday(LocalDate bornDay) {
var currentDate = localDateService.getCurrentDate();
var birthday = LocalDate.of(currentDate.getYear(), bornDay.getMonth(), bornDay.getDayOfMonth());
if (currentDate.isAfter(birthday)) {
birthday = birthday.plusYears(1);
}
return birthday;
}
}
Mas é exatamente essa forma de escrita que causa o todo o problema de utilizar um mock nos testes unitários, o que pode ser resolvido passando a dependência via construtor ou via setter
, como vemos abaixo.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class BirthdayService {
public static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd/MM/yyyy");
private LocalDateService localDateService;
@Autowired
public void setLocalDateService(LocalDateService localDateService) {
this.localDateService = localDateService;
}
public String getBirthdayMessage(String name, LocalDate bornDay) {
//...
}
}
Usando o @Autowired
no método de setter
da dependência irá fazer com que o Spring consiga realizar a injeção normalmente, e nos permite escrever o teste passando o mock por parâmetro para a classe testada.
Neste exemplo vou usar o Mockito como biblioteca de mock.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.time.LocalDate;
import org.junit.jupiter.api.Test;
public class BirthdayServiceTest {
@Test
public void shouldReturnBirthdayMessage() {
var name = "Murilo";
var bornDay = LocalDate.of(1989, 7, 10);
LocalDateService localDateServiceMock = mock(LocalDateService.class);
when(localDateServiceMock.getCurrentDate()).thenReturn(LocalDate.of(2021, 3, 15));
BirthdayServiceAlternative birthdayService = new BirthdayServiceAlternative();
birthdayService.setLocalDateService(localDateServiceMock); //atribuindo o mock
var message = birthdayService.getBirthdayMessage(name, bornDay);
assertEquals(message, "Olá Murilo, seu próximo aniversário será no dia 10/07/2021!");
}
}
E assim resolvemos de maneira rápida e fácil o problema de mock.
Usando o Mockito extension
Mas eu sei que a realidade de muitos projetos é bem diferente e a abordagem mais utilizada é adicionar o @Autowired
diretamente no atributo, por isso temos que recorrer a outros meios para conseguir prover um mock.
1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class BirthdayService {
public static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd/MM/yyyy");
@Autowired
private LocalDateService localDateService;
public String getBirthdayMessage(String name, LocalDate bornDay) {
//...
}
}
Pensando nisso algumas bibliotecas de mock começaram a adicionar recursos extras permitindo que o teste fosse executado utilizando mais recursos dessas bibliotecas, geralmente usando a anotação @RunWith
, fazendo com que essa injeção de mock acontecesse de forma transparente, e partir do JUnit 5 esse tipo de recurso foi incorporado ao framework usando o nome de extensions, que agora permitem fazer isso de forma super simples.
Aqui vamos usar a extension provida pelo Mockito 1.10.19, e vamos anotar o nosso mock com a anotação @Mock
e nossa classe a ser testado com o @InjectMocks
, que irá injetar o mock.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@ExtendWith(MockitoExtension.class)
public class BirthdayServiceWithMockitoExtensionTest {
@Mock
private LocalDateService localDateServiceMock;
@InjectMocks
private BirthdayService birthdayService;
@Test
public void shouldReturnBirthdayMessage() {
var name = "Murilo";
var bornDay = LocalDate.of(1989, 7, 10);
when(localDateServiceMock.getCurrentDate()).thenReturn(LocalDate.of(2021, 3, 15));
var message = birthdayService.getBirthdayMessage(name, bornDay);
assertEquals("Olá Murilo, seu próximo aniversário será no dia 10/07/2021!", message);
}
}
E pronto. Muito simples não é mesmo?
E não é somente o Mockito que provê uma extension.
Usando o EasyMock extension
Além do Mockito, o EasyMock é uma das libs de mock mais utilizadas em Java, e também provê o mecanismo de extension para injeção de mocks.
A forma de utilizar é praticamente idêntica ao Mockito, com a diferença que a classe a ser testada recebe a anotação @TestSubject
para que sejam injetadas as dependências, e é claro que o EasyMock possui métodos diferentes para declaração dos comportamentos esperados.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.replay;
import org.easymock.EasyMockExtension;
import org.easymock.Mock;
import org.easymock.TestSubject;
//... outros imports
@ExtendWith(EasyMockExtension.class)
public class BirthdayServiceWithEasyMockExtensionTest {
@Mock
private LocalDateService localDateServiceMock;
@TestSubject
private BirthdayService birthdayService;
@Test
public void shouldReturnBirthdayMessage() {
var name = "Murilo";
var bornDay = LocalDate.of(1989, 7, 10);
expect(localDateServiceMock.getCurrentDate()).andReturn(LocalDate.of(2021, 3, 15));
replay(localDateServiceMock);
var message = birthdayService.getBirthdayMessage(name, bornDay);
assertEquals("Olá Murilo, seu próximo aniversário será no dia 10/07/2021!", message);
}
}
Muito fácil não é mesmo?
Neste exemplo usamos a versão 4.2 do EasyMock.
Usando o Spring Framework
Mas e se estivermos usando versões antigas do JUnit, Mockito ou EasyMock, onde essas possibilidades não existem, como poderemos usar esses mocks?
Talvez foi pensando nisso que o Spring passou a prover uma forma de também injetar os mocks em nossos testes unitários, que não é tão prática como as mostradas anteriormente, porém também funciona muito bem.
O Spring disponibiliza desde muito tempo a classe utilitária ReflectionTestUtils
que permite injetarmos os mocks usando reflections, o que com um pouco mais de trabalho também resolve o problema.
Nesse caso nós criamos uma instância da classe a ser testada e injetamos os mocks utilizando o método setField
da classe ReflectionTestUtils
, passando como parâmetro o objeto onde será injetada as dependências, o nome do atributo dentro da classe testada e o mock.
Vou usar o Mockito novamente como lib de mock aqui.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import org.springframework.test.util.ReflectionTestUtils;
public class BirthdayServiceWithReflectionTestUtilsTest {
private LocalDateService localDateServiceMock;
@BeforeEach
public void setUp() {
localDateServiceMock = mock(LocalDateService.class);
}
@Test
public void shouldReturnBirthdayMessage() {
var name = "Murilo";
var bornDay = LocalDate.of(1989, 7, 10);
when(localDateServiceMock.getCurrentDate()).thenReturn(LocalDate.of(2021, 3, 15));
BirthdayService birthdayService = buildBirthdayService();
var message = birthdayService.getBirthdayMessage(name, bornDay);
assertEquals("Olá Murilo, seu próximo aniversário será no dia 10/07/2021!", message);
}
private BirthdayService buildBirthdayService() {
BirthdayService birthdayService = new BirthdayService();
ReflectionTestUtils.setField(birthdayService, "localDateService", localDateServiceMock);
return birthdayService;
}
}
Perceba que foi necessário alguns códigos a mais para inicializar o objeto a ser testado com as dependências e também criar o mock toda vez que um teste for executado (@BeforeEach
), mas assim conseguimos executar nosso teste unitário normalmente.
E com essa última dica eu encerro esse post na esperança que te ajude em suas próximas escritas de testes unitários, e caso tenha alguma dúvida ou sugestão por favor deixe nos comentários.
Projeto completo no Github.
Até mais.