Test driven development with pytest
Test driven development with pytest
简介
TDD(Test Driven Development) 测试驱动开发是一种软件开发方法,它要求开发者为新功能添加测试案例,利用自动测试工具对测试案例进行自动测试。
Pytest就是python开发的自动测试框架。
测试驱动开发过程由以下三步组成:
1) 编写新功能的测试案例(Red-表示有错误,测试不过)
2) 实现新功能使测试案例测试通过(Green-测试通过)
3) 优化,重构代码,使其更加合理,高效(Refactor-重构)
通常被称为 Red-Green-Refactor 周期。
简单示例:test_prime.py
创建一个prime目录,然后添加两个文件:prime.py和test_prime.py。
启动开发环境
python3 -m venv env
. env/bin/activate
pip install pytest
编辑test_prime.py
from prime import is_prime
from prime import sum_of_primes
def test_prime_low_number():
assert is_prime(1) == False
def test_prime_prime_number():
assert is_prime(29) == True
def test_sum_of_primes_empty_list():
assert sum_of_primes([]) == 0
def test_sum_of_primes_mixed_list():
assert sum_of_primes([11, 15, 17, 18, 20]) == 28
def is_prime(num):
if isinstance(num, int) == False:
return False
if num < 2:
return False
for n in range(2, num):
if num % n == 0:
return False
return True
def sum_of_primes(nums):
sum = 0
for n in nums:
if is_prime(n):
sum = sum + n
return sum
启动测试
pytest prime
$ pytest prime
==============test session starts ====================
platform cygwin -- Python 3.6.8, pytest-4.6.3, py-1.8.0, pluggy-0.12.0
rootdir: /cygdrive/f/liudh/pytest/venvtest
collected 4 items
prime/test_prime.py ....
[100%]
============== 4 passed in 0.20 seconds ==============
(venvtest)
示例:inventory
inventory包含若干stock,有容量(商品数量)限制,有当前的stock数量。每种stock包含名称,单件价格,数量等要素。inventory有add_new_stock和remove_stock操作。
test_inventory.py
from inventory import Inventory, InvalidQuantityException, NoSpaceException, ItemNotFoundException
import pytest
def test_buy_and_sell_nikes_adidas():
# Create inventory object
inventory = Inventory()
assert inventory.limit == 100
assert inventory.total_items == 0
# Add the new Nike sneakers
inventory.add_new_stock('Nike Sneakers', 50.00, 10)
assert inventory.total_items == 10
# Add the new Adidas sweatpants
inventory.add_new_stock('Adidas Sweatpants', 70.00, 5)
assert inventory.total_items == 15
# Remove 2 sneakers to sell to the first customer
inventory.remove_stock('Nike Sneakers', 2)
assert inventory.total_items == 13
# Remove 1 sweatpants to sell to the next customer
inventory.remove_stock('Adidas Sweatpants', 1)
assert inventory.total_items == 12
@pytest.fixture
def no_stock_inventory():
"""return an empty invetory that can store 10 items"""
return Inventory(10)
def test_add_new_stock_success(no_stock_inventory):
no_stock_inventory.add_new_stock('Test Jacket', 10.00, 5)
assert no_stock_inventory.total_items == 5
assert no_stock_inventory.stocks['Test Jacket']['price'] == 10.00
assert no_stock_inventory.stocks['Test Jacket']['quantity'] == 5
@pytest.mark.parametrize('name, price, quantity, exception',[
('Test Jacket', 10.00, 0, InvalidQuantityException('Cannot add a quantity of 0. All new stocks must have at least 1 item')),
('Test Jacket', 10.00, 25, NoSpaceException('Cannot add these 25 items. Only 10 more items can be stored'))
])
def test_add_new_stock_bad_input(name, price, quantity, exception):
inventory = Inventory(10)
try:
inventory.add_new_stock(name, price, quantity)
except (InvalidQuantityException, NoSpaceException) as inst:
# First ensure the exception is of the right type
assert isinstance(inst, type(exception))
# Ensure that exceptions have the same message
assert inst.args == exception.args
else:
pytest.fail("Expected error but found none")
@pytest.mark.parametrize('name, price, quantity, exception',[
('Test Jacket', 10.00, 0, InvalidQuantityException('Cannot add a quantity of 0. All new stocks must have at least 1 item')),
('Test Jacket', 10.00, 25, NoSpaceException('Cannot add these 25 items. Only 10 more items can be stored')),
('Test Jacket', 10.00, 5, None)
])
def test_add_new_stock(no_stock_inventory, name, price, quantity, exception):
try:
no_stock_inventory.add_new_stock(name, price, quantity)
except (InvalidQuantityException, NoSpaceException) as inst:
# First ensure the exception is of the right type
assert isinstance(inst, type(exception))
# Ensure that exceptions have the same message
assert inst.args == exception.args
@pytest.fixture
def ten_stock_inventory():
"""Returns an inventory with some test stock items"""
inventory = Inventory(20)
inventory.add_new_stock('Puma Test', 100.00, 8)
inventory.add_new_stock('Reebok Test', 25.50, 2)
return inventory
# ...
# Note the extra parameters, we need to set our expectation of
# what totals should be after our remove action
@pytest.mark.parametrize('name,quantity,exception,new_quantity,new_total', [
('Puma Test', 0,
InvalidQuantityException(
'Cannot remove a quantity of 0. Must remove at least 1 item'),
0, 0),
('Not Here', 5,
ItemNotFoundException(
'Could not find Not Here in our stocks. Cannot remove non-existing stock'),
0, 0),
('Puma Test', 25,
InvalidQuantityException(
'Cannot remove these 25 items. Only 8 items are in stock'),
0, 0),
('Puma Test', 5, None, 3, 5)
])
def test_remove_stock(ten_stock_inventory, name, quantity, exception,
new_quantity, new_total):
try:
ten_stock_inventory.remove_stock(name, quantity)
except (InvalidQuantityException, NoSpaceException, ItemNotFoundException) as inst:
assert isinstance(inst, type(exception))
assert inst.args == exception.args
else:
assert ten_stock_inventory.stocks[name]['quantity'] == new_quantity
assert ten_stock_inventory.total_items == new_total
class InvalidQuantityException(Exception):
pass
class NoSpaceException(Exception):
pass
class ItemNotFoundException(Exception):
pass
class Inventory:
def __init__(self, limit=100):
self.limit = limit
self.total_items = 0
self.stocks = {}
def add_new_stock(self, name, price, quantity):
if quantity <= 0:
raise InvalidQuantityException(
'Cannot add a quantity of {}. All new stocks must have at least 1 item'.format(quantity))
if self.total_items + quantity > self.limit:
remaining_space = self.limit - self.total_items
raise NoSpaceException('Cannot add these {} items. Only {} more items can be stored'.format(quantity, remaining_space))
self.stocks[name] = {
'price':price,
'quantity':quantity
}
self.total_items += quantity
def remove_stock(self, name, quantity):
if quantity <= 0:
raise InvalidQuantityException(
'Cannot remove a quantity of {}. Must remove at least 1 item'.format(quantity))
if name not in self.stocks:
raise ItemNotFoundException(
'Could not find {} in our stocks. Cannot remove non-existing stock'.format(name))
if self.stocks[name]['quantity'] - quantity < 0:
raise InvalidQuantityException(
'Cannot remove these {} items. Only {} items are in stock'.format(
quantity, self.stocks[name]['quantity']))
self.stocks[name]['quantity'] -= quantity
self.total_items -= quantity
单元测试与集成测试
单元测试指对一个模块或函数进行测试,确保其行为符合预期。集成测试是对多个模块或函数进行测试,确保其联合交互符合预期。
在prime的测试方法中,我们使用的是单元测试。test_buy_and_sell_nikes_adidas则展示了集成测试。它首先初始化一个Inventory对象,然后添加了10件Nike,检查其total_items是否符合预期,然后又添加了5件Adidas,检查其total_items是否符合预期,然后卖掉了2件Nike,检查其total_items是否符合预期,最后卖掉了1件Adidas,检查其total_items是否符合预期。
pytest fixture
fixture是一种已知的固定的状态,一些测试已此状态为起始状态,这样,测试的结果也是确定的。
使用方法见no_stock_inventory和test_add_new_stock_success。
1)用pytest.fixture装饰一个函数
2)将该函数作为参数传递给一个测试函数
3)在测试函数中将该函数作为对象调用
pytest参数化函数
参数化函数,可以用一个测试函数支持多个测试场景。
使用方法见test_add_new_stock_bad_input。
1)用pytest.mark.parametrize装饰一个测试函数
2)在pytest.mark.parametrize装饰器中指定参数列表,以及测试场景列表(一组参数对应一组测试场景)
3)函数的参数列表必须包含装饰器的参数列表
4)实现函数体
pytest fixture和参数化函数联合使用
见test_new_stock和test_remove_stock。
原文链接:
https://stackabuse.com/test-driven-development-with-pytest/#advancedexamplewritinganinventorymanager
推荐阅读
-
测试驱动开发实践 - Test-Driven Development(转)
-
[Python]Test Driven Development in Flask application
-
荐 pytest框架走出 test -> fixture <-> fixture 调用限制的魔咒
-
让数据库应用开发不再裸奔 Test-Driven Database Development译
-
让数据库应用开发不再裸奔 Test-Driven Database Development译
-
[Python]Test Driven Development in Flask application
-
荐 pytest框架走出 test -> fixture <-> fixture 调用限制的魔咒
-
Test driven development with pytest
-
Test-Driven Development(测试驱动开发) tddxp单元测试