Introducció

El desenvolupament basat en proves és un conjunt de tècniques que qualsevol programador pot seguir, que fomenta dissenys senzills i suites de proves que inspiren confiança. Si ets un geni, no necessites aquestes regles. Si sou un ximple, les regles no us ajudaran. Tanmateix, per a la gran majoria de nosaltres entremig, seguir aquestes dues regles senzilles ens pot portar a treballar molt més a prop del nostre potencial:

  • Escriviu una prova automatitzada fallida abans d'escriure cap codi
  • Elimina la duplicació

Com fer-ho exactament, les gradacions subtils a l'hora d'aplicar aquestes regles i la longitud a la qual podeu empènyer aquestes dues regles senzilles són el tema d'aquest document.

El ritme del desenvolupament basat en proves és:

  1. Afegeix ràpidament una prova
  2. Executeu totes les proves i comproveu que la nova falla
  3. Fes un petit canvi
  4. Executeu totes les proves i comproveu que totes tenen èxit
  5. Refactoritza per eliminar la duplicació

Escriurem una funció que dividirà dos nombres. Però no comencem amb funcions, comencem amb proves.

Quan escrivim una prova, imaginem la interfície perfecta per al nostre funcionament. Ens estem explicant una història sobre com es veurà l'operació des de fora. La nostra història no sempre es farà realitat, però és millor començar des de la millor API possible i treballar enrere que fer les coses complicades, lletjos i "realistes" des del primer moment.

Per tant, el primer pas és instal·lar el mòdul pytest. Fa que sigui fàcil escriure proves petites i llegibles i es pot escalar per suportar proves funcionals complexes per a aplicacions i biblioteques.

Executeu l'ordre següent a la vostra línia d'ordres:

$ python3 -m venv .venv
$ source .venv/bin/activate
$ pip install pytest

Creeu un fitxer nou anomenat test_division.py, que conté una test:

def test_division():
   assert division(6,3) == 2

I executa pytest:

$ pytest
>   	assert division(6,3) == 2
E   	NameError: name 'division' is not defined

El test que acabem d'escriure ni tan sols es compila. Això és prou fàcil d'arreglar. Què és el mínim que podem fer perquè es compile, encara que no s'executi?

Necessitem una implementació bàsica de division().

De nou farem el mínim de treball possible només per fer que la prova es compila:

def division(a,b):
   0

def test_division():
   assert division(6,3) == 2

Ara podem executar la prova i veure que falla.

$ pytest
>   	assert division(6,3) == 2
E   	assert None == 2
E    	+  where None = division(6, 3)

test_division.py:6: AssertionError

El nostre marc de proves ha executat el petit fragment de codi amb el qual vam començar i ens vam adonar que tot i que esperàvem "2" com a resultat, vam veure "Cap". Tristesa.

No, no. El fracàs és progrés. Ara tenim una mesura concreta del fracàs. Això és millor que només saber vagament que estem fallant. El nostre problema de programació s'ha transformat de "fer una divisió" a "fer que aquesta prova funcioni i després fer que la resta de proves funcionin". Molt més senzill. Un marge molt més petit per a la por. Podem fer que aquesta prova funcioni.

Probablement no us agradarà la solució, però l'objectiu ara mateix no és obtenir la resposta perfecta, l'objectiu és aprovar la prova. Més tard farem el nostre sacrifici a l'altar de la veritat i la bellesa.

Aquí teniu el canvi més petit que podria imaginar que faria que la nostra prova passés:

def division(a,b):
   2

I la prova falla. Que passa?

En alguns llenguatges una funció sempre retorna un resultat que és la útlima sentència, però per lo vist en Python no i ho hem d'indicar de manera explícita amb return.

Gràcies a escriure la prova primer, l'hem après. Ho podem arreglar:

def division(a,b):
   return 2

def test_division():
   assert division(6,3) == 2

I la nostra prova passa:

$ pytest
test_division.py .                                                   	[100%]
==================== 1 passed in 0.01s =======================

Però, podem estar segurs que la funció està ben implementada? Escrivim un altre afirmació per provar la funció de divisió perquè qui sap...

def test_division():
   assert division(6,3) == 2
   assert division(9,3) == 3

I quan provem la nostra funció tenim una petita sorpresa. La prova ha fallat!

$ pytest
E   	assert 2 == 3
E    	+  where 2 = division(9, 3)
test_division.py:7: AssertionError

Bé, hi ha alguna cosa malament en la nostra funció de divisió. Fixem la funció de divisió:

def division(a,b):
   return a / b

Ara tornem a obtenir la barra verda:

============================== 1 passat en 0,01 s =============== ========

Et semblen massa petits aquests passos? Recordeu que el TDD no es tracta de fer petits passos, sinó de poder fer petits passos.

Codificaria el dia a dia amb passos tan petits? No, però quan les coses es fan una mica estranyes, estic content de poder-ho.

Proveu petits passos amb un exemple de la vostra pròpia elecció. Si podeu fer passos massa petits, segur que podeu fer els passos de la mida adequada. Si només feu passos més grans, mai sabreu si els passos més petits són adequats.

Perquè què passarà si fem aquesta afirmació?

assert division(5, 2) == 2.5

Amb Python el quocient retornat per l'operador / sempre és un float encara que els operands siguin int i el resultat pugui ser representat amb un int.

Però això no és cert en altres idiomes. Per exemple, en Java si dividim dos int el resultat és un int, en aquest cas el resultat seria 2.

I què passa amb division(2,3) i la division(4,0)? Quin resultat esperes?

assert division(2, 3) == ???
assert division(4, 0) == ???

Amb totes aquestes proves implementades, podeu estar segur que si alguna cosa canvia a la funció de divisió ho sabràs.

Sí, sé que l’operador / no canviarà el seu comportament. Però les funcions no són tan senzilles, i algunes són molt complexes i depenen d'altres funcions i biblioteques. Podeu estar segur que executant totes les proves que no s'esperava que no hagi canviat.

Aprèn fent tests

Un altre gran ús de les proves de programari és provar les vostres hipòtesis sobre com funciona el programari que s'està provant, que pot incloure provar la vostra comprensió dels mòduls i paquets de tercers, i fins i tot de les estructures de dades de Python integrades.

namedtuple() és un funció disponible a collections. Us permet crear subclasses tuples ambcamps anomenats.

Pots accedir als valors d'una tupla anomenada determinada utilitzant la notació de punts i els noms dels camps, com a obj.attr.

namedtuplees va crear per millorar la llegibilitat del codi proporcionant una manera d'accedir als valors mitjançant noms de camps descriptius en lloc d'índexs enters, que la majoria de vegades no proporcionen cap context sobre quins són els valors. Aquesta característica també fa que el codi sigui més net i més fàcil de mantenir.

Per crear una nova namedtuple, heu de proporcionar dos arguments posicionals a la funció:

  1. typename proporciona el nom de classe per la namedtuple retornat per namedtuple(). Heu de passar un string amb aidentificador de Python vàlid a aquest argument.

  2. field_names proporciona els noms de camp que utilitzareu per accedir als valors de la tupla

Per obtenir més informació sobre namedtuple llegiu això: Write Pythonic and Clean Code With namedtuple

Comenceu amb aquest fitxer de prova:

"""Test the Task data type."""
from collections import namedtuple

Task = namedtuple('Task',['summary','owner','done','id'])

def test_defaults():
   """Using no parameters should invoke defaults."""
   t1 = Task()
   t2 = Task(None, None, False, None)
   assert t1 == t2

Suposo que la tupla anomenada es crearà amb valors predeterminats. Perquè no? Python és un llenguatge amigable. Però som desenvolupadors de TDD, així que deixarem que pytest comprove si aquesta suposició és certa:

$ pytest
FAILED test_task.py::test_defaults - TypeError: Task.__new__() missing 4 required positional arguments: 'summary...
============================== 1 failed in 0.02s ===============================

I estàs veient la temida barra vermella.

El problema de Python és que és un llenguatge amigable sense escriptura estàtica, de manera que no pot saber quins tipus fan les nostres tuples i inferir quins valors predeterminats utilitzar. Després d'aprendre sobre namedtuple, he descobert que pots utilitzar:

Task = namedtuple('Task',['summary','owner','done','id'])
Task.__new__.__defaults__ = (None,None,False,None)

Ara tenim la barra verda, legendaria en cançons i història:

$ pytest
test_task.py .                                                       	[100%]
============================== 1 passed in 0.01s ===============================

Ara provarem com accedir als membres per nom i no per índex, que és un dels motius principals per utilitzar namedtuples. Afegiu aquesta prova a test_task.py:

def test_member_access():
   """Check .field functionality of namedtuple."""
 
   t = Task('buy milk', 'brian')
   assert t.summary == 'buy milk'
   assert t.owner == 'brian'
   assert (t.done,t.id) == (False,None)
$ pytest
collected 2 items                                                         	 
test_task.py ..                                                      	[100%]
============================== 2 passed in 0.01s ===============================

La prova passa, però el més important és que la prova anterior també es prova i encara s'aprova. Podeu convertir les instàncies de tuple amb nom existents en diccionaris utilitzant ._asdict(). Aquest mètode retorna un diccionari nou que utilitza els noms dels camps com a claus. Les claus del diccionari resultant estan en el mateix ordre que els camps del namedtuple original

def test_as_dict():

    Person = namedtuple("Person", "name age height")
    jane = Person("Jane", 25, 1.75)
    assert jane._asdict() == ""

Com que les tuples amb nom són immutables, pot semblar contraintuïtiu que l'objecte namedtuple ve amb el mètode ._replace(), que us permet substituir valors en una tupla amb nom. La forma en què això funciona és que anou es crea una tupla anomenada. L'avantatge d'aquest enfocament és que ens permet modificar només valors específics, tot conservant els valors originals

def test_replace():
  
   Person = namedtuple('Person', ['name', 'age', 'location', 'profession'])

   mike = Person('Mike', 33, 'Toronto', 'Veterinari')
   assert mike.age == 33

   newMike = mike._replace(age=44)
   assert newMike.age == 44

   assert mike.age == 33
   assert mike.age != newMike.age

Activitats

1.- Explora mitjançant tests com funcionen les llistes en python: Python List (With Examples)

def test_asdict():
   """_asdict() should return a dictionary."""

   task = Task('do something', 'tokken', True, 21)
   dict = task._asdict()
   expected =  {
    'summary':'do something',
    'owner': 'tokken',
    'done': True,
    'id': 21
   }

   assert dict == expected

2.- Explora mitjançant tests com funcionen els diccionaris en python: Dictionaries in Python

def test_replace():
   """replace() should change passed in fields."""
      
   task = Task('finish book', 'brian', False)
   task = task._replace(id=10, done=True)
   expected = Task('finish book', 'brian', True, 10)
      
   assert task == expected

Saber-ne més