Python unittest: базовые возможности


Источник: blog.OpenQuality.ru

Добрый день.

Модуль unittest входит в стандартную библиотеку Python и служит базовым инструментом для организации регрессионных unit-тестов. Рассмотрим небольшой пример. Файл account.py содержит класс BankAccount, предоставляющий средства для работы с банковским счетом: создание счета с начислением бонуса, добавление и снятие денег, начисление процентов.

class BankAccount:
    def __init__ (self, bonus):
        self.balance = bonus
 
    def deposit (self, amount):
        self.balance += amount
 
    def withdraw (self, amount):
        if self.balance > amount:
            self.balance -= amount
        else:
            self.balance = 0
 
    def interest (self, rate):
        self.balance = self.balance + (self.balance * rate)/100
 
    def get(self):
        return self.balance
 
if __name__ == '__main__':
    account = BankAccount(30)
    account.deposit(50)
    account.withdraw(10)
    account.interest(8.5)
    balance = account.get()
    print balance

Запуск account.py из командной строки cлужит примером работы с BankAccount (результат = 75.95). Подготовим тесты для трех методов: deposit, withdraw и interest. Разместим их в модуле test_account.py:

from account import BankAccount
import unittest
 
class TestBankAccount (unittest.TestCase):
 
    def setUp(self):
        self.account = BankAccount (100)
 
    def testBankAccountDeposit(self):
        test_balance = 170
        self.account.deposit (70) 
        self.assertEqual(self.account.balance, test_balance)
 
    def testBankAccountWithdraw(self):
        test_balance = 30
        self.account.withdraw (70)
        self.assertEqual(self.account.balance, test_balance)
        self.account.withdraw (270)
        self.assertEqual(self.account.balance, 0)
 
    def testBankAccountInterest(self):
        test_balance = 108.5
        self.account.interest (8.5)
        self.assertEqual(self.account.balance, test_balance)
 
if __name__ == "__main__":
    unittest.main()

Метод setUp() – служебный. Он вызывается перед запуском каждого теста и подготавливает среду выполнения. В нашем случае метод setUp() создает банковский аккаунт и помещает на счет 100 единиц. Имена остальных методов начинаются с “test” (необходимое условие для нахождения тестов в коде модуля). Запуск test_account.py:

[capt@rh Work]# python test_account.py -v
testBankAccountDeposit (__main__.TestBankAccount) ... ok
testBankAccountInterest (__main__.TestBankAccount) ... ok
testBankAccountWithdraw (__main__.TestBankAccount) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.005s

OK

Приложение, достойное модульного тестирования, как правило содержит больше одного модуля. Соответственно, unittest предоставляет возможности для централизованного управления всеми тестами. Создадим агрегатор, который будет запускать тесты из класса TestBankAccount и тесты нахождения палиндромов.

Прежде всего, внесем изменения в test_account.py, добавив в него создание комплекта тестов класса BankAccount:

from account import BankAccount
import unittest
 
class TestBankAccount (unittest.TestCase):
 
    def setUp(self):
        self.account = BankAccount (100)
 
    def testBankAccountDeposit(self):
        test_balance = 170
        self.account.deposit (70) 
        self.assertEqual(self.account.balance, test_balance)
 
    def testBankAccountWithdraw(self):
        test_balance = 30
        self.account.withdraw (70)
        self.assertEqual(self.account.balance, test_balance)
        self.account.withdraw (270)
        self.assertEqual(self.account.balance, 0)
 
    def testBankAccountInterest(self):
        test_balance = 108.5
        self.account.interest (8.5)
        self.assertEqual(self.account.balance, test_balance)
 
def suite():
    suite = unittest.TestSuite()
    suite.addTest(unittest.makeSuite(TestBankAccount))
    return suite

Далее, подготовим служебный файл test_transformation с результатами doctest-проверки наличия палиндромов:

>>> from transformation import is_palindrome
>>> is_palindrome("hello hhh again")
hhh
>>> is_palindrome("")
Traceback (most recent call last):
    ...
ValueError: Empty String!

Создадим скрипт-агрегатор test_aggregator.py:

import unittest
import test_account
import doctest
import transformation
 
suiteAccount = test_account.suite()
 
suitePalindrome = unittest.TestSuite()
suitePalindrome.addTest(doctest.DocFileSuite("test_transformation"))
 
suite = unittest.TestSuite()
suite.addTest(suiteAccount)
suite.addTest(suitePalindrome)
 
unittest.TextTestRunner(verbosity=2).run(suite)

Запуск test_aggregator.py:

[capt@rh Work]# python test_aggregator.py
testBankAccountDeposit (test_account.TestBankAccount) ... ok
testBankAccountInterest (test_account.TestBankAccount) ... ok
testBankAccountWithdraw (test_account.TestBankAccount) ... ok
Doctest: test_transformation ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.005s

OK

Убедимся, что при изменении алгоритма метода withdraw наш тест закончится неудачей. Изменим этот метод в модуле account.py:

def withdraw (self, amount):
     self.balance -= amount

Запуск test_aggregator.py:

[capt@rh Work]# python test_aggregator.py
testBankAccountDeposit (test_account.TestBankAccount) ... ok
testBankAccountInterest (test_account.TestBankAccount) ... ok
testBankAccountWithdraw (test_account.TestBankAccount) ... FAIL
Doctest: test_transformation ... ok

======================================================================
FAIL: testBankAccountWithdraw (test_account.TestBankAccount)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/Work/test_account.py", line 19, in testBankAccountWithdraw
    self.assertEqual(self.account.balance, 0)
AssertionError: -240 != 0

----------------------------------------------------------------------
Ran 4 tests in 0.006s

FAILED (failures=1)

Результат соответствует ожидаемому.

Дополнительную информацию о модуле unittest можно почерпнуть в документации Python.

Успехов в модульных тестах. Оставайтесь с нами.