[工程] python下的测试利器pytest
目录
- 引言
- 准备
- 项目
- 总结
引言
我想现在应该很少有公司一直在主推TDD了,因为这无形之中增加了工作量,在追求极致开发的时候,一般都是能先上就先上,把主体的功能不管三七二十一怼出来再说,出了问题就直接改,也不管你的代码和之前兼容不兼容,一个大的工程,测试是绝对少不了的,覆盖率更是必须达到一定的程度,这既保证了当前的开发的新的feature 与整体能兼容,也能保证工程的整体稳定性,在构思新的功能之前,我们应该从整体规划我们新的功能应该表现出来的输出结果和异常,这并不是说测试是万能的,有了测试是万无一失的,因为总有一些地主是测试没有覆盖的地方,我们需要在现有,人为,可控的情况下将错误降低,而不是让我们的代码腐烂和难以维护。
准备
-
pythont版本: 2.7
-
使用pip 安装以下库
pytest-cov==2.5.1 pytest==3.4.0
项目
简单测试
一般来说,当我信要写一个测试py文件时候,名字都要以test为前缀,或者用test结尾,我们先来创建一个简单的测试除法的python文件,名为test_div.py
,直接上代码
# -*- coding: utf-8 -*-
# test_div.py
def div_input(x):
return 10 / x
def test_div_input():
assert div_input(10) == 1
细看代码,你就知道咱们的测试的function同样需要使用test开头(这是默认的约束),我们使用assert断言语法,关于assert
断言的使用,你可以自己先上网找找,OK,测试的文件,测试的方法咱们已经写好了,我们要开始跑测试了,切换到这个测试文件所在的目录,然后在shell下执行命令
pytest
接着,我们就可以看到我们第一个测试跑出的结果了
============================================================================================================ test session starts =============================================================================================================
platform darwin -- Python 2.7.13, pytest-3.4.1, py-1.5.2, pluggy-0.6.0
rootdir: /Users/brucedone/Projects/github/web_scheduler_demo/tests, inifile:
plugins: cov-2.5.1
collected 1 item
test_div.py . [100%]
========================================================================================================== 1 passed in 0.01 seconds ==========================================================================================================
咱们这个function有一个地方肯定报错,就是当我们输入0的时候,所以当我们遇到这些function会特定的抛出一些"可控范围"内的错误时,我们也要在测试中反应出来
# -*- coding: utf-8 -*-
import pytest
def div_input(x):
return 10 / x
def test_div_input():
assert div_input(10) == 1
def test_raise_div_error_on_pass_zero():
with pytest.raises(TypeError):
div_input(0)
这个时候咱们跑一下测试,结果如下:
====================================================== FAILURES ======================================================
_________________________________________ test_raise_div_error_on_pass_zero __________________________________________
def test_raise_div_error_on_pass_zero():
with pytest.raises(TypeError):
> div_input(0)
test_div.py:15:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
x = 0
def div_input(x):
> return 10 / x
E ZeroDivisionError: integer division or modulo by zero
test_div.py:6: ZeroDivisionError
========================================= 1 failed, 1 passed in 0.03 seconds =========================================
报错的地方咱们没有捕获到,出现了除0的错误 ,这个时候咱们就要回头修改咱们的function,使得他能正常的处理一些这些特别输入的地方:
def div_input(x):
if x == 0:
raise TypeError('we can not input zero directly!')
return 10 / x
这次咱们再跑一下测试,就可以通过了,关于输入字符串等其它非正常的输入,我就不一一讲解了,我们来讲下一个pytest的特性Fixtures
使用Pytest的 Fixtures
在接下来的章节中,我们将探索使用pytest的一些高级特性,我们先准备一个小型的项目,接下来,我们将写一个wallet
的应用,这个应用允许用户增加或者消费钱。他主要有两个方法:spend_cash
以及add_cash
。我们来编写一个test_wallet.py
的文件
# test_wallet.py
import pytest
from wallet import Wallet, InsufficientAmount
def test_default_initial_amount():
wallet = Wallet()
assert wallet.balance == 0
def test_setting_initial_amount():
wallet = Wallet(100)
assert wallet.balance == 100
def test_wallet_add_cash():
wallet = Wallet(10)
wallet.add_cash(90)
assert wallet.balance == 100
def test_wallet_spend_cash():
wallet = Wallet(20)
wallet.spend_cash(10)
assert wallet.balance == 10
def test_wallet_spend_cash_raises_exception_on_insufficient_amount():
wallet = Wallet()
with pytest.raises(InsufficientAmount):
wallet.spend_cash(100)
首先我们在测试用例中,导入Wallet
class InsufficientAmount
这个异常,当我们使用的钱超出钱包所持有的钱的时候,我们就会抛出这个异常出来. 你可以明显的看到,当我们要测试这个wallet
的方方面面的行为的时候.在我们使用花钱或者增加钱的时候,我们这个wallet
类可以准确的反应出我们的当前持有的金额,并且,当我们超额透支的时候,也会直接抛出InsufficientAmount
的error 来,OK,我们根据我们的测试反过来编写我们的功能类
# wallet.py
class InsufficientAmount(Exception):
pass
class Wallet(object):
def __init__(self, initial_amount=0):
self.balance = initial_amount
def spend_cash(self, amount):
if self.balance < amount:
raise InsufficientAmount('Not enough available to spend {}'.format(amount))
self.balance -= amount
def add_cash(self, amount):
self.balance += amount
Ok,根据测试编写的功能类写完之后,我们跑一下测试
pytest -q test_wallet.py
.....
5 passed in 0.01 seconds
改进我们的测试
你可以明显的看到,当我们要测试wallet 的方方面面的时候,我们都首先要实例化这样一个类,然后再进行下一步的操作,这就是pytest fixtures
为什么这么方便好用了. 他可以帮我们省不少code 的编写,不多说了,直接上代码吧.
# test_wallet.py
import pytest
from wallet import Wallet, InsufficientAmount
@pytest.fixture
def empty_wallet():
'''Returns a Wallet instance with a zero balance'''
return Wallet()
@pytest.fixture
def wallet():
'''Returns a Wallet instance with a balance of 20'''
return Wallet(20)
def test_default_initial_amount(empty_wallet):
assert empty_wallet.balance == 0
def test_setting_initial_amount(wallet):
assert wallet.balance == 20
def test_wallet_add_cash(wallet):
wallet.add_cash(80)
assert wallet.balance == 100
def test_wallet_spend_cash(wallet):
wallet.spend_cash(10)
assert wallet.balance == 10
def test_wallet_spend_cash_raises_exception_on_insufficient_amount(empty_wallet):
with pytest.raises(InsufficientAmount):
empty_wallet.spend_cash(100)
我们通过使用装饰器 ptyest.fixture
,将我们empty_wallet
以及wallet
参数化我们的测试用例的每个方法里面,这样不仅节省了非常多的代码量,而且我们的测试更直观了。
参数化测试方法
现在我们已经独立的测试了Wallet
这个class 里面的各个方法,接下来我们要测试各种组合形式的方法了。我们假定这样的测试"钱包里面有30,我们先支出20,然后增加100,再然后又消费了50,这个时候余额是多少"。这个时候一想,这不是在每一个操作之后都要写大量的assert 了吗。这个时候,咱们就需要使用这个方案了: 参数化测试方法,为了应对以上的种种情况,我们编写如下的代码
# test_wallet.py
@pytest.mark.parametrize("earned,spent,expected", [
(30, 10, 20),
(20, 2, 18),
])
def test_transactions(earned, spent, expected):
my_wallet = Wallet()
my_wallet.add_cash(earned)
my_wallet.spend_cash(spent)
assert my_wallet.balance == expected
我们可以看到,我们的参数 earned , spend ,expected 参数 在第一次测试的时候取值为
30 ,10,20
在第二次测试的时候取值为
20,2,18
这样就方便你理解了。
总结
这次我们引出为什么要使用测试,并使用TDD反过来编写我们的功能类,并且针对我们测试的情况的逐步改进,引入了pytest的一些高级特性的使用
- 原文作者:大鱼
- 原文链接:https://brucedone.com/archives/1121/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议. 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。