Bei dem Versuch, eine Funktion in meinem Python-Code zu spiegeln, bin ich auf diesen hervorragenden Blog von Durga Swaroop Perla gestoßen. Der Blog zeigt, wie Sie pytest-mock verwenden, um eine Funktion durch eine Testversion zu ersetzen. Dies wäre nützlich, wenn Sie Code testen, der eine Datenbank oder einen externen Dienst aufruft. Die Lösung stützt sich auf die Möglichkeit in Python, eine Definition im Rahmen einer Funktion durch eine neue Version zu ersetzen.
Das sah gut aus, also wollte ich die Lösung auf meinen eigenen Code anwenden. Mein Code folgt einem Service/Repository-Setup. Ich begann mit einfachen SQL-Abfragen und fügte Filterung und andere Entscheidungen auf der Grundlage von Daten hinzu. So konnte ich in meinen Tests zunächst nur die Datenbank simulieren, aber später wollte ich auch die Logik testen, die mit der Ergebnismenge ausgeführt wurde. Also dachte ich mir, ich sollte den Code in einen Teil aufteilen, der Abfragen verarbeitet, und einen Teil, der Daten manipuliert. Duh, werden Sie sagen, und ich stimme Ihnen zu. Zu meiner Verteidigung muss ich sagen, dass ich nur experimentiert habe und die Dinge aus dem Ruder gelaufen sind. Okay, tut mir leid!
Ich habe also versucht, die Lösung von Durga auf mein Setup anzuwenden und war von den Ergebnissen verwirrt. In meinem Fall wurden die Mocks ignoriert. Die Verwirrung und Frustration wurde durch das lange Herumprobieren nur noch größer, so dass ich einen Schritt zurücktrat und das Problem auf das absolute Minimum reduzierte.
Meine erste Version ist unten abgebildet. Beachten Sie, dass der Code auf drei Dateien verteilt ist, was sehr wichtig ist. Der Name der Dateien steht in den Kommentaren in den Codeschnipseln, die Quellen finden Sie hier
# service.py
from repository import my_repository
def my_service():
print('my_service - calling my_repository')
return my_repository()
# repository.py
def my_repository():
print('my_repository - this is the real thing')
return True
# test/test_service.py
from service import my_service
def test_my_service(mocker):
mocker.patch('repository.my_repository', return_value=False)
assert my_service() == False
Ich bin also davon ausgegangen, dass mocker.patch('repository.my_repository', return_value=False) in test/test_service.py die Methode my_repository() durch eine Konstante False anstelle von True ersetzen würde. Aber das ist nicht passiert, der Test ist fehlgeschlagen. Autsch!
Was ist der Unterschied zu Durgas Version? In diesem Beispiel ruft der Testcode eine Funktion auf, die eine andere Funktion aufruft, die im Test gespottet wird. Genau wie ich es gemacht habe, oder? Nicht ganz, wie sich herausstellt. Der Unterschied besteht darin, dass in meinem Fall der Code auf drei Dateien verteilt ist, eine Service- und eine Repository-Datei sowie eine Testdatei. In Durgas Blog hingegen befinden sich der Service- und der Repository-Code in einer einzigen Datei. Das macht einen Unterschied in der Funktionsweise des Mocks.
Um die Annahme einer einzigen Datei zu bestätigen, habe ich den Code für den Dienst und das Repository in einer einzigen Datei zusammengefasst, etwa so:
# service_v3.py
def my_repository():
print('my_repository - this is the real thing')
return True
def my_service():
print('my_service - calling my_repository')
return my_repository()
# test/test_service_v3.py
from service_v3 import my_service
def test_my_service(mocker):
mocker.patch('service_v3.my_repository', return_value=False)
assert my_service() == False
Das funktioniert, aber in meinem Fall würde ich mit einer großen Datei enden, die zwei Aspekte meines Codes kombiniert, die meiner Meinung nach getrennt werden sollten. Also, was nun?
Nun, ich dachte, ich ersetze vielleicht nicht das Richtige? Ich habe den Test so geändert, dass er wie folgt aussieht:
from service import my_service
def test_my_service(mocker):
mocker.patch('service.my_repository', return_value=False)
# ^------- service, because why not?
assert my_service() == False
Die entscheidende Änderung ist diese:
mocker.patch('repository.my_repository', return_value=False)
wurde ersetzt durch
mocker.patch('service.my_repository', return_value=False)
Das ergab für mich nicht viel Sinn, aber mein Kollege Arjan Molenaar erklärte die Sache so:
Sie können ein Modul als eine Karte (Wörterbuch) sehen. Wenn Sie repository.my_repository nicht funktioniert, da dies nicht der Name von my_repository im Servicemodul ist. Wenn der Test läuft, führe ich tatsächlich service.my_repository statt repository.my_repository aus.
Es gibt eine Alternative, die die Dinge ein wenig deutlicher machen könnte:
# service_v5.py
import repository # <------ this is the magic incantation...
def my_service():
print('my_service - calling my_repository')
return repository.my_repository()
# repository.py
def my_repository():
print('my_repository - this is the real thing')
return True
# test/test_service_v5.py
from service_v5 import my_service
def test_my_service(mocker):
mocker.patch('repository.my_repository', return_value=False)
# ^------------------ ...so we can do this
assert my_service() == False
In diesem Fall ruft service_v5.py import repository auf, und dann return repository.my_repository(). Jetzt können wir also
Zusammenfassung
Es ist also leicht, sich über das Falsche zu mokieren. Ich hoffe, das hilft Ihnen.
Kredite
Ich bin meinem Kollegen Arjan Molenaar zu Dank verpflichtet, der mir erklärt hat, wie Python mit Mocking umgeht.
Bild Wikipedia -
Verfasst von

Jan Vermeir
Developing software and infrastructure in teams, doing whatever it takes to get stable, safe and efficient systems in production.
Unsere Ideen
Weitere Blogs
Contact




