Connaissance des tests en Python. Partie 1

Bonne journée à tous!

De notre table à la vôtre ... Autrement dit, de notre cours de développeur Python, malgré la nouvelle année qui approche à grands pas, nous avons préparé pour vous une traduction intéressante sur les différentes méthodes de test en Python.

Ce guide est destiné à ceux qui ont déjà écrit une application Python sympa mais qui n'ont pas encore écrit pour
les tests.

Les tests en Python sont un sujet étendu avec un tas de subtilités, mais il n'est pas nécessaire de compliquer les choses. En quelques étapes simples, vous pouvez créer des tests simples pour l'application, augmentant progressivement la complexité basée sur eux.

Dans ce guide, vous apprendrez comment créer un test de base, l'exécuter et trouver tous les bugs avant que les utilisateurs ne le fassent! Vous découvrirez les outils disponibles pour écrire et exécuter des tests, vérifier les performances des applications et même étudier les problèmes de sécurité.



Test de code

Vous pouvez tester le code de plusieurs manières. Dans ce guide, vous découvrirez les méthodes des plus simples aux plus avancées.

Automatisé vs Test manuel

Bonne nouvelle! Vous avez probablement déjà fait le test, mais vous ne l'avez pas encore réalisé. Rappelez-vous comment vous avez commencé l'application et l'avez utilisée? Avez-vous testé les fonctions et les avez-vous expérimentées? Ce processus est appelé test exploratoire et c'est une forme de test manuel.

Tests de recherche - tests effectués sans plan. Lors des tests de recherche, vous recherchez l'application.

Pour créer une liste complète de tests manuels, il suffit de faire une liste de toutes les fonctions de l'application, des différents types d'entrées qu'elle accepte et des résultats attendus. Maintenant, chaque fois que vous modifiez quelque chose dans le code, vous devez revérifier chacun des éléments de cette liste.

Cela semble sombre, non?

Par conséquent, des tests automatiques sont nécessaires. Test automatique - l'exécution du plan de test (parties de l'application qui nécessitent des tests, l'ordre des tests et les résultats attendus) à l'aide d'un script, et non par des mains humaines. Python dispose déjà d'un ensemble d'outils et de bibliothèques pour vous aider à créer des tests automatisés pour votre application. Regardons ces outils et bibliothèques dans notre tutoriel.

Tests unitaires VS. Tests d'intégration

Le monde des tests est plein de termes, et maintenant, connaissant la différence entre les tests manuels et automatisés, nous allons descendre d'un niveau plus profond.

Pensez à comment tester les phares d'une voiture? Vous allumez les phares (appelons cela l'étape de test), sortez de la voiture vous-même ou demandez à un ami de vérifier que les phares sont allumés (et c'est une proposition de test). Le test de plusieurs composants est appelé test d'intégration.

Pensez à toutes les choses qui devraient fonctionner correctement pour qu'une tâche simple produise le résultat correct. Ces composants sont similaires à des parties de votre application: toutes ces classes, fonctions, modules que vous avez écrits.

La principale difficulté des tests d'intégration survient lorsque le test d'intégration ne donne pas le résultat correct. Il est difficile d'évaluer le problème, ne pouvant isoler la partie cassée du système. Si les phares ne sont pas allumés, les ampoules peuvent être cassées. Ou peut-être que la batterie est faible? Ou peut-être que le problème vient du générateur? Ou même un crash dans l'ordinateur de la machine?

Les voitures modernes elles-mêmes vous informeront d'une ampoule cassée. Ceci est déterminé à l'aide d'un test unitaire.

Le test unitaire (test unitaire) est un petit test qui vérifie le bon fonctionnement d'un composant individuel. Le test unitaire permet d'isoler la panne et de la réparer plus rapidement.

Nous avons parlé de deux types de tests:

  1. Un test d'intégration qui vérifie les composants du système et leur interaction les uns avec les autres;
  2. Un test unitaire qui teste un seul composant d'une application.
  3. Vous pouvez créer les deux tests en Python. Pour écrire un test pour la fonction sum () intégrée, vous devez comparer la sortie de sum () avec des valeurs connues.

Par exemple, vous pouvez ainsi vérifier que la somme des nombres (1, 2, 3) est 6:

>>> assert sum([1, 2, 3]) == 6, "Should be 6" 

Les valeurs sont correctes, donc rien ne sera émis vers REPL. Si le résultat de sum() incorrect, une AssertionError sera AssertionError avec le message «devrait être 6». Vérifiez à nouveau l'instruction, mais maintenant avec des valeurs non valides pour obtenir une AssertionError :

 >>> assert sum([1, 1, 1]) == 6, "Should be 6" Traceback (most recent call last): File "<stdin>", line 1, in <module> AssertionError: Should be 6 

Dans REPL, vous verrez AssertionError puisque la valeur sum() n'est pas 6.

Au lieu de REPL, placez-le dans un nouveau fichier Python appelé test_sum.py et exécutez-le à nouveau:

 def test_sum(): assert sum([1, 2, 3]) == 6, "Should be 6" if __name__ == "__main__": test_sum() print("Everything passed") 

Vous disposez maintenant d'un scénario de test écrit (scénario de test), d'une instruction et d'un point d'entrée (ligne de commande). Maintenant, cela peut être fait sur la ligne de commande:

 $ python test_sum.py Everything passed 

Vous voyez le résultat réussi, "Tout est passé".

sum() en Python accepte tout itérable comme premier argument. Vous avez vérifié la liste. Essayons de tester le tuple. Créez un nouveau fichier appelé test_sum_2.py avec le code suivant:

 def test_sum(): assert sum([1, 2, 3]) == 6, "Should be 6" def test_sum_tuple(): assert sum((1, 2, 2)) == 6, "Should be 6" if __name__ == "__main__": test_sum() test_sum_tuple() print("Everything passed") 

test_sum_2.py , le script test_sum_2.py erreur, car s um() (1, 2, 2) doit être test_sum_2.py 5, et non 6. Par conséquent, le script affiche un message d'erreur, une ligne de code et un retraçage:

 $ python test_sum_2.py Traceback (most recent call last): File "test_sum_2.py", line 9, in <module> test_sum_tuple() File "test_sum_2.py", line 5, in test_sum_tuple assert sum((1, 2, 2)) == 6, "Should be 6" AssertionError: Should be 6 

Vous pouvez voir comment une erreur dans le code provoque une erreur dans la console avec des informations sur son emplacement et le résultat attendu.

De tels tests conviennent à une vérification simple, mais qu'en est-il s'il y a plus d'erreurs que dans un seul? Les coureurs d'essai viennent à la rescousse. Test Executor est une application spéciale conçue pour effectuer des tests, vérifier les données de sortie et fournir des outils de débogage et de diagnostic des tests et des applications.

Choix d'un exécuteur de test

Il existe de nombreux exécuteurs de test disponibles pour Python. Par exemple, unittest est intégré à la bibliothèque standard Python. Dans ce guide, nous utiliserons des cas de test et des exécuteurs de test les plus simples. Les principes de fonctionnement les plus simples sont facilement adaptables à d'autres cadres. Nous listons les exécuteurs de test les plus populaires:

  • unittest;
  • nez ou nez2;
  • pytest.

Il est important de choisir un entrepreneur d'essai qui répond à vos exigences et à votre expérience.

unittest

unittest est intégré à la bibliothèque standard Python depuis la version 2.1. Vous le rencontrerez probablement dans des applications commerciales Python et des projets open source.
Unittest a un framework de test et un lanceur de test. Lors de l'écriture et de l'exécution de tests, vous devez respecter certaines exigences importantes.

unittest nécessite:

  • Mettez les tests dans les classes comme mĂ©thodes;
  • Utilisez des mĂ©thodes d'approbation spĂ©ciales. La classe TestCase au lieu de l'expression assert intĂ©grĂ©e habituelle.


Pour transformer un exemple précédemment écrit en scénario de test le plus simple, vous devez:

  1. Importez unittest depuis la bibliothèque standard;
  2. Créez une classe appelée TestSum qui héritera de la classe TestCase ;
  3. Convertissez les fonctions de test en méthodes en ajoutant self comme premier argument;
  4. Modifiez les instructions en ajoutant l'utilisation de la méthode self.assertEqual() dans la classe TestCase ;
  5. Modifiez le point d'entrée sur la ligne de commande pour appeler unittest.main() .

En suivant ces étapes, créez un nouveau fichier test_sum_unittest.py avec ce code:

 import unittest class TestSum(unittest.TestCase): def test_sum(self): self.assertEqual(sum([1, 2, 3]), 6, "Should be 6") def test_sum_tuple(self): self.assertEqual(sum((1, 2, 2)), 6, "Should be 6") if __name__ == '__main__': unittest.main() 

En faisant cela sur la ligne de commande, vous obtiendrez un succès (indiqué par.) Et un échec (indiqué par F):

 $ python test_sum_unittest.py .F ====================================================================== FAIL: test_sum_tuple (__main__.TestSum) ---------------------------------------------------------------------- Traceback (most recent call last): File "test_sum_unittest.py", line 9, in test_sum_tuple self.assertEqual(sum((1, 2, 2)), 6, "Should be 6") AssertionError: Should be 6 ---------------------------------------------------------------------- Ran 2 tests in 0.001s FAILED (failures=1) 

Ainsi, vous avez effectué deux tests à l'aide du lanceur de test le plus complet.

Remarque: Si vous écrivez des cas de test pour Python 2 et 3, soyez prudent. Dans les versions de Python 2.7 et inférieures, unittest est appelé unittest 2. Lorsque vous importez depuis unittest, vous obtiendrez différentes versions avec différentes fonctions dans Python 2 et Python 3.

Pour en savoir plus sur unittest, lisez la documentation unittest .

nez

Au fil du temps, après avoir écrit des centaines voire des milliers de tests pour une application, il devient de plus en plus difficile de comprendre et d'utiliser les données de sortie les plus instables.

nose est compatible avec tous les tests écrits avec le framework le plus simple, et peut remplacer son exécuteur de test. Le développement de nose, en tant qu'application open source, a commencé à ralentir, et nose2 a été créé. Si vous partez de zéro, il est recommandé d'utiliser nose2.

Pour commencer avec nose2, vous devez l'installer à partir de PyPl et l'exécuter sur la ligne de commande. nose2 essaiera de trouver tous les scripts de test*.py avec test*.py dans le nom et tous les cas de test hérités de unittest.TestCase dans votre répertoire actuel:

 $ pip install nose2 $ python -m nose2 .F ====================================================================== FAIL: test_sum_tuple (__main__.TestSum) ---------------------------------------------------------------------- Traceback (most recent call last): File "test_sum_unittest.py", line 9, in test_sum_tuple self.assertEqual(sum((1, 2, 2)), 6, "Should be 6") AssertionError: Should be 6 ---------------------------------------------------------------------- Ran 2 tests in 0.001s FAILED (failures=1) 

C'est ainsi que le test créé dans test_sum_unittest.py partir du lanceur de test nose2. nose2 fournit de nombreux drapeaux de ligne de commande pour filtrer les tests exécutables. Pour plus d'informations, consultez la documentation Nose 2 .

pytest

pytest prend en charge les cas de test les plus simples. Mais le véritable avantage de pytest réside dans ses cas de test. Les cas de test pytest sont une série de fonctions dans un fichier Python avec test_ au début du nom.

Il contient d'autres fonctionnalités utiles:

  • Prise en charge des expressions d'assertion intĂ©grĂ©es au lieu d'utiliser des mĂ©thodes spĂ©ciales self.assert * ();
  • Prise en charge du filtrage des cas de test;
  • La possibilitĂ© de redĂ©marrer Ă  partir du dernier test ayant Ă©chouĂ©;
  • Un Ă©cosystème de centaines de plugins qui Ă©tendent les fonctionnalitĂ©s.

Un exemple de scénario de test TestSum pour pytest ressemblera à ceci:

 def test_sum(): assert sum([1, 2, 3]) == 6, "Should be 6" def test_sum_tuple(): assert sum((1, 2, 2)) == 6, "Should be 6" 

Vous vous êtes débarrassé de TestCase, en utilisant des classes et des points d'entrée de ligne de commande.
Plus d'informations peuvent être trouvées sur le site de documentation Pytest .

Écrire le premier test

Combinez tout ce que nous avons déjà appris, et au lieu de la fonction sum() intégrée, nous testons une implémentation simple avec les mêmes exigences.

Créez un nouveau dossier pour le projet, à l'intérieur duquel créez un nouveau dossier appelé my_sum. Dans my_sum, créez un fichier vide appelé _init_.py . La présence de ce fichier signifie que le dossier my_sum peut être importé en tant que module à partir du répertoire parent.

La structure des dossiers ressemblera Ă  ceci:

project/
│
└── my_sum/
└── __init__.py


Ouvrez my_sum/__init__.py et créez une nouvelle fonction appelée sum() , qui prend les entrées my_sum/__init__.py (liste, tuple, set) et ajoute les valeurs.

 def sum(arg): total = 0 for val in arg: total += val return total 

Cet exemple crée une variable appelée total , itère sur toutes les valeurs de arg et ajoute au total . Ensuite, à la fin de l'itération, le résultat est renvoyé.

OĂą Ă©crire un test

Vous pouvez commencer à écrire un test en créant un fichier test.py qui contiendra votre premier test.py test. Pour les tests, le fichier devrait pouvoir importer votre application, alors mettez test.py dans le dossier au-dessus du package. L'arborescence des répertoires ressemblera à ceci:

project/
│
├── my_sum/
│ └── __init__.py
|
└── test.py


Vous remarquerez que lorsque vous ajoutez de nouveaux tests, votre fichier devient plus lourd et difficile à maintenir, nous vous recommandons donc de créer le dossier tests/ et de diviser les tests en plusieurs fichiers. Assurez-vous que les noms de tous les fichiers commencent par test_ , afin que les test_ test comprennent que les fichiers Python contiennent des tests qui doivent être exécutés. Sur les grands projets, les tests sont divisés en plusieurs répertoires, selon leur fonction ou leur utilisation.

Remarque: Et quelle est votre application est un script unique?
Vous pouvez importer n'importe quel attribut de script: classes, fonctions ou variables, à l'aide de la fonction intégrée __import__() . Au lieu de la from my_sum import sum écrivez ce qui suit:

 target = __import__("my_sum.py") sum = target.sum 

Lorsque vous utilisez __import__() vous n'avez pas à transformer le dossier de projet en package et vous pouvez spécifier le nom du fichier. Cela est utile si le nom de fichier entre en conflit avec les noms des bibliothèques de packages standard. Par exemple, si math.py conflit avec le module mathématique.

Comment structurer un test simple

Avant d'écrire des tests, vous devez résoudre quelques questions:

  1. Que voulez-vous tester?
  2. Ecrivez-vous un test unitaire ou un test d'intégration?

Vous testez actuellement sum() . Vous pouvez tester différents comportements pour cela, par exemple:

  • Est-il possible de rĂ©sumer une liste d'entiers?
  • Est-il possible de rĂ©sumer un tuple ou un ensemble?
  • Puis-je rĂ©sumer une liste de nombres Ă  virgule flottante?
  • Que se passe-t-il si vous donnez une mauvaise valeur Ă  l'entrĂ©e: un seul entier ou une chaĂ®ne?
  • Que se passe-t-il si l'une des valeurs est nĂ©gative?

Le moyen le plus simple de tester est une liste d'entiers. Créez un fichier test.py avec le code suivant:

 import unittest from my_sum import sum class TestSum(unittest.TestCase): def test_list_int(self): """ Test that it can sum a list of integers """ data = [1, 2, 3] result = sum(data) self.assertEqual(result, 6) if __name__ == '__main__': unittest.main() 

Le code dans cet exemple:

  • Importe sum() du package my_sum() que vous avez crĂ©Ă©;
  • DĂ©finit une nouvelle classe de cas de test appelĂ©e TestSum qui hĂ©rite de unittest.TestCase ;
  • DĂ©finit une .test_list_int() test .test_list_int() pour tester une liste entière. La mĂ©thode .test_list_int() fera ce qui suit
:
  1. DĂ©clare une variable de data avec une liste de valeurs (1, 2, 3) ;
  2. my_sum.sum(data) valeur my_sum.sum(data) result variable;
  3. Détermine que la valeur du résultat est 6 à l'aide de la méthode .assertEqual() sur la classe unittest.TestCase .

  • DĂ©finit un point d'entrĂ©e de ligne de commande qui lance le .main() test .main() .

Si vous ne savez pas ce qu'est self ou comment est défini .assertEqual() , vous pouvez actualiser vos connaissances en programmation orientée objet avec la programmation orientée objet Python 3 .

Comment écrire des déclarations

La dernière étape de l'écriture d'un test consiste à vérifier que la sortie correspond aux valeurs connues. C'est ce qu'on appelle une assertion. Il existe plusieurs directives générales pour la rédaction de déclarations:

  • VĂ©rifiez que les tests sont reproductibles et exĂ©cutez-les plusieurs fois pour vous assurer qu'ils donnent les mĂŞmes rĂ©sultats Ă  chaque fois;
  • VĂ©rifiez et confirmez les rĂ©sultats qui s'appliquent Ă  votre saisie - vĂ©rifiez que le rĂ©sultat correspond bien Ă  la somme des valeurs de l'exemple sum() .

Unittest dispose de nombreuses méthodes pour confirmer les valeurs, les types et l'existence de variables. Voici quelques-unes des méthodes les plus couramment utilisées:

La méthodeÉquivalent
.assertEqual (a, b)a == b
.assertTrue (x)bool (x) est True
.assertFalse (x)bool (x) est False
.assertIs (a, b)a est b
.assertIsNone (x)x est Aucun
.assertIn (a, b)a en b
.assertIsInstance (a, b)isinstance (a, b)


.assertIs() , .assertIsNone() , .assertIn() et .assertIsInstance() ont des méthodes opposées appelées .assertIsNot() et ainsi de suite.

Effets secondaires

Écrire des tests est plus difficile que de simplement regarder la valeur de retour d'une fonction. Souvent, l'exécution de code modifie d'autres parties de l'environnement: attributs de classe, fichiers du système de fichiers, valeurs dans la base de données. Il s'agit d'une partie importante des tests appelés effets secondaires. Décidez si vous testez un effet secondaire avant de l'inclure dans votre liste de revendications.

Si vous constatez qu'il y a beaucoup d'effets secondaires dans le bloc de code que vous souhaitez tester, alors vous violez le principe de responsabilité unique . La violation du principe de la responsabilité unique signifie qu'un morceau de code fait trop de choses et nécessite une refactorisation. Suivre le principe de la responsabilité exclusive est un excellent moyen de concevoir du code pour lequel il ne sera pas difficile d'écrire des tests unitaires simples et reproductibles et, finalement, de créer des applications fiables.

Lancement du premier test

Vous avez créé le premier test et vous devez maintenant essayer de l'exécuter. Il est clair qu'il sera réussi, mais avant de créer des tests plus complexes, vous devez vous assurer que même de tels tests réussissent.

Exécution d'exécuteurs de test

Test Executor - Une application Python qui exécute le code de test, valide les assertions et affiche les résultats des tests dans la console. À la fin de test.py, ajoutez ce petit morceau de code:

 if __name__ == '__main__': unittest.main() 

Il s'agit du point d'entrée de ligne de commande. Si vous exécutez ce script en exécutant python test.py sur la ligne de commande, il appellera unittest.main() . Cela démarre le testeur unittest.TestCase détectant toutes les classes de ce fichier qui héritent de unittest.TestCase .

C'est l'une des nombreuses façons d'exécuter le lanceur de test le plus complet. Si vous avez un seul fichier de test appelé test.py , appeler python test.py est un excellent moyen de commencer.

Une autre façon consiste à utiliser la ligne de commande la plus uniforme. Essayons:

 $ python -m unittest test 

Cela exécutera le même module de test (appelé test ) via la ligne de commande. Vous pouvez ajouter des paramètres supplémentaires pour modifier la sortie. L'un d'eux est -v pour verbeux. Essayons ce qui suit:

 $ python -m unittest -v test test_list_int (test.TestSum) ... ok ---------------------------------------------------------------------- Ran 1 tests in 0.000s 

Nous avons exécuté un test à partir de test.py et publié les résultats sur la console. Le mode détaillé répertorie les noms des tests effectués et les résultats de chacun d'eux.

Au lieu de fournir le nom du module contenant les tests, vous pouvez demander la découverte automatique à l'aide de ce qui suit:

 $ python -m unittest discover 

Cette commande recherchera dans le répertoire courant les fichiers avec test*.py dans le nom pour les tester.

Si vous disposez de plusieurs fichiers de test et que vous suivez le modèle de dénomination test*.py , vous pouvez transmettre le nom du répertoire à l'aide de l'indicateur -s et du nom du dossier.

 $ python -m unittest discover -s tests 

unittest exécutera tous les tests dans un seul plan de test et produira les résultats.
Enfin, si votre code source n'est pas dans le répertoire racine, mais dans un sous-répertoire, par exemple, dans un dossier appelé src /, vous pouvez dire à unittest où exécuter les tests à l'aide de l'indicateur -t pour importer correctement les modules:

 $ python -m unittest discover -s tests -t src 

unittest trouvera tous test*.py fichiers test*.py dans le répertoire src/ intérieur des tests , puis les exécutera.

Comprendre les résultats des tests

C'était un exemple très simple où tout s'est bien passé, essayons donc de comprendre la sortie d'un test qui a échoué.

sum() doit accepter d'autres listes de type numérique, par exemple des fractions.

Au début du code dans test.py ajoutez une expression pour importer le type Fraction depuis le module fractions de la bibliothèque standard.

 from fractions import Fraction 

Ajoutez maintenant un test avec une instruction, en attendant une valeur incorrecte. Dans notre cas, nous nous attendons Ă  ce que la somme de ÂĽ, ÂĽ et â…– soit Ă©gale Ă  1:

 import unittest from my_sum import sum class TestSum(unittest.TestCase): def test_list_int(self): """ Test that it can sum a list of integers """ data = [1, 2, 3] result = sum(data) self.assertEqual(result, 6) def test_list_fraction(self): """ Test that it can sum a list of fractions """ data = [Fraction(1, 4), Fraction(1, 4), Fraction(2, 5)] result = sum(data) self.assertEqual(result, 1) if __name__ == '__main__': unittest.main() 

Si vous exécutez à nouveau les tests avec le test python -m unittest, obtenez les éléments suivants:

 $ python -m unittest test F. ====================================================================== FAIL: test_list_fraction (test.TestSum) ---------------------------------------------------------------------- Traceback (most recent call last): File "test.py", line 21, in test_list_fraction self.assertEqual(result, 1) AssertionError: Fraction(9, 10) != 1 ---------------------------------------------------------------------- Ran 2 tests in 0.001s FAILED (failures=1) 

Dans cette sortie, vous voyez ce qui suit:

  • La première ligne montre les rĂ©sultats de tous les tests: un Ă©chouĂ© (F), un rĂ©ussi (.);
  • FAIL montre quelques dĂ©tails de l'Ă©chec du test:

  1. Le nom de la méthode de test ( test_list_fraction );
  2. Module de test ( test ) et cas de test ( TestSum );
  3. Chaînes de trace avec une erreur;
  4. Détails de la déclaration avec le résultat attendu (1) et le résultat réel (fraction (9, 10))

N'oubliez pas que vous pouvez ajouter des informations supplémentaires à la sortie de test à l'aide de l'indicateur -v à la python -m unittest .

Exécution de tests de PyCharm

Si vous utilisez l'IDE PyCharm, vous pouvez exécuter unittest ou pytest en procédant comme suit:

  1. Dans la fenêtre de l'outil Projet, sélectionnez le répertoire des tests.
  2. Dans le menu contextuel, sélectionnez la commande d'exécution la plus unitaire. Par exemple, «Unittests in my Tests ...».

Cela exécutera unittest dans la fenêtre de test et retournera les résultats dans PyCharm:



Plus d'informations sont disponibles sur le site Web de PyCharm .

Exécution de tests à partir du code Visual Studio

Si vous utilisez l'IDE de code Microsoft Visual Studio, la prise en charge de unittest, nose et pytest est déjà intégrée au plug-in Python.

Si vous l'avez installé, vous pouvez configurer la configuration du test en ouvrant la palette de commandes avec Ctrl + Maj + P et en écrivant «Test Python». Vous verrez une liste d'options:



Sélectionnez Déboguer tous les tests unitaires, après quoi VSCode enverra une demande de configuration du cadre de test. Cliquez sur l'engrenage pour sélectionner le lanceur de test (unittest) et le répertoire personnel (.).

Une fois la configuration terminée, vous verrez l'état des tests en bas de l'écran et vous pouvez accéder rapidement aux journaux de test et redémarrer les tests en cliquant sur les icônes:



Nous constatons que les tests sont en cours, mais certains ont échoué.

LA FIN

Dans la prochaine partie de l'article, nous examinerons les tests de frameworks tels que Django et Flask.

Nous attendons vos questions et commentaires ici et, comme toujours, vous pouvez vous rendre à Stanislav lors d'une journée portes ouvertes .

Deuxième partie

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


All Articles