angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Funktionelles Testen einer mehrsprachigen Flask -Website mit Pytest

Mit Pytest fixtures und hooks und Flask test_client können wir Tests durchführen und Texte manuell genehmigen.

25 Juli 2020 Aktualisiert 25 Juli 2020
In Testing
post main image
https://unsplash.com/@marco_blackboulevard

Testen ist harte Arbeit. Es ist völlig anders als das Erstellen oder Ändern von Funktionalität. Ich weiß, ich habe auch Computer-Hardware, integrierte Schaltkreise und Hardware-Testsysteme entwickelt. Ich schrieb Tests für CPUs, Computerprodukte, entwickelte Testsysteme. Mit Hardware kann man keine Fehler machen. Fehler können das Ende Ihrer Firma sein.

Willkommen in der wunderbaren Welt des Software-Testens

Bei Software sind viele Dinge anders. Fehler werden toleriert. Ich gebe kleinen Unternehmen keine Schuld. Aber große Technologieriesen wie Google, Apple, Facebook, Microsoft sollte die Schuld gegeben werden. Die meiste Software ist komplex oder verwendet zumindest Bibliotheken von Drittanbietern oder stellt eine Verbindung zu entfernten Systemen her. Das Testen einer Anwendung ist zu einer komplexen Aufgabe geworden.

Warum ich mich für das Schreiben von Funktionstests entscheide (zuerst)

Es gibt viele verschiedene Arten von Tests für Software, um die bei Entwicklern bekanntesten zu nennen:

  • Unit-Test
  • Integrationstest
  • Funktionaler Test
  • End-to-End-Test

Unit-Tests sind einfach zu schreiben, aber selbst wenn sie alle bestehen, garantiert dies nicht, dass meine Anwendung Flask funktioniert. Integrationstests sind auch nicht so schwierig. Wenn Sie Bibliotheken schreiben, machen diese Tests durchaus Sinn. Ziehen Sie einfach die Klassen heran und schreiben Sie ein paar Tests. Aber es gibt keine Garantie, dass meine Anwendung funktioniert, wenn nur diese Tests durchgeführt werden.

Wie kann ich mit begrenzter Zeit sicherstellen, dass meine Flask CMS/Blog-Anwendung tatsächlich funktioniert? Ich habe mich für die Funktionstests und die Tests von end-to-end entschieden. Das ist viel mehr Arbeit, aber zumindest weiß ich, dass der Besucher der Website die richtigen Dinge sieht.

Werkzeuge zum Testen

Ich habe eine 'Test'-Option zu meinem Docker-Setup hinzugefügt. Dies ist wie meine 'Entwicklungs'-Option, mit Code außerhalb des Containers, aber mit ein paar zusätzlichen Paketen,

  • Flake8
  • Pytest
  • Pytest-cov
  • Pytest-flake8

Pytest ist ein netter Test framework , wenn Sie Ihre Anwendung Flask testen möchten. Wenn Sie Pytest-flake8 installieren, können Sie Flake8 auch von Pytest aus ausführen:

python3 -m pytest  app_frontend/blueprints/demo  --flake8  -v

Aber ich ziehe es vor, flake8 allein auszuführen, wobei ich in diesem Fall lange Zeilen ignoriere:

flake8  --ignore=E501  app_frontend/blueprints/demo

Testaufbau

Mit Pytest zu beginnen ist nicht sehr schwierig. Ich habe ein Verzeichnis tests_app_frontend im Projektverzeichnis erstellt:

.
|-- alembic
|--  app_admin
|--  app_frontend
|   |--  blueprints
|   |-- templates
|   |-- translations
|   |-- __init__.py
|   `-- ...
|-- shared
|-- tests_frontend
|   |-- helpers
|   |   |-- helpers_cleanup.py
|   |   |-- helpers_globals.py
|   |   `-- helpers_utils.py
|   |-- language_logs
|   |-- language_logs_ok
|   |-- conftest.py
|   |--  pytest.ini
|   |-- ...
|   |-- test_ay_account.py
|   |-- ...
|   |-- test_an_auth_login.py
|   `-- ...

Die Datei pytest.ini :

[pytest]
norecursedirs=tests_frontend/helpers

cache_dir=/home/flask/project/tests_frontend/cache
console_output_style=progress

log_file=/home/flask/project/tests_frontend/testresults_frontend.log
log_file_level=DEBUG
log_file_format=%(asctime)s %(levelname)s %(message)s
log_file_date_format=%Y-%m-%d %H:%M:%S

log_level=DEBUG
python_files=test_*.py

flake8-max-line-length=200

Das Hilfsmittelverzeichnis enthält Funktionen, die Ihnen das Leben viel leichter machen können. Nachdem ich einige Tests geschrieben hatte, habe ich mir vorgenommen, meinen Testcode zu minimieren, und viel Zeit damit verbracht, diese Hilfsfunktionen zu schreiben.

Bereinigungsfunktionen in helpers_cleanup.py sind Teil des Tests und werden vor dem eigentlichen Test ausgeführt, Beispiele

def delete_test_contact_forms_from_db(client=None,  lang_code=None, dbg=False):
    ...
    
def delete_test_contact_form_emails_from_db(client=None,  lang_code=None, dbg=False):
    ...

Einige der Globals wurden in helpers.globals gespeichert:

# all test emails have the same email_host
email_host = 'test_email_host.test'

# test  user  present in the database
my_test_user_email = 'mytestuser@mytestuser.mytestuser'
my_test_user_password = 'nio7FKCYUHTgd765edfglfsSDgn'
my_test_user_new_password = 'LUI76tlfvutjlFLJFgf'

Einige der Hilfsfunktionen in helper_utils.py:

def get_attr_from_html_page_by_elem_id(rv, elem_id, attr_name, form_prefix=None, dbg=False):
    ...

def get_unique_test_email():
    ...

def page_has_tag_with_text(rv, tag, text, dbg=False):
    ...

def client_get(client, url, follow_redirects=False, dbg=False):
    ...

def client_post(client, url, form_field2values=None, form_prefix=None, follow_redirects=False, dbg=False):
    ...

def my_test_user_restore_password(dbg=False):
    ...

def my_test_user_login(client,  lang_code=None, email=None, password=None, check_message_logged_in=True, dbg=False):
    ...

def my_test_user_logout(client,  lang_code=None, check_message_logged_out=True, dbg=False):
    ...

def get_auth_login_url(client=None,  lang_code=None, dbg=False):
    ...

def get_auth_logout_url(client=None,  lang_code=None, dbg=False):
    ...

Ich habe eine Funktion client_get() und client_post() hinzugefügt, die sich fast identisch zu client.get() und client.post() verhält, aber auch die Position der Seite nach Umleitungen zurückgibt. Die Funktion client_post() hat auch einen optionalen Parameter form_prefix , der verwendet werden kann, wenn das Formular prefixed lautet, siehe Dokumentation zu WTForms .

Die Funktion my_test_user_login() kann verwendet werden, wenn sie nicht authentifiziert ist, um den Test user anzumelden. Alle Hilfsdateien haben ein optionales dbg-Argument (Debug). Wenn Sie bei der Entwicklung von Tests oft sehen wollen, was passiert, gibt mir die Einstellung dbg=True alle Details, auch in der Protokolldatei.

Testdateinamen, Funktionsnamen und Authentifizierung

In Pytest können Sie 'markieren' verwenden, um Tests zu gruppieren, aber ich habe dies noch nicht verwendet. Stattdessen habe ich zwei Hauptkategorien. Tests, wenn der user nicht authentifiziert ist, an = nicht authentifiziert, und Tests, wenn der user authentifiziert ist, ay = authentifiziert. Testdateien und Tests beginnen mit der gleichen Zeichenfolge. Testdatei:

test_an_auth_login.py

hat Tests wie:

def test_an_auth_login_enable_disable(client_lang_code_an):
    client,  lang_code  =  client_lang_code_an
    ...

Und die Testdatei:

test_ay_account.py

hat Tests wie:

def test_ay_account_change_password_to_new_password(client_lang_code_ay):
    client,  lang_code  =  client_lang_code_ay
    ...

Die Parameter 'client_lang_code_an' und 'client_lang_code_ay' sind Tuples , die den (Test-)Client und den Sprachcode enthalten.
Sie werden in der Testfunktion ausgepackt. Es gibt ein Paket Pytest-cases , das das Entpacken vermeidet, aber das wollte ich nicht verwenden.

Starten von Tests mit fixtures

Ich möchte, dass die Tests nacheinander für alle verfügbaren Sprachen durchgeführt werden, oder nur für eine oder mehrere Sprachen, die ich auf der Befehlszeile angibt. Das bedeutet, dass der Umfang von fixture 'Funktion' sein muss. Beispiel einer Ausgabe von Pytest:

tests_frontend/test_ay_account.py::test_ay_account_change_password_logout_login[de] PASSED
tests_frontend/test_ay_account.py::test_ay_account_change_password_logout_login[en] PASSED
tests_frontend/test_ay_account.py::test_ay_account_change_password_logout_login[es] PASSED
tests_frontend/test_ay_account.py::test_ay_account_change_password_logout_login[fr] PASSED
tests_frontend/test_an_auth_login.py::test_an_auth_login_email_too_many_attempts[de] PASSED
tests_frontend/test_an_auth_login.py::test_an_auth_login_email_too_many_attempts[en] PASSED
tests_frontend/test_an_auth_login.py::test_an_auth_login_email_too_many_attempts[es] PASSED
tests_frontend/test_an_auth_login.py::test_an_auth_login_email_too_many_attempts[fr] PASSED

Man könnte argumentieren, dass es schneller ist, zuerst alle Tests für die Sprache X und dann alle Tests für die Sprache Y durchzuführen. Sie haben Recht, siehe auch unten "Änderung des Umfangs von Pytest ", aber ich wollte die Möglichkeit haben, die Dinge zusammenzuhalten, wenn mehrere neue Tests durchgeführt werden. Hier ist der Hauptteil von conftest.py:

from  app_frontend  import  create_app


def pytest_addoption(parser):
    parser.addoption('--lc', action='append', default=[],
        help='lc (one or more possible)')


def pytest_generate_tests(metafunc):
    default_opts = ['de', 'en', 'es']
	lc_opts = metafunc.config.getoption('lc') or default_opts
    if 'client_lang_code_an' in metafunc.fixturenames:
        metafunc.parametrize('client_lang_code_an', lc_opts, indirect=True)
    if 'client_lang_code_ay' in metafunc.fixturenames:
        metafunc.parametrize('client_lang_code_ay', lc_opts, indirect=True)


@pytest.fixture(scope='function')
def  client_lang_code_an(request):

     lang_code  = request.param
    project_config = os.getenv('PROJECT_CONFIG')
    flask_app =  create_app(project_config)

    with flask_app.test_client() as client:
        with flask_app.app_context():
            rv = client.get(get_language_root_url(lang_code))
            yield (client,  lang_code)


@pytest.fixture(scope='function')
def  client_lang_code_ay(request):

     lang_code  = request.param
    project_config = os.getenv('PROJECT_CONFIG')
    flask_app =  create_app(project_config)

    with flask_app.test_client() as client:
        with flask_app.app_context():
            rv = client.get(get_language_root_url(lang_code))
            my_test_user_restore_password()
            my_test_user_login(client,  lang_code, change_to_language=True)
            yield (client,  lang_code)
            my_test_user_logout(client)

Hier gibt es nicht wirklich viel Besonderes. Tests, die mit einem authentifizierten user laufen müssen, haben den Parameter 'client_lang_code_ay'. Dies bezieht sich auf den fixture mit diesem Namen. In diesem Fall wird der Client vor dem Aufruf des Tests angemeldet.

Wenn ich das '--lc' auf der Kommandozeile nicht spezifiziere, werden alle Sprachen für einen Test verwendet. Ich kann eine oder mehrere Sprachen auswählen, indem ich z.B. das '--lc de' in der Befehlszeile angibt. Tatsächlich habe ich später auch einige hinzugefügt, die es mir erlauben, '--lc=fr,en,de' einzugeben.

Datenbank

Da dies ein Funktionstest eines CMS/Blogs ist, benötige ich einige Daten in der Datenbank. Vor einem Test muss ich einige Daten bereinigen, um Interferenzen mit dem Test zu vermeiden. Diesen Aufräumcode muss ich selbst schreiben. Das ist ein Mehraufwand, aber nicht so schlimm, weil ich Teile dieses Codes später auch in der Anwendung verwenden kann. Wie beim Entfernen eines user.

Einige Tests

Wenn die Situation nicht authentifiziert ist, ist sie nicht sehr komplex. Hier ist ein Test, der die Fehlermeldung mit einem zu kurzen Passwort überprüft.

def test_an_auth_login_password_too_short(client_lang_code_an):
    client,  lang_code  =  client_lang_code_an
    """
    TEST: submit login_form with too short password
    EXPECT: proper error message
    """

    cleanup_db_auth()

    auth_login_url = get_auth_login_url()

    auth_login_info = AuthLoginInfo()
    login_form_prefix  = auth_login_info.login_form_prefix

    input_validation = InputValidationAuth()
    length_min = input_validation.password.length_min
    error_message = check_text(lang_code, input_validation.password.error_message_length_min)

    url, rv = client_post(
        client,
        auth_login_url,
        form_field2values=get_login_form_form_field2values_for_submit(
            password='x' * (length_min - 1)
        ),
        form_prefix=login_form_prefix,
        follow_redirects=True
    )
    assert page_contains_text(rv, error_message)

Vor dem eigentlichen Test werden einige Daten aus der Datenbank entfernt, falls vorhanden, um Interferenzen zu vermeiden. Die Form prefix, Mindestlänge und Fehlermeldung erhalte ich durch Einziehen einiger Klassen.

Es wird komplexer, wenn es authentifiziert wird. Das hat damit zu tun, dass ein registrierter user die bevorzugte Sprache im Konto einstellen kann. Zum Beispiel ist der user mit Deutsch (Deutsch) als bevorzugte Sprache auf der englischen Version der Website und meldet sich dann an. Nach der Anmeldung wird die Sprache der Website automatisch auf Deutsch (Deutsch) umgestellt.

Da ich Nachrichten in allen Sprachen überprüfen möchte, habe ich der Funktion my_test_user_login() etwas Code hinzugefügt, um mir das Leben leichter zu machen. Wenn die Sprache, lang_code, angegeben ist, dann wird nach einer Anmeldung die user in die angegebene Sprachversion verschoben.

def test_ay_auth_try_to_login_when_loggedin_get(client_lang_code_ay):
    client,  lang_code  =  client_lang_code_ay
    """
    TEST: logged in test  user, try to login
    EXPECT: proper error message
    """

    cleanup_db_auth()

    auth_login_url = get_auth_login_url()

    auth_login_info = AuthLoginInfo()
    error_message = check_text(lang_code, auth_login_info.error_message_you_are_already_logged_in)

    rv = client.get(auth_login_url, follow_redirects=True)
    assert page_contains_text(rv, error_message), 'not found, error_message = {}'.format(error_message)

Test-Zeit

Um einen Test zu timen, können Sie einfach den Befehl time vor den Befehl Pytest setzen:

time python3 -m pytest --cov=shared/blueprints/auth --cov-report term-missing  tests_frontend  -k "test_ay_auth_try_to_login_when_loggedin_get" -s -v

druckt am Ende auf das Terminal:

real	0m 16.67s
user	0m 9.65s
sys	0m 0.29s

Dies ist das Ergebnis für einen einzigen Test mit sechs Sprachen, durchschnittlich 2,5 Sekunden pro Sprache.

Eine einfachere Möglichkeit, die Ausführungszeiten von Tests im Detail zu sehen, besteht darin, den Parameter --durations=N in die Befehlszeile einzufügen. Wenn N=0 erhalten Sie die Ausführungszeiten für jeden Test. Mit N=1 erhalten Sie die Ausführungszeit des langsamsten Tests. Mit N=2 erhalten Sie die Ausführungszeit der beiden langsamsten Tests, usw. Um zum Beispiel die langsamsten 4 Kontaktformtests zu erhalten, lautet der Befehl:

time python3 -m pytest --cov=shared/blueprints/auth --cov-report term-missing  tests_frontend  -k "test_an_contact" -s -vv  --durations=4 --lc=fr

Das Ergebnis beinhaltet:

========== slowest 4 test durations ==========
14.88s call     test_an_contact_form.py::test_an_contact_form_multiple_same_ip_address[fr]
6.96s call     test_an_contact_form.py::test_an_contact_form_multiple_same_email[fr]
6.36s call     test_an_contact_form.py::test_an_contact_form_missing_form_field[fr]
2.87s call     test_an_contact_form.py::test_an_contact_form_thank_you_logged_in[fr]
========== 15 passed, 24 deselected, 1 warning in 51.92s ==========

Timings innerhalb der fixture

Um zu sehen, wohin die ganze Zeit geht, habe ich einen Zeittest durchgeführt, indem ich Zeiten an verschiedenen Stellen in den authentifizierten fixture, client_lang_code_ay() gesammelt habe.

0.0  client_lang_code_ay
0.141031 after: flask_app =  create_app(project_config)
0.141062 after: with flask_app.test_client() as client
0.141078 after: with flask_app.app_context()
0.448304 after: rv = client.get(get_language_root_url(lang_code))
0.531201 after: my_test_user_restore_password()
1.154249 after: my_test_user_login(client,  lang_code)
2.553381 after: yield (client,  lang_code)
2.624607 after: my_test_user_logout(client,  lang_code)

Die Zeit bis zum Einloggen ist länger als Sie erwarten, was durch eine absichtliche Verzögerung beim Einloggen verursacht wird. Tests mit dem nicht authentifizierten Befehl fixture, client_lang_code_an(), dauern durchschnittlich 1,5 Sekunden, was ohne Anmeldung zu erwarten ist.

Verkürzung der Testzeit durch Änderung des Umfangs von Pytest

Da der Umfang von Pytest fixture 'Funktion' ist, erhalten wir für jeden Test ein neues test_client . Ich habe die Funktion scope='function' verwendet, um die für alle Sprachen durchgeführten Tests zu sehen. Die Ausgabe von Pytest:

test1[de] PASSED
test1[en] PASSED
test1[es] PASSED
test1[fr] PASSED
test2[de] PASSED
test2[en] PASSED
test2[es] PASSED
test2[fr] PASSED
test3[de] PASSED
test3[en] PASSED
test3[es] PASSED
test3[fr] PASSED

Das ist sehr schön, nimmt aber auch viel Zeit in Anspruch.

Um die Testzeit zu verkürzen, ändere ich den Umfang von fixture in 'Sitzung'. In diesem Fall wird nur für jede Sprache ein neuer test_client erstellt. Zuerst werden alle Tests für die Sprache 'de' ausgeführt, dann werden alle Tests für die Sprache 'en' ausgeführt, usw.. Die Ausgabe von Pytest:

test1[de] PASSED
test2[de] PASSED
test3[de] PASSED
test1[en] PASSED
test2[en] PASSED
test3[en] PASSED
test1[es] PASSED
test2[es] PASSED
test3[es] PASSED
test1[fr] PASSED
test2[fr] PASSED
test3[fr] PASSED

Die Testzeit wird auf diese Weise dramatisch verkürzt. Aber natürlich nur, wenn man viele, alle, Tests auswählt. Im Allgemeinen werden die meisten Tests sehr schnell abgeschlossen und nur wenige sind für 80% der Testzeit verantwortlich.

Die Testzeit für 14 (contact_form) Tests mit der scope='function' betrug 00:06:11. Nach der Änderung des Umfangs in 'Sitzung' war die Zeit 00:04:56. In diesem Fall beträgt die Reduzierung der Testzeit 20%. In einem anderen Fall betrug die Reduzierung mehr als 50%.

Entwurf für Test

Je mehr Daten wir aus unserer Anwendung beziehen können, desto mehr können wir das Testen automatisieren. Was ist zum Beispiel das prefix des Anmeldeformulars? Was sind die Fehlermeldungen beim Versuch, sich einzuloggen, wenn man bereits eingeloggt ist? Was ist die minimale und maximale Anzahl von Zeichen für ein Passwort? Diese Daten sind in der Anwendung vorhanden, wie bekommen wir sie heraus?

Für eine Reihe von Klassen habe ich eine 'Info'-Klasse hinzugefügt, die das Elternteil der eigentlichen Klasse ist.

class AuthLoginInfo:

    def __init__(self):

        self.error_message_you_are_already_logged_in = _('You are already logged in.')
        self.error_message_login_is_temporary_suspended = _('Log in is temporary suspended.')
        ...
        self.message_you_are_logged_in = _('You are logged in.')

        self.login_form_prefix  = self.login_form_id


class AuthLogin(AuthLoginInfo):

    def __init__(self):
        super().__init__()

    def login(self):

        if already_logged_in():
            flash(self.error_message_you_are_already_logged_in, 'error')
            return redirect(already_logged_in_redirect_url())
        ...

Ich weiß immer noch nicht, ob dies besser gemacht werden kann, aber es ist machbar.

Automatisierung der Sprachprüfung

Bei den oben genannten Tests wissen wir nicht, ob die Meldungen tatsächlich korrekt und für jede Sprache übersetzt sind. Wir müssen die Texte manuell überprüfen, können aber versuchen, unsere Arbeit zu minimieren.

Ich habe eine Funktion check_text() erstellt, die drei Dinge tut:

  • Den Text in einem Wörterbuch speichern
  • Prüfen Sie, ob der gleiche Text bereits in einem Wörterbuch vorhanden ist
  • Speichern Sie den Text in einer Sprachprotokolldatei

Die Namen dieser Sprachprotokolldateien sind die Namen der Tests. Ein Test wird für verschiedene Sprachen durchgeführt. Wenn der Text bereits im Wörterbuch steht, dann ist wahrscheinlich etwas falsch, z.B. der Text ist nicht übersetzt. In diesem Fall rufe ich in der Funktion check_text() 'assert' auf. Dies zeigt einen Fehler an, so dass wir untersuchen können, was falsch ist.

Wenn alles in Ordnung ist, überprüfe ich die Sprach-Log-Datei, um mir die Texte anzusehen. Die durch Tabulatoren getrennten Felder der Zeilen in dieser Datei sind test_name, text_id, lang_code, text:

test_ay_auth_try_to_login_when_loggedin_get	default	de	Sie sind bereits eingeloggt.
test_ay_auth_try_to_login_when_loggedin_get	default	en	You are already logged in.
test_ay_auth_try_to_login_when_loggedin_get	default	es	Ya está conectado.
test_ay_auth_try_to_login_when_loggedin_get	default	fr	Vous êtes déjà connecté.
test_ay_auth_try_to_login_when_loggedin_get	default	nl	Je bent al ingelogd.
test_ay_auth_try_to_login_when_loggedin_get	default	ru	Вы уже вошли в систему.

Ich kann kein Russisch und nur ein wenig Spanisch und Französisch, aber ich finde, das sieht gut aus.

Mehr über die Funktion check_text()

Ich kann eine text_id angeben, wenn ich mehr als einen Text in einer Testfunktion prüfen möchte. Wenn wir diese nicht angeben, wird 'default' verwendet.
Ich habe auch ein Flag für Texte hinzugefügt, die wir nicht (automatisch) prüfen wollen. Der Name des Tests, der Aufrufer, wird aus dem Stack extrahiert. Die ersten Zeilen der Funktion check_text() :

def check_text(lang_code, text, text_id=None, do_not_check=False, dbg=False):
    test_name = inspect.stack()[1][3]
    ...

Wir haben eine Menge getan, aber wir müssen immer noch jedes Mal nach einem Testlauf die Texte manuell überprüfen. Das ist immer noch viel zu viel Arbeit, wir haben die Texte, die Übersetzungen, in der Sprach-Log-Datei überprüft und wollen das nicht noch einmal tun!

Um dies zu vermeiden, habe ich ein Verzeichnis 'language_logs_ok' angelegt und die Sprachprotokolldateien kopiert, die wir hier genehmigt haben. Aber immer noch zu viel Arbeit, wir wollen die Dateien nicht manuell kopieren!

Mit etwas Extra-Code können wir am Ende eines Tests prüfen, ob diese ok_file existiert. Wenn sie nicht existiert, können wir um die Genehmigung der Texte bitten, und wenn sie mit Ja beantwortet wird, kopieren wir die Sprach-Logdatei auch in das Verzeichnis 'language_logs_ok'. Wenn sie existiert, können wir die Texte des aktuellen Tests mit den genehmigten Texten vergleichen und einen Fehler melden, wenn sie nicht übereinstimmen.

Pytest hat hooks , mit dem vor Beginn der Tests und nach Abschluss der Tests Funktionalität hinzugefügt werden kann. Hier verwende ich nur das letztere:

def pytest_sessionfinish(session, exitstatus):
    ....

Ich fügte den Code hinzu, und jetzt ist das Ergebnis:

test_an_auth_login_password_too_long: NO ok file yet WARNING
texts:
test_an_auth_login_password_too_long	default	de	Das Passwort muss zwischen 6 und 40 Zeichen lang sein.
test_an_auth_login_password_too_long	default	en	Password must be between 6 and 40 characters long.
test_an_auth_login_password_too_long	default	es	La contraseña debe tener entre 6 y 40 caracteres.
test_an_auth_login_password_too_long	default	fr	Le mot de passe doit comporter entre 6 et 40 caractères.
test_an_auth_login_password_too_long	default	nl	Het wachtwoord moet tussen 6 en 40 tekens lang zijn.
test_an_auth_login_password_too_long	default	ru	Пароль должен иметь длину от 6 до 40 символов.
Do you approve these texts (create an ok file)? [yN]

Wenn ich diese Texte genehmige, wird das ok_file erstellt. Dann wird beim nächsten Durchlauf, nach den Testergebnissen von Pytest , die folgende Meldung angezeigt:

test_an_auth_login_password_too_long: language match with ok_file PASSED

Das nimmt nicht nur viel Stress, sondern auch eine Menge Zeit weg!

Ein weiterer Test mit mehreren Texten

Ich verwende das Paket WTForms . Ich kann das Formular in den Test ziehen, die Formularfeldbezeichnungen extrahieren und überprüfen, ob sie auf der Seite sind. Durch die Automatisierung der Prüfung der Formularfeldbezeichner eliminieren wir fehlende oder falsche Übersetzungen.

Das Anmeldeformular:

class AuthLoginForm(FlaskForm):

    email = StringField(
        _l('Email'),
        filters=[strip_whitespace],
        validators=[InputRequired()])

    password = PasswordField(
        _l('Password'),
        render_kw={'autocomplete': 'off'},
        filters=[strip_whitespace],
        validators=[InputRequired()])

    remember = BooleanField(_l('Remember me'))

    submit = SubmitField(_l('Log in'))

Der Test:

def test_an_auth_login_form_labels(client_lang_code_an):
    client,  lang_code  =  client_lang_code_an
    """
    TEST: check login form labels
    EXPECT: proper error message
    """
    fname = get_fname()

    cleanup_db_auth()

    auth_login_url = get_auth_login_url()

    auth_login_info = AuthLoginInfo()
    auth_login_form_prefix  = auth_login_info.login_form_prefix

    auth_login_form = AuthLoginForm(prefix=auth_login_form_prefix)

    # select page
    location, rv = client_get(client, auth_login_url, follow_redirects=True)

    assert page_contains_form_field_label(rv, 'email', auth_login_form,  lang_code, form_prefix=auth_login_form_prefix)
    assert page_contains_form_field_label(rv, 'password', auth_login_form,  lang_code, form_prefix=auth_login_form_prefix)
    assert page_contains_form_field_label(rv, 'remember', auth_login_form,  lang_code, form_prefix=auth_login_form_prefix)
    assert page_contains_form_field_label(rv, 'submit', auth_login_form,  lang_code, form_prefix=auth_login_form_prefix)

Und das Ergebnis nach einem erfolgreichen Test:

tests_frontend/test_an_auth_login.py::test_an_auth_login_form_labels[de] PASSED
tests_frontend/test_an_auth_login.py::test_an_auth_login_form_labels[en] PASSED
tests_frontend/test_an_auth_login.py::test_an_auth_login_form_labels[es] PASSED
tests_frontend/test_an_auth_login.py::test_an_auth_login_form_labels[fr] PASSED
tests_frontend/test_an_auth_login.py::test_an_auth_login_form_labels[nl] PASSED
tests_frontend/test_an_auth_login.py::test_an_auth_login_form_labels[ru] PASSED
test_an_auth_login_form_labels: language NO ok file yet WARNING
texts:
test_an_auth_login_form_labels	email	de	E-Mail
test_an_auth_login_form_labels	email	en	Email
test_an_auth_login_form_labels	email	es	Correo electrónico
test_an_auth_login_form_labels	email	fr	Courriel
test_an_auth_login_form_labels	email	nl	E-mail
test_an_auth_login_form_labels	email	ru	Электронная почта
test_an_auth_login_form_labels	password	de	Passwort
test_an_auth_login_form_labels	password	en	Password
test_an_auth_login_form_labels	password	es	Contraseña
test_an_auth_login_form_labels	password	fr	Mot de passe
test_an_auth_login_form_labels	password	nl	Wachtwoord
test_an_auth_login_form_labels	password	ru	Пароль
test_an_auth_login_form_labels	remember	de	Erinnern Sie sich an mich
test_an_auth_login_form_labels	remember	en	Remember me
test_an_auth_login_form_labels	remember	es	Recuérdame
test_an_auth_login_form_labels	remember	fr	Se souvenir de moi
test_an_auth_login_form_labels	remember	nl	Herinner mij
test_an_auth_login_form_labels	remember	ru	Помните меня
test_an_auth_login_form_labels	submit	de	Einloggen
test_an_auth_login_form_labels	submit	en	Log in
test_an_auth_login_form_labels	submit	es	Iniciar sesión
test_an_auth_login_form_labels	submit	fr	Ouvrir une session
test_an_auth_login_form_labels	submit	nl	Aanmelden
test_an_auth_login_form_labels	submit	ru	Войти
Do you approve these texts (create an ok file)? [yN] 

Testabdeckung mit Pytest-cov

Ich habe auch Pytest-cov installiert, um Einzelheiten darüber zu erhalten, welche Menge des Codes während eines Tests verwendet wurde. Auch dies ist beim Testen einer Bibliothek, eines Pakets sehr lohnenswert. Sie erhalten die Zeilennummern des Codes, der in den Tests nicht verwendet wurde. Sie sollten 100% Abdeckung erhalten!

Bei Funktionstests ist es anfangs sehr schwierig, diese 100% zu erreichen, da wir Teile der Anwendung testen. Wenn wir zum Beispiel eine Klasse zur Eingabevalidierung haben, können wir zu Beginn unserer Funktionstests viele Eingabevalidierungsmethoden überspringen.

Manchmal füge ich zusätzlichen Code hinzu, um erneut zu prüfen, ob ein Wert korrekt ist. Das liegt daran, dass ich sicherstellen möchte, dass ich nicht irgendwo einen Fehler gemacht habe, und manchmal vertraue ich auch einem von mir verwendeten Paket eines Drittanbieters nicht. Dieser Code bleibt ungetestet. Ich muss hooks hinzufügen, damit dieser Code getestet werden kann ... eines Tages.

Parallele Tests

Wenn ich Tests parallel ausführen kann, ist der Stapel schneller fertig. Ich habe bereits über absichtliche Verzögerungen gesprochen, die beim Einloggen eingefügt werden. Was für eine Verschwendung von Testzeit. Aber ich kann nicht einfach alle Tests gleichzeitig ausführen. Einige Tests modifizieren den test_user, andere können die Einschränkungen beim E-Mail-Versand beeinträchtigen, usw. Ich muss sorgfältig auswählen, welche Tests voneinander unabhängig sind. Das Paket Pytest-xdist kann dabei helfen. Ich werde mich in diesen Tagen damit befassen.

Probleme

Als ich mit meinem ersten Pytest fixture begann und den Flask test_client laufen ließ, erhielt ich folgenden Fehler:

AssertionError: a  localeselector  function is already registered

Flask-Babel mit Pytest ergibt AssertionError: eine Funktion localeselector ist bereits registriert. Mit dem ersten Parameter ('de') gibt es keine Probleme, aber mit dem nächsten ('en', 'es', ...) wird die obige Fehlermeldung angezeigt. Dies geschieht, wenn ein neuer test_client angelegt wird.

Meine Abhilfe, die anscheinend gut funktioniert, ist die Verwendung einer globalen Variablen, die anzeigt, ob der localeselector bereits dekoriert wurde. Wenn nicht, dekoriere ich ihn selbst.

    # removed @babel.localeselector
    def get_locale():
        do something

    global babel_localeselector_already_registered
    if not babel_localeselector_already_registered:
        get_locale = babel.localeselector(get_locale)
        babel_localeselector_already_registered = True

Zusammenfassung

Dies ist meine erste Implementierung von hauptsächlich funktionalen Tests für diese mehrsprachige Flask CMS/Blog-Website. Das Einrichten und Schreiben der Hilfsfunktionen hat einige Zeit in Anspruch genommen. Aber diese sind für das Schreiben kompakter Tests unerlässlich. Und ich bin sicher, dass ich neue Hilfsfunktionen für neue Testkategorien schreiben muss.

Ich möchte keine Texte und Parameter in die Tests hardcodieren. Texte und Parameter können sich während der Lebensdauer eines Produktes ändern. Ich habe einigen Code geändert und 'Info'-Klassen hinzugefügt, damit ich die Texte und Parameter, die ich testen möchte, einziehen kann.

Tests mit Texten erfordern Interaktion und müssen manuell genehmigt werden. Nach der Genehmigung kann ich die gleichen Tests durchführen und die Texte werden automatisch gegen die genehmigten Texte geprüft. Wenn sich Texte ändern, wird ein FEHLER angezeigt, und wir werden erneut aufgefordert, diese Änderungen zu genehmigen.

Ich habe auch einige 30+ -Tests mit Selenium implementiert. Aber ich beschloss, mich zunächst auf die Pytest + Flask zu konzentrieren.

Es gibt noch viel zu wünschen übrig, aber lassen Sie mich zuerst noch etwas Code und Tests schreiben ...

Links / Impressum

Create and import helper functions in tests without creating packages in test directory using py.test
https://stackoverflow.com/questions/33508060/create-and-import-helper-functions-in-tests-without-creating-packages-in-test-di

how to testing flask-login? #40
https://github.com/pytest-dev/pytest-flask/issues/40

Pytest - How to override fixture parameter list from command line?
https://stackoverflow.com/questions/51992562/pytest-how-to-override-fixture-parameter-list-from-command-line

pytest cheat sheet
https://gist.github.com/kwmiebach/3fd49612ef7a52b5ce3a

Python Pytest assertion error reporting
https://code-maven.com/pytest-assert-error-reporting

Testing Flask Applications
https://flask.palletsprojects.com/en/1.1.x/testing/

Einen Kommentar hinterlassen

Kommentieren Sie anonym oder melden Sie sich zum Kommentieren an.

Kommentare (1)

Eine Antwort hinterlassen

Antworten Sie anonym oder melden Sie sich an, um zu antworten.

avatar

Thank you for such a thorough setup. I'll be copying things from here for sure!