Python Language
Unit Testing
Suche…
Bemerkungen
Es gibt verschiedene Gerätetest-Tools für Python. Dieses Dokumentationsthema beschreibt das grundlegende unittest . Andere Testwerkzeuge umfassen py.test und nosetests . Diese Python-Dokumentation zum Testen vergleicht mehrere dieser Tools, ohne in die Tiefe zu gehen.
Ausnahmen testen
Programme werfen Fehler, wenn zum Beispiel eine falsche Eingabe erfolgt. Aus diesem Grund muss sichergestellt werden, dass ein Fehler ausgegeben wird, wenn eine falsche Eingabe erfolgt. Aus diesem Grund müssen wir nach einer genauen Ausnahme suchen. In diesem Beispiel werden wir die folgende Ausnahme verwenden:
class WrongInputException(Exception):
pass
Diese Ausnahme wird ausgelöst, wenn falsche Eingaben gemacht werden, in dem folgenden Kontext, in dem immer eine Zahl als Texteingabe erwartet wird.
def convert2number(random_input):
try:
my_input = int(random_input)
except ValueError:
raise WrongInputException("Expected an integer!")
return my_input
Um zu prüfen, ob eine Ausnahme assertRaises wurde, verwenden wir assertRaises , um diese Ausnahme zu überprüfen. assertRaises kann auf zwei Arten verwendet werden:
- Verwenden des regulären Funktionsaufrufs. Das erste Argument hat den Ausnahmetyp, zweitens eine aufrufbare Funktion (normalerweise eine Funktion), und die übrigen Argumente werden an diese aufrufbare Komponente übergeben.
- Verwenden Sie eine
withKlausel, und geben Sie der Funktion nur den Ausnahmetyp an. Dies hat den Vorteil, dass mehr Code ausgeführt werden kann, der jedoch mit Vorsicht verwendet werden sollte, da mehrere Funktionen dieselbe Ausnahme verwenden können, was problematisch sein kann. Ein Beispiel: with self.assertRaises (WrongInputException): convert2number ("keine Zahl")
Dieser erste wurde in dem folgenden Testfall implementiert:
import unittest
class ExceptionTestCase(unittest.TestCase):
def test_wrong_input_string(self):
self.assertRaises(WrongInputException, convert2number, "not a number")
def test_correct_input(self):
try:
result = convert2number("56")
self.assertIsInstance(result, int)
except WrongInputException:
self.fail()
Es kann auch erforderlich sein, nach einer Ausnahme zu suchen, die nicht hätte ausgelöst werden sollen. Ein Test schlägt jedoch automatisch fehl, wenn eine Ausnahme ausgelöst wird, und ist daher möglicherweise überhaupt nicht erforderlich. Um die Optionen zu zeigen, zeigt die zweite Testmethode einen Fall, wie geprüft werden kann, ob eine Ausnahme nicht ausgelöst wird. Grundsätzlich wird die Ausnahme erkannt und der Test mit der fail Methode fail .
Verspottungsfunktionen mit unittest.mock.create_autospec
Eine Möglichkeit zum create_autospec einer Funktion ist die Verwendung der Funktion create_autospec , mit der ein Objekt gemäß seinen Angaben verspottet wird. Mit Funktionen können wir dies nutzen, um sicherzustellen, dass sie entsprechend aufgerufen werden.
Mit einer Funktion in custom_math.py multiply :
def multiply(a, b):
return a * b
Und eine Funktion multiples_of in process_math.py :
from custom_math import multiply
def multiples_of(integer, *args, num_multiples=0, **kwargs):
"""
:rtype: list
"""
multiples = []
for x in range(1, num_multiples + 1):
"""
Passing in args and kwargs here will only raise TypeError if values were
passed to multiples_of function, otherwise they are ignored. This way we can
test that multiples_of is used correctly. This is here for an illustration
of how create_autospec works. Not recommended for production code.
"""
multiple = multiply(integer,x, *args, **kwargs)
multiples.append(multiple)
return multiples
Wir können multiples_of alleine testen, multiply wir multiply . Im folgenden Beispiel wird die Python-Standardbibliothek unittest verwendet, dies kann jedoch auch mit anderen Testframeworks, wie z.
from unittest.mock import create_autospec
import unittest
# we import the entire module so we can mock out multiply
import custom_math
custom_math.multiply = create_autospec(custom_math.multiply)
from process_math import multiples_of
class TestCustomMath(unittest.TestCase):
def test_multiples_of(self):
multiples = multiples_of(3, num_multiples=1)
custom_math.multiply.assert_called_with(3, 1)
def test_multiples_of_with_bad_inputs(self):
with self.assertRaises(TypeError) as e:
multiples_of(1, "extra arg", num_multiples=1) # this should raise a TypeError
Testen Sie Setup und Teardown innerhalb eines Tests. TestCase
Manchmal möchten wir einen Kontext für jeden Test erstellen, unter dem ausgeführt wird. Die setUp Methode wird vor jedem Test in der Klasse ausgeführt. tearDown wird am Ende jedes Tests ausgeführt. Diese Methoden sind optional. Denken Sie daran, dass Testfälle häufig in kooperativer Mehrfachvererbung verwendet werden. Sie sollten also immer den super - setUp in diesen Methoden tearDown , damit auch die Methoden setUp und tearDown der setUp aufgerufen werden. Die TestCase von TestCase stellt leere setUp und tearDown Methoden tearDown , sodass sie ohne Ausnahmen aufgerufen werden können:
import unittest
class SomeTest(unittest.TestCase):
def setUp(self):
super(SomeTest, self).setUp()
self.mock_data = [1,2,3,4,5]
def test(self):
self.assertEqual(len(self.mock_data), 5)
def tearDown(self):
super(SomeTest, self).tearDown()
self.mock_data = []
if __name__ == '__main__':
unittest.main()
Beachten Sie, dass es in python2.7 + auch die addCleanup Methode gibt, die Funktionen registriert, die nach dem Ausführen des Tests aufgerufen werden sollen. Im Gegensatz zu tearDown das nur aufgerufen wird, wenn setUp erfolgreich ist, werden über addCleanup registrierte Funktionen auch im Fall einer nicht behandelten Ausnahme in setUp . Als ein konkretes Beispiel kann diese Methode häufig gesehen werden, wenn verschiedene Mocks entfernt werden, die während des Tests registriert wurden:
import unittest
import some_module
class SomeOtherTest(unittest.TestCase):
def setUp(self):
super(SomeOtherTest, self).setUp()
# Replace `some_module.method` with a `mock.Mock`
my_patch = mock.patch.object(some_module, 'method')
my_patch.start()
# When the test finishes running, put the original method back.
self.addCleanup(my_patch.stop)
Ein weiterer Vorteil der Registrierung von Bereinigungen auf diese Weise ist, dass der Programmierer den Bereinigungscode neben den Setup-Code stellen kann und Sie tearDown falls ein Subclasser vergisst, super in tearDown .
Ausnahmen geltend machen
Sie können mit zwei integrierten Methoden testen, ob eine Funktion mit dem integrierten Test eine Ausnahme auslöst.
Verwenden eines Kontextmanagers
def division_function(dividend, divisor):
return dividend / divisor
class MyTestCase(unittest.TestCase):
def test_using_context_manager(self):
with self.assertRaises(ZeroDivisionError):
x = division_function(1, 0)
Dadurch wird der Code im Kontext-Manager ausgeführt. Wenn er erfolgreich ist, schlägt der Test fehl, da die Ausnahme nicht ausgelöst wurde. Wenn der Code eine Ausnahme des richtigen Typs auslöst, wird der Test fortgesetzt.
Sie können auch den Inhalt der ausgelösten Ausnahme abrufen, wenn Sie weitere Zusicherungen dagegen ausführen möchten.
class MyTestCase(unittest.TestCase):
def test_using_context_manager(self):
with self.assertRaises(ZeroDivisionError) as ex:
x = division_function(1, 0)
self.assertEqual(ex.message, 'integer division or modulo by zero')
Durch die Bereitstellung einer aufrufbaren Funktion
def division_function(dividend, divisor):
"""
Dividing two numbers.
:type dividend: int
:type divisor: int
:raises: ZeroDivisionError if divisor is zero (0).
:rtype: int
"""
return dividend / divisor
class MyTestCase(unittest.TestCase):
def test_passing_function(self):
self.assertRaises(ZeroDivisionError, division_function, 1, 0)
Die zu prüfende Ausnahme muss der erste Parameter sein und eine aufrufbare Funktion muss als zweiter Parameter übergeben werden. Alle anderen angegebenen Parameter werden direkt an die aufgerufene Funktion übergeben, sodass Sie die Parameter angeben können, die die Ausnahme auslösen.
Assertions innerhalb Unittests auswählen
Während Python eine assert Anweisung hat , hat das Test-Framework für Python-Einheiten bessere Assertions für Tests: Sie sind informativer bei Fehlern und hängen nicht vom Debug-Modus der Ausführung ab.
Die einfachste Behauptung ist assertTrue , die folgendermaßen verwendet werden kann:
import unittest
class SimplisticTest(unittest.TestCase):
def test_basic(self):
self.assertTrue(1 + 1 == 2)
Dies wird gut funktionieren, aber die Zeile oben durch ersetzen
self.assertTrue(1 + 1 == 3)
wird versagen.
Die assertTrue Behauptung ist wahrscheinlich die allgemeinste Behauptung, da alles, was getestet wurde, als boolesche Bedingung angesehen werden kann, aber oft gibt es bessere Alternativen. Wenn Sie wie oben auf Gleichheit prüfen, sollten Sie besser schreiben
self.assertEqual(1 + 1, 3)
Wenn der Erstere fehlschlägt, wird die Nachricht angezeigt
======================================================================
FAIL: test (__main__.TruthTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "stuff.py", line 6, in test
self.assertTrue(1 + 1 == 3)
AssertionError: False is not true
Wenn letzteres fehlschlägt, wird die Nachricht angezeigt
======================================================================
FAIL: test (__main__.TruthTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "stuff.py", line 6, in test
self.assertEqual(1 + 1, 3)
AssertionError: 2 != 3
was informativer ist (es wertete tatsächlich das Ergebnis der linken Seite aus).
Die Liste der Zusicherungen finden Sie in der Standarddokumentation . Im Allgemeinen ist es eine gute Idee, die Assertion zu wählen, die der Bedingung am besten entspricht. Wie oben gezeigt, ist es daher besser, assertEqual zu verwenden als assertTrue , um zu behaupten, dass 1 + 1 == 2 ist. assertIsNone zu behaupten, dass a is None , ist es besser, assertIsNone zu verwenden als assertEqual .
Beachten Sie auch, dass die Aussagen negative Formen haben. So hat assertEqual seinen negativen Gegenwert assertNotEqual und assertIsNone seinen negativen Gegenwert assertIsNotNone . Die Verwendung der negativen Gegenstücke führt gegebenenfalls zu klareren Fehlermeldungen.
Unit-Tests mit Pytest
pytest installieren:
pip install pytest
Tests vorbereiten:
mkdir tests
touch tests/test_docker.py
Funktionen zum Testen in docker_something/helpers.py :
from subprocess import Popen, PIPE
# this Popen is monkeypatched with the fixture `all_popens`
def copy_file_to_docker(src, dest):
try:
result = Popen(['docker','cp', src, 'something_cont:{}'.format(dest)], stdout=PIPE, stderr=PIPE)
err = result.stderr.read()
if err:
raise Exception(err)
except Exception as e:
print(e)
return result
def docker_exec_something(something_file_string):
fl = Popen(["docker", "exec", "-i", "something_cont", "something"], stdin=PIPE, stdout=PIPE, stderr=PIPE)
fl.stdin.write(something_file_string)
fl.stdin.close()
err = fl.stderr.read()
fl.stderr.close()
if err:
print(err)
exit()
result = fl.stdout.read()
print(result)
Der Test importiert test_docker.py :
import os
from tempfile import NamedTemporaryFile
import pytest
from subprocess import Popen, PIPE
from docker_something import helpers
copy_file_to_docker = helpers.copy_file_to_docker
docker_exec_something = helpers.docker_exec_something
Verspotten einer Datei wie eines Objekts in test_docker.py :
class MockBytes():
'''Used to collect bytes
'''
all_read = []
all_write = []
all_close = []
def read(self, *args, **kwargs):
# print('read', args, kwargs, dir(self))
self.all_read.append((self, args, kwargs))
def write(self, *args, **kwargs):
# print('wrote', args, kwargs)
self.all_write.append((self, args, kwargs))
def close(self, *args, **kwargs):
# print('closed', self, args, kwargs)
self.all_close.append((self, args, kwargs))
def get_all_mock_bytes(self):
return self.all_read, self.all_write, self.all_close
Affen-Patches mit pytest in test_docker.py :
@pytest.fixture
def all_popens(monkeypatch):
'''This fixture overrides / mocks the builtin Popen
and replaces stdin, stdout, stderr with a MockBytes object
note: monkeypatch is magically imported
'''
all_popens = []
class MockPopen(object):
def __init__(self, args, stdout=None, stdin=None, stderr=None):
all_popens.append(self)
self.args = args
self.byte_collection = MockBytes()
self.stdin = self.byte_collection
self.stdout = self.byte_collection
self.stderr = self.byte_collection
pass
monkeypatch.setattr(helpers, 'Popen', MockPopen)
return all_popens
Beispieltests müssen mit dem Präfix test_ in der Datei test_docker.py :
def test_docker_install():
p = Popen(['which', 'docker'], stdout=PIPE, stderr=PIPE)
result = p.stdout.read()
assert 'bin/docker' in result
def test_copy_file_to_docker(all_popens):
result = copy_file_to_docker('asdf', 'asdf')
collected_popen = all_popens.pop()
mock_read, mock_write, mock_close = collected_popen.byte_collection.get_all_mock_bytes()
assert mock_read
assert result.args == ['docker', 'cp', 'asdf', 'something_cont:asdf']
def test_docker_exec_something(all_popens):
docker_exec_something(something_file_string)
collected_popen = all_popens.pop()
mock_read, mock_write, mock_close = collected_popen.byte_collection.get_all_mock_bytes()
assert len(mock_read) == 3
something_template_stdin = mock_write[0][1][0]
these = [os.environ['USER'], os.environ['password_prod'], 'table_name_here', 'test_vdm', 'col_a', 'col_b', '/tmp/test.tsv']
assert all([x in something_template_stdin for x in these])
Ausführen der Tests nacheinander:
py.test -k test_docker_install tests
py.test -k test_copy_file_to_docker tests
py.test -k test_docker_exec_something tests
alle Tests in den laufenden tests Ordner:
py.test -k test_ tests