Une introduction simple au développement piloté par les tests avec Python

Je suis un développeur débutant autodidacte capable d'écrire des applications simples. Mais j'ai une confession à faire. Il est impossible de se rappeler comment tout est interconnecté dans ma tête.

Cette situation est aggravée si je reviens au code que j'ai écrit après quelques jours. Il s'avère que ce problème pourrait être surmonté en suivant une méthodologie de développement piloté par les tests (TDD).

Qu'est-ce que le TDD et pourquoi est-ce important?

En termes simples, TDD recommande d'écrire des tests qui vérifieraient la fonctionnalité de votre code avant d'écrire le code réel. Ce n'est que lorsque vous êtes satisfait de vos tests et des fonctionnalités qu'il teste que vous commencez à écrire le code réel afin de satisfaire les conditions imposées par le test qui leur permettraient de passer.

Suivre ce processus garantit que vous planifiez soigneusement le code que vous écrivez afin de réussir ces tests. Cela évite également la possibilité que des tests d'écriture soient reportés à une date ultérieure, car ils pourraient ne pas être jugés nécessaires par rapport aux fonctionnalités supplémentaires qui pourraient être créées pendant cette période.

Les tests vous donnent également confiance lorsque vous commencez à refactoriser le code, car vous êtes plus susceptible d'attraper des bogues en raison du retour instantané lorsque les tests sont exécutés.

Comment commencer?

Pour commencer à écrire des tests en Python, nous utiliserons le unittestmodule fourni avec Python. Pour ce faire, nous créons un nouveau fichier mytests.py, qui contiendra tous nos tests.

Commençons par le «bonjour le monde» habituel:

import unittestfrom mycode import *
class MyFirstTests(unittest.TestCase):
def test_hello(self): self.assertEqual(hello_world(), 'hello world')

Notez que nous importons une helloworld()fonction à partir d'un mycodefichier. Dans le fichier, mycode.pynous allons d'abord inclure simplement le code ci-dessous, qui crée la fonction mais ne renvoie rien à ce stade:

def hello_world(): pass

L'exécution python mytests.pygénérera la sortie suivante dans la ligne de commande:

F
====================================================================
FAIL: test_hello (__main__.MyFirstTests)
--------------------------------------------------------------------
Traceback (most recent call last):
File "mytests.py", line 7, in test_hello
self.assertEqual(hello_world(), 'hello world')
AssertionError: None != 'hello world'
--------------------------------------------------------------------
Ran 1 test in 0.000s
FAILED (failures=1)

Cela indique clairement que le test a échoué, ce qui était attendu. Heureusement, nous avons déjà écrit les tests, nous savons donc qu'il sera toujours là pour vérifier cette fonction, ce qui nous donne confiance pour repérer les bogues potentiels à l'avenir.

Pour vous assurer que le code passe, passons mycode.pyà ce qui suit:

def hello_world(): return 'hello world'

En exécutant à python mytests.pynouveau, nous obtenons la sortie suivante dans la ligne de commande:

.
--------------------------------------------------------------------
Ran 1 test in 0.000s
OK

Félicitations! Vous venez de passer votre premier test. Passons maintenant à un défi un peu plus difficile. Nous allons créer une fonction qui nous permettrait de créer une compréhension de liste numérique personnalisée en Python.

Commençons par écrire un test pour une fonction qui créerait une liste de longueur spécifique.

Dans le fichier, mytests.pyce serait une méthode test_custom_num_list:

import unittestfrom mycode import *
class MyFirstTests(unittest.TestCase):
def test_hello(self): self.assertEqual(hello_world(), 'hello world') def test_custom_num_list(self): self.assertEqual(len(create_num_list(10)), 10)

Cela testerait que la fonction create_num_listrenvoie une liste de longueur 10. Créons une fonction create_num_listdans mycode.py:

def hello_world(): return 'hello world'
def create_num_list(length): pass

L'exécution python mytests.pygénérera la sortie suivante dans la ligne de commande:

E.
====================================================================
ERROR: test_custom_num_list (__main__.MyFirstTests)
--------------------------------------------------------------------
Traceback (most recent call last):
File "mytests.py", line 14, in test_custom_num_list
self.assertEqual(len(create_num_list(10)), 10)
TypeError: object of type 'NoneType' has no len()
--------------------------------------------------------------------
Ran 2 tests in 0.000s
FAILED (errors=1)

Ceci est comme prévu, donc nous allons aller de l' avant et la fonction de changement create_num_listdans mytest.pyafin de passer le test:

def hello_world(): return 'hello world'
def create_num_list(length): return [x for x in range(length)]

L'exécution python mytests.pysur la ligne de commande démontre que le deuxième test a également réussi:

..
--------------------------------------------------------------------
Ran 2 tests in 0.000s
OK

Let’s now create a custom function that would transform each value in the list like this: const * ( X ) ^ power . First let’s write the test for this, using method test_custom_func_ that would take value 3 as X, take it to the power of 3, and multiply by a constant of 2, resulting in the value 54:

import unittestfrom mycode import *
class MyFirstTests(unittest.TestCase):
def test_hello(self): self.assertEqual(hello_world(), 'hello world')
def test_custom_num_list(self): self.assertEqual(len(create_num_list(10)), 10) def test_custom_func_x(self): self.assertEqual(custom_func_x(3,2,3), 54)

Let’s create the function custom_func_x in the file mycode.py:

def hello_world(): return 'hello world'
def create_num_list(length): return [x for x in range(length)]
def custom_func_x(x, const, power): pass

As expected, we get a fail:

F..
====================================================================
FAIL: test_custom_func_x (__main__.MyFirstTests)
--------------------------------------------------------------------
Traceback (most recent call last):
File "mytests.py", line 17, in test_custom_func_x
self.assertEqual(custom_func_x(3,2,3), 54)
AssertionError: None != 54
--------------------------------------------------------------------
Ran 3 tests in 0.000s
FAILED (failures=1)

Updating function custom_func_x to pass the test, we have the following:

def hello_world(): return 'hello world'
def create_num_list(length): return [x for x in range(length)]
def custom_func_x(x, const, power): return const * (x) ** power

Running the tests again we get a pass:

...
--------------------------------------------------------------------
Ran 3 tests in 0.000s
OK

Finally, let’s create a new function that would incorporate custom_func_x function into the list comprehension. As usual, let’s begin by writing the test. Note that just to be certain, we include two different cases:

import unittestfrom mycode import *
class MyFirstTests(unittest.TestCase):
def test_hello(self): self.assertEqual(hello_world(), 'hello world')
def test_custom_num_list(self): self.assertEqual(len(create_num_list(10)), 10)
def test_custom_func_x(self): self.assertEqual(custom_func_x(3,2,3), 54)
def test_custom_non_lin_num_list(self): self.assertEqual(custom_non_lin_num_list(5,2,3)[2], 16) self.assertEqual(custom_non_lin_num_list(5,3,2)[4], 48)

Now let’s create the function custom_non_lin_num_list in mycode.py:

def hello_world(): return 'hello world'
def create_num_list(length): return [x for x in range(length)]
def custom_func_x(x, const, power): return const * (x) ** power
def custom_non_lin_num_list(length, const, power): pass

As before, we get a fail:

.E..
====================================================================
ERROR: test_custom_non_lin_num_list (__main__.MyFirstTests)
--------------------------------------------------------------------
Traceback (most recent call last):
File "mytests.py", line 20, in test_custom_non_lin_num_list
self.assertEqual(custom_non_lin_num_list(5,2,3)[2], 16)
TypeError: 'NoneType' object has no attribute '__getitem__'
--------------------------------------------------------------------
Ran 4 tests in 0.000s
FAILED (errors=1)

In order to pass the test, let’s update the mycode.py file to the following:

def hello_world(): return 'hello world'
def create_num_list(length): return [x for x in range(length)]
def custom_func_x(x, const, power): return const * (x) ** power
def custom_non_lin_num_list(length, const, power): return [custom_func_x(x, const, power) for x in range(length)]

Running the tests for the final time, we pass all of them!

....
--------------------------------------------------------------------
Ran 4 tests in 0.000s
OK

Congrats! This concludes this introduction to testing in Python. Make sure you check out the resources below for more information on testing in general.

The code is available here on GitHub.

Useful resources for further learning!

Web resources

Below are links to some of the libraries focusing on testing in Python

25.3. unittest - Unit testing framework - Python 2.7.14 documentation

The Python unit testing framework, sometimes referred to as "PyUnit," is a Python language version of JUnit, by Kent…docs.python.orgpytest: helps you write better programs - pytest documentation

The framework makes it easy to write small tests, yet scales to support complex functional testing for applications and…docs.pytest.orgWelcome to Hypothesis! - Hypothesis 3.45.2 documentation

It works by generating random data matching your specification and checking that your guarantee still holds in that…hypothesis.readthedocs.iounittest2 1.1.0 : Python Package Index

The new features in unittest backported to Python 2.4+.pypi.python.org

YouTube videos

If you prefer not to read, I recommend watching the following videos on YouTube.