@Buchfink: Die übliche Konstellation in der ich das einsetze sind Klassen ganz allgemein. Ich suche da nicht nach Klassen wo ich das einsetzen könnte, sondern es kommt manchmal vor, dass ich es aus irgendwelchen Gründen *nicht* einsetze. Zum Beispiel bei GUI-Klassen wo ja in der Regel in der `__init__()` mehr passieren muss, als einfach nur Attribute zu setzen. Ansonsten ist mir alleine das mir der immer gleiche Code von `__init__()` und `__repr__()` abgenommen wird, ein Grund das fast immer einzusetzen.
Das man die dann auch sortieren kann ist ein netter Nebeneffekt, wobei es dann so etwas wie eine ”natürliche” Reihenfolge geben muss und man die Attribute auch in der Reihenfolge angeben muss, die diese Reihenfolge beim sortieren erzeugt. Wobei auch das bei der Fehlersuche oder in (Unit-)Tests nett sein kann, wenn man eine Sequenz mit Objekten in eine kanonische Reihenfolge bringen kann, ob die nun irgendwie ”natürlich” ist, oder nicht.
Was Tests angeht, unbedingt `pytest` anschauen bevor man blindlings das `unittest` aus der Standardbibliothek verwendet. Das in der Standardbibliothek bietet eine auf XUnit basierende API, die man auch von anderen Sprachen kennt. Das ist aber so ähnlich wie XML und (Mini)DOM. Die Idee die gleiche API für alle möglichen Sprachen zu haben ist nur solange nett, wie man in Python nicht Code schreiben muss der wie Java aussieht. Oder noch schlimmer, denn selbst in Java gibt es Alternativen. Wenn die API ”portierbar” sein soll, muss man sich wohl oder übel auf die kleinste Schnittmenge an Spracheigenschaften beschränken, die alle Sprachen bieten. Wobei `pytest` *auch* mit Unit-Tests klar kommt, die mit der `unittest`-API erstellt wurden. Man muss sich da nicht entscheiden, oder wenn man schon klassische Tests hat, alles wegwerfen und neu schreiben.
Mal das Beispiel aus der `unittest`-Dokumentation:
Code: Alles auswählen
import unittest
class TestStringMethods(unittest.TestCase):
def test_upper(self):
self.assertEqual("foo".upper(), "FOO")
def test_isupper(self):
self.assertTrue("FOO".isupper())
self.assertFalse("Foo".isupper())
def test_split(self):
s = "hello world"
self.assertEqual(s.split(), ["hello", "world"])
# check that s.split fails when the separator is not a string
with self.assertRaises(TypeError):
s.split(2)
Mit `pytest`:
Code: Alles auswählen
import pytest
def test_upper():
assert "foo".upper() == "FOO"
@pytest.mark.parametrize("text, expected", [("FOO", True), ("Foo", False)])
def test_isupper(text, expected):
assert text.isupper() == expected
def test_split():
text = "hello world"
assert text.split() == ["hello", "world"]
# check that text.split fails when the separator is not a string
with pytest.raises(TypeError):
text.split(2)
Wobei es hier schon einen Unterschied in der Anzahl der Testfälle gibt: `test_isupper()` sind bei der `pytest`-Variante *zwei* Tests die unabhängig voneinander durchgeführt werden. Auch wenn der erste fehlschlägt, wird der zweite noch durchgeführt.
Die `setUp()` und `tearDown()` Varianten von XUnit kann man auch nutzen, aber `pytest` führt dafür auch einen Fixture-Mechanismus ein mit dem man Funktionen per Namen registrieren kann, und die werden dann ausgeführt wenn der registrierte Name in den Argumenten des Tests (oder einer anderen Fixturefunktion) auftauchen. Das Beispiel aus der `unittest`-Doku:
Code: Alles auswählen
import unittest
class WidgetTestCase(unittest.TestCase):
def setUp(self):
self.widget = Widget("The widget")
def test_default_widget_size(self):
self.assertEqual(
self.widget.size(), (50, 50), "incorrect default size"
)
def test_widget_resize(self):
self.widget.resize(100, 150)
self.assertEqual(
self.widget.size(), (100, 150), "wrong size after resize"
)
def tearDown(self):
self.widget.dispose()
Kann mit `pytest` so aussehen:
Code: Alles auswählen
import pytest
@pytest.fixture(name="widget")
def create_widget():
widget = Widget("The widget")
yield widget
widget.dispose()
def test_default_widget_size(widget):
assert widget.size() == (50, 50), "incorrect default size"
def test_widget_resize(widget):
widget.resize(100, 150)
assert widget.size == (100, 150), "wrong size after resize"
Ja, dass das über die Argumentnamen geht ist ein bisschen magisch. Quasi „dependency injection“ über Namen statt über Typen. Es hat aber beispielsweise den Vorteil, dass man Fixtures leicht kombinieren kann, denn man kann ja über weitere Argumente mit Namen mehr als ein Fixture anfordern. Das ist IMHO deutlich leichter ”composable” als die `setUp()`/`tearDown()`-Methoden. Spielzeugbeispiel:
Code: Alles auswählen
import pytest
@pytest.fixture(name="paragraph")
def create_paragraph():
return Paragraph("Lorem ipsum…")
@pytest.fixture(name="document")
def create_document():
return Document()
@pytest.fixture(name="filled_document")
def create_filled_document(document, paragraph):
document.add_title("Test-Titel")
document.body.add(paragraph)
...
return document
def test_document_save(filled_document, tmpdir):
path = tmpdir / "test.doc"
filled_document.save(path)
assert path.exists() and path.is_file()
content = path.read_text(encoding="utf-8")
assert "Test-Titel" in content
...
Wobei im Test `tmpdir` verwendet wird, was eines der praktischen Fixtures ist, die schon mitgeliefert werden. Da bekommt man pro Test ein leeres temporäres Verzeichnis herein gereicht, das nach dem Test samt Inhalt wieder gelöscht wird.