OpenQuality.ru

Качество программного обеспечения

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

Добрый день.

Модуль 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.

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

02.02.2009 Капитан Аляска | Модульные тесты | Комментарии (6)

Комментарии (6) »

  1. […] возьмем файл account.py, содержащий класс BankAccount, из нашего примера c unittest. Далее создадим файл test.py, в котором […]

    Pingback : OpenQuality.ru | Ступень Мартина, или двойники в Python | April 21, 2009

  2. Немного из своего опыта.
    Очень удобна структура построения тестового фреймворка на базе чистого unittest, если необходимо иметь четкую структуру тестов, основанных на базовом классе.

    Но гораздо удобнее использовать nosetests (надстройка для unitttest) или py.test (часть библиотеки py.lib). Оба обладают схожими возможностями и позволяет эффективно управлять тестами.

    Автор : Ochir | June 23, 2009

  3. Ochir, спасибо за ценный комментарий! Да, в сторону nose я смотрел, но пока функциональности unittest хватало. py.test – пока нет. Много их, framework’ов :)

    Автор : Капитан Аляска | June 23, 2009

  4. а чего в nose не хватает?
    это же надстройка вроде, он все возможности unittest в себя включает и можно писать обычные unittest, но потом отлаживать с помощью nosetests с опциями.

    Или если unittest совсем не нравится, можно сразу py.test пробовать.

    Автор : Ochir | June 24, 2009

  5. Я сформулировал это для себя несколько иначе: не увидел в nose чего-нибудь такого, чтобы захотелось его использовать. Функциональности unittest для моих задач было достаточно. А дальше – время покажет.

    Автор : Капитан Аляска | June 24, 2009

  6. согласен, это дело удобства и привычки.

    Автор : Ochir | June 24, 2009

Рады вас слышать!