Zurück Weiter 
Dieses Buch ist das fehlende Kapitel, das in jedem umfassenden Python-Buch fehlt.
Frank Ruiz
Principal Site Reliability Engineer, Box, Inc.

Die Beispiele in diesem Buch wurden mit Python 3.6 und pytest 3.2 geschrieben. pytest 3.2 unterstützt Python 2.6, 2.7 und Python 3.3+
Der Quellcode für das Aufgabenprojekt sowie für alle in diesem Buch gezeigten Tests ist unter dem Link auf der Webseite des Buches unter pragprog.com verfügbar . Sie müssen den Quellcode nicht herunterladen, um den Testcode zu verstehen. Der Testcode wird in den Beispielen in einer praktischen Form dargestellt. Um jedoch die Aufgaben des Projekts zu verfolgen oder Testbeispiele anzupassen, um Ihr eigenes Projekt zu testen (Ihre Hände sind losgebunden!), Müssen Sie auf die Webseite des Buches gehen und die Arbeit herunterladen. Dort, auf der Webseite des Buches, gibt es einen Link für Errata- Nachrichten und ein Diskussionsforum .
Unter dem Spoiler befindet sich eine Liste der Artikel dieser Reihe.
Nachdem Sie die Grundlagen von pytest kennengelernt haben, wenden wir uns den Vorrichtungen zu, die für die Strukturierung des Testcodes für fast jedes nicht triviale Softwaresystem erforderlich sind. Vorrichtungen sind Funktionen, die von pytest vor (und manchmal nach) den eigentlichen Testfunktionen ausgeführt werden. Der Fixture Code kann alles tun, was Sie brauchen. Sie können Fixtures verwenden, um einen Datensatz zum Testen abzurufen. Sie können Fixtures verwenden, um das System in einen bekannten Zustand zu versetzen, bevor Sie den Test ausführen. Vorrichtungen werden auch verwendet, um Daten für mehrere Tests zu erhalten.
Hier ist ein einfaches Beispiel für ein Gerät, das eine Zahl zurückgibt:
ch3 / test_fixtures.py
import pytest @pytest.fixture() def some_data(): """Return answer to ultimate question.""" return 42 def test_some_data(some_data): """Use fixture return value in a test.""" assert some_data == 42
Der @pytest.fixture()
wird verwendet, um pytest mitzuteilen, dass die Funktion ein Fixture ist. Wenn Sie den Fixture-Namen in die Parameterliste der Testfunktion aufnehmen, weiß pytest, wie er ausgeführt wird, bevor der Test ausgeführt wird. Fixtures können die Arbeit erledigen oder Daten an eine Testfunktion zurückgeben.
Der Test test_some_data()
hat den test_some_data()
Namen some_data
als Parameter. pytest erkennt dies und findet ein Gerät mit diesem Namen. Der Name ist im Pytest von Bedeutung. pytest sucht im Testmodul nach einem Gerät mit diesem Namen. Er wird auch in conftest.py suchen, wenn er es darin nicht findet.
Bevor wir mit der Erforschung von Fixtures (und der Datei conftest.py) beginnen, muss ich berücksichtigen, dass der Begriff Fixture in der Programmier- und Test-Community und sogar in der Python-Community viele Bedeutungen hat. Ich verwende das fixture
, die fixture function
und die fixture method
austauschbar, um auf die in diesem Kapitel beschriebenen @pytest.fixture()
-Funktionen zu verweisen. Fixture kann auch verwendet werden, um eine Ressource anzugeben, auf die von einer Fixture-Funktion verwiesen wird. Fixture-Funktionen richten häufig Daten ein oder rufen sie ab, mit denen der Test arbeiten kann. Manchmal werden diese Daten als Fixture angesehen. Beispielsweise verwendet die Django-Community häufig Fixtures, um einige der Rohdaten anzugeben, die zu Beginn der Anwendung in die Datenbank geladen werden.
Unabhängig von anderen Bedeutungen beziehen sich Testvorrichtungen in pytest und in diesem Buch auf den Mechanismus, den pytest bereitstellt, um den Code „Vorbereitungen für“ und „Aufräumen nach“ von Ihren Testfunktionen zu trennen.
Pytest-Geräte sind eine der einzigartigen Eigenschaften, die Pytest gegenüber anderen Testumgebungen auslösen und der Grund sind, warum viele angesehene Menschen zu ... wechseln und bei Pytest bleiben. Fixtures in Pytest unterscheiden sich jedoch von Fixtures in Django und unterscheiden sich von Setup- und Teardown-Verfahren, die in Unittest und Nase zu finden sind. Es gibt viele Funktionen und Nuancen, wenn es um Geräte geht. Sobald Sie ein gutes mentales Modell ihrer Funktionsweise erhalten, werden Sie sich besser fühlen. Sie müssen jedoch eine Weile mit ihnen spielen, um hineinzufahren, also fangen wir an.
Fixtures über conftest.py teilen
Sie können Fixtures in separaten Testdateien ablegen. Um Fixtures jedoch in mehreren Testdateien freizugeben , ist es besser, die Datei conftest.py an einem gemeinsamen Ort zentral für alle Tests zu verwenden. Bei einem Task-Projekt befinden sich alle Fixtures in tasks_proj/tests/conftest.py
.
Von dort aus können die Vorrichtungen von jedem Teig geteilt werden. Sie können Geräte in separate Testdateien einfügen, wenn das Gerät nur für Tests dieser Datei verwendet werden soll. Ebenso können Sie andere conftest.py- Dateien in Unterverzeichnissen des obersten Testverzeichnisses haben. In diesem Fall stehen die in diesen conftest.py-Dateien auf niedriger Ebene definierten Fixtures für Tests in diesem Verzeichnis und in diesen Unterverzeichnissen zur Verfügung. Bisher wurden jedoch Geräte im Aufgabenprojekt für jeden Test entwickelt. Daher ist es am sinnvollsten , alle unsere Tools in der Datei conftest.py im Stammverzeichnis des Tests, tasks_proj/tests
, zu verwenden.
Obwohl conftest.py ein Python-Modul ist, sollte es nicht von Testdateien importiert werden. Conftest nicht importieren wann! Die Datei conftest.py wird von pytest gelesen und als lokales Plug-In betrachtet. Dies wird deutlich, wenn wir in Kapitel 5 „Plug-Ins“ auf Seite 95 über Plug-Ins sprechen. Betrachten Sie zunächst tests/conftest.py
als den Ort, an dem wir Fixtures verwenden können Alle Tests im Testverzeichnis. Dann überarbeiten wir einige unserer Tests für task_proj
, um die Fixtures korrekt zu verwenden.
Verwenden von Vorrichtungen zum Einrichten und Herunterfahren
Bei den meisten Tests im Aufgabenprojekt wird davon ausgegangen, dass die Aufgaben-Datenbank bereits konfiguriert, ausgeführt und bereit ist. Und wir müssen am Ende einige Einträge entfernen, wenn eine Reinigung erforderlich ist. Möglicherweise müssen Sie auch die Verbindung zur Datenbank trennen. Glücklicherweise wurde das meiste im Aufgabencode mit tasks.start_tasks_db(<directory to store db\>, 'tiny' or 'mongo')
und tasks.stop_tasks_db()
; Wir müssen sie nur zum richtigen Zeitpunkt aufrufen und wir brauchen auch ein temporäres Verzeichnis.
Glücklicherweise enthält pytest ein ausgezeichnetes Gerät namens tmpdir. Wir können es zum Testen verwenden und müssen uns nicht um das Aufräumen kümmern. Dies ist keine Magie, sondern nur eine gute Codierungspraxis der neugierigsten Menschen. (Keine Sorge, wir werden tmpdir analysieren und mit tmpdir_factory im Abschnitt „Verwenden von tmpdir und tmpdir_factory“ auf Seite 71 detaillierter schreiben.)
Angesichts all dieser Komponenten funktioniert dieses Gerät hervorragend:
ch3 / a / tasks_proj
/tests/conftest.py
import pytest import tasks from tasks import Task @pytest.fixture() def tasks_db(tmpdir): """ , .""" # Setup : start db tasks.start_tasks_db(str(tmpdir), 'tiny') yield # # Teardown : stop db tasks.stop_tasks_db()
Der Wert von tmpdir ist keine Zeichenfolge, sondern ein Objekt, das ein Verzeichnis darstellt. Es implementiert jedoch __str__
, sodass wir str()
, um die Zeichenfolge an start_tasks_db()
. Im Moment verwenden wir noch tiny für TinyDB.
Die Fixture-Funktion wird vor Tests ausgeführt, die sie verwenden. Wenn die Funktion jedoch Ausbeute hat , stoppt sie dort, die Kontrolle wird an die Tests und die nächste Zeile übergeben, nachdem die Ausbeute ausgeführt wurde, nachdem die Tests abgeschlossen sind. Stellen Sie sich daher den obigen Code als "Setup" und den Code nach Yield als "Teardown" vor. Der Code nach dem "Teardown" wird unabhängig davon ausgeführt, was während der Tests passiert. Wir geben keine Daten mit Ausgabe in diesem Gerät zurück. Aber du kannst.
Lassen Sie uns einen unserer tasks.add()
Tests ändern, um dieses Gerät zu verwenden:
ch3 / a / tasks_proj
/ tests / test_add
/ test_add
.py
import pytest import tasks from tasks import Task def test_add_returns_valid_id(tasks_db): """tasks.add(<valid task>) .""" # GIVEN # WHEN # THEN task_id int new_task = Task('do something') task_id = tasks.add(new_task) assert isinstance(task_id, int)
Die wichtigste Änderung hierbei ist, dass das zusätzliche Gerät in der Datei entfernt wurde und wir der Liste der tasks_db
hinzugefügt tasks_db
. Ich mag es, Tests im GIVEN / WHEN / THEN- Format (DANO / WHEN / AFTER) mit Kommentaren zu strukturieren, insbesondere wenn dies aus dem Code nicht ersichtlich ist, was passiert. Ich denke, das ist in diesem Fall nützlich. Hoffentlich helfen GIVEN initialisierte Datenbankaufgaben dabei herauszufinden, warum tasks_db
als tasks_db
verwendet wird.
Stellen Sie sicher, dass Tasks installiert ist.
In diesem Kapitel, das zuerst in Kapitel 2 installiert wurde, schreiben wir noch Tests für das Aufgabenprojekt. Wenn Sie dieses Kapitel übersprungen haben, müssen Sie die Aufgaben mit CD-Code installieren. pip install ./tasks_proj/
.
Tracing Fixture Execution mit –setup-show
Wenn Sie den Test im letzten Abschnitt ausführen, sehen Sie nicht, welche Geräte ausgeführt werden:
$ cd /path/to/code/ $ pip install ./tasks_proj/ # $ cd /path/to/code/ch3/a/tasks_proj/tests/func $ pytest -v test_add.py -k valid_id ===================== test session starts ====================== collected 3 items test_add.py::test_add_returns_valid_id PASSED ====================== 2 tests deselected ====================== ============ 1 passed, 2 deselected in 0.02 seconds ============
Wenn ich Leuchten entwerfe, muss ich sehen, was wann funktioniert. Glücklicherweise bietet pytest ein solches Befehlszeilenflag, -- setup-show
, das genau das tut:
$ pytest --setup-show test_add.py -k valid_id ============================= test session starts ============================= collected 3 items / 2 deselected test_add.py SETUP S tmpdir_factory SETUP F tmpdir (fixtures used: tmpdir_factory) SETUP F tasks_db (fixtures used: tmpdir) func/test_add.py::test_add_returns_valid_id (fixtures used: tasks_db, tmpdir, tmpdir_factory). TEARDOWN F tasks_db TEARDOWN F tmpdir TEARDOWN S tmpdir_factory =================== 1 passed, 2 deselected in 0.18 seconds ====================
Unser Test befindet sich in der Mitte und der Pytest bezeichnet den SETUP- und TEARDOWN-Teil für jedes Gerät. Beginnend mit test_add_returns_valid_id
Sie, dass tmpdir
vor dem Test funktioniert hat. Und davor tmpdir_factory
. tmpdir
scheint es als Fixture tmpdir
benutzen.
F und S vor den Gerätenamen geben den Bereich an. F für den Bereich und S für den Sitzungsbereich. Ich werde den Bereich im Abschnitt "Scope Fixture Specification" auf Seite 56 behandeln.
Verwenden von Vorrichtungen für Testdaten
Geräte sind ein großartiger Ort, um Daten zum Testen zu speichern. Sie können alles zurückgeben. Hier ist ein Gerät, das ein Tupel vom gemischten Typ zurückgibt:
ch3 / test_fixtures.py
@pytest.fixture() def a_tuple(): """ - """ return (1, 'foo', None, {'bar': 23}) def test_a_tuple(a_tuple): """Demo the a_tuple fixture.""" assert a_tuple[3]['bar'] == 32
Da test_a_tuple()
fehlschlagen sollte (23! = 32) , werden wir sehen, was passiert, wenn der Fixture-Test fehlschlägt:
$ cd /path/to/code/ch3 $ pytest test_fixtures.py::test_a_tuple ============================= test session starts ============================= collected 1 item test_fixtures.py F [100%] ================================== FAILURES =================================== ________________________________ test_a_tuple _________________________________ a_tuple = (1, 'foo', None, {'bar': 23}) def test_a_tuple(a_tuple): """Demo the a_tuple fixture.""" > assert a_tuple[3]['bar'] == 32 E assert 23 == 32 test_fixtures.py:38: AssertionError ========================== 1 failed in 0.17 seconds ===========================
Zusammen mit dem Stapelverfolgungsabschnitt zeigt pytest die Wertparameter der Funktion an, die die Ausnahme oder die fehlgeschlagene Bestätigung verursacht hat. Bei Tests sind Vorrichtungen die Parameter für den Test, daher werden sie mithilfe der Stapelverfolgung gemeldet. Was passiert, wenn Assert (oder Ausnahme) im Fixture auftritt?
$ pytest -v test_fixtures.py::test_other_data ============================= test session starts ============================= test_fixtures.py::test_other_data ERROR [100%] =================================== ERRORS ==================================== ______________________ ERROR at setup of test_other_data ______________________ @pytest.fixture() def some_other_data(): """Raise an exception from fixture.""" x = 43 > assert x == 42 E assert 43 == 42 test_fixtures.py:21: AssertionError =========================== 1 error in 0.13 seconds ===========================
Ein paar Dinge passieren. Die Stapelverfolgung zeigt korrekt an, dass die Bestätigung in der Fixture-Funktion aufgetreten ist. Außerdem wird test_other_data
nicht als FAIL , sondern als ERROR gemeldet. Dies ist ein großer Unterschied. Wenn der Test plötzlich fehlschlägt, wissen Sie, dass der Fehler im Test selbst aufgetreten ist und nicht von einem Gerät abhängt.
Aber was ist mit dem Aufgabenprojekt? Für das Aufgabenprojekt könnten wir wahrscheinlich einige Datenvorrichtungen verwenden, möglicherweise unterschiedliche Aufgabenlisten mit unterschiedlichen Eigenschaften:
ch3 / a / task_proj / tests / conftest.py
# Task constructor # Task(summary=None, owner=None, done=False, id=None) # summary # owner done # id @pytest.fixture() def tasks_just_a_few(): """ .""" return ( Task('Write some code', 'Brian', True), Task("Code review Brian's code", 'Katie', False), Task('Fix what Brian did', 'Michelle', False)) @pytest.fixture() def tasks_mult_per_owner(): """ .""" return ( Task('Make a cookie', 'Raphael'), Task('Use an emoji', 'Raphael'), Task('Move to Berlin', 'Raphael'), Task('Create', 'Michelle'), Task('Inspire', 'Michelle'), Task('Encourage', 'Michelle'), Task('Do a handstand', 'Daniel'), Task('Write some books', 'Daniel'), Task('Eat ice cream', 'Daniel'))
Sie können sie direkt aus Tests oder aus anderen Geräten verwenden. Lassen Sie uns mit ihrer Hilfe nicht leere Datenbanken zum Testen erstellen.
Verwenden mehrerer Geräte
Sie haben bereits gesehen, dass tmpdir tmpdir_factory verwendet. Und Sie haben tmpdir in unserem Fixture task_db verwendet. Lassen Sie uns die Kette fortsetzen und einige spezielle Vorrichtungen für nicht leere Basen des Aufgabenprojekts hinzufügen:
ch3 / a / task_proj / tests / conftest.py
@pytest.fixture() def db_with_3_tasks(tasks_db, tasks_just_a_few): """ 3 , .""" for t in tasks_just_a_few: tasks.add(t) @pytest.fixture() def db_with_multi_per_owner(tasks_db, tasks_mult_per_owner): """ 9 , 3 owners, 3 .""" for t in tasks_mult_per_owner: tasks.add(t)
Alle diese tasks_db
enthalten zwei tasks_db
in ihrer Parameterliste: tasks_db
und Datensatz. Ein Dataset wird verwendet, um der Datenbank Aufgaben hinzuzufügen. Jetzt können Tests sie verwenden, wenn der Test mit einer nicht leeren Datenbank beginnen soll, zum Beispiel:
ch3 / a / task_proj / tests / func / test_add.py
def test_add_increases_count(db_with_3_tasks): """Test tasks.add() tasks.count().""" # GIVEN db 3 # WHEN tasks.add(Task('throw a party')) # THEN 1 assert tasks.count() == 4
Dies zeigt auch einen der Hauptgründe für die Verwendung von Vorrichtungen: Den Test auf das zu konzentrieren, was Sie tatsächlich testen, anstatt auf das, was Sie tun mussten, um sich auf den Test vorzubereiten. Ich verwende gerne Kommentare für GIVEN / WHEN / THEN und versuche aus zwei Gründen, so viele Daten (GIVEN) wie möglich in Fixtures zu pushen. Erstens macht es den Test lesbarer und damit wartbarer. Zweitens führt eine Bestätigung oder Ausnahme in einem Gerät zu einem Fehler (ERROR), während eine Bestätigung oder Ausnahme in einer Testfunktion zu einem Fehler (FAIL) führt. Ich möchte nicht, dass test_add_increases_count()
wenn die Initialisierung der Datenbank fehlschlägt. Es ist nur verwirrend. Ich möchte, dass der Fehler (FAIL) von test_add_increases_count()
nur möglich ist, wenn add ()
den Zähler wirklich nicht ändern kann. Lassen Sie uns sehen, wie alle Geräte funktionieren:
$ cd /path/to/code/ch3/a/tasks_proj/tests/func $ pytest --setup-show test_add.py::test_add_increases_count ============================= test session starts ============================= collected 1 item test_add.py SETUP S tmpdir_factory SETUP F tmpdir (fixtures used: tmpdir_factory) SETUP F tasks_db (fixtures used: tmpdir) SETUP F tasks_just_a_few SETUP F db_with_3_tasks (fixtures used: tasks_db, tasks_just_a_few) func/test_add.py::test_add_increases_count (fixtures used: db_with_3_tasks, tasks_db, tasks_just_a_few, tmpdir, tmpdir_factory). TEARDOWN F db_with_3_tasks TEARDOWN F tasks_just_a_few TEARDOWN F tasks_db TEARDOWN F tmpdir TEARDOWN S tmpdir_factory ========================== 1 passed in 0.20 seconds ===========================
Wir haben wieder ein paar Fs und Ss für den Funktions- und Sitzungsbereich. Mal sehen, was es ist.
Scope Fixture Specification
Fixtures enthalten einen optionalen Parameter namens scope , der bestimmt, wie oft Fixtures eingerichtet und heruntergefahren werden. Der Bereichsparameter für @ pytest.fixture()
kann Funktions-, Klassen-, Modul- oder Sitzungswerte haben. Der Bereich ist standardmäßig eine Funktion. Die Einstellungen fürasks_db und alle Geräte definieren noch keinen Bereich. Sie sind also funktionale Vorrichtungen.
Das Folgende ist eine kurze Beschreibung jedes Bereichswerts :
scope = 'function'
Es wird einmal für jede Funktion des Tests durchgeführt. Der Setup-Teil wird vor jedem Test mit dem Gerät ausgeführt. Der Abreißteil beginnt nach jedem Test mit der Vorrichtung. Dies ist der Standardbereich, wenn der Bereichsparameter nicht angegeben ist.
scope = 'class'
Es wird einmal für jede Testklasse ausgeführt, unabhängig von der Anzahl der Testmethoden in der Klasse.
scope = 'module'
Es wird einmal für jedes Modul ausgeführt, unabhängig davon, wie viele Testfunktionen oder Methoden oder andere Vorrichtungen bei Verwendung des Moduls verwendet werden.
scope = 'session'
Es wird einmal pro Sitzung durchgeführt. Alle Testmethoden und -funktionen, die Sitzungsumfangsgeräte verwenden, verwenden einen einzigen Setup- und Teardown-Aufruf.
So sehen die Bereichswerte in Aktion aus:
ch3 / test_scope.py
"""Demo fixture scope.""" import pytest @pytest.fixture(scope='function') def func_scope(): """A function scope fixture.""" @pytest.fixture(scope='module') def mod_scope(): """A module scope fixture.""" @pytest.fixture(scope='session') def sess_scope(): """A session scope fixture.""" @pytest.fixture(scope='class') def class_scope(): """A class scope fixture.""" def test_1(sess_scope, mod_scope, func_scope): """ , .""" def test_2(sess_scope, mod_scope, func_scope): """ .""" @pytest.mark.usefixtures('class_scope') class TestSomething(): """Demo class scope fixtures.""" def test_3(self): """Test using a class scope fixture.""" def test_4(self): """Again, multiple tests are more fun."""
Verwenden --setup-show
, um zu demonstrieren, dass die Anzahl der Fixture- und Setup-Aufrufe in Verbindung mit dem Teardown je nach Region ausgeführt wird:
$ cd /path/to/code/ch3/ $ pytest --setup-show test_scope.py ============================= test session starts ============================= collected 4 items test_scope.py SETUP S sess_scope SETUP M mod_scope SETUP F func_scope test_scope.py::test_1 (fixtures used: func_scope, mod_scope, sess_scope). TEARDOWN F func_scope SETUP F func_scope test_scope.py::test_2 (fixtures used: func_scope, mod_scope, sess_scope). TEARDOWN F func_scope SETUP C class_scope test_scope.py::TestSomething::()::test_3 (fixtures used: class_scope). test_scope.py::TestSomething::()::test_4 (fixtures used: class_scope). TEARDOWN C class_scope TEARDOWN M mod_scope TEARDOWN S sess_scope ========================== 4 passed in 0.11 seconds ===========================
Jetzt sehen Sie nicht nur F und S für Funktion und Sitzung, sondern auch C und M für Klasse und Modul.
Der Umfang wird mithilfe von Fixtures definiert. Ich weiß, dass dies aus dem Code ersichtlich ist, aber dies ist ein wichtiger Punkt, um sicherzustellen, dass Sie vollständig stöhnen. verstehe "). Der Bereich wird in der Fixture-Definition definiert und nicht am Ort des Aufrufs. Testfunktionen, die Geräte verwenden, steuern nicht, wie oft (SETUP) und Geräte unterbrochen werden (TEARDOWN).
Geräte können nur von anderen Geräten mit demselben oder einem erweiterten Umfang abhängen. Daher kann das Funktionsbereichsgerät von einem anderen Funktionsbereichsgerät abhängen (standardmäßig und wird weiterhin im Aufgabenprojekt verwendet). Der Funktionsumfang kann auch von der Klasse, dem Modul und den Geräten des Sitzungsbereichs abhängen, jedoch niemals in umgekehrter Reihenfolge.
Änderungsbereich für Aufgaben Projektvorrichtungen
Angesichts dieser Kenntnis des Umfangs ändern wir nun den Umfang einiger Vorrichtungen des Task-Projekts.
Bisher hatten wir keine Probleme mit der Testzeit. Sie müssen jedoch zugeben, dass es sinnlos ist, für jeden Test ein temporäres Verzeichnis und eine neue Datenbankverbindung zu erstellen. Solange wir bei Bedarf eine leere Datenbank bereitstellen können, sollte dies ausreichen.
Um so etwas wie tasks_db
als Sitzungsbereich zu verwenden, müssen Sie tmpdir_factory
, da tmpdir
der Funktionsumfang und tmpdir_factory
der Sitzungsbereich ist. Glücklicherweise ist dies nur eine Zeile der Codeänderung (zwei, wenn Sie tmpdir->tmpdir_factory
in der Parameterliste berücksichtigen):
ch3 / b / task_proj / tests / conftest.py
"""Define some fixtures to use in the project.""" import pytest import tasks from tasks import Task @pytest.fixture(scope='session') def tasks_db_session(tmpdir_factory): """Connect to db before tests, disconnect after.""" temp_dir = tmpdir_factory.mktemp('temp') tasks.start_tasks_db(str(temp_dir), 'tiny') yield tasks.stop_tasks_db() @pytest.fixture() def tasks_db(tasks_db_session): """An empty tasks db.""" tasks.delete_all()
Hier haben wir tasks_db
abhängig von tasks_db_session
geändert und alle Einträge gelöscht, um sicherzustellen, dass sie leer sind. Da wir den Namen nicht geändert haben, sollte sich keines der Geräte oder Tests, die ihn bereits enthalten, ändern.
Datenvorrichtungen geben einfach einen Wert zurück, sodass es wirklich keinen Grund gibt, ständig zu arbeiten. Einmal pro Sitzung ist genug:
ch3 / b / task_proj / tests / conftest.py
# Reminder of Task constructor interface # Task(summary=None, owner=None, done=False, id=None) # summary is required # owner and done are optional # id is set by database @pytest.fixture(scope='session') def tasks_just_a_few(): """All summaries and owners are unique.""" return ( Task('Write some code', 'Brian', True), Task("Code review Brian's code", 'Katie', False), Task('Fix what Brian did', 'Michelle', False)) @pytest.fixture(scope='session') def tasks_mult_per_owner(): """Several owners with several tasks each.""" return ( Task('Make a cookie', 'Raphael'), Task('Use an emoji', 'Raphael'), Task('Move to Berlin', 'Raphael'), Task('Create', 'Michelle'), Task('Inspire', 'Michelle'), Task('Encourage', 'Michelle'), Task('Do a handstand', 'Daniel'), Task('Write some books', 'Daniel'), Task('Eat ice cream', 'Daniel'))
Nun wollen wir sehen, ob all diese Änderungen mit unseren Tests funktionieren:
$ cd /path/to/code/ch3/b/tasks_proj $ pytest ===================== test session starts ====================== collected 55 items tests/func/test_add.py ... tests/func/test_add_variety.py ............................ tests/func/test_add_variety2.py ............ tests/func/test_api_exceptions.py ....... tests/func/test_unique_id.py . tests/unit/test_task.py .... ================== 55 passed in 0.17 seconds ===================
Alles scheint in Ordnung zu sein. Schauen wir uns die Geräte für eine einzelne Testdatei an, um zu sehen, wie verschiedene Bereiche gemäß unseren Erwartungen funktionieren:
$ pytest --setup-show tests/func/test_add.py ============================= test session starts ============================= platform win32 -- Python 3.6.5, pytest-3.9.3, py-1.7.0, pluggy-0.8.0 rootdir: c:\_BOOKS_\pytest_si\bopytest-code\code\ch3\b\tasks_proj\tests, inifile: pytest.ini collected 3 items tests\func\test_add.py SETUP S tmpdir_factory SETUP S tasks_db_session (fixtures used: tmpdir_factory) SETUP F tasks_db (fixtures used: tasks_db_session) func/test_add.py::test_add_returns_valid_id (fixtures used: tasks_db, tasks_db_session, tmpdir_factory). TEARDOWN F tasks_db SETUP F tasks_db (fixtures used: tasks_db_session) func/test_add.py::test_added_task_has_id_set (fixtures used: tasks_db, tasks_db_session, tmpdir_factory). TEARDOWN F tasks_db SETUP S tasks_just_a_few SETUP F tasks_db (fixtures used: tasks_db_session) SETUP F db_with_3_tasks (fixtures used: tasks_db, tasks_just_a_few) func/test_add.py::test_add_increases_count (fixtures used: db_with_3_tasks, tasks_db, tasks_db_session, tasks_just_a_few, tmpdir_factory). TEARDOWN F db_with_3_tasks TEARDOWN F tasks_db TEARDOWN S tasks_db_session TEARDOWN S tmpdir_factory TEARDOWN S tasks_just_a_few ========================== 3 passed in 0.24 seconds ===========================
Ja. . tasks_db_session
, task_db
.
Specifying Fixtures with usefixtures
, , , . , @pytest.mark.usefixtures('fixture1', 'fixture2')
. usefixtures , , . — . :
ch3/test_scope.py
@pytest.mark.usefixtures('class_scope') class TestSomething(): """Demo class scope fixtures.""" def test_3(self): """Test using a class scope fixture.""" def test_4(self): """Again, multiple tests are more fun."""
usefixtures , . , , . , - usefixtures , .
autouse Fixtures That Always Get Used ( )
, , ( usefixtures ). autouse=True , . , , . :
ch3/test_autouse.py
""" autouse fixtures.""" import pytest import time @pytest.fixture(autouse=True, scope='session') def footer_session_scope(): """ session().""" yield now = time.time() print('--') print('finished : {}'.format(time.strftime('%d %b %X', time.localtime(now)))) print('-----------------') @pytest.fixture(autouse=True) def footer_function_scope(): """ .""" start = time.time() yield stop = time.time() delta = stop - start print('\ntest duration : {:0.3} seconds'.format(delta)) def test_1(): """ .""" time.sleep(1) def test_2(): """ .""" time.sleep(1.23)
, . :
$ cd /path/to/code/ch3 $ pytest -v -s test_autouse.py ===================== test session starts ====================== collected 2 items test_autouse.py::test_1 PASSED test duration : 1.0 seconds test_autouse.py::test_2 PASSED test duration : 1.24 seconds -- finished : 25 Jul 16:18:27 ----------------- =================== 2 passed in 2.25 seconds ===================
autouse . , . , .
, autouse , , tasks_db
. Tasks , , , API . . , .
Fixtures
, , , . , pytest name @pytest.fixture()
:
ch3/ test_rename_fixture.py
""" fixture renaming.""" import pytest @pytest.fixture(name='lue') def ultimate_answer_to_life_the_universe_and_everything(): """ .""" return 42 def test_everything(lue): """ .""" assert lue == 42
lue fixture
, fixture_with_a_name_much_longer_than_lue
. , --setup-show
:
$ pytest --setup-show test_rename_fixture.py ======================== test session starts ======================== collected 1 items test_rename_fixture.py SETUP F lue test_rename_fixture.py::test_everything (fixtures used: lue). TEARDOWN F lue ===================== 1 passed in 0.01 seconds ======================
, lue , pytest --fixtures
. , , , :
$ pytest --fixtures test_rename_fixture.py ======================== test session starts ======================= ... ------------------ fixtures defined from test_rename_fixture ------------------ lue Return ultimate answer. ================= no tests ran in 0.01 seconds =================
— . , , , , , . , lue . «Tasks»:
$ cd /path/to/code/ch3/b/tasks_proj $ pytest --fixtures tests/func/test_add.py ======================== test session starts ======================== ... tmpdir_factory Return a TempdirFactory instance for the test session. tmpdir Return a temporary directory path object which is unique to each test function invocation, created as a sub directory of the base temporary directory. The returned object is a `py.path.local`_ path object. ----------------------- fixtures defined from conftest ------------------------ tasks_db An empty tasks db. tasks_just_a_few All summaries and owners are unique. tasks_mult_per_owner Several owners with several tasks each. db_with_3_tasks Connected db with 3 tasks, all unique. db_with_multi_per_owner Connected db with 9 tasks, 3 owners, all with 3 tasks. tasks_db_session Connect to db before tests, disconnect after. =================== no tests ran in 0.01 seconds ====================
Großartig! conftest.py . tmpdir
tmpdir_factory
, .
[Parametrized Testing] , . 42, . . - , , :
ch3/b/tasks_proj/tests/func/test_add_variety2.py
"""Test the tasks.add() API function."""
import pytest
import tasks
from tasks import Task
tasks_to_try = (Task('sleep', done=True),
Task('wake', 'brian'),
Task('breathe', 'BRIAN', True),
Task('exercise', 'BrIaN', False))
task_ids = ['Task({},{},{})'.format(t.summary, t.owner, t.done)
for t in tasks_to_try]
def equivalent(t1, t2):
"""Check two tasks for equivalence."""
return ((t1.summary == t2.summary) and
(t1.owner == t2.owner) and
(t1.done == t2.done))
, , a_task
:
ch3/b/tasks_proj/tests/func/ test_add_variety2.py
@pytest.fixture(params=tasks_to_try)
def a_task(request):
""" ."""
return request.param
def test_add_a(tasks_db, a_task):
""" a_task ( ids)."""
task_id = tasks.add(a_task)
t_from_db = tasks.get(task_id)
assert equivalent(t_from_db, a_task)
, fixture, , . . param, , params @pytest.fixture(params=tasks_to_try)
.
a_task
— request.param
, . , , :
$ cd /path/to/code/ch3/b/tasks_proj/tests/func $ pytest -v test_add_variety2.py::test_add_a ===================== test session starts ====================== collected 4 items test_add_variety2.py::test_add_a[a_task0] PASSED test_add_variety2.py::test_add_a[a_task1] PASSED test_add_variety2.py::test_add_a[a_task2] PASSED test_add_variety2.py::test_add_a[a_task3] PASSED =================== 4 passed in 0.03 seconds ===================
, pytest , () . , :
ch3/b/tasks_proj/tests/func/ test_add_variety2.py
@pytest.fixture(params=tasks_to_try, ids=task_ids)
def b_task(request):
""" ."""
return request.param
def test_add_b(tasks_db, b_task):
""" b_task, ."""
task_id = tasks.add(b_task)
t_from_db = tasks.get(task_id)
assert equivalent(t_from_db, b_task)
:
$ pytest -v test_add_variety2.py::test_add_b ===================== test session starts ====================== collected 4 items test_add_variety2.py::test_add_b[Task(sleep,None,True)] PASSED test_add_variety2.py::test_add_b[Task(wake,brian,False)] PASSED test_add_variety2.py::test_add_b[Task(breathe,BRIAN,True)] PASSED test_add_variety2.py::test_add_b[Task(exercise,BrIaN,False)] PASSED =================== 4 passed in 0.04 seconds ===================
ids
, , . , :
ch3/b/tasks_proj/tests/func/ test_add_variety2.py
def id_func(fixture_value):
""" ."""
t = fixture_value
return 'Task({},{},{})'.format(t.summary, t.owner, t.done)
@pytest.fixture(params=tasks_to_try, ids=id_func)
def c_task(request):
""" (id_func) ."""
return request.param
def test_add_c(tasks_db, c_task):
""" ."""
task_id = tasks.add(c_task)
t_from_db = tasks.get(task_id)
assert equivalent(t_from_db, c_task)
. Task, id_func()
Task , namedtuple Task Task . , , :
$ pytest -v test_add_variety2.py::test_add_c ===================== test session starts ====================== collected 4 items test_add_variety2.py::test_add_c[Task(sleep,None,True)] PASSED test_add_variety2.py::test_add_c[Task(wake,brian,False)] PASSED test_add_variety2.py::test_add_c[Task(breathe,BRIAN,True)] PASSED test_add_variety2.py::test_add_c[Task(exercise,BrIaN,False)] PASSED =================== 4 passed in 0.04 seconds ===================
. , , . , !
Fixtures Tasks Project
, Tasks. TinyDB . , . , , , , TinyDB , MongoDB .
( ), , start_tasks_db()
tasks_db_session
:
ch3/b/tasks_proj/tests/conftest.py
""" ."""
import pytest
import tasks
from tasks import Task
@pytest.fixture(scope='session')
def tasks_db_session(tmpdir_factory):
""" , ."""
temp_dir = tmpdir_factory.mktemp('temp')
tasks.start_tasks_db(str(temp_dir), 'tiny')
yield
tasks.stop_tasks_db()
@pytest.fixture()
def tasks_db(tasks_db_session):
""" tasks."""
tasks.delete_all()
db_type
start_tasks_db()
. , :
tasks_proj/src/tasks/api.py
def start_tasks_db(db_path, db_type):
MongoDB, db_type mongo. :
ch3/c/tasks_proj/tests/conftest.py
import pytest import tasks from tasks import Task
params=['tiny',' mongo'] -. request
temp_db db_type request.param
, "tiny" "mongo".
--verbose
-v
pytest , pytest . , .
MongoDB installieren
MongoDB, , MongoDB pymongo . MongoDB, https://www.mongodb.com/download-center . pymongo pip— pip install pymongo . MongoDB ; 7 .
:
$ cd /path/to/code/ch3/c/tasks_proj $ pip install pymongo $ pytest -v --tb=no ===================== test session starts ====================== collected 92 items test_add.py::test_add_returns_valid_id[tiny] PASSED test_add.py::test_added_task_has_id_set[tiny] PASSED test_add.py::test_add_increases_count[tiny] PASSED test_add_variety.py::test_add_1[tiny] PASSED test_add_variety.py::test_add_2[tiny-task0] PASSED test_add_variety.py::test_add_2[tiny-task1] PASSED ... test_add.py::test_add_returns_valid_id[mongo] FAILED test_add.py::test_added_task_has_id_set[mongo] FAILED test_add.py::test_add_increases_count[mongo] PASSED test_add_variety.py::test_add_1[mongo] FAILED test_add_variety.py::test_add_2[mongo-task0] FAILED ... ============= 42 failed, 50 passed in 4.94 seconds =============
Hm. . , , - Mongo. , pdb: , . 125. TinyDB.
Übungen
test_fixtures.py
.
2. fixtures—functions @pytest.fixture()
, . , , .- , .
- , .
pytest --setup-show test_fixtures.py
. ?scope= 'module'
4.pytest --setup-show test_fixtures.py
. Was hat sich geändert?- 6
return <data>
yield <data>
. yield
.pytest -s -v test_fixtures.py
. ?
Was weiter
pytest fixture , , building blocks , setup teardown , (, Mongo TinyDB). , , .
pytest, , (builtin) tmpdir tmpdir_factory. (builtin) .
Zurück Weiter 