Ricerca…


Osservazioni

Esistono diversi strumenti di test delle unità per Python. Questo argomento di documentazione descrive il modulo di unittest base. Altri strumenti di test includono py.test e nosetests . Questa documentazione su Python confronta diversi di questi strumenti senza approfondire.

Test delle eccezioni

I programmi generano errori quando, ad esempio, viene fornito un input errato. Per questo motivo, è necessario assicurarsi che venga generato un errore quando viene fornito un input errato. Per questo motivo dobbiamo verificare un'eccezione esatta, per questo esempio useremo la seguente eccezione:

class WrongInputException(Exception):
    pass

Questa eccezione viene sollevata quando viene fornito un input errato, nel seguente contesto in cui ci si aspetta sempre un numero come input di testo.

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

Per verificare se è stata sollevata un'eccezione, utilizziamo assertRaises per verificare tale eccezione. assertRaises può essere utilizzato in due modi:

  1. Usando la normale chiamata di funzione. Il primo argomento accetta il tipo di eccezione, il secondo un callable (di solito una funzione) e il resto degli argomenti viene passato a questo callable.
  2. Usando una clausola with , dando solo il tipo di eccezione alla funzione. Ciò ha il vantaggio che è possibile eseguire più codice, ma dovrebbe essere usato con attenzione poiché più funzioni possono utilizzare la stessa eccezione che può essere problematica. Un esempio: con self.assertRaises (WrongInputException): convert2number ("non un numero")

Questo primo è stato implementato nel seguente caso di test:

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

Potrebbe anche esserci la necessità di verificare un'eccezione che non avrebbe dovuto essere generata. Tuttavia, un test fallirà automaticamente quando viene lanciata un'eccezione e quindi potrebbe non essere affatto necessaria. Solo per mostrare le opzioni, il secondo metodo di test mostra un caso su come si può verificare che non venga lanciata un'eccezione. Fondamentalmente, questo sta rilevando l'eccezione e quindi fallendo il test usando il metodo fail .

Funzioni di simulazione con unittest.mock.create_autospec

Un modo per prendere in giro una funzione è usare la funzione create_autospec , che metterà a punto un oggetto secondo le sue specifiche. Con le funzioni, possiamo usarlo per assicurarci che siano chiamati appropriatamente.

Con una funzione si multiply in custom_math.py :

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

E una funzione si 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

Possiamo testare i multiples_of da solo dividendo multiply . L'esempio seguente usa la libreria standard Python unittest, ma può essere usato anche con altri framework di testing, come pytest o 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 e Teardown in un unestest.TestCase

A volte vogliamo preparare un contesto per ogni test da eseguire. Il metodo setUp viene eseguito prima di ogni test della classe. tearDown viene eseguito alla fine di ogni test. Questi metodi sono opzionali. Ricorda che i TestCase sono spesso usati nell'ereditarietà multipla cooperativa quindi devi stare attento a chiamare sempre super in questi metodi in modo che anche i metodi setUp e tearDown della classe base vengano richiamati. L'implementazione base di TestCase fornisce metodi di setUp e tearDown vuoti in modo che possano essere richiamati senza generare eccezioni:

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

Si noti che in python2.7 + esiste anche il metodo addCleanup che registra le funzioni da chiamare dopo l'esecuzione del test. Al contrario di tearDown che viene chiamato solo se setUp successo, le funzioni registrate tramite addCleanup verranno chiamate anche in caso di un'eccezione non gestita in setUp . Come esempio concreto, questo metodo può essere visto spesso rimuovendo vari mock che sono stati registrati mentre il test era in esecuzione:

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)

Un altro vantaggio della registrazione delle pulizie in questo modo è che consente al programmatore di inserire il codice di pulizia accanto al codice di configurazione e che ti protegge nel caso in cui un subclasser si dimentichi di chiamare super in tearDown .

Asserendo sulle eccezioni

È possibile testare che una funzione genera un'eccezione con l'unittest integrato attraverso due metodi diversi.

Utilizzando un gestore di contesto

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)

Questo eseguirà il codice all'interno del gestore di contesto e, se riesce, fallirà il test perché l'eccezione non è stata sollevata. Se il codice genera un'eccezione del tipo corretto, il test continuerà.

È anche possibile ottenere il contenuto dell'eccezione sollevata se si desidera eseguire ulteriori affermazioni su di esso.

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

Fornendo una funzione callable

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)

L'eccezione da verificare deve essere il primo parametro e una funzione callable deve essere passata come secondo parametro. Qualsiasi altro parametro specificato verrà passato direttamente alla funzione chiamata, consentendo di specificare i parametri che attivano l'eccezione.

Scegliere le asserzioni all'interno di Unittests

Mentre Python ha un assert dichiarazione , il quadro unit testing Python ha una migliore affermazioni specializzati per le prove: sono più informativo sui fallimenti, e non dipendono modalità di debug della esecuzione.

Forse l'asserzione più semplice è assertTrue , che può essere usata in questo modo:

import unittest

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

Ciò funzionerà correttamente, ma sostituendo la riga precedente con

        self.assertTrue(1 + 1 == 3)

avrà esito negativo.

L'affermazione L'affermazione assertTrue è molto probabilmente l'affermazione più generale, poiché qualsiasi cosa testata può essere lanciata come alcune condizioni booleane, ma spesso ci sono alternative migliori. Quando si prova per l'uguaglianza, come sopra, è meglio scrivere

        self.assertEqual(1 + 1, 3)

Quando il primo fallisce, il messaggio è

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

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

ma quando quest'ultimo fallisce, il messaggio è

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

FAIL: test (__main__.TruthTest)

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

Traceback (most recent call last):

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

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

che è più informativo (in realtà ha valutato il risultato della parte sinistra).

È possibile trovare l'elenco di asserzioni nella documentazione standard . In generale, è una buona idea scegliere l'asserzione più adatta alla condizione. Quindi, come mostrato sopra, per affermare che 1 + 1 == 2 è meglio usare assertEqual di assertTrue . Allo stesso modo, per affermare che a is None , è meglio usare assertIsNone che assertEqual .

Si noti inoltre che le asserzioni hanno forme negative. Così assertEqual ha la sua controparte negativa assertNotEqual , e assertIsNone ha la sua controparte negativa assertIsNotNone . Ancora una volta, usando le controparti negative, se appropriato, porterà a messaggi di errore più chiari.

Test unitari con pytest

installazione pytest:

pip install pytest

preparando i test:

mkdir tests
touch tests/test_docker.py

Funzioni da testare 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)

Il test importa 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

deridere un file come oggetto 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

Patch di scimmia con 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

I test di esempio, devono iniziare con il prefisso test_ nel file 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])

eseguendo i test uno alla volta:

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

eseguendo tutti i test nella cartella tests :

py.test -k test_ tests


Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow