Python Language
Testen van een eenheid
Zoeken…
Opmerkingen
Er zijn verschillende eenheidstesttools voor Python. In dit documentatie-onderwerp wordt de basale unittest
module beschreven. Andere testtools zijn py.test
en nosetests
. In deze python-documentatie over testen worden verschillende van deze tools vergeleken zonder diepgang.
Uitzonderingen testen
Programma's gooien fouten wanneer bijvoorbeeld verkeerde invoer wordt gegeven. Daarom moet ervoor worden gezorgd dat er een fout wordt gegenereerd wanneer de werkelijke verkeerde invoer wordt gegeven. Daarom moeten we controleren op een exacte uitzondering, voor dit voorbeeld gebruiken we de volgende uitzondering:
class WrongInputException(Exception):
pass
Deze uitzondering wordt opgeworpen wanneer verkeerde invoer wordt gegeven, in de volgende context waarin we altijd een getal als tekstinvoer verwachten.
def convert2number(random_input):
try:
my_input = int(random_input)
except ValueError:
raise WrongInputException("Expected an integer!")
return my_input
Om te controleren of een uitzondering is opgeworpen, gebruiken we assertRaises
om op die uitzondering te controleren. assertRaises
kunnen op twee manieren worden gebruikt:
- Gebruik van de normale functieaanroep. Het eerste argument heeft het uitzonderingstype, ten tweede een opvraagbare (meestal een functie) en de rest van de argumenten worden doorgegeven aan dit opvraagbare.
- Met behulp van een
with
clausule, waardoor alleen de uitzondering type om de functie. Dit heeft als voordeel dat meer code kan worden uitgevoerd, maar moet voorzichtig worden gebruikt, omdat meerdere functies dezelfde uitzondering kunnen gebruiken die problematisch kan zijn. Een voorbeeld: met self.assertRaises (WrongInputException): convert2number ("not a number")
Deze eerste is geïmplementeerd in de volgende testcase:
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()
Het kan ook nodig zijn om te controleren op een uitzondering die niet had moeten worden weggegooid. Een test mislukt echter automatisch wanneer een uitzondering wordt gegenereerd en is dus helemaal niet nodig. Om de opties te tonen, toont de tweede testmethode een geval over hoe men kan controleren of een uitzondering niet wordt gegooid. Kortom, dit is het vangen van de uitzondering en vervolgens mislukt de test met de fail
methode.
Mocking-functies met unittest.mock.create_autospec
Een manier om een functie te bespotten is om de functie create_autospec
te gebruiken, die een object volgens zijn specificaties zal bespotten. Met functies kunnen we dit gebruiken om ervoor te zorgen dat ze op de juiste manier worden aangeroepen.
Met een functie multiply
in custom_math.py
:
def multiply(a, b):
return a * b
En een functie 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
We kunnen multiples_of
alleen testen door multiply
bespotten. In het onderstaande voorbeeld wordt de Python-standaardbibliotheek unittest gebruikt, maar deze kan ook worden gebruikt met andere testkaders, zoals pytest of nose:
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
Test Setup en Teardown binnen een unittest.TestCase
Soms willen we een context voorbereiden voor elke test die moet worden uitgevoerd. De setUp
methode wordt voorafgaand aan elke test in de klas uitgevoerd. tearDown
wordt aan het einde van elke test uitgevoerd. Deze methoden zijn optioneel. Vergeet niet dat TestCases vaak worden gebruikt in coöperatieve meervoudige overerving, dus wees voorzichtig dat u altijd super
in deze methoden tearDown
, zodat de setUp
en tearDown
methoden van base class ook worden aangeroepen. De basisuitvoering van de TestCase
biedt lege setUp
en tearDown
methoden, zodat ze zonder verhoging van uitzondering kan worden genoemd:
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()
Merk op dat in python2.7 + er ook de addCleanup
methode is die functies registreert die moeten worden aangeroepen nadat de test is uitgevoerd. In tegenstelling tot tearDown
die alleen wordt aangeroepen als setUp
slaagt, worden functies die zijn geregistreerd via addCleanup
zelfs aangeroepen in het geval van een onverwerkte uitzondering in setUp
. Als concreet voorbeeld kan deze methode vaak worden gezien om verschillende mocks te verwijderen die werden geregistreerd terwijl de test werd uitgevoerd:
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)
Een ander voordeel van het registreren van opruimingen op deze manier is dat het de programmeur in staat stelt om de opruimcode naast de installatiecode te plaatsen en het beschermt u in het geval dat een subclasser vergeet super
in tearDown
te bellen.
Beweren op uitzonderingen
U kunt testen dat een functie een uitzondering genereert met de ingebouwde unittest via twee verschillende methoden.
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)
Hiermee wordt de code in de contextmanager uitgevoerd en, als dit lukt, mislukt de test omdat de uitzondering niet is opgetreden. Als de code een uitzondering van het juiste type oproept, gaat de test verder.
U kunt ook de inhoud van de verhoogde uitzondering krijgen als u hiertegen aanvullende beweringen wilt uitvoeren.
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')
Door een opvraagbare functie te bieden
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)
De uitzondering waarop moet worden gecontroleerd, moet de eerste parameter zijn en een opvraagbare functie moet worden doorgegeven als de tweede parameter. Alle andere opgegeven parameters worden direct doorgegeven aan de functie die wordt aangeroepen, zodat u de parameters kunt opgeven die de uitzondering activeren.
Beweringen kiezen binnen Unittests
Hoewel Python een assert
statement heeft , heeft het testkader van Python unit betere beweringen die gespecialiseerd zijn voor tests: ze zijn informatiever over fouten en zijn niet afhankelijk van de foutopsporingsmodus van de uitvoering.
Misschien is de eenvoudigste bewering assertTrue
, die als volgt kan worden gebruikt:
import unittest
class SimplisticTest(unittest.TestCase):
def test_basic(self):
self.assertTrue(1 + 1 == 2)
Dit zal prima werken, maar de regel hierboven vervangen door
self.assertTrue(1 + 1 == 3)
zal mislukken.
De assertTrue
bewering is waarschijnlijk de meest algemene bewering, omdat alles wat wordt getest als een booleaanse aandoening kan worden assertTrue
, maar vaak zijn er betere alternatieven. Bij het testen op gelijkheid, zoals hierboven, is het beter om te schrijven
self.assertEqual(1 + 1, 3)
Wanneer de eerste mislukt, is het bericht
======================================================================
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
maar wanneer het laatste faalt, is de boodschap
======================================================================
FAIL: test (__main__.TruthTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "stuff.py", line 6, in test
self.assertEqual(1 + 1, 3)
AssertionError: 2 != 3
wat informatief is (het evalueerde eigenlijk het resultaat aan de linkerkant).
U vindt de lijst met beweringen in de standaarddocumentatie . Over het algemeen is het een goed idee om de bewering te kiezen die het meest specifiek bij de voorwaarde past. Dus, zoals hierboven getoond, is het voor het beweren dat 1 + 1 == 2
beter om assertEqual
te gebruiken dan assertTrue
. Om te beweren dat a is None
, is het beter om assertIsNone
te gebruiken dan assertEqual
.
Merk ook op dat de beweringen negatieve vormen hebben. assertEqual
heeft dus zijn negatieve tegenhanger assertNotEqual
en assertIsNone
heeft zijn negatieve tegenhanger assertIsNotNone
. Nogmaals, het gebruik van de negatieve tegenhangers, indien van toepassing, zal leiden tot duidelijkere foutmeldingen.
Eenheidstests met pytest
pytest installeren:
pip install pytest
de tests klaarmaken:
mkdir tests
touch tests/test_docker.py
Functies om te 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)
De test importeert 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
bespotten van een bestandachtig object 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
Monkey-patching met 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
Voorbeeldtests moeten beginnen met het voorvoegsel test_
in het bestand 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])
de tests één voor één uitvoeren:
py.test -k test_docker_install tests
py.test -k test_copy_file_to_docker tests
py.test -k test_docker_exec_something tests
het uitvoeren van alle tests in de tests
map:
py.test -k test_ tests