Tvorba internetových aplikací

Jakub Klinkovský

:: České vysoké učení technické v Praze
:: Fakulta jaderná a fyzikálně inženýrská
:: Katedra softwarového inženýrství

Akademický rok 2023-2024

Testování (webových) aplikací

Testování je důležitá součást vývoje každého software, která má zajistit správnost implementace a použitelnost v praxi. Může probíhat různými způsoby:

  • analýza z hlediska úplnosti a použitelnosti zadaných požadavků
  • analýza z hlediska výkonnosti, bezpečnosti, kompatibility apod.
  • analýza kvality kódu
  • analýza chování aplikace při spuštění v různých prostředích
  • monitorování aplikace a její interakce s jinými systémy
  • a další...

Úvod do automatického testování

Manuální testování je časově náročné a nespolehlivé:

  • pro rozsáhlejší aplikace roste počet míst, kde se může projevit malá změna v kódu
  • ověřovat všechny případy ručně po každé změně není časově možné
  • navíc lidský faktor vede k nespolehlivosti pro opakující se činnost

Automatické testování řeší tyto problémy a přináší další výhody:

  • ověření testů je mnohem rychlejší a spolehlivější → lze provádět častěji
  • v případě selhání testu je okamžitě vidět, která část kódu nefunguje správně
  • testy slouží jako první uživatel vašeho kódu, často jsou základem pro psaní dokumentace nebo vytváření příkladů
  • existují vývojové techniky přímo založené na testech (viz dále)

Metodiky testování software

Další typy testů: black box, white box, canary, smoke, conformance, acceptance, functional, performance, load a stress testy.

Unit testing

  • slouží k testování jednotlivých částí kódu (units) – funkce, třídy, struktury
  • každý projekt by měl mít sadu testů, které pokrývají celou funkčnost projektu
  • jednotlivé testy by měly být izolované – pokud nějaký test selže, mělo by být snadné dohledat problematickou část kódu

center

Unit testing v praxi

  • mělo by se testovat:
    • všechny možné kombinace vstupních parametrů
      (v praxi: významné/charakteristické kombinace)
    • ověřit chování pro platné vstupy
    • ověřit chování pro neplatné vstupy (např. nastane výjimka)
  • v praxi nelze zachytit všechny chyby pomocí testů (nelze pokrýt všechny možné kombinace parametrů, stavů systému, apod.), ale stejně můžou podstatně ulehčit vývoj kódu

Test-driven development (TDD)

Test-driven development je proces vývoje software, který je založený na těchto krocích:

  1. Přidat test pro novou funkčnost (např. unit test).
  2. Spustit všechny testy, nový test samozřejmě selže.
  3. Implementovat co nejjednodušší kód, který zajistí splnění všech testů. (V této fázi nový kód ani nemusí být obecný, může řešit jen konkrétní případ daný testem.)
  4. Ověřit, že všechny testy projdou bez chyb.
  5. Refaktorování kódu dle potřeby. Pomocí testů ověřit, že se nezměnilo chování.
  6. (volitelně) Ověřit kvalitu kódu pomocí vhodných nástrojů.
  7. (volitelně) Commitovat změny do systému pro správu kódu (např. git).

Tyto kroky se opakují pro každou novou funkčnost, kterou je třeba v programu implementovat.

Programování stylem TDD

TDD souvisí s dalšími principy, které vedou k čistotě a přehlednosti kódu:

  • Keep it simple, stupid! (KISS)
  • You aren't gonna need it (YAGNI) – neimplementujte věci, které nejsou potřeba
  • Fake it until you make it – viz 3. bod

Výhody psaní testů před samotnou implementací:

  • v první řadě umožňuje testovatelnost (programátor musí už od začátku přemýšlet nad testy, naopak přidávání testů pro existující aplikaci je mnohem náročnější)
  • zaručí, že pro každou funkcionalitu bude nějaký test
  • vede k lepšímu pochopení požadavků zákazníka
  • zajišťuje spojitý důraz na kvalitu kódu

Příklady refaktorování

V pátém kroku TDD probíhá refaktorování kódu – změny, které zlepšují kvalitu kódu, ale nemění chování aplikace. Např.:

  • přesun kódu na logičtější místo
  • odstranění duplicitního kódu
  • přejmenování proměnných, funkcí, tříd
  • rozdělení funkcí a metod na menší části (to ale často znamená přidání nového testu, neboť každá funkce či metoda by měla být testovaná samostatně)
  • změna hierarchie dědičnosti mezi třídami

TDD a nástroje pro správu verzí

Fáze refaktorování kódu a implementace nových funkcí by měly být striktně oddělené, např. pomocí samostatných commitů v gitu. Jinak se v historii kódu nikdo nevyzná:

  • vznikne velmi nepřehledný diff obsahující mnoho různých změn
  • code review a případné debugování budou náročné, nebo prakticky nemožné

Změny a testy by měly být malé a inkrementální a commitované samostatně:

  • v případě selhání velkého množství testů se pak lze jednoduše vrátit k poslední funkční verzi
  • v případě objevení nové chyby lze jednoduše identifikovat problematickou změnu, která ji způsobila
    (např. git poskytuje metodu bisekce, git bisect)

Behavior-driven development (BDD)

Behavior-driven development je technika agilního vývoje software, která vychází z principů TDD:

  • zdůrazňuje komunikaci v mezioborových týmech
  • pro specifikaci očekávaného chování se používá doménově specifický jazyk, který využívá konstrukce přirozeného jazyka (typicky angličtiny)

Příklad BDD testu v jazyku Gherkin:

Feature: Calculator
Calculator for adding two numbers

Scenario: Add two numbers
Given I have entered 50 into the calculator
And I have entered 70 into the calculator

When I press add
Then the result should be 120 on the screen

Nástroje pro automatické testování

Použití přístupu TDD nebo BDD v týmech vedlo k vyvinutí nástrojů, které umožňují automatické testování ve sdíleném prostředí:

  • continuous integration (např. GitLab CI, GitHub Actions)

    • časté spouštění testů v jednotném prostředí během týmového vývoje
    • spolupráce s nástroji pro správu verzí kódu
  • continuous delivery, continuous deployment – časté vytváření testovacích verzí, které jsou nasazovány pomocí automatických nástrojů v testovacím prostředí simulujícím reálné chování aplikace v praxi

Knihovny pro implementaci automatických testů

Jazyk Python

Více informací: Getting Started With Testing in Python na realpython.org

Většina knihoven je kompatibilní s nástrojem coverage, který umožňuje generování tzv. coverage reportů pro posouzení úplnosti unit testů.

Jazyk C++

Testování webových aplikací ve frameworku Django

Testování webové aplikace je komplexní, protože se skládá z mnoha komponent:

  • zpracování HTTP požadavků
  • práce s databází pomocí ORM (modely)
  • validace a zpracování dat z formulářů
  • renderování šablon

Django poskytuje několik tříd pro usnadnění testování pomocí modulu unittest, který umožňuje psát např. unit testy nebo testy integrace.

Organizace testů v projektu

  • test suite – sada všech testů pro danou aplikaci nebo projekt
    • vyskytuje se v submodulu tests v dané aplikaci (buď soubor tests.py nebo adresář tests obsahující soubory s prefixem test_), např.:
      catalog/
        /tests/
          __init__.py
          test_models.py
          test_forms.py
          test_views.py
      
  • test case – podmnožina souvisejících testů (unit)
    • třída odvozená od django.test.TestCase
  • test – implementace pro jednu konkrétní kontrolu
    • metoda s názvem začínajícím test_
    • může obsahovat několik asercí

Ukázka testu ve frameworku Django

Ukázkový soubor tests.py (nebo tests/test_models.py):

import datetime

from django.test import TestCase
from django.utils import timezone

from .models import Question

class QuestionModelTests(TestCase):
    def test_was_published_recently_with_future_question(self):
        """
        was_published_recently() returns False for questions whose pub_date
        is in the future.
        """
        time = timezone.now() + datetime.timedelta(days=30)
        future_question = Question(pub_date=time)
        self.assertIs(future_question.was_published_recently(), False)

Aserce

Aserce provádí kontrolu specifikovaných hodnot (výrazů).               (Existují i negace.)

Metoda Testovaný výraz
.assertEqual(a, b) a == b
.assertTrue(x) bool(x) is True
.assertFalse(x) bool(x) is False
.assertIs(a, b) a is b
.assertIsNone(x) x is None
.assertIn(a, b) a in b
.assertIsInstance(a, b) isinstance(a, b)

A další, viz dokumentace.

Spouštění testů

Spouštění testů se provádí příkazem python manage.py test.

Defaultně se spustí všechny nalezené testy. Množinu spouštěných testů lze filtrovat:

# Spustí všechny testy v aplikaci "appname":
python manage.py test appname

# Spustí všechny testy v modulu "appname.tests":
python manage.py test appname.tests

# Spustí konkrétní test case:
python manage.py test appname.tests.QuestionModelTests

# Spustí konkrétní test:
python manage.py test appname.tests.QuestionModelTests.test_was_published_recently

Další parametry: viz python manage.py test --help ☺️

Test fixtures

Test fixture představuje data popisující počáteční stav, ve kterém probíhají testy.

Práce s fixtures:

  • metoda setUp – vytvoří počáteční stav pro každý test ve stejné třídě
  • metoda tearDown – zrušení dat po skončení testu
  • metoda setUpTestData – vytvoří počáteční stav pro celý test case
    • volá se jen jednou pro každou instanci třídy (rychlejší než setUp, ale testy nejsou úplně izolované)
  • metody setUpClass a tearDownClass – mají interní použití (pozor při
    předefinování), také se volají jen jednou pro každou instanci třídy

Použití test fixtures – testování modelů

from django.test import TestCase
from myapp.models import Animal

class AnimalTestCase(TestCase):
    def setUp(self):
        Animal.objects.create(name="lion", sound="roar")
        Animal.objects.create(name="cat", sound="meow")

    def test_animals_can_speak(self):
        """Animals that can speak are correctly identified"""
        lion = Animal.objects.get(name="lion")
        cat = Animal.objects.get(name="cat")
        self.assertEqual(lion.speak(), 'The lion says "roar"')
        self.assertEqual(cat.speak(), 'The cat says "meow"')

Použití metody setUpTestData

from django.test import TestCase

class MyTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        # Set up data for the whole TestCase
        cls.foo = Foo.objects.create(bar="Test")
        ...

    def test1(self):
        # Some test using self.foo
        ...

    def test2(self):
        # Some other test using self.foo
        ...

Testování s HTTP požadavky

Pro testování požadavků a odpovědí slouží třída django.test.Client, která je dostupná jako atribut client ve třídě TestCase.

  • nevyžaduje web server – testovací požadavek se předá přímo hierarchii middleware a funkci view
  • lze použít přímo v Python shellu, např.:
    from django.test import Client
    c = Client()
    response = c.post("/login/", {"username": "john", "password": "smith"})
    assert response.status_code == 200
    
  • požadavky se specifikují pomocí cesty (tzn. bez domény)

Příklad s metodou setUpTestData a atributem client

from django.test import TestCase
from django.urls import reverse

from catalog.models import Author

class AuthorListViewTest(TestCase):
    @classmethod
    def setUpTestData(cls):
        # Create 13 authors for pagination tests
        number_of_authors = 13

        for author_id in range(number_of_authors):
            Author.objects.create(
                first_name=f"John {author_id}",
                last_name=f"Doe {author_id}",
            )
# ...
# ...
    def test_view_url_exists_at_desired_location(self):
        response = self.client.get('/catalog/authors/')
        self.assertEqual(response.status_code, 200)

    def test_view_url_accessible_by_name(self):
        response = self.client.get(reverse('authors'))
        self.assertEqual(response.status_code, 200)

    def test_view_uses_correct_template(self):
        response = self.client.get(reverse('authors'))
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, 'catalog/author_list.html')

    # atd...

Pokročilé techniky pro testování webových aplikací

  • testování views, které vyžadují přihlášení – třída Client má metody login, force_login a logout
  • testování trvalého stavu – třída Client má atributy cookies a session
  • testování UI – třída LiveServerTestCase a integrace s frameworkem Selenium

Další informace