Skip to content

Commit 422f8a6

Browse files
Game development with TDD
0 parents  commit 422f8a6

File tree

6 files changed

+662
-0
lines changed

6 files changed

+662
-0
lines changed

README.fr.md

+313
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
# Développements Pilotés par les Tests
2+
3+
Le développement piloté par les tests (alias **TDD** Test Driven Development) est un processus de développement logiciel qui s'appuie sur la répétition d'un cycle de développement très court : red-green-refactor. L'idée de ce processus est de transformer une fonctionnalité du code en un ou deux cas de test spécifiques, d'exécuter ces tests pour s'assurer qu'ils sont rouges (red), puis d'implémenter le code pour rendre ces tests verts (green). Une troisième étape consiste à refactoriser le code tout en gardant les tests verts.
4+
5+
![](https://res.cloudinary.com/wagon/image/upload/v1560715040/tdd_y0eq2v.png)
6+
7+
Le modèle de test recommandé est en quatre phases et est décrit dans cet [article de blog de Thoughtbot](https://robots.thoughtbot.com/four-phase-test)
8+
9+
## Le mot le plus long
10+
11+
Pratiquons le TDD avec un jeu simple que nous utiliserons jusqu'à la fin de la journée. Nous allons mettre en œuvre "Le mot le plus long", un jeu où, à partir d'une liste de neuf lettres, vous devez trouver le plus long mot anglais possible formé par ces lettres.
12+
13+
Exemple :
14+
15+
```
16+
Grid: OQUWRBAZE
17+
Longest word: BAROQUE
18+
```
19+
20+
Le mot [`baroque`](https://en.wiktionary.org/wiki/baroque) est valide car il existe dans le dictionnaire anglais (même si son origine est française 🇫🇷 😋).
21+
22+
Notez que le mot [`bower`](https://en.wiktionary.org/wiki/bower) est également valide. Le but ici n'est **pas** d'écrire un code qui trouve le mot le plus long, mais d'analyser la tentative du joueur humain et de juger si ce mot est valide ou non par rapport à la grille donnée !
23+
24+
### Une première approche
25+
26+
Nous devons **décomposer** le problème en petits morceaux. Nous devons également trouver le bon niveau de **modélisation** par rapport au paradigme Orienté Objet.
27+
28+
Dans le paradigme TDD, une question que nous nous posons toujours est :
29+
30+
> Comment puis-je le tester ?
31+
32+
Se poser cette question signifie que vous devez considérer votre code comme une boîte noire. Il prendra certains paramètres en entrée et vous observerez la sortie, en la comparant à un résultat attendu.
33+
34+
❓ Prenez quelques minutes pour réfléchir aux **deux fonctions principales** de notre jeu.
35+
36+
<details><summary markdown="span">Voir la solution
37+
</summary>
38+
39+
Nous avons besoin d'une première fonction pour construire une grille de neuf lettres aléatoires :
40+
41+
```python
42+
def random_grid():
43+
pass
44+
```
45+
46+
Nous avons aussi besoin d'une autre fonction qui, à partir d'une grille de neuf lettres, indique si un mot est valide :
47+
48+
```python
49+
def is_valid(word, grid):
50+
pass
51+
```
52+
53+
</details>
54+
55+
<br>
56+
57+
❓ Comment pouvons-nous utiliser le paradigme Orienté Objet sur ce problème ? Encore une fois, prenez le temps d'y réfléchir.
58+
59+
<details><summary markdown='span'>Voir la solution
60+
</summary>
61+
62+
Nous pouvons créer une classe `Game` qui aura le modèle suivant :
63+
64+
1. Générer et maintenir une liste aléatoire de 9 lettres
65+
1. Testez la validité d'un mot par rapport à cette grille
66+
67+
</details>
68+
69+
<br>
70+
71+
### Démarrer le projet en TDD
72+
73+
Maintenant que nous avons une meilleure idée de l'objet que nous voulons construire, nous pouvons commencer à écrire un test. Tout d'abord, créons un nouveau projet Python :
74+
75+
```bash
76+
cd ~/code/<user.github_nickname>
77+
mkdir longest-word && cd $_
78+
pipenv --python 3.8
79+
pipenv install nose pylint --dev
80+
pipenv install --pre --dev astroid # Fix for https://github.com/PyCQA/pylint/issues/2241
81+
82+
touch game.py
83+
mkdir tests
84+
touch tests/test_game.py
85+
86+
code .
87+
```
88+
89+
Créons notre classe de test, héritant de [`unittest.TestCase`](https://docs.python.org/3.8/library/unittest.html#basic-example)
90+
91+
```python
92+
# tests/test_game.py
93+
import unittest
94+
import string
95+
from game import Game
96+
97+
class TestGame(unittest.TestCase):
98+
def test_game_initialization(self):
99+
new_game = Game()
100+
grid = new_game.grid
101+
self.assertIsInstance(grid, list)
102+
self.assertEqual(len(grid), 9)
103+
for letter in grid:
104+
self.assertIn(letter, string.ascii_uppercase)
105+
106+
```
107+
108+
Lisez ce code. Si vous avez _des_ questions à son sujet, demandez à un professeur. Vous pouvez copier/coller ce code dans `tests/test_game.py`.
109+
110+
Maintenant, il est temps de l'exécuter pour s'assurer que ces tests **échouent** :
111+
112+
```bash
113+
nosetests
114+
```
115+
116+
Et ensuite ? Maintenant, vous devez **lire le message d'erreur**, et essayer de le **corriger**, seulement celui-ci (n'anticipez pas). Faisons le premier ensemble :
117+
118+
```bash
119+
E
120+
======================================================================
121+
ERROR: Failure: ImportError (cannot import name 'Game' from 'game' (/Users/seb/code/ssaunier/longest-word/game.py))
122+
----------------------------------------------------------------------
123+
Traceback (most recent call last):
124+
# [...]
125+
File ".../longest-word/tests/test_game.py", line 2, in <module>
126+
from game import Game
127+
ImportError: cannot import name 'Game' from 'game' (.../longest-word/game.py)
128+
129+
----------------------------------------------------------------------
130+
Ran 1 test in 0.004s
131+
132+
FAILED (errors=1)
133+
```
134+
135+
Le message d'erreur est donc `ImportError : cannot import name 'Game' from 'game'`. Il ne trouve pas le type `Game`.
136+
137+
❓ Comment pouvons-nous le résoudre ?
138+
139+
<details><summary markdown='span'>Voir la solution
140+
</summary>
141+
142+
Nous devons créer une classe `Game` dans le fichier `./game.py` :
143+
144+
```python
145+
# game.py
146+
# pylint: disable=missing-docstring
147+
148+
class Game:
149+
pass
150+
```
151+
152+
</details>
153+
154+
<br>
155+
156+
Exécutons à nouveau les tests :
157+
158+
```bash
159+
nosetests
160+
```
161+
162+
Nous obtenons ce message d'erreur :
163+
164+
```
165+
E
166+
======================================================================
167+
ERROR: test_game_initialization (test_game.TestGame)
168+
----------------------------------------------------------------------
169+
Traceback (most recent call last):
170+
File ".../longest-word/tests/test_game.py", line 7, in test_game_initialization
171+
grid = new_game.grid
172+
AttributeError: 'Game' object has no attribute 'grid'
173+
174+
----------------------------------------------------------------------
175+
Ran 1 test in 0.004s
176+
177+
FAILED (errors=1)
178+
```
179+
180+
🎉 NOUS PROGESSONS !!! Nous avons un **nouveau** message d'erreur : `AttributeError: 'Game' object has no attribute 'grid'`.
181+
182+
![](https://res.cloudinary.com/wagon/image/upload/v1560715000/new-error_pvqomj.jpg)
183+
184+
### A votre tour !
185+
186+
Vous avez compris cette boucle de rétroaction rapide ? Nous exécutons le test, nous obtenons un message d'erreur, nous trouvons un moyen de le corriger, nous exécutons à nouveau le test et nous passons à un nouveau message d'erreur !
187+
188+
❓ Essayez d'implémenter le code de la classe `Game` pour faire passer ce test. Ne regardez pas encore la solution, essayez d'appliquer le TDD sur ce problème !
189+
190+
💡 Vous pouvez utiliser `print()` ou `import pdb; pdb.set_trace()`en association avec `nosetests -s`.
191+
192+
<details><summary markdown='span'>Voir la solution
193+
</summary>
194+
195+
Une des implémentations possibles est :
196+
197+
```python
198+
# game.py
199+
# pylint: disable=missing-docstring
200+
201+
import string
202+
import random
203+
204+
class Game:
205+
def __init__(self):
206+
self.grid = []
207+
for _ in range(9):
208+
self.grid.append(random.choice(string.ascii_uppercase))
209+
```
210+
211+
</details>
212+
213+
<br>
214+
215+
## Vérifier la validité d'un mot
216+
217+
Passons à la deuxième méthode de notre classe `Game`.
218+
219+
Nous utilisons le **TDD**, ce qui signifie que nous devons écrire le test **en premier**. Pour le premier test, nous vous avons donné le code.
220+
221+
❓ C'est à votre tour d'implémenter un test pour cette nouvelle méthode `is_valid(self, word)` ! Vous voyez, nous vous avons déjà donné la [signature](https://en.wikipedia.org/wiki/Type_signature#Method_signature) de la méthode...
222+
223+
<details><summary markdown='span'>Voir la solution
224+
</summary>
225+
226+
Une implémentation possible de ce test serait :
227+
228+
```python
229+
# tests/test_game.py
230+
231+
# [...]
232+
233+
def test_empty_word_is_invalid(self):
234+
new_game = Game()
235+
self.assertIs(new_game.is_valid(''), False)
236+
237+
def test_is_valid(self):
238+
new_game = Game()
239+
new_game.grid = list('KWEUEAKRZ') # Forcer la grille à un scénario de test :
240+
self.assertIs(new_game.is_valid('EUREKA'), True)
241+
self.assertEqual(new_game.grid, list('KWEUEAKRZ')) # S'assurer que la grille n'a pas été modifiée
242+
243+
def test_is_invalid(self):
244+
new_game = Game()
245+
new_game.grid = list('KWEUEAKRZ') # Forcer la grille à un scénario de test :
246+
self.assertIs(new_game.is_valid('SANDWICH'), False)
247+
self.assertEqual(new_game.grid, list('KWEUEAKRZ')) # S'assurer que la grille n'a pas été modifiée
248+
```
249+
</details>
250+
251+
<br>
252+
253+
Exécutez les tests pour vous assurer qu'ils ne passent pas :
254+
255+
```bash
256+
nosetests
257+
```
258+
259+
❓ C'est à votre tour ! Mettez à jour l'implémentation de `game.py` pour que les tests passent !
260+
261+
<details><summary markdown='span'>Voir la solution
262+
</summary>
263+
264+
Une implémentation possible est :
265+
266+
```python
267+
# game.py
268+
269+
# [...]
270+
271+
def is_valid(self, word):
272+
if not word:
273+
return False
274+
letters = self.grid.copy() # Consume letters from the grid
275+
for letter in word:
276+
if letter in letters:
277+
letters.remove(letter)
278+
else:
279+
return False
280+
return True
281+
```
282+
283+
</details>
284+
285+
<br>
286+
287+
288+
## Style
289+
290+
Assurez-vous de rendre `pylint` content :
291+
292+
```bash
293+
pipenv run pylint game.py
294+
```
295+
296+
Vous pouvez désactiver ces règles :
297+
298+
```python
299+
# pylint: disable=missing-docstring
300+
# pylint: disable=too-few-public-methods
301+
```
302+
303+
## C'est terminé !
304+
305+
Avant de passer à l'exercice suivant, sauvegardez votre avancement avec ce qui suit :
306+
307+
```bash
308+
cd ~/code/<user.github_nickname>/reboot-python
309+
cd 02-Best-Practices/02-TDD
310+
touch DONE.md
311+
git add DONE.md && git commit -m "02-Best-Practices/02-TDD done"
312+
git push origin master
313+
```

0 commit comments

Comments
 (0)