Lettuce - testy w postaci scenariuszy

Testy BDD w Pythonie z wykorzystaniem Lettuce

lettuce to system testów oparty o zachowania jaki stosuje się w BDD - metodzie wytwarzania sterowaną przez zachowania. Testy BDD opisywane są "ekspresyjnie" jako historyjki "zakładając że, jeśli, to" (Given, When, Then) pisane w języku angielskim. Scenariusze są bardzo czytelne i zrozumiałe dla osób nie będących programistami. Silnik testów taki jak lettuce parsuje te historyjki i wykonuje stosowne testy.

Lettuce można używać w połączeniu z Django, czy innymi frameworkami lub niezależnie. Instalacja jest standardowa - pip install lettuce. W przypadku Django do INSTALLED_APPS dodajemy 'lettuce.django'.

Struktura testów lettuce

W aplikacji Django, dla której chcemy stworzyć testy lettuce tworzymy katalog features. W tym katalogu umieszczamy parę plików o takich samych nazwach - jeden .py, drugi .feature. Np. index.feature, index.py. Pliki feature zawierają scenariusze - historyjki testowe, a pliki py kod pythona wykonywane w odpowiedzi na kroki historyjek.

Oto przykładowy plik .feature:
Feature: Main Page should show custom welcome messages to anonymous and logged in users.


Scenario: Main Page works
    Given I access url "/"
    Then Server sends a 200 response

Na początek określamy funkcjonalność jakiej będą dotyczyć scenariusze. Zawartość nagłówka "Feature" nie jest brana pod uwagę przez parser testów. Jest to rola czysto informacyjna. Kolejny element pliku feature to scenariusz. Może być ich wiele w jednym pliku. W powyższym przykładzie mamy scenariusz, który zakłada że strona główna działa - jeśli wejdę na stronę pod adresem / to serwer zwróci odpowiedź 200.

Scenariusze mogą być bardziej złożone - mogą zawierać więcej kroków (np. zaloguj, wejdź na adres, wypełnij formularz, wyślij...). Struktura scenariuszy wygląda ogólnie tak:

Scenario: ...
    Given ...
    Then ...


Scenario: ...
    Given ...
    And ...
    And ...
    Then ...
Plik *.py wygląda następująco:
from lettuce import *
from django.test.client import Client
from nose.tools import assert_equals


@before.all
def set_browser():
    world.browser = Client()


@step(r'I access url "(.*)"')
def access_url(step, url):
    world.response = world.browser.get(url)


@step(r'Server sends a ([0-9]+) response')
def compare_server_response(step, expected_code):
    code = world.response.status_code
    assert_equals(int(expected_code), code)

Mapowanie funkcji na kroki scenariuszy odbywa się za pomocą dekoratorów i wyrażeń regularnych. Dostępne są też specjalne dekoratory do wykonywania funkcji przed/po scenariuszach, czy przed czy po wykonaniu wszystkich testów (np. tworzenie i czyszczenie bazy danych).

W powyższym przykładzie za pomocą dekoratora @before.all ustawiam obiekt "przeglądarki" używany do testów. W tym przypadku jest to djangowski Client (może to być też przeglądarka sterowana przez Selenium). Zmienna "world" jest przenoszona pomiędzy funkcjami wykonywanymi w obrębie testów (coś jak "self" w klasie). Dekorator @step pozwala oznaczać funkcje będące krokiem w scenariuszu. W przykładzie mamy dwa kroki. Jako parametr dekoratora podajemy tekst kroku. Zmienne mogą być oznaczone za pomocą wyrażeń regularnych - np. ścieżka URL, czy kod odpowiedzi serwera.

Testy odpalamy za pomocą python manage.py harvest. Lettuce wykorzystuje port 8000 i nasz deweloperski serwer nie może działać na tym porcie. "Sałata" będzie szukać testów w aplikacjach i wykona te, które znajdzie:

Feature: Main Page should show custom welcome messages to anonymous and logged in users. # myapp/features/index.feature:1

  Scenario: Main Page works for anonymous user                                           # myapp/features/index.feature:4
    Given I access url "/"                                                               # myapp/features/index.py:13
    Then Server sends a 200 response                                                     # myapp/features/index.py:19

1 feature (1 passed)
1 scenario (1 passed)
2 steps (2 passed)

terrain.py

Niektóre kroki, np. te konfiguracyjne będą potrzebne zawsze. Definiowanie ich we wszystkich plikach to duplikacja kodu. Możemy umieścić takie udekorowane funkcje w pliku terrain.py umieszczonego w katalogu z testami lettuce (dostępny dla wszystkich testów z tego katalogu) lub w głównym katalogu projektu django (dostępny dla wszystkich testów).

W terrain.py możemy umieścić np:
from django.test.client import Client
from lettuce import before
from lettuce import world


@before.all
def initial_setup():
    world.browser = Client()
Oraz definicje kroków używanych w wielu testach.

Lettuce i testowa baza danych Django

Ze względu na np. Google App Engine testy Lettuce nie będą używać testowej bazy danych tworzonej przez Django do swoich testów. Problem ten można rozwiązać podając w --settings= nazwę "testowego" pliku ustawień z testową bazą danych. Druga opcja to czekać na zmerdżowanie stosownych zmian w Lettuce. Bez tego Lettuce będzie operować na głównej bazie danych i nie będzie usuwać testowych danych.

Korzystając ze zmian wprowadzonych przez Stevena bez problemów można operować na bazie danych bez konieczności konfiguracji środowiska testowego. Oto przykład nieco bardziej rozbudowanego testu:

Feature: Main Page should show custom welcome messages to anonymous and logged in users.


Scenario: Main Page works for anonymous user
    Given I access url "/"
    Then Server sends a 200 response
    And Page displays "Hi!" response

Scenario: Main Page works for authenticated user
    Given I am a logged-in user
    And I access url "/"
    Then Server sends a 200 response
    And Page displays "Welcome back!" response
from django.contrib.auth.models import User
from lettuce import *
from nose.tools import assert_equals


@step(r'I access url "(.*)"')
def access_url(step, url):
    world.response = world.browser.get(url)


@step(r'Server sends a ([0-9]+) response')
def compare_server_response(step, expected_code):
    code = world.response.status_code
    assert_equals(int(expected_code), code)


@step(r'Page displays "(.*)" response')
def compare_response_content(step, expected_response):
    assert_equals(expected_response, world.response.content)


@step(r'I am a logged-in user')
def loggin_user(step, **kwargs):
    user = User.objects.create_user('test', '1@1.com', 'testpass')
    world.browser.login(username='test', password='testpass')
W powyższym przykładzie sprawdzam odpowiedź (world.response.content) zwracaną przez serwer (kod strony HTML). W drugim scenariuszu przed otworzeniem strony loguję użytkownika na testowe konto (tutaj można by zastosować także Factory Boy). Testowany widok zwraca różne odpowiedzi w zależności od tego czy użytkownik jest zalogowany czy nie.

Splinter - Selenium w testach

splinter to narzędzie do testowania wykorzystujące testy w przeglądarkach (dzięki Selenium). Splinter może zastąpić obiekt "Client" oferowany przez Django:

from lettuce import after
from lettuce import before
from lettuce import world
from splinter import Browser


@before.all
def initial_setup():
    world.browser = Browser()


@after.all
def teardown_browser(total):
    world.browser.quit()

Dzięki temu testujemy w przeglądarce wykonujące te same akcje, jakie może zrobić użytkownik. Testy z Selenium pozwalają na znacznie lepsze przetestowanie realnego działania frontendu (np. JavaScriptu) niż zwykłe testy.

Po zastosowaniu splintera nasze testy mogłyby wyglądać tak:

from lettuce import *
from lettuce.django import django_url
from nose.tools import assert_true


@step(r'I access url "(.*)"')
def access_url(step, url):
    world.response = world.browser.visit(django_url(url))


@step(r'Page displays "(.*)" response')
def compare_response_content(step, expected_response):
    assert_true(world.browser.is_text_present(expected_response))

W przypadku testów Selenium nie można już "w tle" przemycić logowania użytkownika.

Lettuce i Django-Jenkins

Django-jenkins obsługuje testy lettuce. Wystarczy do JENKINS_TASKS w settingsach dodać 'django_jenkins.tasks.lettuce_tests',. W konfiguracji "joba" w Jenkinsie w Publish JUnit test result report dodajemy reports/lettuce.xml (w efekcie powinniśmy mieć: "reports/junit.xml, reports/lettuce.xml").

Po tych zabiegach wszystko powinno działać. Sprawdź jednak czy Jenkins reaguje na nieprzechodzące testy lettuce.

W sieci

blog comments powered by Disqus

Kategorie

Strony