欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Test driven development with pytest

程序员文章站 2022-03-11 16:16:13
...

简介

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

编辑prime.py

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

inventory.py

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

相关标签: pytest