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

编辑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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值