Hoofdstuk 11 - Code Testen

01 - Een functie testen
def get_formatted_name(first, last):
    """Maak een net opgemaakte naam"""
    full_name = first + ' ' + last
    return full_name.title()
  • Om te controleren of bovenstaande functie ook werkt, kun je een programma maken dat deze functie gebruikt
def get_formatted_name(first, last):
    """Maak een net opgemaakte naam"""
    full_name = first + ' ' + last
    return full_name.title()

print("Enter 'q' om te stoppen")
while True:
    first = input("\nType een voornaam: ")
    if first == 'q':
        break

    last = input("Type een achternaam: ")
    if last == 'q':
        break

    formatted_name = get_formatted_name(first, last)
    print("\tDe opgegeven naam is : " + formatted_name + '.')
  • Bovenstaande code verwerkt de namen goed
  • Python heeft een efficiënte manier om het testen van de uitvoer van een functie te automatiseren
  • De module unittest uit de standaard bibliotheek van Python biedt hulpmiddelen voor het testen van je code
  • Een unit-test controleert of een specifiek aspect van het gedrag van je functie correct is
  • Een test-case is een verzameling unit-tests die gezamenlijk aantonen dat een functie goed werkt
  • Een goede test-case houdt rekening met alle soorten invoer die een functie kan ontvangen
  • Een test-case met full coverage kan bij grote projecten intimiderend zijn
  • Vaak is het voldoende om tests te schrijven voor het kritieke gedrag van je code
import unittest

def get_formatted_name(first, last):
    """Maak een net opgemaakte naam"""
    full_name = first + ' ' + last
    return full_name.title()


class NamesTestCase(unittest.TestCase):
    """Test voor de get_formatted_name()-functie"""

    def test_first_last_name(self):
        formatted_name = get_formatted_name('claes', 'compaen')
        self.assertEqual(formatted_name, 'Claes Compaen')

unittest.main()
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
  • Je kunt elke naam kiezen als naam voor je class, maar het beste verwerk je Test in de naam
  • De class moet overerven van de class unittest.TestCase
  • Elke methode die begint met test_ in de naamgeving zal automatisch worden uitgevoerd bij het aanroepen van de class
  • Er zit één methode in de class die controleert of de naam correct wordt geretourneerd
  • Je kunt in de test-methode de functie aanroepen die je wilt testen
  • Stel je wilt de functie aanpassen met een tweede voornaam
def get_formatted_name(first, middle, last):
    """Maak een net opgemaakte naam"""
    full_name = first + ' ' + middle + ' ' + last
    return full_name.title()
  • Deze versie werkt wel voor mensen met een tweede naam maar werkt niet meer voor mensen zonder tweede naam
  • Als je nu de test erop zou uitvoeren krijgen we een melding dat de test niet slaagt
E
======================================================================
ERROR: test_first_last_name (__main__.NamesTestCase.test_first_last_name)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<exec>", line 13, in test_first_last_name
TypeError: get_formatted_name() missing 1 required positional argument: 'last'
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (errors=1)
  • De uitgebreide foutmelding laat precies zien welke unit test een error geeft
  • Wanneer er veel unit tests worden uitgevoerd, is een uitgebreide foutmelding belangrijk
  • Er wordt ook vermeld hoeveel unit tests er zijn uitgevoerd: Ran 1 test
  • Repareer de fout door de tweede naam optioneel te maken
def get_formatted_name(first, last, middle=''):
    """Maak een net opgemaakte naam"""
    if middle:
        full_name = first + ' ' + middle + ' ' + last
    else:
        full_name = first + ' ' + last
    return full_name.title()
  • Na deze wijziging zal de test weer goed werken als iemand alleen één voornaam en achternaam opgeeft
  • Maar deze nieuwe mogelijkheid moet op zijn beurt ook getest worden
import unittest

def get_formatted_name(first, last, middle=''):
    """Maak een net opgemaakte naam"""
    if middle:
        full_name = first + ' ' + middle + ' ' + last
    else:
        full_name = first + ' ' + last
    return full_name.title()


class NamesTestCase(unittest.TestCase):
    """Test voor de get_formatted_name()-functie"""

    def test_first_last_name(self):
        """Test één voornaam en achternaam"""
        formatted_name = get_formatted_name('claes', 'compaen')
        self.assertEqual(formatted_name, 'Claes Compaen')

    def test_first_last_middle_name(self):
        """Test twee voornamen en achternaam"""
        formatted_name = get_formatted_name('Jan', 'Reyning', 'Erasmus')
        self.assertEqual(formatted_name, 'Jan Erasmus Reyning')


unittest.main()
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK
  • Het is logischer om de test class niet bij het programma te zetten maar in een aparte module
  • De naam van methoden in de test class mogen best lang zijn, ze worden immers automatisch aangeroepen
02 - Een class testen
  • Python biedt een aantal assert-methoden in de unittest.TestCase-class
  • Assert-methoden testen of een conditie, waarvan jij denkt dat die waar moet zijn, ook daadwerkelijk waar is
Methode Doel Voorbeeld
assertEqual(a, b) Controleert of a == b self.assertEqual(2 + 2, 4)
assertNotEqual(a, b) Controleert of a != b self.assertNotEqual(len("abc"), 5)
assertTrue(x) Controleert of x waar is self.assertTrue(3 < 5)
assertFalse(x) Controleert of x onwaar is self.assertFalse(10 in [1, 2, 3])
assertIn(a, b) Controleert of a in b zit self.assertIn("py", "python")
assertNotIn(a, b) Controleert of a niet in b zit self.assertNotIn(4, [1, 2, 3])
assertGreater(a, b) Controleert of a > b self.assertGreater(10, 5)
assertGreaterEqual(a, b) Controleert of a ≥ b self.assertGreaterEqual(5, 5)
assertLess(a, b) Controleert of a < b self.assertLess(2, 10)
assertLessEqual(a, b) Controleert of a ≤ b self.assertLessEqual(3, 3)
assertAlmostEqual(a, b) Vergelijkt afrondingsverschil (handig bij floats) self.assertAlmostEqual(0.1 + 0.2, 0.3)
assertRaises(Exception) Controleert of een fout wordt opgegooid with self.assertRaises(ValueError): int("abc")
  • Het testen van een class is vergelijkbaar met het testen van een functie
  • In onderstaande code wordt de class getest met invoer van de gebruiker
class Rekenmachine:
    """Eenvoudige rekenmachine die optelt, vermenigvuldigt
    en bijhoudt hoeveel berekeningen zijn uitgevoerd."""

    def __init__(self):
        self.aantal_bewerkingen = 0

    def tel_op(self, x, y):
        self.aantal_bewerkingen += 1
        return x + y

    def vermenigvuldig(self, x, y):
        self.aantal_bewerkingen += 1
        return x * y

    def reset(self):
        """Zet de teller terug naar 0."""
        self.aantal_bewerkingen = 0


# Gebruik van de class Rekenmachine met gebruikersinvoer

rekenmachine = Rekenmachine()

print("Welkom bij de rekenmachine!")
print("Kies: 1 = optellen, 2 = vermenigvuldigen, q = stoppen")

while True:
    keuze = input("\nMaak een keuze: ")

    if keuze == "q":
        print("Programma beëindigd.")
        break

    if keuze not in ("1", "2"):
        print("Ongeldige keuze. Kies 1, 2 of q.")
        continue

    try:
        x = float(input("Eerste getal: "))
        y = float(input("Tweede getal: "))
    except ValueError:
        print("Je moet wel een geldig getal invoeren!")
        continue

    if keuze == "1":
        resultaat = rekenmachine.tel_op(x, y)
        print("Uitkomst:", resultaat)

    elif keuze == "2":
        resultaat = rekenmachine.vermenigvuldig(x, y)
        print("Uitkomst:", resultaat)

    print("Aantal uitgevoerde bewerkingen:", rekenmachine.aantal_bewerkingen)
  • De class kan ook getest worden met een unit-test
  • Het is een goed idee om de unit-test in een andere module te zetten voor het overzicht
# rekenmachine.py

class Rekenmachine:
    """Eenvoudige rekenmachine die optelt, vermenigvuldigt
    en bijhoudt hoeveel berekeningen zijn uitgevoerd."""

    def __init__(self):
        self.aantal_bewerkingen = 0

    def tel_op(self, x, y):
        self.aantal_bewerkingen += 1
        return x + y

    def vermenigvuldig(self, x, y):
        self.aantal_bewerkingen += 1
        return x * y

    def reset(self):
        """Zet de teller terug naar 0."""
        self.aantal_bewerkingen = 0

# test_rekenmachine.py

import unittest
from rekenmachine import Rekenmachine


class TestRekenmachine(unittest.TestCase):
    """Unittests voor de Rekenmachine-class."""

    def setUp(self):
        """Wordt vóór elke test uitgevoerd."""
        self.rm = Rekenmachine()

    def test_tel_op_geeft_juiste_som(self):
        resultaat = self.rm.tel_op(2, 3)
        self.assertEqual(resultaat, 5)

        resultaat = self.rm.tel_op(-1, 1)
        self.assertEqual(resultaat, 0)

        resultaat = self.rm.tel_op(2.5, 0.5)
        self.assertAlmostEqual(resultaat, 3.0)

    def test_vermenigvuldig_geeft_juiste_product(self):
        resultaat = self.rm.vermenigvuldig(4, 5)
        self.assertEqual(resultaat, 20)

        resultaat = self.rm.vermenigvuldig(-2, 3)
        self.assertEqual(resultaat, -6)

        resultaat = self.rm.vermenigvuldig(2.5, 2)
        self.assertAlmostEqual(resultaat, 5.0)

    def test_aantal_bewerkingen_loopt_op(self):
        # In het begin: 0
        self.assertEqual(self.rm.aantal_bewerkingen, 0)

        self.rm.tel_op(1, 2)          # 1 bewerking
        self.assertEqual(self.rm.aantal_bewerkingen, 1)

        self.rm.vermenigvuldig(2, 3)  # 2e bewerking
        self.assertEqual(self.rm.aantal_bewerkingen, 2)

        self.rm.tel_op(10, 10)        # 3e bewerking
        self.assertEqual(self.rm.aantal_bewerkingen, 3)

    def test_reset_zet_teller_terug_naar_nul(self):
        self.rm.tel_op(1, 1)
        self.rm.vermenigvuldig(2, 2)
        self.assertGreater(self.rm.aantal_bewerkingen, 0)

        self.rm.reset()
        self.assertEqual(self.rm.aantal_bewerkingen, 0)


if __name__ == "__main__":
    unittest.main()
Oefening
Quiz

Vraag 1. Welke module gebruik je in Python om unit-tests te schrijven?




Goed! unittest zit in de standaardbibliotheek van Python.
Niet helemaal. Voor dit hoofdstuk gebruiken we unittest uit de standaardbibliotheek.

Vraag 2. Van welke class moet je testclass erven bij unittest?




Klopt. Je testclass erft van unittest.TestCase.
Let op. De basisclass is unittest.TestCase.

Vraag 3. Welke methodenaam wordt automatisch als test uitgevoerd door unittest?




Goed! Methoden die starten met test_ worden automatisch uitgevoerd.
Nee. Alleen methoden die starten met test_ telt unittest als tests.

Vraag 4. Welke assert controleert of twee waarden gelijk zijn?




Precies. assertEqual controleert of a == b.
Niet goed. Gebruik self.assertEqual(a, b) om gelijkheid te testen.

Vraag 5. Welke onderdelen horen bij een minimale unittest-file?




Goed! Dit zijn de belangrijkste bouwstenen van een minimale unittest.
Niet helemaal. Je hebt unittest, een TestCase-class en een test_-methode nodig.

Vraag 6. Wat doet deze regel aan het einde van een testbestand?

unittest.main()



Klopt. unittest.main() zoekt tests en voert ze uit.
Nee. unittest.main() is de “runner” die de tests uitvoert.

Vraag 7. Je wil testen of een fout wordt opgegooid. Welke constructie hoort daarbij?




Goed! Met assertRaises test je of een exception voorkomt.
Niet juist. Gebruik with self.assertRaises(...): om exceptions te testen.

Vraag 8. Wat is het doel van setUp() in een unittest-class?




Precies. setUp() draait vóór elke testmethode.
Let op. setUp() gebruik je om per test alvast objecten/data klaar te zetten.

Vraag 9. Welke output betekent meestal dat al je tests geslaagd zijn?




Goed! OK betekent: alle tests zijn geslaagd.
Nee. E is error, F is failure. Bij succes zie je meestal OK.

Vraag 10. Waarom zet je tests vaak in een apart bestand, zoals test_rekenmachine.py?




Klopt. Het houdt je project overzichtelijk en onderhoudbaar.
Niet helemaal. Je zet tests apart vooral voor overzicht en onderhoud.