Sollen wir PyUnit oder PyTest für unsere Python Unittests verwenden? Falls du dir genau diese Frage stellst, lies weiter.
PyUnit vs. PyTest
Nachdem ich zwei Jahre lang PyUnit für das Testing eines Embedded Systems angewendet habe, ist es nun Zeit für einen Blick über den Tellerrand. Schon nach kurzer Recherche hat sich gezeigt, dass PyTest die naheliegende Alternative zu PyUnit ist und auch eine beachtliche Fanbase aufweist.
Deshalb werde ich die beiden Libraries einander gegenüberstellen und die Basic Features, die ich beim Testen täglich gebraucht habe, miteinander vergleichen. Am Ende des Blogs habe ich eine Liste mit weiteren Features zusammengestellt, für die sich ein Vergleich lohnen würde, die aber etwas tiefer in die Materie eindringen.
Interne vs. externe Library
Falls eine Anforderung ist, dass möglichst wenige externe Libraries verwendet werden sollen, ist die Wahl vielleicht bereits getroffen. PyUnit wird nämlich mit der Python Installation mitinstalliert, wohingegen PyTest nachträglich installiert werden muss. Am einfachsten mit pip:
> pip install -U pytest
Wie lässt man einen Test laufen
Im gewünschten Verzeichnis können Tests mit verschiedenen Optionen in der Kommandozeile aufgerufen werden.
Spezifisches Test-Files aufrufen:
- PyUnit: > py -m unittest <file_name>
- PyTest: > py -m pytest <file_name>
Alle Test-Files aufrufen, deren Filenamen mit „test“ beginnen:
- PyUnit: > py -m unittest
- PyTest: > py -m pytest
Alle Test-Files aufrufen, die ein bestimmtes Namenmuster erfüllen:
- PyUnit: > py -m unittest discover –pattern <pattern>
- PyTest: > py -m pytest -k <pattern>
Wie sieht ein simpler Test aus
Syntax
So sieht ein einfacher Test mit PyUnit aus:
import unittest class TestPyUnit(unittest.TestCase): def test_001_sum(self): for i in range(10000): r = sum([i] * i) self.assertEqual(r, i**3, "Error message")
Und so mit PyTest:
class TestPyTest(): def test_001_sum(self): for i in range(10000): r = sum([i] * i) assert r == i**3, "Error message"
Auf den ersten Blick, sieht es so aus, als könnte man sich mit PyTest einen Import sparen. Dieser wird aber zu einem späteren Zeitpunkt noch dazu kommen und wird auch in den meisten Fällen nötig sein.
Output
PyUnit Output:
test_001_sum (__main__.TestUnitTest) ... FAIL ====================================================================== FAIL: test_001_sum (__main__.TestUnitTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\Users\philip.dietrich\eclipse\pydev_workspace\testing-with-python\blog\test_with_python.py", line 24, in test_001_sum self.assertEqual(r, i**3, "Error message") AssertionError: 4 != 8 : Error message ---------------------------------------------------------------------- Ran 1 test in 0.001s FAILED (failures=1)
PyTest Output:
test_with_python.py F [100%] ================================== FAILURES =================================== _____________________________ TestPyTest.test_001_sum _____________________________ self = <test_with_python.TestPyTest object at 0x000001A7437AFFC8> def test_001_sum(self): for i in range(10000): r = sum([i] * i) > assert r == i**3, "Error message" E AssertionError: Error message E assert 4 == (2 ** 3) test_with_python.py:15: AssertionError ============================== 1 failed in 0.09s ==============================
Der Output der gescheiterten Tests zeigt in beiden Fällen etwa den selben Informationsgehalt, wie Filename, Testname, Zeilennummer des AssertionErrors, Error Message sowie die Werte, mit welchen der Test gefailed hat. Grösster Unterschied ist, dass im PyTest Output noch einige Zeilen des Codes vor dem Error ausgegeben werden. Dies kann angenehm sein, ist aber in meinen Augen eine Frage der Vorlieben des Testers, ob dies erwünscht ist oder nicht. Deshalb kann die Verbosity auch in beiden Frameworks den Bedürfnissen des Testers angepasst werden.
Der wohl wichtigste Unterschied liegt hier aber bei der Testdauer wo PyUnit mit 0.001 Sekunden PyTest mit 0.09 Sekunden in den Schatten stellt.
Wie sieht ein Test mit Setup und Teardown Methode aus
Oft möchte man gewisse Konfigurationen oder Setup Methoden vor und nach jedem Test oder allenfalls vor und nach einem ganzen Testset ausführen. Mögliche Anwendungsbeispiele sind das Öffnen und Schliessen eines Ports oder Files, generieren von Printausgaben in der Konsole, etc.
PyUnit
PyUnit bietet hierfür die vier Methoden setUp(), setUpClass(), tearDown und tearDownClass() an, welche von unittes.TestCase zur Verfügung gestellt werden und überschrieben werden können. Beim Aufruf von unittest.main() werden diese Methoden dann automatisch aufgerufen, wobei setUp()/tearDown() vor und nach jedem Test und setUpClass()/tearDownClass() vor und nach dem kompletten Testset ausgeführt werden. Und hier gleich ein Beispiel einer Anwendung sämtlicher Methoden:
import unittest class TestPyUnit(unittest.TestCase): def setUp(self): print("### Start Time: %s ###" % datetime.datetime.now()) @classmethod def setUpClass(cls): print("############## TEST SET PyUnit START ###############") def tearDown(self): print("### End Time: %s ###" % datetime.datetime.now()) @classmethod def tearDownClass(cls): print("################ TEST SET PyUnit END ###############") def test_001_sum(self): print("### Test sum() with values in range(10000) ###") for i in range(10000): r = sum([i] * i) self.assertEqual(r, i**2, "Error message") def test_002_abs(self): print("### Test abs() with values in range(10000) ###") for i in range(10000): r = abs(i * (-1)) self.assertEqual(r, i, "Error message")
############## TEST SET PyUnit START ############### ### Start Time: 2020-04-29 14:44:36.568388 ### ### Test sum() with values in range(10000) ### ### End Time: 2020-04-29 14:44:38.083498 ### ### Start Time: 2020-04-29 14:44:38.084506 ### ### Test abs() with values in range(10000) ### ### End Time: 2020-04-29 14:44:38.122504 ### ################ TEST SET PyUnit END ############### ---------------------------------------------------------------------- Ran 2 tests in 1.556s OK
PyTest
Da der Konsolenoutput von PyTest nicht so schön aussieht wie der von PyUnit und um auch gleich noch ein anderes Beispiel zu verwenden, habe ich hier nicht auf die Konsole sondern in ein log File geschrieben. PyTest bietet für setup und teardown fixtures an, welche mit verschiedenen Parametern konfiguriert werden können. Ich habe hier den Parameter scope verwendet, um eine Klassenweites setup und teardown zu kreieren und autouse auf True gesetzt, damit jeder Test automatisch die setup_teardown Methode verwendet. Wenn nicht mit autouse gearbeitet wird, muss die Fixture-Methode per Dependency Injection der Test-Methode übergeben werden.
import pytest class TestPyTest(): f = open("testlog.log", "a") @pytest.fixture(autouse=True) def setup_teardown(self): self.f.write("### Start Time: %s ###\n" % datetime.now()) yield self.f.write("### End Time: %s ###\n" % datetime.now()) @pytest.fixture(scope='class', autouse=True) def setup_teardown_class(self): self.f.write("############# TEST SET PyTest START ##############\n") yield self.f.write("############### TEST SET PyTest END ##############\n") self.f.close() def test_001_sum(self): self.f.write("### Test sum() with values in range(10000) ###\n") for i in range(10000): r = sum([i] * i) assert r == i**2, "Error message" def test_002_abs(self): self.f.write("### Test abs() with values in range(10000) ###\n") for i in range(10000): r = abs(i * (-1)) assert r == i, "Error message"
############## TEST SET PyTest START ############### ### Start Time: 2020-05-15 22:15:26.919716 ### ### Test sum() with values in range(10000) ### ### End Time: 2020-05-15 22:15:27.610318 ### ### Start Time: 2020-05-15 22:15:27.613320 ### ### Test abs() with values in range(10000) ### ### End Time: 2020-05-15 22:15:27.616318 ### ################ TEST SET PyTest END ###############
Erkenntnisse
Ich konnte bisher für keine der beiden Tools ein Ausschlusskriterium finden. PyTest erlaubt es praktisch den selben Output zu generieren wie PyUnit. Vom Syntax her spricht mich die PyUnit Umsetzung mehr an, da ich zwei separate Methoden für setUp und tearDown habe, um deren Ausführung ich mich nicht mehr kümmern muss. Die Umsetzung von PyTest mit den Fixtures bietet dem Tester aber mehr Flexibilität. Ich kann mir durchaus vorstellen, in einem anderen Projekt PyTest zu verwenden.
Ausblick
Hier eine Liste von weiteren Themen für die sich ein Vergleich der beiden Tools gelohnt hätte, die aber den Rahmen eines einzelnen Blogs gesprengt hätten:
- Speed (in diesem Blog nur kurz erwähnt)
- Skips
- Subtests
- Mocks & Stubs
- Testreport generieren
Das konzept der fixtures bei Pytest ist deutlich besser.