Göm menyn

Testning

För att försäkra sig om att koden man har skrivit är korrekt bör man använda sig av tester. Det finns olika sätt att testa program på, men vi kommer främst fokusera på enhetstester.

Olika nivåer

Detta är de vanligaste typerna av tester man genomför:

  1. Enhetstest (eng. unit test): Varje enhet (t.ex. funktion) testas var för sig.
  2. Integrationstest: Testar att flera enheter fungerar tillsammans.
  3. Systemtest: Testar hela systemet, t.ex. säkerhet, prestanda, användbarhet.
  4. Acceptanstest: Kunden testar den slutgiltiga produkten.

Två strategier

  • Black box test: Den som utformar testerna har inte sett koden utan utgår helt ifrån specifikationen.
  • White box test: Testerna baseras på analys av programkoden.

Enhetstestning

Hur gör man?

Enhetstester består av ett antal testfall (indata och förväntad utdata) samlade i en testsvit samt kod som kör testfallen. För att skapa enhetstester systematiskt använder man ofta ett testramverk, men dessa är lite för stora för de funktioner vi jobbar med, därför skriver vi egna enhetstester.

Vad uppnår vi med enhetstestning?

  • Försäkrar oss om att implementationen är korrekt.
  • Lättare att underhålla koden. Passerar alla våra tester så vet vi att vår definierade funktionalitet inte har ändrats (förutsatt att testerna är bra skrivna).
  • Underlättar integrationstestning.
  • Ersätter till viss del dokumentation.

Metod 1: Assertion

I exemplet nedan ser vi hur man kan försäkra (eng. assert) att ett uttryck är sant.

Detta är alltså ett påstående vi gör: Så här är det faktiskt!. Men det är inte vilket påstående som helst, utan ett som kan testas automatiskt, eftersom det skrivs som ett uttryck i Python.

Detta kan till exempel användas för att försäkra sig om att en funktion ger den förväntade utdatan givet en viss indata. Genom att använda satsen assert uttryck kommer ett AssertionError kastas om det givna uttrycket har sanningsvärdet False. Om uttrycket har sanningsvärdet True kommer inget att hända.

Dokumentation

def remove(seq, x):
    """ Removes any occurences of the element x from seq or sublists of seq """
    if not seq:
        return []
    elif isinstance(seq[0], list):
        return [remove(seq[0], x)] + remove(seq[1:], x)
    elif seq[0] == x:
        return remove(seq[1:], x)
    else:
        return [seq[0]] + remove(seq[1:], x)

assert remove([], 7) == []
assert remove([1, 2, 3], 2) == [1, 3]
assert remove([1, 2, [3, 2, 4], 5], 2) == [1, [3, 4], 5]

Om vi skriver asserts på toppnivån i filen (längst till vänster) kommer de alltid att köras när filen laddas in. Detta blir dock ganska omständligt om någon annan vill importera vår fil då de måste vänta på att alla tester körs färdigt innan de kan fortsätta. En lösning är att testerna endast körs då filen körs direkt och inte vid importering.

def remove(seq, x):
    """ Removes any occurences of the element x from seq or sublists of seq """
    if not seq:
        return []
    elif isinstance(seq[0], list):
        return [remove(seq[0], x)] + remove(seq[1:], x)
    elif seq[0] == x:
        return remove(seq[1:], x)
    else:
        return [seq[0]] + remove(seq[1:], x)

if __name__ == "__main__":
    assert remove([], 7) == []
    assert remove([1, 2, 3], 2) == [1, 3]
    assert remove([1, 2, [3, 2, 4], 5], 2) == [1, [3, 4], 5]

Detta kan åstadkommas med en if-sats som kollar en av Pythons speciella variabler: __name__. Variabeln sätts till namnet på den modul som importerade filen och om filen körs direkt (som huvudprogram) så har den värdet "__main__".

Assertions kan så klart finnas precis var som helst. Vi behöver alltså inte alltid testa resultatet av enskilda funktionsanrop utan kan också göra andra typer av tester, inuti funktioner:

def remove(seq, x):
    """ Removes any occurences of the element x from seq or sublists of seq. """
    assert isinstance(x, list)
    if not seq:
        return []
    elif isinstance(seq[0], list):
        result = [remove(seq[0], x)] + remove(seq[1:], x)
        assert x not in result
        return result
    elif seq[0] == x:
        result = remove(seq[1:], x)
        assert x not in result
        return result
    else:
        result = [seq[0]] + remove(seq[1:], x)
        assert x not in result
        return result

Om man kör sitt program med python -O kommer assertions att plockas bort ut den kod som faktiskt körs, vilket innebär att de då inte har någon effekt på prestandan. Detta gör det enkelt att välja om koden ska köras med assert-tester påslagna (för att hitta buggar) eller avstängda (för att få bättre prestanda).

Metod 2: Särskild testfunktion

Här har vi skrivit en funktion som använder assertions för att testa koden. Testfallen här består av en tupel med testfall, där varje fall representeras av en tupel som innehåller indata samt förväntad utdata. Testfunktionen säkerställer att om man anropar sort() på indatan ska resultatet se ut som den definierade utdatan, annars misslyckas testet.

def test_sorted():
    """ Performs several tests on the sorted function """
    testcases = (([], []), \
        ([3, 2, 1], [1, 2, 3]), \
        (['c', 'a', 'b'], ['a', 'b', 'c']), \
        (['a', 'b'], ['a', 'b']))

    for case in testcases:
        assert sorted(case[0]) == case[1]

Vilka testfall behövs?

Då det är högst opraktiskt att testa för alla indata måste testfall noga väljas ut. Detta är mycket bra att öva på innan tentan i TDDE24. Är du bra på att hitta testfall, har du lättare att se om den kod du skriver på tentan faktiskt uppfyller kraven.

  • Utforma testfall så att alla delar av funktionen körs någon gång, t.ex. alla alternativ i en sammansatt if-sats. Finns det helt otestad kod har du ingen aning om dess kvalitet.
  • Testa normalfallet. (Vad är det mest typiska invärdet?)
  • Testa extremvärden (eng. edgecases). Det kan finnas olika extremvärden beroende på vad funktionen gör. Tomma listan, tomma strängen, eller andra "tomma" värden är bra att använda eftersom de ofta kräver specialhantering i koden. Talen 0 och 1 också vara extremvärden.
  • Testa andra värden som kan kännas speciella. Handlar det om nästlade listor, så testa fall som [[[[[[[[1]]]]]]]] som både är djupa (många nivåer) och "smala" (ett enda element per nivå). Handlar det om listor av strängar, så kontrollera fallet ["", "", ""]. Kanske det är intressant att testa med enbart udda tal, enbart negativa tal, extremt stora tal (1000000000000000), långa listor, korta listor, stigande sekvenser (1,2,3,4,5), sjunkande sekvenser (5,4,3,2,1), eller andra mönster.
  • Testa olika typer av indata (om det är tänkt att funktionen ska kunna hantera det).

Tillhörande quiz

Finnes här


Sidansvarig: Peter Dalenius
Senast uppdaterad: 2021-12-03