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:

  1. 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.
  2. 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.

Een contextmanager gebruiken

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


Modified text is an extract of the original Stack Overflow Documentation
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow