Szukaj…


Uwagi

Istnieje kilka narzędzi do testowania jednostkowego dla Pythona. W tym temacie dokumentacji opisano podstawowy moduł unittest . Inne narzędzia testujące obejmują py.test i nosetests . Ta dokumentacja Pythona dotycząca testowania porównuje kilka z tych narzędzi bez wchodzenia w głąb.

Testowanie wyjątków

Programy zgłaszają błędy, gdy na przykład podano nieprawidłowe dane wejściowe. Z tego powodu należy upewnić się, że wystąpi błąd, gdy podane zostaną rzeczywiste nieprawidłowe dane wejściowe. Z tego powodu musimy sprawdzić dokładny wyjątek, w tym przykładzie użyjemy następującego wyjątku:

class WrongInputException(Exception):
    pass

Ten wyjątek pojawia się, gdy podane zostaną nieprawidłowe dane wejściowe, w następującym kontekście, w którym zawsze oczekujemy liczby jako tekstu.

def convert2number(random_input):
    try:
        my_input = int(random_input)
    except ValueError:
        raise WrongInputException("Expected an integer!")
    return my_input

Aby sprawdzić, czy wyjątek został zgłoszony, używamy assertRaises do sprawdzenia tego wyjątku. assertRaises można korzystać na dwa sposoby:

  1. Korzystanie ze zwykłego wywołania funkcji. Pierwszy argument przyjmuje typ wyjątku, drugi wywoływalny (zwykle funkcja), a pozostałe argumenty są przekazywane do tego wywołania.
  2. Za with klauzuli with , podając funkcji tylko typ wyjątku. Ma to tę zaletę, że można wykonać więcej kodu, ale należy go używać ostrożnie, ponieważ wiele funkcji może korzystać z tego samego wyjątku, który może być problematyczny. Przykład: z self.assertRaises (WrongInputException): convert2number („not a number”)

To pierwsze zostało zaimplementowane w następującym przypadku testowym:

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()

Może zaistnieć potrzeba sprawdzenia wyjątku, który nie powinien zostać zgłoszony. Jednak test zakończy się automatycznie niepowodzeniem po zgłoszeniu wyjątku, a zatem może nie być konieczny. Aby pokazać opcje, druga metoda testowa pokazuje przypadek, w jaki sposób można sprawdzić, czy wyjątek nie zostanie zgłoszony. Zasadniczo jest to wychwytywanie wyjątku, a następnie niepowodzenie testu przy użyciu metody fail .

Wyśmiewanie funkcji za pomocą unittest.mock.create_autospec

Jednym ze sposobów wyśmiewania się z funkcji jest użycie funkcji create_autospec , która wyśmiewa obiekt zgodnie z jego specyfikacją. Dzięki funkcjom możemy to wykorzystać, aby zapewnić, że są one odpowiednio wywoływane.

Z funkcją multiply w custom_math.py :

def multiply(a, b):
    return a * b

I funkcja multiples_of w 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

Możemy przetestować multiples_of samodzielnie, wyśmiewając multiply . Poniższy przykład korzysta ze standardowej biblioteki Pythona, ale można jej również używać z innymi strukturami testowymi, takimi jak pytest lub 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

Przetestuj konfigurację i porzucenie w ramach unittest.TestCase

Czasami chcemy przygotować kontekst dla każdego testu. Metoda setUp jest uruchamiana przed każdym testem w klasie. tearDown jest uruchamiany na końcu każdego testu. Te metody są opcjonalne. Pamiętaj, że TestCases są często używane w wielokrotnym dziedziczeniu kooperacyjnym, dlatego powinieneś uważać, aby zawsze wywoływać super w tych metodach, aby tearDown również metody setUp i tearDown klasy podstawowej. Podstawowa implementacja TestCase zapewnia puste metody setUp i tearDown , dzięki czemu można je wywoływać bez zgłaszania wyjątków:

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()

Zauważ, że w python2.7 + istnieje również metoda addCleanup , która rejestruje funkcje, które mają być wywoływane po uruchomieniu testu. W przeciwieństwie do tearDown który jest wywoływany tylko wtedy, gdy setUp powiedzie, funkcje zarejestrowane przez addCleanup będą wywoływane nawet w przypadku nieobsługiwanego wyjątku w setUp . Jako konkretny przykład często można zaobserwować tę metodę usuwania różnych prób, które zostały zarejestrowane podczas testu:

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)

Kolejną zaletą rejestrowania oczyszczeń w ten sposób jest to, że pozwala programiście umieścić kod czyszczenia obok kodu instalacyjnego i chroni cię na wypadek, tearDown zapomniał wywołać super w tearDown .

Zapewnienie wyjątków

Możesz przetestować, czy funkcja zgłasza wyjątek z wbudowanym unittest za pomocą dwóch różnych metod.

Korzystanie z menedżera kontekstu

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)

Spowoduje to uruchomienie kodu w menedżerze kontekstu, a jeśli się powiedzie, test zakończy się niepowodzeniem, ponieważ wyjątek nie został zgłoszony. Jeśli kod zgłosi wyjątek poprawnego typu, test będzie kontynuowany.

Możesz także uzyskać treść zgłoszonego wyjątku, jeśli chcesz wykonać dodatkowe twierdzenia przeciwko niemu.

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')

Zapewniając funkcję wywoływania

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)

Wyjątkiem do sprawdzenia musi być pierwszy parametr, a funkcja wywoływalna musi zostać przekazana jako drugi parametr. Wszelkie inne określone parametry zostaną przekazane bezpośrednio do wywoływanej funkcji, umożliwiając określenie parametrów wyzwalających wyjątek.

Wybieranie asercji w ramach Unittests

Podczas gdy Python ma instrukcję assert , struktura testowania jednostek Python ma lepsze asercje specjalizowane dla testów: są one bardziej pouczające o awariach i nie zależą od trybu debugowania wykonania.

Być może najprostszym stwierdzeniem jest assertTrue , którego można użyć w następujący sposób:

import unittest

class SimplisticTest(unittest.TestCase):
    def test_basic(self):
        self.assertTrue(1 + 1 == 2)

To zadziała dobrze, ale zastąpi powyższą linię

        self.assertTrue(1 + 1 == 3)

zawiedzie.

assertTrue asercja jest najprawdopodobniej najbardziej ogólną asercją, ponieważ wszystko, co jest testowane, może być rzutowane jako warunek logiczny, ale często istnieją lepsze alternatywy. Podczas testowania równości, jak wyżej, lepiej pisać

        self.assertEqual(1 + 1, 3)

Gdy pierwszy z nich zawiedzie, komunikat brzmi

======================================================================

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

ale gdy to drugie zawiedzie, wiadomość brzmi

======================================================================

FAIL: test (__main__.TruthTest)

----------------------------------------------------------------------

Traceback (most recent call last):

  File "stuff.py", line 6, in test

    self.assertEqual(1 + 1, 3)
AssertionError: 2 != 3

co jest bardziej pouczające (w rzeczywistości ocenił wynik lewej strony).

Możesz znaleźć listę twierdzeń w standardowej dokumentacji . Ogólnie rzecz biorąc, dobrym pomysłem jest wybranie twierdzenia, które najlepiej pasuje do danego warunku. Tak więc, jak pokazano powyżej, do potwierdzenia, że 1 + 1 == 2 lepiej jest użyć assertEqual niż assertTrue . Podobnie, aby stwierdzić, że a is None , lepiej użyć assertIsNone niż assertEqual .

Zauważ też, że twierdzenia mają formy negatywne. Zatem assertEqual ma swój negatywny odpowiednik assertNotEqual , a assertIsNone ma swój negatywny odpowiednik assertIsNotNone . Ponownie użycie negatywnych odpowiedników, gdy jest to właściwe, doprowadzi do wyraźniejszych komunikatów o błędach.

Testy jednostkowe z pytestem

instalacja pytest:

pip install pytest

przygotowanie testów:

mkdir tests
touch tests/test_docker.py

Funkcje do testowania w 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)

Test importuje 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

wyśmiewanie pliku podobnego do obiektu w 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

Łatowanie małp za pomocą pytest w 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

Przykładowe testy należy rozpocząć prefiks test_ w test_docker.py pliku:

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])

uruchamianie testów pojedynczo:

py.test -k test_docker_install tests
py.test -k test_copy_file_to_docker tests
py.test -k test_docker_exec_something tests

uruchamianie wszystkich testów w folderze tests :

py.test -k test_ tests


Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow