Bom, ruim, mau - testando em um projeto para iniciantes

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.

Source: https://habr.com/ru/post/pt450290/


All Articles