Moduł unittest

https://docs.python.org/3/library/unittest.html

WPROWADZENIE

Moduł wspiera automatyzację testów, grupowanie testów w kolekcje, niezależność testów od narzędzi raportuących wyniki testów (reporting frameworks). Moduł dostarcza klasy, które ułatwiają te zadania. Ważne pojęcia:

Testy tworzymy w klasach wywiedzionych z unittest.TestCase. Jednostką testowania (test case) jest metoda bez parametrów, o nazwie rozpoczynającej się od "test", która ma sprawdzać jedną konkretną rzecz dotyczącą kodu. Test ma być przeprowadzony automatycznie, ma być niezależny od innych testów, a wynik ma być automatycznie zinterpretowany.

Testy mogą być grupowane w zestawach (test suits), typowy zestaw tworzą metody z jednej klasy wywiedzionej z unittest.TestCase.

Poniżej mamy przykład prostego modułu zawierającego funkcję average() do obliczania średniej arytmetycznej listy liczb. Kod testujący funkcję został umieszczony w tym samym module. Kod testujący zostanie wykonany, jeżeli moduł zostanie uruchomiony jako moduł główny.

W praktyce kod testujący moduł zwykle umieszcza się w osobnym pliku. Testy dla modułu spam.py umieszcza się w pliku test_spam.py. Często testy (pliki test_*.py) umieszcza się w osobnym katalogu tests.


import unittest

def average(values):
    """Oblicza średnią arytmetyczną listy liczb."""
    return sum(values, 0.0) / len(values)

# testcase tworzymy przez dziedziczenie z unittest.TestCase.
# Kolejne testy to metody w klasie o nazwach zaczynających się
# od "test" (to jest konwencja dla test runnera).
# W każdej metodzie kluczowe jest zastosowanie funkcji:

# assertEqual(first, second, msg=None)
# - sprawdzanie wyniku (test first == second),
# msg to komunikat do wypisania przy wystąpieniu błędu;
# assertEqual(expression_to_test, expected_result)   # typowa kolejność

# assertAlmostEqual(first, second, places=7, msg=None)
# assertAlmostEqual(first, second, places=7, msg=None, delta=None) [Py3.2+]
# - sprawdzanie wyniku typu float z domyślną dokładnością 7 cyfr;
# jeżeli podano delta, to sprawdzane jest |first - second| < delta

# assertNotEqual(first, second, msg=None)
# - sprawdzanie wyniku (test first != second);

# assertNotAlmostEqual(first, second, places=7, msg=None)
# assertNotAlmostEqual(first, second, places=7, msg=None, delta=None) [Py3.2+]
# - sprawdzanie wyniku;

# assertTrue(expr, msg=None)
# - sprawdzanie wartości logicznej (expr == True);

# assertFalse(expr, msg=None)
# - sprawdzanie wartości logicznej (expr == False);

# assertRaises(exception, callable, ...)
# - sprawdzenie wystąpienia spodziewanego wyjątku;

# assertRaises(exception, func, *args, **keywords) 
# - sprawdzenie wyjątku func(*args, **keywords)

class TestStatisticalFunctions(unittest.TestCase):

    # Czynności przygotowawcze dla test suit (cała klasa) [Py3.2+].
    # Można utworzyć obiekt zbyt kosztowny, aby go tworzyć dla każdego test case.
    @classmethod
    def setUpClass(cls): pass

    # Czynności przygotowawcze dla test case (metody test_*).
    def setUp(self): pass

    # Dla poprawnych danych wejściowych nie ma prawa pojawić się wyjątek.
    def test_average_good(self):
        # W testach nie używamy docstringów.
        # Chcemy zachować standardowe komunikaty o błędach.
        self.assertEqual(average([20, 30, 70]), 40.0)
        self.assertEqual(round(average([1, 5, 7]), 1), 4.3)

    # Nieprawidłowe dane powinny generować konkretny wyjątek.
    def test_average_bad(self):
        # Nie wstawiamy docstringu.
        self.assertRaises(ZeroDivisionError, average, [])
        self.assertRaises(TypeError, average, 20, 30, 70)

    # Czynności czyszczące dla test case.
    def tearDown(self): pass

    # Czynności czyszczące dla test suit [Py3.2+].
    @classmethod
    def tearDownClass(cls): pass

# Załóżmy, że mamy w danym module zdefiniowane inne funkcje.
# Przygotowujemy nową klasę do testowania tych funkcji.

class TestOtherFunctions(unittest.TestCase):

    def test_other(self):
        pass

# Prosty sposób uruchomienia wszystkich testów z obu klas
# TestStatisticalFunctions i TestOtherFunctions.

if __name__ == '__main__':
    unittest.main()     # włącza wszystkie testy
    #unittest.main(verbosity=2)    # Python 2.7, więcej informacji
    #unittest.main(argv=[''], exit=False)   # w trybie interaktywnym (Jupyter Notebook)

# Jeżeli powyższy kod umieścimy w module 'average.py', to polecenie
# python3 average.py -v
# wyświetli szczegółowe informacje o testach.

# Zakładamy, że istnieje tylko klasa TestStatisticalFunctions.
# Uruchomienie testów, przy którym dostajemy więcej informacji.
# Mamy możliwość wyboru testów.

if __name__ == '__main__':
    suite = unittest.TestLoader().loadTestsFromTestCase(TestStatisticalFunctions)
    unittest.TextTestRunner(verbosity=2).run(suite)

# Przy wielu klasach z testami możemy wybierać, z których klas
# mają pochodzić testy.

if __name__ == '__main__':
    suite1 = unittest.TestLoader().loadTestsFromTestCase(TestStatisticalFunctions)
    suite2 = unittest.TestLoader().loadTestsFromTestCase(TestOtherFunctions)
    #suite = unittest.TestSuite([suite1, suite2])  # wszystkie testy
    suite = unittest.TestSuite([suite2])           # wybrany zestaw testów
    unittest.TextTestRunner(verbosity=2).run(suite)

# Możemy ręcznie wstawiać nazwy testów do zestawu.

if __name__ == '__main__':
    suite = unittest.TestSuite()    # pusty zestaw testów
    suite.addTest(TestStatisticalFunctions("test_average_good"))
    unittest.TextTestRunner(verbosity=2).run(suite)

# Przykład z książki Turnquista.
# Zakładamy, że istnieje tylko klasa TestStatisticalFunctions.
# Nazwy testów podajemy w wierszu poleceń:
# python skrypt.py test_name1 test_name2 ...

if __name__ == '__main__':
    import sys
    suite = unittest.TestSuite()
    if len(sys.argv) == 1:
        suite = unittest.TestLoader().loadTestsFromTestCase(
            TestStatisticalFunctions)
    else:
        for test_name in sys.argv[1:]:
            suite.addTest(TestStatisticalFunctions(test_name))
    unittest.TextTestRunner(verbosity=2).run(suite)

Przykład tworzenia testów dla funkcji konwertujących liczby zapisane w systemie rzymskim można znaleźć w książce Marka Pilgrima Dive Into Python, dostępną za darmo pod adresem http://www.diveintopython.net/.

POMIJANIE TESTÓW

Moduł 'unittest' dostarcza kilka dekoratorów używanych do pomijania testów (skip, skipIf, skipUnless, expectedFailure) oraz jedną metodę (TestCase.skipTest). Można pominąć pojedynczy test lub klasę z testami. Dla pominiętych testów nie działają setUp() i tearDown(). Dla pominiętych klas nie działają setUpClass() i tearDownClass().


class MyTestCase(unittest.TestCase):

    @unittest.skip("demonstrating skipping")
    def test_nothing(self):
        self.fail("shouldn't happen")

    @unittest.skipIf(mylib.__version__ < (1, 3),
                     "not supported in this library version")
    def test_format(self):
        # Tests that work for only a certain version of the library.
        pass

    @unittest.skipUnless(sys.platform.startswith("win"), "requires Windows")
    def test_windows_support(self):
        # windows specific testing code
        pass

    def test_maybe_skipped(self):
        if not external_resource_available():
            self.skipTest("external resource not available") # metoda
        # test code that depends on the external resource
        pass

    @unittest.expectedFailure
    def test_fail(self):
        self.assertEqual(1, 0, "broken")


@unittest.skip("showing class skipping")
class MySkippedTestCase(unittest.TestCase):

    def test_not_run(self): pass

COMMAND-LINE INTERFACE

Moduł unittest może być użyty w wierszu poleceń do uruchomienia testów z modułów, klas, czy metod testujących [Py3.2+].


$ python3 -m unittest test_module1 test_module2
$ python3 -m unittest test_module.TestClass
$ python3 -m unittest test_module.TestClass.test_method

# Testowanie w trybie "Test Discovery" [Py3.2+].

$ python3 -m unittest discover