Ga naar inhoud
Deel 3: Het vakmanschap 7 min leestijd

Les 10

Testen voor vertrouwen

100% coverage met slechte tests is erger dan 80% met goede

Laten we eerlijk zijn. Je hebt tests geschreven zoals deze:

def test_user_exists():
    user = User(name="Test")
    assert user.name == "Test"

Die test vangt nooit een echte bug. Hij test of Python assignment werkt. Je coverage-getal stijgt. Je vertrouwen blijft op nul.

Goede tests stellen een andere vraag: als deze test slaagt, durf ik dan te deployen?

Wat je altijd moet testen

  • Business rules: de logica die geld oplevert of rechtszaken voorkomt. Mag een project maar een actieve taak per sprint hebben? Test die constraint.
  • Autorisatie: kan gebruiker A echt niet bij de data van gebruiker B? Test de grens. Niet het happy path.
  • Data-isolatie: isoleert multi-tenancy ook echt? Bewijs het met een test die probeert de grens te overschrijden.
  • Edge cases: lege inputs, dubbele entries, gelijktijdige operaties. Hier verstoppen productie-bugs zich.

Wat je kunt overslaan

  • Framework-gedrag: je ORM werkt gewoon. Je hoeft niet te testen dat User.query.get(id) een gebruiker teruggeeft.
  • Triviale getters en setters: als het een property access is, hoeft er geen test bij.
  • UI layout tests: breken bij elke redesign en vangen bijna nooit functionele bugs.

Benoem tests zodat ze gedrag documenteren

Als een test faalt moet de naam je precies vertellen wat er kapot is. Zonder dat je het bestand hoeft te openen.

# Slecht: vertelt je niks als het faalt
def test_task_1():
def test_thing_works():

# Goed: beschrijft gedrag, scenario en verwacht resultaat
def test_create_task_with_past_due_date_raises_validation_error():
def test_deactivate_user_unassigns_all_open_tasks():
def test_admin_can_view_all_workspace_sessions():

Patroon: test_{actie}_{scenario}_{verwacht_resultaat}

Je testnamen worden levende documentatie. Vraagt iemand "wat gebeurt er als een gebruiker twee taken aan hetzelfde sprint slot probeert toe te wijzen?" Dan staat het antwoord in de testnaam.

Twee soorten tests, twee doelen

Unit tests: snel, gefocust, geen database

async def test_calculate_story_points_rounds_to_fibonacci():
    result = estimate_points(raw_score=37)
    assert result == 40  # Rondt af naar dichtstbijzijnde Fibonacci-getal

Test service-logica. Draait in milliseconden. Mockt de database. Vangt logische fouten meteen.

Integratietests: realistisch, met database en RLS

async def test_member_cannot_see_other_workspace_projects(db_session):
    # Stel context in op Workspace A
    await set_workspace_context(db_session, workspace_a_id)

    # Probeer projecten van Workspace B op te vragen
    projects = await project_service.list_projects(db_session)
    assert len(projects) == 0  # RLS blokkeert cross-workspace toegang

Test de volledige stack. Langzamer, maar bewijst dat je beveiliging echt werkt. Dit zijn de tests waardoor je 's nachts kunt slapen.

De coverage-bodem

Zet --cov-fail-under=80 in je CI pipeline. Niet omdat 80% magisch is. Maar omdat een bodem erosie voorkomt. Zonder die grens zakt je coverage elke maand een procent, totdat testen in de praktijk optioneel wordt.

80% is de bodem, niet het plafond. Sommige domeinen (authenticatie, facturatie, data-isolatie) moeten op 95%+ zitten. Andere, zoals simpele CRUD, kunnen op 70% blijven. De bodem houdt het gemiddelde eerlijk.