Ich möchte in Python schreiben! (3) Verwenden Sie Mock

Hallo. Das ist leo1109. Auch dieses Mal werden wir nach dem vorherigen über das Testen sprechen.

Der im Artikel verwendete Code wurde auf GitHub hochgeladen.

Die Geschichte, die diesmal vorgestellt werden soll

Es geht darum, einen Test zu schreiben. Patch verwenden.

Erstellen Sie ein Modul zum Testen

Wir haben das vorherige Skript verbessert und die folgenden Funktionen hinzugefügt.

Darüber hinaus wurden die Erfassung der Fibonacci-Nummer und die Erfassung der Gesamtsumme in die definierte Implementierung geändert.


# python 3.5.2

import time
from functools import wraps


def required_natural_number(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        k = args[0]
        if isinstance(k, bool):
            raise TypeError
        if not isinstance(k, int) or k < 1:
            raise TypeError
        return func(*args, **kwargs)
    return wrapper


def _f(k):
    if k == 1 or k == 2:
        return 1
    else:
        return _f(k-1) + _f(k-2)


def _sum(k):
    if k == 1:
        return 1
    else:
        return k + _sum(k-1)


@required_natural_number
def fibonacci(k):
    return _f(k)


@required_natural_number
def sum_all(k):
    return _sum(k)


@required_natural_number
def delta_of_sum_all_and_fibonacci(k):
    x = sum_all(k) - fibonacci(k)
    return x if x >= 0 else -x


@required_natural_number
def surplus_time_by(k):
    return int(time.time() % k)

Schreiben Sie sofort einen Test

Fügen Sie einen Test für "sum_all_minus_fibonacci (x)" hinzu, um die Differenz zwischen der Summe und der Anzahl der Fibonacci zu erhalten. Ich habe den Wert des Testfalls entsprechend gewählt.

my_math_2_test.py


# python 3.5.2

from unittest.mock import patch

import pytest

import my_math


class TestFibonacci:
    def test(self):
        assert my_math.fibonacci(1) == 1
        assert my_math.fibonacci(5) == 5
        assert my_math.fibonacci(10) == 55
        assert my_math.fibonacci(20) == 6765
        assert my_math.fibonacci(30) == 832040
        assert my_math.fibonacci(35) == 9227465


class TestSumAll:
    def test(self):
        assert my_math.sum_all(1) == 1
        assert my_math.sum_all(5) == 15
        assert my_math.sum_all(10) == 55
        assert my_math.sum_all(20) == 210
        assert my_math.sum_all(30) == 465
        assert my_math.sum_all(35) == 630


class TestDeltaOfSumAllAndFibonacci:

    def test(self):
        assert my_math.delta_of_sum_all_and_fibonacci(1) == 1 - 1
        assert my_math.delta_of_sum_all_and_fibonacci(5) == 15 - 5
        assert my_math.delta_of_sum_all_and_fibonacci(10) == 55 - 55
        assert my_math.delta_of_sum_all_and_fibonacci(20) == -1 * (210 - 6765)
        assert my_math.delta_of_sum_all_and_fibonacci(30) == -1 * (465 - 832040)
        assert my_math.delta_of_sum_all_and_fibonacci(35) == -1 * (630 - 9227465)

Lassen Sie uns den Test ausführen.


$ pytest -v  my_math_2_test.py
================================================================== test session starts ==================================================================
collected 3 items

my_math_2_test.py::TestFibonacci::test PASSED
my_math_2_test.py::TestSumAll::test PASSED
my_math_2_test.py::TestDeltaOfSumAllAndFibonacci::test PASSED

=============================================================== 3 passed in 21.01 seconds ===============================================================

Es dauerte 20 Sekunden. Es scheint, dass der Fibonacci-Zahlentest und der Differenztest jeweils etwa 10 Sekunden dauern.

Die Ursache ist, dass die Methode zum Abrufen der Fibonacci-Nummer rekursiv umgeschrieben wurde. (Da der Zweck dieses Artikels ein Test ist, werde ich die Erklärung zur Berechnungszeit weglassen, aber die Implementierung mit Wiederholung kann sehr viel Rechenzeit in Anspruch nehmen.)

Dieser Test beinhaltet jedoch tatsächlich unnötige Verarbeitung außer Implementierungsproblemen. Indem Sie diesen Punkt festlegen, können Sie den Test beschleunigen.

Patchen Sie die Funktion

Es ist ein Bild, das von oben eingefügt werden soll.

Patch langsame Funktionen

Der Grund für den langsamen Test ist die Methode, um die Fibonacci-Zahl zu erhalten. Daher möchte ich vermeiden, sie so oft wie möglich auszuführen. Wenn Sie sich den Testinhalt ansehen, können Sie sehen, dass der Fibonacci (x) -Test und das Delta_of_sum_all_and_fibonacci (x) beide Fibonacci (x) mit denselben Argumenten aufrufen. ... das ist ein bisschen Verschwendung. Da fibonacci (x) alleine getestet wurde, möchte ich es nicht tun, wenndelta_of_sum_all_and_fibonacci (x)getestet wird. (Weil es Zeit braucht) Verwenden wir dieses Mal "patch" und versuchen, "fibonacci (x)" nicht auszuführen.

Nachdem ich den vorhandenen Test übersprungen hatte, fügte ich einen Test hinzu, der Fibonacci (x) "patte".

class TestDeltaOfSumAllAndFibonacci:
    @pytest.mark.skip
    def test_slow(self):
        assert my_math.delta_of_sum_all_and_fibonacci(1) == 1 - 1
        assert my_math.delta_of_sum_all_and_fibonacci(5) == 15 - 5
        assert my_math.delta_of_sum_all_and_fibonacci(10) == 55 - 55
        assert my_math.delta_of_sum_all_and_fibonacci(20) \
            == -1 * (210 - 6765)
        assert my_math.delta_of_sum_all_and_fibonacci(30) \
            == -1 * (465 - 832040)
        assert my_math.delta_of_sum_all_and_fibonacci(35) \
            == -1 * (630 - 9227465)

    def test_patch(self):
        with patch('my_math.fibonacci') as mock_fib:
            mock_fib.return_value = 1
            assert my_math.delta_of_sum_all_and_fibonacci(1) == 1 - 1
            assert my_math.delta_of_sum_all_and_fibonacci(5) == 15 - 5
            assert my_math.delta_of_sum_all_and_fibonacci(10) == 55 - 55
            assert my_math.delta_of_sum_all_and_fibonacci(20) \
                == -1 * (210 - 6765)
            assert my_math.delta_of_sum_all_and_fibonacci(30) \
                == -1 * (465 - 832040)
            assert my_math.delta_of_sum_all_and_fibonacci(35) \
                == -1 * (630 - 9227465)

Patch schreibt den Ausführungsinhalt der Methode des angegebenen Moduls mithilfe der with-Anweisung und des Decorators neu. Als Bestätigung der Operation gibt fibonacci (x) immer 1 zurück. Lass es uns laufen.


$ pytest -v my_math_2_test.py::TestDeltaOfSumAllAndFibonacci
================================================================== test session starts ==================================================================
collected 2 items

my_math_2_test.py::TestDeltaOfSumAllAndFibonacci::test_slow SKIPPED
my_math_2_test.py::TestDeltaOfSumAllAndFibonacci::test_patch FAILED

======================================================================= FAILURES ========================================================================
_______________________________________________________ TestDeltaOfSumAllAndFibonacci.test_patch ________________________________________________________


self = <my_math_2_test.TestDeltaOfSumAllAndFibonacci object at 0x103abcf28>

    def test_patch(self):
        with patch('my_math.fibonacci') as mock_fib:
            mock_fib.return_value = 1
            assert my_math.delta_of_sum_all_and_fibonacci(1) == 1 - 1
>           assert my_math.delta_of_sum_all_and_fibonacci(5) == 15 - 5
E           assert 14 == (15 - 5)
E            +  where 14 = <function delta_of_sum_all_and_fibonacci at 0x103ac0840>(5)
E            +    where <function delta_of_sum_all_and_fibonacci at 0x103ac0840> = my_math.delta_of_sum_all_and_fibonacci

my_math_2_test.py:45: AssertionError
========================================================== 1 failed, 1 skipped in 0.21 seconds ==========================================================

Der Test schlägt jetzt fehl. Im Testfall von "delta_of_sum_all_and_fibonacci (5)" gibt "fibonacci (5)", das ursprünglich 5 erhalten sollte, jetzt 1 zurück, sodass der Test als "14 == (15 - 5)" fehlschlägt. Ich werde.

Nachdem wir bestätigt haben, dass der Patch wie erwartet funktioniert, werden wir den Test verbessern, um den korrekten Wert zurückzugeben.

    def test_patch(self):
        with patch('my_math.fibonacci') as mock_fib:
            mock_fib.return_value = 1
            assert my_math.delta_of_sum_all_and_fibonacci(1) == 1 - 1
            mock_fib.return_value = 5
            assert my_math.delta_of_sum_all_and_fibonacci(5) == 15 - 5
            mock_fib.return_value = 55
            assert my_math.delta_of_sum_all_and_fibonacci(10) == 55 - 55
            mock_fib.return_value = 6765
            assert my_math.delta_of_sum_all_and_fibonacci(20) \
                == -1 * (210 - 6765)
            mock_fib.return_value = 832040
            assert my_math.delta_of_sum_all_and_fibonacci(30) \
                == -1 * (465 - 832040)
            mock_fib.return_value = 9227465
            assert my_math.delta_of_sum_all_and_fibonacci(35) \
                == -1 * (630 - 9227465)

Versuchen Sie es nochmal.

$ pytest -v my_math_2_test.py::TestDeltaOfSumAllAndFibonacci
================================================================== test session starts ==================================================================
collected 2 items

my_math_2_test.py::TestDeltaOfSumAllAndFibonacci::test_slow SKIPPED
my_math_2_test.py::TestDeltaOfSumAllAndFibonacci::test_patch PASSED

========================================================== 1 passed, 1 skipped in 0.05 seconds ==========================================================

Der Test war erfolgreich. Die Ausführungszeit beträgt 0,05 (Sek.), Was erheblich schneller als zuvor ist. Wenn alle ausgeführt würden, würde es natürlich genauso lange dauern wie die Ausführung des ersten "Fibonacci (x)", aber trotzdem waren es 20 Sekunden -> 10 Sekunden, was einer Reduzierung um 50% entspricht.

Patch-Funktionen mit unbestimmten Rückgabewerten

Im vorherigen Abschnitt durch Verspotten einer langsamen Funktion. Es ist uns gelungen, den Test zu optimieren. Übrigens ist "surplus_time_by (k)" eine Funktion, die die Unixtime der aktuellen Zeit (UTC) abruft und den Rest zurückgibt. Da sich die aktuelle Zeit jedoch bei jeder Ausführung ändert, setzen Sie einen festen Wert wie zuvor. Ich kann den Test, den ich geschrieben habe, nicht machen.

Der folgende Test ist beispielsweise erfolgreich, wenn der Wert von "time.time ()" ein Vielfaches von 5 ist, ansonsten jedoch nicht.


   assert my_math.surplus_time_by(5) == 5

In diesen Fällen sollten Sie "time.time ()" patchen. time ist eine Standard-Python-Bibliothek. Vorausgesetzt, sie ist bereits gut getestet, ist sie Teil der Berechnung des Restbetrags.

Um die in "my_math" verwendete "time.time ()" zu patchen, schreiben Sie:

class TestSurplusTimeBy:
    def test(self):
        with patch('my_math.time') as mock_time:
            mock_time.time.return_value = 1000
            assert my_math.surplus_time_by(3) == 1
            assert my_math.surplus_time_by(5) == 0
            mock_time.time.return_value = 1001
            assert my_math.surplus_time_by(3) == 2
            assert my_math.surplus_time_by(5) == 1

Es ist auch möglich, bis zu "time.time" zu "patchen", wie unten gezeigt.

    def test2(self):
        with patch('my_math.time.time') as mock_time:
            mock_time.return_value = 1000
            assert my_math.surplus_time_by(3) == 1
            assert my_math.surplus_time_by(5) == 0
            mock_time.return_value = 1001
            assert my_math.surplus_time_by(3) == 2
            assert my_math.surplus_time_by(5) == 1

Der Unterschied oben ist, wie viel Spott gemacht wird. Wenn Sie sich die Ausführungsergebnisse unten ansehen, können Sie sehen, dass das Zeitmodul selbst als Mock-Objekt neu geschrieben wurde, wenn die Zeit gepatcht ist. Wenn Sie beispielsweise etwas anderes als "time ()" under "time" verwenden, müssen Sie "time.time" explizit angeben oder alle von Ihnen verwendeten Methoden patchen.

# time.Patch-Zeit
>>> with patch('my_math.time.time') as m:
...   print(my_math.time)
...   print(my_math.time.time)
...
<module 'time' (built-in)>
<MagicMock name='time' id='4329989904'>

#Patch-Zeit
>>> with patch('my_math.time') as m:
...   print(my_math.time)
...   print(my_math.time.time)
...
<MagicMock name='time' id='4330034680'>
<MagicMock name='time.time' id='4330019136'>

Informationen zu der für den Patch angegebenen Zeichenfolge

In diesem Beispiel wird das Modul "time" in "my_math" importiert. Schreiben Sie es also als "my_math.time". Wenn daher ein anderes Modul "my_module" angezeigt wird, das "my_math" verwendet, müssen Sie "my_module.my_math.time" schreiben.

Wenn Sie die Instanzmethode einer Klasse patchen, müssen Sie bis zur Instanz angeben.

my_class.py


# Python 3.5.2


class MyClass:
    def __init__(self, prefix='My'):
        self._prefix = prefix

    def my_method(self, x):
        return '{} {}'.format(self._prefix, x)

my_main.py


# Python 3.5.2

from my_class import MyClass


def main(name):
    c = MyClass()
    return c.my_method(name)


if __name__ == '__main__':
    print(main('Python'))

Nachdem Sie die obigen Schritte ausgeführt haben, sehen Sie "My Python". Wenn Sie nun "MyClass.my_method" patchen möchten, können Sie schreiben:

my_main_test.py


# python 3.5.2

from unittest.mock import patch

import my_main


class TestFunction:
    def test(self):
        assert my_main.function('Python') == 'My Python'

    def test_patch_return_value(self):
        with patch('my_class.MyClass.my_method') as mock:
            mock.return_value = 'Hi! Perl'
            assert my_main.function('Python') == 'Hi! Perl'

    def test_patch_side_effect(self):
        with patch('my_class.MyClass.my_method') as mock:
            mock.side_effect = lambda x: 'OLA! {}'.format(x)
            assert my_main.function('Python') == 'OLA! Python'

Patch-Methoden für den externen Zugriff

Patches sind auch nützlich, wenn Sie Module testen, auf die extern zugegriffen wird. Mit dem Modul "Anfragen" habe ich eine Methode erstellt, die den Statuscode zurückgibt, wenn die angegebene URL GET lautet. Testen Sie dies.


# Python 3.5.2

import requests


def get_status_code(url):
    r = requests.get(url)

    return r.status_code

Ich werde einen Test schreiben.

# python 3.5.2

import pytest

import my_http


class TestGetStatusCode:
    @pytest.fixture
    def url(self):
        return 'http://example.com'


    def test(self, url):
        assert my_http.get_status_code(url) == 200

Dieser Test besteht problemlos. Aber was ist, wenn Sie offline sind? Ich werde die detaillierte Stapelverfolgung weglassen, aber der Test wird fehlschlagen.


...
>           raise ConnectionError(e, request=request)
E           requests.exceptions.ConnectionError: HTTPConnectionPool(host='example.com', port=80): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x1039a4cf8>: Failed to establish a new connection: [Errno 8] nodename nor servname provided, or not known',))

../../../.pyenv/versions/3.5.2/lib/python3.5/site-packages/requests/adapters.py:504: ConnectionError                                               =============================================================== 1 failed in 1.06 seconds ================================================================

Um diesen Test offline zu bestehen, patchen Sie request.get. Da der Rückgabewert ein Objekt ist, muss in return_value explizit "MagicMock ()" angegeben werden.


# python 3.5.2

from unittest.mock import MagicMock, patch

import pytest

import my_http


class TestGetStatusCode:
    @pytest.fixture
    def url(self):
        return 'http://example.com'

    @pytest.mark.skip
    def test_online(self, url):
        assert my_http.get_status_code(url) == 200

    def test_offline(self, url):
        with patch('my_http.requests') as mock_requests:
            mock_response = MagicMock(status_code=200)
            mock_requests.get.return_value = mock_response

            assert my_http.get_status_code(url) == 200

Der obige Test wird auch offline bestanden. Wenn Sie einen MagicMock-Statuscode von 400, 500 usw. angeben, können Sie einen Fehlerfalltest hinzufügen, während die URL unverändert bleibt. Es kann verwendet werden, um den DB-Zugriff in einer Umgebung ohne Datenbank- oder Token-Erfassung von einem externen Dienst zu verspotten.

Patch-Tipps

Hier sind einige Tipps zum Patchen.

Wenn Sie mehrere Methoden patchen möchten

In der Python3-Serie ist "ExitStack ()" praktisch.


>>> with ExitStack() as stack:
...   x = stack.enter_context(patch('my_math.fibonacci'))
...   y = stack.enter_context(patch('my_math.sum_all'))
...   x.return_value = 100
...   y.return_value = 200
...   z = my_math.delta_of_sum_all_and_fibonacci(99999)
...   print(z)
...
100

Die Rückgabewerte beider Funktionen wurden neu geschrieben, sodass "my_math.delta_of_sum_all_and_fibonacci (99999)" die Differenz zwischen 200 und 100 zurückgibt.

Ich möchte eine Ausnahme zurückgeben

Wenn Sie ein Modell verwenden, möchten Sie möglicherweise eine Ausnahme zurückgeben. Verwenden Sie in diesem Fall mock.side_effect.


>>> with patch('my_math.fibonacci') as m:
...   m.side_effect = ValueError
...   my_math.fibonacci(1)
...
Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
  File "/Users/sho/.pyenv/versions/3.5.2/lib/python3.5/unittest/mock.py", line 917, in __call__
    return _mock_self._mock_call(*args, **kwargs)
  File "/Users/sho/.pyenv/versions/3.5.2/lib/python3.5/unittest/mock.py", line 973, in _mock_call
    raise effect
ValueError

Überprüfen Sie, ob der Mock ausgeführt wurde

Da das Aufrufobjekt im Scheinobjekt gespeichert ist, ist es zweckmäßig, es zu überprüfen. Methoden, die mit assert_ beginnen, lösen eine Ausnahme aus, wenn die Bedingung nicht erfüllt ist, sodass Sie assert nicht schreiben müssen. Die Argumente, wenn der Mock ausgeführt wird, werden in einem Taple in mock.call_args gespeichert. Weitere Informationen finden Sie in der offiziellen Dokumentation.

Es werden nur typische vorgestellt.

--mock.called Ob es ausgeführt wurde (bool) --mock.call_count Anzahl der Ausführungen


   def test_called(self):
        with patch('my_class.MyClass.my_method') as mock:
            my_main.function('Python')

        assert mock.called
        assert mock.call_count == 1
        mock.assert_called_with('Python')

nächstes Mal

Es ist bis zum Ende des Obon-Urlaubs unentschlossen!

Recommended Posts

Ich möchte in Python schreiben! (3) Verwenden Sie Mock
Ich möchte den Fortschritt in Python anzeigen!
Ich möchte in Python schreiben! (1) Überprüfung des Codeformats
Ich möchte in Python schreiben! (2) Schreiben wir einen Test
Ich möchte R-Datensatz mit Python verwenden
Ich wollte den AWS-Schlüssel nicht in das Programm schreiben
Ich habe den Code geschrieben, um den Brainf * ck-Code in Python zu schreiben
Ich möchte Dunnetts Test in Python machen
Ich möchte mit Python ein Fenster erstellen
Tipps zum Schreiben werden in Python kurz abgeflacht
Ich möchte mit Python in eine Datei schreiben
Ich möchte das Ergebnis von "Zeichenfolge" .split () in Python stapelweise konvertieren
Ich möchte die abstrakte Klasse (ABCmeta) von Python im Detail erklären
Ich habe versucht, die in Python installierten Pakete grafisch darzustellen
Ich möchte Timeout einfach in Python implementieren
Auch mit JavaScript möchte ich Python `range ()` sehen!
Ich möchte eine Datei mit Python zufällig testen
Ich möchte mit Python-Datenklasse nach hinten erben
Ich möchte mit einem Roboter in Python arbeiten.
Ich möchte am Ende etwas mit Python machen
Ich möchte Strings in Kotlin wie Python manipulieren!
Ich möchte Python in der Umgebung von pyenv + pipenv unter Windows 10 verwenden
Ich möchte den Dateinamen, die Zeilennummer und den Funktionsnamen in Python 3.4 erhalten
Im Python-Befehl zeigt Python auf Python3.8
Ich habe die Warteschlange in Python geschrieben
Ich möchte mit Python debuggen
Ich habe den Stack in Python geschrieben
Ich möchte initialisieren, wenn der Wert leer ist (Python)
maya Python Ich möchte die gebackene Animation wieder reparieren.
Ich möchte so etwas wie Uniq in Python sortieren
[Python] Ich möchte die Option -h mit argparse verwenden
Ich habe versucht, die Mail-Sendefunktion in Python zu implementieren
Ich möchte die Natur von Python und Pip kennenlernen
Ich möchte den Wörterbuchtyp in der Liste eindeutig machen
Ich möchte die gültigen Zahlen im Numpy-Array ausrichten
Ich möchte Python mit VS-Code ausführen können
Ich möchte eine schöne Ergänzung zu input () in Python hinzufügen
Ich möchte nur das 95% -Konfidenzintervall des Unterschieds im Bevölkerungsverhältnis in Python ermitteln
Ich möchte Spyder an die Taskleiste anheften
Ich habe versucht, PLSA in Python zu implementieren
Ich möchte kühl auf die Konsole ausgeben
Ich möchte das Wetter mit LINE bot feat.Heroku + Python wissen
Ich habe versucht, Permutation in Python zu implementieren
Ich möchte in der Einschlussnotation drucken
[Linux] Ich möchte das Datum wissen, an dem sich der Benutzer angemeldet hat
Ich möchte mit dem Reim Teil1 umgehen
Schreiben Sie den Test in die Python-Dokumentzeichenfolge
Ich möchte APG4b mit Python lösen (nur 4.01 und 4.04 in Kapitel 4)
Das 15. Offline-Echtzeit-Schreiben eines Referenzproblems in Python
Ich möchte mit dem Reim part3 umgehen
Ich möchte den Anfang des nächsten Monats mit Python ausgeben
Ich möchte ein Glas aus Python verwenden
Ich möchte eine Python-Umgebung erstellen
Ich möchte Python GUI ausführen, wenn Raspberry Pi startet
Ich möchte Protokolle mit Python analysieren
LINEbot-Entwicklung möchte ich den Betrieb in der lokalen Umgebung überprüfen
Ich möchte mit aws mit Python spielen
[Python / AWS Lambda-Ebenen] Ich möchte nur Module in AWS Lambda-Ebenen wiederverwenden
Ich habe versucht, ADALINE in Python zu implementieren