Prefácio: a universidade recebeu uma tarefa - montar uma equipe de scrum, selecionar um projeto e trabalhar nele por um semestre. Nossa equipe escolheu o desenvolvimento de aplicativos da web (reag + flask). Neste artigo, tentarei dizer quais deveriam ser os testes e analisar o que fizemos no back-end.

Expectativas
Os testes são necessários, antes de tudo, para convencer a todos (inclusive a nós mesmos) de que o programa se comporta como deveria em situações de teste . Em segundo lugar, eles garantem o desempenho do código coberto por testes no futuro. Escrever testes é um processo útil, porque, em seu processo, muitas vezes é possível tropeçar em áreas problemáticas, relembrar alguns casos extremos, ver problemas com interfaces etc.
Ao desenvolver qualquer sistema, você precisa se lembrar de pelo menos três tipos de testes:
- Testes de unidade são testes que verificam se as funções fazem o que precisam.
- Testes de integração são testes que verificam se várias funções juntas fazem a coisa certa.
- Testes do sistema são testes que verificam se o sistema inteiro faz o que precisa.
Em uma das postagens do google, uma tabela foi publicada com uma descrição de três tipos de testes. "Pequeno", "Médio" e "Grande".

Testes unitários
Os testes de unidade correspondem a pequenos testes - eles devem ser rápidos e apenas verificar a exatidão de partes específicas do programa. Eles não devem acessar o banco de dados, não devem funcionar em ambientes multiencadeados complexos. Eles controlam a conformidade com as especificações / padrões, geralmente têm o papel de testes de regressão .
Testes de integração
Testes de integração são aqueles que podem afetar vários módulos e funções. Esses testes requerem mais tempo e podem exigir ambientes especiais. Eles são necessários para garantir que módulos e funções individuais possam trabalhar entre si. I.e. os testes de unidade verificam a conformidade das interfaces reais com os esperados e os testes de integração - que funções e módulos interagem corretamente entre si.
Testes do sistema
Este é o nível mais alto de teste automático. Os testes do sistema verificam se todo o sistema funciona, se suas partes executam suas tarefas e são capazes de interagir corretamente.
Por que acompanhar os tipos
Normalmente, com o crescimento do projeto, a base de código também aumentará. A duração das verificações automáticas aumentará; o suporte a um grande número de testes de integração e sistema se tornará cada vez mais difícil. Portanto, o desafio para os desenvolvedores é minimizar os testes necessários. Para fazer isso, tente usar testes de unidade sempre que possível e reduza a integração usando "zombarias" (zombarias).
Realidade
Teste de API típico
def test_user_reg(client): return json.loads( client.post(url, json=data, content_type='application/json').data ) response = client.post('api/user.reg', json={ 'email': 'name@mail.ru', 'password': 'password1', 'first_name': 'Name', 'last_name': 'Last Name' }) data = json.loads(response.data) assert data['code'] == 0
A partir da documentação oficial do balão, obtemos uma receita pronta para inicializar o aplicativo e criar o banco de dados. Aqui está o trabalho com o banco de dados. Este não é um teste de unidade, mas não um teste de sistema. Este é um teste de integração que usa um aplicativo de teste de banco de dados.
Por que integração ao invés de modular? Porque no processamento de consultas, a interação é realizada com o balão, com o ORM, com nossa lógica de negócios. Os manipuladores agem como um link unificador de outras partes do projeto, portanto, escrever testes de unidade para eles não é muito fácil (você precisa substituir o banco de dados por zombarias, lógica interna) e não é muito prático (os testes de integração verificarão aspectos semelhantes - "foram chamadas as funções necessárias?", " Os dados foram recebidos corretamente? ", Etc.).
Nomes e agrupamento de testes
def test_not_empty_errors(): assert validate_not_empty('email', '') == ('email is empty',) assert validate_not_empty('email', ' ') == ('email is empty',) assert validate_email_format('email', "") == ('email is empty',) assert validate_password_format('pass', "") == ('pass is empty',) assert validate_datetime('datetime', "") == ('datetime is empty',)
Neste teste, todas as condições para testes "pequenos" são atendidas - o comportamento da função sem dependências é verificado quanto à conformidade com o esperado. Mas o design levanta questões.
É uma boa prática escrever testes focados em um aspecto específico do programa. Neste exemplo, existem funções diferentes - validate_password_format
, validate_password_format
, validate_datetime
. As verificações de agrupamento não se baseiam no resultado, mas nos objetos de teste.
O nome do teste ( test_not_empty_errors
) não descreve o objeto de teste (qual método está sendo testado), mas descreve apenas o resultado (os erros não estão vazios). Este método deve ser chamado test__validate_not_empty__error_on_empty
. Este nome descreve o que está sendo testado e qual resultado é esperado. Isso se aplica a quase todos os nomes de teste no projeto devido ao fato de não ter sido necessário tempo para discutir as convenções de nomenclatura dos testes.
Testes de regressão
def test_datetime_errors(): assert validate_datetime('datetime', '0123-24-31T;431') == ('datetime is invalid',) assert validate_datetime('datetime', '2018-10-18T20:21:21+-23:1') == ('datetime is invalid',) assert validate_datetime('datetime', '2015-13-20T20:20:20+20:20') == ('datetime is invalid',) assert validate_datetime('datetime', '2015-02-29T20:20:20+20:20') == ('datetime is invalid',) assert validate_datetime('datetime', '2015-12-20T25:20:20+20:20') == ('datetime is invalid',) assert validate_datetime('datetime', '2015-12-20T20:61:20+22:20') == ('datetime is invalid',) assert validate_datetime('datetime', '2015-12-20T20:20:61+20:20') == ('datetime is invalid',) assert validate_datetime('datetime', '2015-12-20T20:20:20+25:20') == ('datetime is invalid',) assert validate_datetime('datetime', '2015-12-20T20:20:20+20:61') == ('datetime is invalid',) assert validate_datetime('datetime', '2015-13-35T25:61:61+61:61') == ('datetime is invalid',)
Este teste consistiu originalmente nas duas primeiras assert
. Depois disso, um "bug" foi descoberto - em vez de verificar a data, apenas a expressão regular foi verificada, ou seja, 9999-99-99
foi considerada uma data normal. O desenvolvedor corrigiu. Naturalmente, depois de corrigir o bug, você precisa adicionar testes para impedir uma regressão futura. Em vez de adicionar um novo teste para escrever por que esse teste existe, foram adicionadas verificações a esse teste.
Como deve ser chamado um novo teste para adicionar a verificação? Provavelmente test__validate_datetime__error_on_bad_datetime
.
Ignorando ferramentas
def test_get_providers(): class Tmp: def __init__(self, id_external, token, username): self.id_external = id_external self.token = token self.username = username ...
Tmp
? Esta é uma substituição de um objeto que não é usado neste teste. O desenvolvedor parece não saber sobre a existência de @patch
e MagicMock
de unittest.mock
. Não há necessidade de complicar o código, resolvendo problemas ingenuamente quando existem ferramentas mais adequadas.
Existe um teste que inicializa os serviços (no banco de dados), usa o contexto do aplicativo.
def test_get_posts(client): def fake_request(*args, **kwargs): return [one, two] handler = VKServiceHandler() handler.request = fake_request services_init() with app.app_context(): posts = handler.get_posts(None) assert len(posts) == 2
Você pode excluir o banco de dados e o contexto do teste simplesmente adicionando um @patch
.
@patch("mobius.services.service_vk.Service") def test_get_posts(mock): def fake_request(*args, **kwargs): return [one, two] handler = VKServiceHandler() handler.request = fake_request posts = handler.get_posts(None) assert len(posts) == 2
Sumário
- Para desenvolver software de qualidade, você precisa escrever testes. No mínimo, certifique-se de escrever o que você precisa.
- Para grandes sistemas de informação, os testes são ainda mais importantes - eles permitem evitar alterações indesejadas na interface ou retornar bugs.
- Para que os testes escritos não se transformem em muitos métodos estranhos ao longo do tempo, você deve prestar atenção à convenção de nomenclatura dos testes, aderir às boas práticas e minimizar os testes.
- Os testes de unidade podem ser uma ótima ferramenta durante o desenvolvimento. Eles podem ser executados após cada pequena alteração para garantir que nada esteja quebrado.
Um ponto muito importante é que os testes não garantem a disponibilidade ou ausência de bugs. Os testes garantem que o resultado real do programa (ou parte dele) seja esperado. Nesse caso, a verificação ocorre apenas para os aspectos para os quais os testes foram gravados. Portanto, ao criar um produto de qualidade, não devemos esquecer outros tipos de teste.