A todos os destemidos no caminho da negação à convicção é dedicado ...

Há uma opinião justa entre os desenvolvedores de que se um programador não cobre o código com testes, ele simplesmente não entende por que eles são necessários e como prepará-los. É difícil discordar disso quando você já entende do que se trata. Mas como esse entendimento precioso pode ser alcançado?
Não é pra ser ...
Acontece que muitas vezes as coisas mais óbvias não têm uma descrição clara entre as toneladas de informações úteis na rede global. Uma espécie de candidato regular decide lidar com a questão urgente "o que são testes de unidade" e se depara com muitos desses exemplos, que são copiados de um artigo para outro como um papel vegetal:
“Temos um método que calcula a soma dos números”
public Integer sum (Integer a, Integer b) {
return a + b
}
“você pode escrever um teste para este método”
Teste
public void testGoodOne () {
assertThat (sum (2,2), is (4));
}
Isso não é uma piada, é um exemplo simplificado de um artigo típico sobre tecnologia de teste de unidade, onde no início e no final há frases gerais sobre benefícios e necessidade, e no meio é ...
Vendo isso e relendo duas vezes por causa da fé, o candidato exclama: "Que ilusão cruel ? .. ”Afinal, em seu código praticamente não existem métodos que obtêm tudo o que precisam por meio de argumentos e, em seguida, fornecem um resultado inequívoco para eles. Esses são métodos utilitários típicos e dificilmente mudam. Mas e quanto a procedimentos complexos, dependências injetadas e métodos sem retornar valores? Lá, essa abordagem não é aplicável a partir da palavra “de forma alguma”.
Se nesta fase o candidato teimoso não acena com a mão e mergulha mais longe, ele logo descobre que os MOCs são usados para dependências, para cujos métodos algum comportamento condicional é definido, na verdade um stub. Aqui, o candidato pode explodir completamente, se não houver um meio / sênior amável e paciente que esteja pronto e capaz de explicar tudo ... Caso contrário, o candidato à verdade perde completamente o significado de "o que são os testes de unidade", já que a maior parte do método testado acaba sendo algum tipo de simulação de ficção , e o que está sendo testado neste caso não está claro. Além disso, não está claro como organizar isso para um aplicativo grande com várias camadas e por que isso é necessário. Assim, na melhor das hipóteses, a questão é adiada para tempos melhores, na pior - ela se esconde em uma caixa de coisas malditas.
O mais ofensivo é que a tecnologia de cobertura de teste é elementar, simples e acessível a todos, e seus benefícios são tão óbvios que qualquer desculpa parece ingênua para pessoas experientes. Mas, para descobrir, um iniciante carece de uma essência elementar muito pequena, como o toque de um botão.
Missão chave
Para começar, proponho formular em poucas palavras a função chave (missão) dos testes de unidade e o ganho chave. Existem várias opções pitorescas aqui, mas proponho considerar esta: A
função-chave dos testes de unidade é capturar o comportamento esperado do sistema.
e este: O
principal benefício dos testes de unidade é a capacidade de "executar" todas as funcionalidades do aplicativo em questão de segundos.
Recomendo lembrar disso nas entrevistas e irei explicar um pouco. Qualquer funcionalidade implica regras de uso e resultados. Esses requisitos vêm da empresa, por meio da análise de sistemas, e são implementados em código. Mas o código está em constante evolução, novos requisitos e melhorias surgem, que podem mudar algo imperceptível e inesperadamente na funcionalidade finalizada. É aqui que os testes de unidade ficam de guarda, que fixam as regras aprovadas de acordo com as quais o sistema deve funcionar! Os testes registram um cenário que é importante para o negócio, e se após a próxima revisão o teste falhar, então algo está faltando: o desenvolvedor ou o analista se enganou, ou os novos requisitos contradizem os existentes e devem ser esclarecidos, etc. O mais importante é que a “surpresa” não escapou.
Um teste de unidade simples e padrão tornou possível detectar o comportamento inesperado e provavelmente indesejável do sistema desde o início. Enquanto isso, o sistema cresce e se expande, a probabilidade de perder seus detalhes também aumenta e apenas os scripts de teste de unidade são capazes de lembrar de tudo e evitar desvios imperceptíveis no tempo. É muito conveniente e confiável, e a principal comodidade é a velocidade. O aplicativo nem precisa iniciar e vagar por centenas de seus campos, formulários ou botões, você precisa executar testes e obter prontidão total ou um bug em questão de segundos.
Portanto, vamos lembrar: conserte o comportamento esperado na forma de scripts de teste de unidade e "execute" instantaneamente o aplicativo sem iniciá-lo. Este é o valor absoluto que os testes de unidade podem atingir.
Mas, droga, como?
Vamos passar para a parte divertida. Os aplicativos modernos estão se livrando ativamente da monoliticidade. Microsserviços, módulos, “camadas” são os princípios básicos de organização do código de trabalho, permitindo obter independência, facilidade de reutilização, troca e transferência para sistemas, etc. Camada e injeção de dependência são fundamentais em nosso tópico.
Considere as camadas de uma aplicação web típica: controladores, serviços, repositórios, etc. Além disso, utilitários, fachadas, modelos e camadas DTO são usados. Os dois últimos não devem conter funcionalidade, ou seja, métodos diferentes de acessadores (getters / setters), então você não precisa cobri-los com testes. Vamos considerar o resto das camadas como alvos para cobertura.
Por mais saborosa que seja essa comparação, o aplicativo não pode ser comparado a um bolo folhado porque essas camadas estão embutidas umas nas outras, como dependências:
- o controlador implementa o (s) serviço (s), que chama para o resultado
- o serviço injeta repositórios (DAO) em si mesmo, pode injetar componentes de utilitário
- a fachada é projetada para combinar o trabalho de muitos serviços ou componentes, respectivamente, ela os implementa
A ideia principal de testar tudo isso em todo o aplicativo: cobrir cada camada independentemente das outras camadas. Uma referência à independência e outras características antimonolíticas. Essa. se um repositório é introduzido no serviço em teste, este “convidado” é simulado como parte do teste do serviço, mas é testado pessoalmente honestamente como parte do teste do repositório. Assim, são criados testes para cada elemento de cada camada, ninguém é esquecido - tudo está nos negócios.
Princípio de massa folhada
Vamos passar para os exemplos, um aplicativo Java Spring Boot simples, o código será elementar, então a essência é fácil de entender e aplicável de forma semelhante a outras linguagens / estruturas modernas. O aplicativo terá uma tarefa simples - multiplique o número por 3, ou seja, triplo, mas ao mesmo tempo criaremos um aplicativo em várias camadas com injeção de dependência e cobertura em camadas da cabeça aos pés.

A estrutura contém pacotes para três camadas: controlador, serviço, repo. A estrutura dos testes é semelhante.
O aplicativo funcionará assim:
- do front-end, uma solicitação GET chega ao controlador com o identificador do número que precisa ser triplicado.
- o controlador solicita o resultado de sua dependência de serviço
- o serviço solicita dados de sua dependência - repositório, multiplica e retorna o resultado para o controlador
- o controlador complementa o resultado e retorna ao front-end
Vamos começar com o controlador:
@RestController
@RequiredArgsConstructor
public class SomeController {
private final SomeService someService; // dependency injection
static final String RESP_PREFIX = ": ";
static final String PATH_GET_TRIPLE = "/triple/{numberId}";
@GetMapping(path = PATH_GET_TRIPLE) // mapping method to GET with url=path
public ResponseEntity<String> triple(@PathVariable(name = "numberId") int numberId) {
int res = someService.tripleMethod(numberId); // dependency call
String resp = RESP_PREFIX + res; // own logic
return ResponseEntity.ok().body(resp);
}
}
Um controlador de resto típico tem injeção de dependência someService. O método triplo é configurado para uma solicitação GET para a URL "/ triple / {numberId}", onde o identificador do número é passado na variável do caminho. O método em si pode ser dividido em dois componentes principais:
- acessar uma dependência - solicitar dados de fora ou chamar um procedimento sem resultado
- própria lógica - trabalhando com dados existentes
Considere um serviço:
@Service
@RequiredArgsConstructor
public class SomeService {
private final SomeRepository someRepository; // dependency injection
public int tripleMethod(int numberId) {
Integer fromDB = someRepository.findOne(numberId); // dependency call
int res = fromDB * 3; // own logic
return res;
}
}
Aqui está uma situação semelhante: injetando a dependência someRepository, o método consiste em acessar a dependência e sua própria lógica.
Finalmente - o repositório, para simplificar, feito sem um banco de dados:
@Repository
public class SomeRepository {
public Integer findOne(Integer id){
return id;
}
}
O método condicional findOne supostamente procura no banco de dados um valor por identificador, mas simplesmente retorna o mesmo número inteiro. Isso não afeta a essência do nosso exemplo.
Se você rodar nosso aplicativo, então pela url configurada você pode ver:

Funciona! Em camadas! Em produção ...
Ah sim, testes ...
Um pouco sobre a essência. Escrever testes também é um processo criativo! Portanto, a desculpa “Eu sou um desenvolvedor, não um testador” é completamente inadequada. Um bom teste, assim como uma boa funcionalidade, requer engenhosidade e beleza. Mas antes de tudo, é necessário determinar a estrutura básica do teste.
A classe de teste contém métodos que testam os métodos da classe de destino. O mínimo que cada método de teste deve conter é uma chamada para o método correspondente da classe de destino, condicionalmente falando assim:
@Test
void someMethod_test() {
// prepare...
int res = someService.someMethod();
// check...
}
Este desafio pode ser cercado por Preparação e Revisão. Preparando dados, incluindo argumentos de entrada e descrevendo o comportamento dos mocks. Validar os resultados geralmente é uma comparação com o valor esperado, lembra de capturar o comportamento esperado? No total, um teste é um cenário que simula uma situação e registra se ela foi aprovada conforme o esperado e retornou os resultados esperados.
Usando o controlador como exemplo, vamos tentar descrever em detalhes o algoritmo básico para escrever um teste. Em primeiro lugar, o método do controlador de destino usa um parâmetro int numberId, vamos adicioná-lo ao nosso script:
int numberId = 42; // input path variable
O mesmo numberId é transmitido em trânsito para a entrada do método de serviço e agora é hora de fornecer a simulação de serviço:
@MockBean
private SomeService someService;
O código do método do próprio controlador trabalha com o resultado recebido do serviço, nós simulamos este resultado, bem como uma chamada que o retorna:
int serviceRes = numberId*3; // result from mock someService
// prepare someService.tripleMethod behavior
when(someService.tripleMethod(eq(numberId))).thenReturn(serviceRes);
Esta entrada significa: "quando someService.tripleMethod é chamado com um argumento igual a numberId, retorna o valor de serviceRes."
Além disso, essa entrada captura o fato de que esse método de serviço deve ser chamado, o que é um ponto importante. Acontece que você precisa consertar uma chamada para um procedimento sem resultado, então uma notação diferente é usada, convencionalmente como - "não faça nada quando ...":
Mockito.doNothing().when(someService).someMethod(eq(someParam));
Novamente, aqui está apenas uma imitação do trabalho de someService, testes honestos com correção detalhada do comportamento de someService serão implementados separadamente. Além disso, nem mesmo importa aqui que o valor deve triplicar, se escrevermos
int serviceRes = numberId*5;
isso não vai quebrar o script atual, uma vez que não é o comportamento de algum serviço que é capturado aqui, mas o comportamento do controlador que considera o resultado de algum serviço como garantido. Isso é completamente lógico, porque a classe de destino não pode ser responsável pelo comportamento da dependência injetada, mas deve confiar nela.
Assim definimos o comportamento do mock em nosso script, portanto, ao executar o teste, quando dentro da chamada do método alvo ele fizer um mock, ele retornará o que foi solicitado - serviceRes, e então o próprio código do controlador trabalhará com este valor.
Em seguida, colocamos uma chamada para o método de destino no script. O método do controlador tem uma peculiaridade - não é chamado explicitamente no código, mas é vinculado por meio do método HTTP GET e da URL, portanto, em testes, é chamado por meio de um cliente de teste especial. No Spring, este é o MockMvc, em outros frameworks existem análogos, por exemplo, WebTestCase.createClient no Symfony. Assim, além disso, é simples executar o método do controlador por meio do mapeamento por GET e URL.
//// mockMvc.perform
MockHttpServletRequestBuilder requestConfig = MockMvcRequestBuilders.get(SomeController.PATH_GET_TRIPLE, numberId);
MvcResult mvcResult = mockMvc.perform(requestConfig)
.andExpect(status().isOk())
//.andDo(MockMvcResultHandlers.print())
.andReturn()
;//// mockMvc.perform
Ao mesmo tempo, também é verificado se esse mapeamento existe. Se a chamada for bem-sucedida, tudo se resume a verificar e corrigir os resultados. Por exemplo, você pode corrigir quantas vezes o método simulado foi chamado:
// check of calling
Mockito.verify(someService, Mockito.atLeastOnce()).tripleMethod(eq(numberId));
No nosso caso, isso é redundante, uma vez que já corrigimos sua única chamada até quando, mas às vezes esse método é apropriado.
E agora o principal - verificamos o comportamento do código do próprio controlador:
// check of result
assertEquals(SomeController.RESP_PREFIX+serviceRes, mvcResult.getResponse().getContentAsString());
Aqui, corrigimos o que o próprio método é responsável - que o resultado recebido de someService é concatenado com o prefixo do controlador, e é essa linha que vai para o corpo da resposta. A propósito, você pode ver com seus próprios olhos o conteúdo de Corpo se descomentar a linha
//.andDo(MockMvcResultHandlers.print())
mas geralmente essa impressão no console é usada apenas como um auxílio para a depuração.
Assim, temos um método de teste na classe de teste do controlador:
@WebMvcTest(SomeController.class)
class SomeControllerTest {
@MockBean
private SomeService someService;
@Autowired
private MockMvc mockMvc;
@Test
void triple() throws Exception {
int numberId = 42; // input path variable
int serviceRes = numberId*3; // result from mock someService
// prepare someService.tripleMethod behavior
when(someService.tripleMethod(eq(numberId))).thenReturn(serviceRes);
//// mockMvc.perform
MockHttpServletRequestBuilder requestConfig = MockMvcRequestBuilders.get(SomeController.PATH_GET_TRIPLE, numberId);
MvcResult mvcResult = mockMvc.perform(requestConfig)
.andExpect(status().isOk())
//.andDo(MockMvcResultHandlers.print())
.andReturn()
;//// mockMvc.perform
// check of calling
Mockito.verify(someService, Mockito.atLeastOnce()).tripleMethod(eq(numberId));
// check of result
assertEquals(SomeController.RESP_PREFIX+serviceRes, mvcResult.getResponse().getContentAsString());
}
}
Agora é hora de testar honestamente o método someService.tripleMethod, onde, da mesma forma, há uma chamada de dependência e seu próprio código. Prepare um argumento de entrada arbitrário e simule o comportamento da dependência someRepository:
int numberId = 42;
when(someRepository.findOne(eq(numberId))).then(AdditionalAnswers.returnsFirstArg());
Tradução: "quando someRepository.findOne é chamado com um argumento igual a numberId, retorna o mesmo argumento." Situação semelhante - aqui não verificamos a lógica da dependência, mas acreditamos em sua palavra. Capturamos apenas a chamada para a dependência dentro deste método. O princípio aqui é a própria lógica do serviço, sua área de responsabilidade:
assertEquals(numberId*3, res);
Corrigimos que o valor recebido do repositório deve ser triplicado pela própria lógica do método. Agora, este teste está protegendo este requisito:
@ExtendWith(MockitoExtension.class)
class SomeServiceTest {
@Mock
private SomeRepository someRepository; // ,
@InjectMocks
private SomeService someService; // ,
@Test
void tripleMethod() {
int numberId = 42;
when(someRepository.findOne(eq(numberId))).then(AdditionalAnswers.returnsFirstArg());
int res = someService.tripleMethod(numberId);
assertEquals(numberId*3, res);
}
}
Como nosso repositório é condicionalmente um brinquedo, o teste acabou sendo apropriado:
class SomeRepositoryTest {
// no dependency injection
private final SomeRepository someRepository = new SomeRepository();
@Test
void findOne() {
int id = 777;
Integer fromDB = someRepository.findOne(id);
assertEquals(id, fromDB);
}
}
No entanto, mesmo aqui, todo o esqueleto está no lugar: preparação, invocação e verificação. Assim, o trabalho correto de someRepository.findOne é corrigido.
Um repositório real requer testes com levantamento do banco de dados na memória ou em um contêiner de teste, migrando a estrutura e os dados, às vezes inserindo registros de teste. Esta é frequentemente a camada de teste mais longa, mas não menos importante porque migração bem-sucedida, salvamento de modelos, seleção correta, etc. são registrados. A organização dos testes de banco de dados está além do escopo deste artigo, mas é precisamente descrita em detalhes nos manuais. Não há injeção de dependência no repositório e não é necessário, sua tarefa é trabalhar com o banco de dados. No nosso caso, seria um teste com um salvamento preliminar do registro no banco de dados e posterior busca por id.
Assim, alcançamos a cobertura total de toda a cadeia funcional. Cada teste é responsável por executar seu próprio código e captura chamadas para todas as dependências. Testar um aplicativo não requer executá-lo com levantamento de contexto completo, o que é difícil e demorado. Manter a funcionalidade com testes de unidade rápidos e fáceis cria um ambiente de trabalho confortável e confiável.
Além disso, os testes melhoram a qualidade do código. Como parte do teste independente em camadas, você geralmente precisa repensar como organiza seu código. Por exemplo, um método foi criado primeiro no serviço, não é pequeno, contém seu próprio código e simulações e, por exemplo, não faz sentido dividi-lo, ele é coberto pelo (s) teste (s) por completo - todas as preparações e verificações estão definidas. Então, alguém decide adicionar um segundo método ao serviço, que chama o primeiro método. Parece uma vez uma situação comum, mas quando se trata de cobertura com um teste, algo não bate ... Para o segundo método, você terá que descrever o segundo cenário e duplicar o primeiro cenário de preparação? Afinal, não funcionará bloquear o primeiro método da própria classe testada.
Talvez, neste caso, seja apropriado pensar em uma organização diferente do código. Existem duas abordagens opostas:
- mova o primeiro método para um componente de utilitário que é injetado como uma dependência no serviço.
- mova o segundo método para uma fachada de serviço que combina diferentes métodos do serviço incorporado ou mesmo vários serviços.
Ambas as opções se encaixam bem no princípio de "camadas" e são convenientemente testadas com simulação de dependência. A beleza é que cada camada é responsável por seu próprio trabalho e, juntas, criam uma estrutura sólida para a invulnerabilidade de todo o sistema.
Na pista ...
Pergunta da entrevista: quantas vezes um desenvolvedor deve executar testes em um ticket? Quantos você quiser, mas pelo menos duas vezes:
- antes de começar a trabalhar, para ter certeza de que está tudo bem, e não para descobrir depois o que já foi quebrado, e você não
- no final do trabalho
Então, por que escrever testes? Então, que não vale a pena tentar lembrar e prever tudo em uma aplicação grande e complexa, deve-se confiar na automação. Um desenvolvedor que não possui autoteste não está pronto para participar de um grande projeto, qualquer entrevistado revelará isso imediatamente.
Portanto, eu recomendo o desenvolvimento dessas habilidades se você deseja se qualificar para salários altos. Você pode começar esta prática emocionante com coisas básicas, ou seja, dentro da estrutura de sua estrutura favorita, aprender como testar:
- componentes com dependências incorporadas, técnicas de simulação
- controladores, porque existem nuances de chamar o ponto final
- DAO, repositórios, incluindo aumento da base de teste e migrações
Espero que este conceito de "massa folhada" tenha ajudado a entender a técnica de testar aplicativos complexos e a sentir o quão flexível e poderosa uma ferramenta nos é apresentada para trabalharmos. Obviamente, quanto melhor a ferramenta, mais habilidoso ela exige.
Aproveite seu trabalho e grande habilidade!
O código de exemplo está disponível no link github.com: https://github.com/denisorlov/examples/tree/main/unittestidea