Bon, mauvais, mauvais - test dans un projet pour débutants

Préface: l'université a reçu une tâche: constituer une équipe de mêlée, sélectionner un projet et travailler dessus pendant un semestre. Notre équipe a choisi le développement d'applications web (react + flask). Dans cet article, je vais essayer de vous dire quels tests auraient dû être et d'analyser ce que nous avons fait sur le backend.



Les attentes


Les tests sont nécessaires, tout d'abord, pour convaincre tout le monde (y compris nous-mêmes) que le programme se comporte comme il se doit dans les situations de test . Deuxièmement, ils assurent à l'avenir les performances du code couvert par les tests . L'écriture de tests est un processus utile, car dans son processus, vous pouvez souvent tomber sur des zones à problèmes, rappeler des cas extrêmes, voir des problèmes d'interfaces, etc.


Lors du développement de systèmes, vous devez vous souvenir d'au moins trois types de tests:


  • Les tests unitaires sont des tests qui vérifient que les fonctions font ce dont elles ont besoin.
  • Les tests d'intégration sont des tests qui vérifient que plusieurs fonctions ensemble agissent correctement.
  • Les tests système sont des tests qui vérifient que l'ensemble du système fait ce dont il a besoin.

Dans l' un des articles de google, un tableau a été publié avec une description de trois types de tests. "Petit", "Moyen" et "Grand".



Tests unitaires


Les tests unitaires correspondent à de petits tests - ils doivent être rapides et ne vérifier que l'exactitude de parties spécifiques du programme. Ils ne doivent pas accéder à la base de données, ne doivent pas fonctionner dans des environnements complexes à plusieurs threads. Ils contrôlent la conformité aux spécifications / normes, ils ont souvent le rôle de tests de régression .


Tests d'intégration


Les tests d'intégration sont les tests qui peuvent affecter plusieurs modules et fonctions. De tels tests nécessitent plus de temps et peuvent nécessiter des environnements spéciaux. Ils sont nécessaires pour garantir que les modules et fonctions individuels peuvent fonctionner ensemble. C'est-à-dire les tests unitaires vérifient la conformité des interfaces réelles aux tests attendus et d'intégration - que les fonctions et les modules interagissent correctement les uns avec les autres.


Tests système


Il s'agit du plus haut niveau de tests automatiques. Les tests système vérifient que l'ensemble du système fonctionne, que ses parties exécutent leurs tâches et sont capables d'interagir correctement.


Pourquoi garder une trace des types


Habituellement, avec la croissance du projet, la base de code augmentera également. La durée des vérifications automatiques augmentera; prendre en charge un grand nombre de tests d'intégration et de système deviendra de plus en plus difficile. Par conséquent, le défi pour les développeurs est de minimiser les tests nécessaires. Pour ce faire, essayez d'utiliser des tests unitaires dans la mesure du possible et réduisez l'intégration à l'aide de «mocks» (mocks).


La réalité


Test API typique


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 

De la documentation officielle du flacon, nous obtenons une recette prête à l'emploi pour initialiser l'application et créer la base de données. Voici le travail avec la base de données. Ce n'est pas un test unitaire, mais pas un test système. Il s'agit d'un test d'intégration qui utilise une application de test de base de données.


Pourquoi l'intégration plutôt que modulaire? Parce que dans le traitement des requêtes, l'interaction est effectuée avec le flacon, avec l'ORM, avec notre logique métier. Les gestionnaires agissent comme un élément unificateur des autres parties du projet, donc écrire des tests unitaires pour eux n'est pas trop facile (vous devez remplacer la base de données par des maquettes, une logique interne) et pas trop pratique (les tests d'intégration vérifieront des aspects similaires - "les fonctions nécessaires ont-elles été appelées?", " Les données ont-elles été correctement reçues? ", Etc.).


Noms et regroupement des tests


 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',) 

Dans ce test, toutes les conditions pour les "petits" tests sont remplies - le comportement de la fonction sans dépendances est vérifié pour la conformité avec les attentes. Mais le design soulève des questions.


Il est recommandé d'écrire des tests qui se concentrent sur un aspect spécifique du programme. Dans cet exemple, il existe différentes fonctions - validate_password_format , validate_password_format , validate_datetime . Le regroupement des contrôles n'est pas basé sur le résultat, mais sur les objets de test.


Le nom du test ( test_not_empty_errors ) ne décrit pas l'objet de test (quelle méthode est testée), il ne décrit que le résultat (les erreurs ne sont pas vides). Cette méthode doit être appelée test__validate_not_empty__error_on_empty . Ce nom décrit ce qui est testé et quel résultat est attendu. Cela s'applique à presque tous les noms de test dans le projet car aucun temps n'a été pris pour discuter des conventions de dénomination des tests.


Tests de régression


 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',) 

Ce test consistait à l'origine des deux premières assert . Après cela, un "bug" a été découvert - au lieu de vérifier la date, seule l'expression régulière a été vérifiée, c'est-à-dire 9999-99-99 était considérée comme une date normale. Le développeur l'a corrigé. Naturellement, après avoir corrigé le bogue, vous devez ajouter des tests pour éviter une future régression. Au lieu d'ajouter un nouveau test dans lequel écrire pourquoi ce test existe, des vérifications ont été ajoutées à ce test.


Comment appeler un nouveau test pour ajouter la vérification? test__validate_datetime__error_on_bad_datetime probablement test__validate_datetime__error_on_bad_datetime .


Ignorer les outils


 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 ? Il s'agit d'une substitution pour un objet qui n'est pas utilisé dans ce test. Le développeur ne semble pas connaître l'existence de @patch et MagicMock de unittest.mock . Pas besoin de compliquer le code en résolvant naïvement les problèmes quand il existe des outils plus adéquats.


Il existe un tel test qui initialise les services (dans la base de données), utilise le contexte de l'application.


 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 

Vous pouvez exclure la base de données et le contexte du test en ajoutant simplement un @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 

Résumé


  • Pour développer un logiciel de qualité, vous devez écrire des tests. Au minimum, pour vous assurer d'écrire ce dont vous avez besoin.
  • Pour les grands systèmes d'information, les tests sont encore plus importants - ils vous permettent d'éviter les modifications d'interface indésirables ou de renvoyer des bogues.
  • Pour que les tests écrits ne se transforment pas en beaucoup de méthodes étranges au fil du temps, vous devez faire attention à la convention de dénomination des tests, respecter les bonnes pratiques et minimiser les tests.
  • Les tests unitaires peuvent être un excellent outil pendant le développement. Ils peuvent être exécutés après chaque petit changement pour s'assurer que rien n'est cassé.

Un point très important est que les tests ne garantissent pas la disponibilité ou l'absence de bugs. Les tests garantissent que le résultat réel du programme (ou une partie de celui-ci) est attendu. Dans ce cas, la vérification n'a lieu que pour les aspects pour lesquels des tests ont été écrits. Par conséquent, lors de la création d'un produit de qualité, nous ne devons pas oublier les autres types de tests.

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


All Articles