目录

  • 引言
  • 准备
  • 项目
  • 总结

引言

我想现在应该很少有公司一直在主推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的一些高级特性的使用