美文网首页单元测试Unit Test Python
指南与踩坑:Python 单元测试

指南与踩坑:Python 单元测试

作者: 苏尚君 | 来源:发表于2017-11-06 10:12 被阅读134次

    本文试图总结编写单元测试的流程,以及自己在写单元测试时踩到的一些坑。如有遗漏,纯属必然,欢迎补充。

    目录概览:

    • 编写思想 x 3
    • 编写方法
      • 基本单元测试框架x3
      • 对基于网站框架搭建的网络应用进行单元测试

    编写思想

    尽可能地按「单元」测试,重点是:保证待测单元内部的所有流程能按设想正确运行、返回预期结果。

    假设我们有一个待测程序如下

    # to_test.py
    
    def func_01(param0101, param0102):
        # 处理参数 param0101 和 param0102 的代码,如
        ret = param0101 * param0102
        # 假设结果保存在名为 ret 的变量中
        return ret
    
    def func_02(param0201, param0202):
        # 处理参数 param0201 和 param0202 的代码,如
        ret = param0201 / param0202
        # 假设结果保存在名为 ret 的变量中
        return ret
        
    
    def call_other_funcs(param0101, param0102, param0201, param0202):
        # 对传入参数 param0x0y 等进行一些处理后,存到了 param0x0y_changed 中。例如
        param0x0y = param0x0y * 10
        
        ret01 = func_01(param0101_changed, param0102_changed)
        ret02 = func_02(param0201_changed, param0202_changed)
        
        # 对得到的 ret01, ret02 进行一些处理,得到最终返回值 ret。例如
        if ret01 and ret02:
           ret = (ret01, ret02)
        elif ret01 and (not ret02):
           ret = (ret01, 0)
        elif (not ret01) and ret02
           ret = (0, ret02)
        else:
           ret = (0, 0)
        return ret
    

    要关注的点有2:

    1. 每个测试仅保证 1 个单元的内部流程正确,即待测单元;这种正确性是不依赖于外部流程的正确性的

    所以要假定该单元内调用的外部单元(如引入的模块、函数等)能返回预期结果——这通过所谓的 Mock 来实现,可理解为就是伪造出预期结果;至于那个外部单元能不能真的按给定输入返回预期结果,那是那个外部单元对应的单元测试应该负责的事。

    以上述示例程序 to_test.py 为例,本原则所关注的点体现在:

    • 对 call_other_funcs 编写单元测试时,我们仅关注上述除了 ret01 赋值和 ret02 赋值以外的代码是否被正确执行:

      • 我们仅关注 param0101..param0202 在输入后会不会执行 +100 的操作乃至生成对应的 param0101_changed...param0202_changed
      • 我们仅关注当 ret01 和 ret02 得到给定值后会不会执行那套 if-elif-elif-else 以生成我们想要的 (ret01, ret02)、(ret01, 0)、(0, ret02) 三者之一
    • 我们并不关注 ret01、ret02 是怎么由 param0101_changed...param0202_changed 通过 func_01、func_02 生成我们想要的值,那是对 func_01、func_02的测试应该负责的部分

      • 比如当我们想测第一个 if 时,我们就令 func_01 与 func_02 均返回 0;要测第 1 个 elif 时,我们就令 func_01 返回 1, 令 func_02 返回 0;遥测第 2 个 elif 时,我们就令 func_01 返回 0, 令 func_02 返回 1;要测 else 时,我们就令 func_01 与 func_02 均返回 0
      • 假设我们不伪造 func_01 和 func_02 的返回结果,而是直接让参数 param0101_changed...param0202_changed 传给 func_01 和 func_02、由它们来返回想要的结果如 (0, 0),我们实际上测试的不是上述的 call_other_funcs,而是在测下面的代码——多测了这两个函数的内部逻辑,换言之我们不仅仅在保证 1 个单元的正确性,而是在同时保证 3 个单元的正确性
    def call_other_funcs_new(param0101, param0102, param0201, param0202):
        # 对传入参数 param0x0y 等进行一些处理后,存到了 param0x0y_changed 中。例如
        param0x0y = param0x0y * 10
        
        ret01 = param0101_changed * param0102_changed  # 我们还测了 func_01 的内部处理逻辑
        ret02 = param0201_changed / param0202_changed  # 我们还测了 func_02 的内部处理逻辑
        
        # 对得到的 ret01, ret02 进行一些处理,得到最终返回值 ret。例如
        if ret01 and ret02:
           ret = (ret01, ret02)
        elif ret01 and (not ret02):
           ret = (ret01, 0)
        elif (not ret01) and ret02
           ret = (0, ret02)
        else:
           ret = (0, 0)
        return ret
    

    2. 测试要尽可能覆盖到所有语句

    仍然以上述函数为例,这里并不只是说测试需要覆盖所有的 if-else 分支,而更着重于强调对上一条原则的配合,即:尽管我们要伪造一些函数的值,但我们也要保证对应的函数调用了指定的参数。

    这是因为函数调用的参数可能依赖于调用函数前的代码,因此确保函数调用了指定参数,这种行为则确保了函数调用前那些(涉及到参数的)代码能够正确执行。例如上述代码中, 如果我们仅仅令 funcs_01 的返回值为某值,而没有去检查 funcs_01 到底调用的参数是不是我们预期的参数,那么实际上我们就并没有测试到像 param0101 = param0101 * 10 这样的代码。

    上述 2 点是基本原则。在此之上,根据我踩的坑,还有 1 点想补充:

    1. 测试要写得「傻」一点

    感谢首席测试小姐姐指出:测试不仅仅是为了保证功能正确,也是一份「代码阅读指南」——即当待测单元的行为不容易理解时,用户可以通过阅读这份代码对应的单元测试来理解程序行为。

    仍以上述对 to_test.py 的测试为例。(下面的 @mock.patch.object 与 mock_funcs_0x.return_value 配合,实现「伪造函数值」)

    坏样例:

    from unittest import mock
    import to_test
    
    class ToTestTestCase(unittest.TestCase):
    
        # ...其他测试函数...
       
        @mock.patch.object(to_test, 'funcs01')
        @mock.patch.object(to_test, 'funcs02')
        def test_call_other_funcs(self, mock_funcs_01, mock_func_02):
            funcs_ret_values = [
                {"funcs_01": 1, "funcs_02": 1},
                {"funcs_01": 1, "funcs_02": 0},
                {"funcs_01": 0, "funcs_02": 1},
                {"funcs_01": 0, "funcs_02": 0}
            ]
            for funcs_ret in funcs_ret_values:
                mock_funcs_01.return_value = funcs_ret["funcs_01"]
                mock_funcs_02.return_value = funcs_ret["funcs_02"]
                # 剩下的测试语句……
    

    好样例:

    from unittest import mock
    import to_test
    
    class ToTestTestCase(unittest.TestCase):
    
        # ...其他测试函数...
       
        @mock.patch.object(to_test, 'funcs01')
        @mock.patch.object(to_test, 'funcs02')
        def test_call_other_funcs_if(self, mock_funcs_01, mock_func_02):
            mock_funcs_01.return_value = 1
            mock_funcs_02.return_value = 1
            # 剩下的测试语句……
            
        @mock.patch.object(to_test, 'funcs01')
        @mock.patch.object(to_test, 'funcs02')
        def test_call_other_funcs_elif_1(self, mock_funcs_01, mock_func_02):
            mock_funcs_01.return_value = 1
            mock_funcs_02.return_value = 0
            # 剩下的测试语句……
            
    
        @mock.patch.object(to_test, 'funcs01')
        @mock.patch.object(to_test, 'funcs02')
        def test_call_other_funcs_elif_2(self, mock_funcs_01, mock_func_02):
            mock_funcs_01.return_value = 0
            mock_funcs_02.return_value = 1
            # 剩下的测试语句……
            
    
        @mock.patch.object(to_test, 'funcs01')
        @mock.patch.object(to_test, 'funcs02')
        def test_call_other_funcs_else(self, mock_funcs_01, mock_func_02):
            mock_funcs_01.return_value = 0
            mock_funcs_02.return_value = 0
            # 剩下的测试语句……
    

    第一种写法看起来更简洁,更「模块化」,对于单个函数的测试被封装到了同一个函数中;但首先要面临的问题就是:

    每次你在阅读测试函数是如何测试目标函数时,就要到上述的 list(如这里的 funcs_ret_values)中去查对应的函数到底被伪造成了什么值。

    乍一看,这在需要伪造的函数值较少时看起来还不是大问题;但当需要伪造的函数数量多起来时,上面的 list of dict 就会变得冗长无比,非常不容易阅读。

    更严重的是第二个问题:

    设想一种情景:你需要测试函数内部的 2 条不同逻辑 A 和 B,而这些不同逻辑会返回同样的值 a。那么,由于测试是在for循环中进行的,当你发现希望返回 a 的时候没有返回 a,你就不知道到底是在测逻辑 A 时出了错,还是在测逻辑 B 时出了错。于是你可能不得不非常仔细地去检查样例,手动再模拟一遍测试的过程,而且还要手动模拟 2 次:既要考虑模拟逻辑 A,也要考虑模拟逻辑 B。这加大了debug测试程序的难度。

    第二种写法虽然看上去更琐碎,但由于测试粒度比较小,上述这两个问题就都不复存在了。

    编写方法

    基本单元测试框架

    基本测试框架如下。先阅读,再解释:

    假设我们有一个类似刚才的 to_test.py 的待测函数 your_mod_name.py

    # 引入单元测试模块(unittest)和伪造模块(mock)
    import unittest
    from unittest import mock
    
    import your_mod_name
    
    class YourModNameTestCase(unittest.TestCase):
    
    
        def setUp(self):
            """
            若每个单元测试前都要用到同一组数据,则在这里编写,如
            
            self.var00 = val00
            self.var01 = val01
            
            不一定要有
            """
            pass
           
             
        def test_funcs_01(self):
            """
                测试 funcs_01
                
                所有要进行通过自动化测试框架的运行都以 test_ 开头
            """
            actual_output = your_mode_name.funcs_01(5, 2)
            self.assertEqual(
                10,
                actual_output
            )
    
    
        def test_funcs_02(self):
            """
                测试 funcs_02
            """
            actual_output = your_mode_name.funcs_02(9, 3)
            self.assertEqual(
                3,
                actual_output
            )
    
    
        @mock.patch.object(your_mod_name, 'funcs_02')
        @mock.patch.object(your_mod_name, 'funcs_01')
        def test_call_other_funcs_branch_01(self, mock_funcs_01, mock_funcs_02):
            mock_funcs_01.return_value = (5 * 10) * (2 * 10)
            mock_funcs_02.return_value = (9 * 10) / (3 * 10)
            actual_output = your_mode_name.funcs_01(5, 2, 9, 3)
            mock_funcs_01.assert_called_with(5 * 10, 2 * 10)
            mock_funcs_02.assert_called_with(9 * 10, 3 * 10)
            self.assertEqual(
                ((5 * 10) * (2 * 10), (9 * 10) / (3 * 10)),
                actual_output
            )
    
        # 对 call_other_funcs 其他分支的测试
        #
        # ……
        #
        # 对其他函数的测试
    
    

    以下解释上述代码

    1. 最简单的测试

    对于像 funcs_01 和 funcs_02 这样的函数写测试是非常简单的:我们只要

    1. 引入待测模块
    2. 把参数传给 待测模块.待测函数,取得返回值 actual_output 即为实际输出
    3. 使用 assertEuqal 之类以 assert 开头的函数来断言:实际行为与预期行为一致。通常至少用 assertEqual 来断言:实际输出(actual_output)与预期输出一致;在安排「实际输出」与「预期输出」在 assertEqual 中的参数顺序时,我的做法是:第一个参数是「预期输出」,第二个参数是「实际输出」;理由是「预期输出」的形状和长度是固定的,「实际输出」的形状和长度通常会有各种变化(当测试出错或函数没有执行预期行为时),把「实际输出」安排在后面,在调试测试函数时,我们的视线关注点是固定的。
    4. 对于比较复杂的函数如 call_other_funcs 的测试,可能还需要使用 assert_called_with 等方法断言函数调用的参数,见 https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.assert_called_with 。当需要断言多次调用时(例如同一个函数 funcs_01 被调用了 3 次,每次传入了不同参数),可以考虑使用 assert_has_calls,见 https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.assert_has_calls
    5. 其他的断言方法可见 https://docs.python.org/3/library/unittest.html#assert-methods 对于 mock 对象的断言方法可见 https://docs.python.org/3/library/unittest.mock.html#the-mock-class

    2. 如何伪造一个对象

    2.1 通用的 mock 框架

    一般来说,我们把「伪造」称为 mock,因为这就是 Python 中的伪造类的名字。

    可以通过装饰器 @mock.patch.object 的写法「制造」 mock 对象,见 https://docs.python.org/3/library/unittest.mock.html#patch-object

    以上述写法为例:

    @mock.patch.object(your_mod_name, 'funcs_02')
    @mock.patch.object(your_mod_name, 'funcs_01')
    def test_call_other_funcs_branch_01(self, mock_funcs_01, mock_funcs_02):
    

    上面这三句的意思是:

    把函数 your_mod_name.funcs_01 伪造成 mock_funcs_01,把 your_mod_name.funcs_02 伪造成 mock_funcs_02。这里的 mock_funcs_01 和 mock_funcs_02 的变量名没有特别的规定,这和一般的变量命名没有两样,也可以命名为 mock_weird_name_007, mock_I_dont_know_why_101 等奇奇怪怪的名字。现在这样命名只是为了方便理解。

    在「制造」出 mock 对象后,我们要给 mock 对象赋值,因为伪造传值才是我们的实际目标。以上述代码为例,该目标通过这两句来完成:

    mock_funcs_01.return_value = (5 * 10) * (2 * 10)
    mock_funcs_02.return_value = (9 * 10) / (3 * 10)
    

    注意:上面这两句,即所有伪造值的语句,都要在调用函数的语句前执行,即下面这句之前执行上述两句:

    actual_output = your_mode_name.funcs_01(5, 2, 9, 3)
    

    而 return_value 也可以在装饰器中就指定,例如:

    @mock.patch.object(your_mod_name, 'funcs_02', return_value=(9 * 10) / (3 * 10))
    @mock.patch.object(your_mod_name, 'funcs_01', return_value=(5 * 10) * (2 * 10))
    def test_call_other_funcs_branch_01(self, mock_funcs_01, mock_funcs_02):
        # 此后就不用再写 mock_funcs_01.return_value = (5 * 10) * (2 * 10) 这样的句子了
    

    除了通过装饰器来伪造,还可以通过上下文管理器(context manager)的方法来伪造 。同样是类似上述代码,可以写为:

    @mock.patch.object(your_mod_name, 'funcs_02')
    @mock.patch.object(your_mod_name, 'funcs_01')
    def test_call_other_funcs_branch_01(self, mock_funcs_01, mock_funcs_02):
        with mock.patch.object(your_mod_name, 'funcs_01', \
              return_value=(5 * 10) * (2 * 10)) as mock_funcs_01,
             mock.patch.object(your_mod_name, 'funcs_02', \
              return_value=(9 * 10) / (3 * 10)) as mock_funcs_02:
    
            actual_output = your_mode_name.funcs_01(5, 2, 9, 3)
            mock_funcs_01.assert_called_with(5 * 10, 2 * 10)
            mock_funcs_02.assert_called_with(9 * 10, 3 * 10)
            self.assertEqual(
                ((5 * 10) * (2 * 10), (9 * 10) / (3 * 10)),
                actual_output
            )
    

    2.2 特殊语句顺序

    所有的伪造值语句,必须要在调用 actual_output 的赋值语句(即实际调用目标函数)之前执行。

    所有的断言语句,包括 assertEqual 或 assert_called_with,必须要在调用 actual_output 的赋值语句(即实际调用目标函数)之后执行。这是因为需要断言的值、对象都要在函数执行后才会产生(需要断言的参数调用也是一种值,也要在函数执行后,才会在内存中留下「痕迹」,在执行之前,程序无法知道待测函数内部调用的其他函数到底调用了什么参数)。

    要注意:调用 assert_called_with 的一定是某个 mock 对象而非 self,这是与 assertEqual 最大的区别

    2.3 使用场景

    更具体地说,分为 2 种情况:(1)在诸多测试用例中,每个对象的返回值之间各自独立;(2)这些返回值之间符合某种函数关系

    根据这 2 种情况,对应的有 4 种伪造对象的写法。其中 2.3.1 对应第 (1) 种情况,2.3.2~2.3.4 对应第 (2) 种情况:

    2.3.1 一般外部模块、网络连接、数据库连接

    常见于伪造一般外部模块(自己或团队其他成员写的模块、开源库模块等)、数据库连接、网络连接的返回结果。都是类似上面对 funcs_01 的 mock 方法。给出 2 个在数据库连接和网络连接方面的 mock 示例代码:

    假设使用数据库连接的原始代码为:

    # your_mod_name.py
    
    import db_conn
    
    # some other function code ...
    
    def funcs_with_db_connection(params):
        # some code ...
        answers = db_conn.query(sql)  # sql 是指定的 SQL 语句字符串
        # some code to process answers
        # 假定 ret 是返回变量
        return ret
    

    则对应的 mock 代码为:

    # 数据库连接
    import your_mod_name
    
    @mock.patch.object(your_mod_name.db_conn, 'query', return_value=['000001', '000002'])
    def test_funcs_with_db_connection(self, mock_db_conn):
        # params 是参数
        # 在有了上面
        actual_output = your_mod_name.funcs_with_db_connection(params)
    

    网络连接则以 requests.get 为例:

    # your_mod_name.py
    
    import requests
    
    # some other function code ...
    
    def funcs_with_requests(params):
        # some code ...
        answers = requests.get('your_url_to_site')  # 从 your_url_to_site 获取信息
        # some code to process answers
        # 假定 ret 是返回变量
        return ret
    

    这时候返回的对象可能有多个属性如 status_coe 和 text,而且都要用上。那么此时对应的 mock 代码可写成:

    @mock.patch.object(your_mod_name.requests, 'get')
    def test_funcs_with_requests(self, mock_requests_get)
        mock_response = mock.Mock()
        mock_response.status_code = status
        mock_response.text = {'key01': 'val01', 'key02': 'val02'}
        mock_requests.return_value = mock_response
    

    2.3.2 伪造成一个指定函数:转发输入

    相当于把原始函数的输入值「转发」到指定函数上。例如原始代码为:

    # your_mod_name.py
    
    def format_answers(raw_string):
        # 处理 raw_string 非常复杂的处理逻辑
        # 假设处理完后保存到 good_string 中
        return good_string
        
    def funcs_with_format_answers(params):
        # 某些代码生成了原始答案字符串 raw_string
        answers = format_answers(raw_string)
        # some code ... 返回 ret
        return ret
    

    比如在测试函数中,我多次调用了该函数,但我不想对每次 mock 都指定一个值,那么可以这么做:

    import your_mod_name
    
    def mock_format_answers(input_string):
        return input_string
        
    @mock.patch.object(your_mod_name, 'format_answers', mock_format_answers):
    def test_funcs_with_format_answers(self):
        # some code for testing
    

    注意到当我们指定了转发目标后,实际上指定了「制造」的 mock 对象为我们设计好的函数,这样就不需要在函数头中再写 mock_format_answers 了(如果写,反而会报错)。

    2.3.3 从一个伪造类生成一个伪造对象

    设想一个情况:你在原始函数中调用了某个类 ClassA 生成了实例 instance_A,并在原始函数中使用了该类的多个方法。那么一次次 mock 这个函数的一个个方法,可能看着或写着繁琐。在这种情况下,我们就可以通过将对应的类转发到我们设计好的伪造类上,并在伪造类下定义需要 mock 的方法,从而 mock 一个类就相当于 mock 了和该类相关的所有方法。

    一个简单的例子是:某个函数内部调用了 time 这个类的 time() 和 sleep() 方法,例如:

    # your_mod_name.py
    
    import time
    
    def funcs_with_time(params):
        t_start = time.time()
        # some code ...
        t_cost = time.time() - t_start
        ret = []
        while t_cost <= 10:
           # do something
           time.sleep(0.5)
           t_cost = time.time() - t_start
           if t_cost > 5:
               ret.append('good')
               
         return ret
    

    在我们编写测试的时候,如果不将 time 这个类 mock 掉,那么程序的行为就不可预测:程序运行时是一个随机行为,我们如果不能「控制时间」,就不能保证测试函数在测试时能走到目标函数中的指定分支。

    我们只要在测试函数中这么写即可:

    import your_mod_name
    
    class MockTime(object):
        """                                                                                                                                                                                       
            用于 time 的 Mock 类
        """
           
        def __init__(self):
            self.time_count = -0.5  # 配合 time() 方法使 ts_start = 0
           
        def time(self):
            """
                每次对象被调用时会运行这里的代码。
            """
            if self.time_count == -0.5:
                self.time_count = 0.0
            return self.time_count
           
        def sleep(self, gap):
            """
                0.5 的增量保证能在 cost 超过 10 之前触发:销毁 Token,返回 URL
            """
            self.time_count += 0.5
            return
            
    mock_time_helper = MockTime()
    
    
    class YourModNameTestCase(unittest.TestCase):
    
        # some code for testing other functions ...
    
        @mock.patch.object(your_mod_name, 'time', mock_time_helper)
        def test_funcs_with_time(self):
            # 这样就可以将不可控的「程序运行时」变为可控的「计数器」
            # some code for testing ...
    

    2.3.4 如何伪造内置函数(built-in functions)的返回结果,如open(path_to_file).readlines()

    这其实是 2.3.3 这个情况的一个特例,但也是一个容易让人抓狂的点。比如有时候我们需要测试的函数内有一个 open 函数,在打开文件后还调用了 readlines() 方法。那么我们如何 mock 掉 open?或者 open 返回的对象类名是啥,我能不能去 mock 那个类对应的 readlines() 方法?其实这个问题的关键在于:内置函数的类是什么?

    答案是:builtins https://docs.python.org/3/library/builtins.html

    那么之后就能够像 2.3.3 一样去处理了。

    例如原始代码是

    # your_mod_name.py
    
    def funcs_with_open(params):
        # some code ...
        tokens = [line.strip('\n') for line in open('path_to_file').readlines()]
        return tokens
    

    对应的测试函数中可以这么写

    import unittest
    import builtins
    
    import your_mod_name
    
    
    class MockOpen(object):
        """  
            内置函数 open 的 mock 类
        """
        def __init__(self, data):
            assert isinstance(data, list), '请输入一个列表: {}'.format((data))
            self.data = data 
     
        def __call__(self, blabla):
            return self 
     
        def readlines(self):
            return self.data
    
    class YourModNameTestCase(unittest.TestCase):
    
        # some code to test other functions
        
        def test_funcs_with_open(self):
            with mock.patch('builtins.open', MockOpen(['test00\n', 'test01\n']))
                # some code to get params
                actual_output = your_mod_name.funcs_with_open(params)
                self.assertEqual(
                    ['test00', 'test01'],
                    actual_output
                )
    

    对基于网站框架搭建的网络应用进行单元测试

    和编写网络应用一样:编写网络应用(即涉及到网络通信的程序)的单元测试,与编写一般程序的单元测试基本一致,最大的差别就在于:网络应用和网络应用的单元测试一般需要额外关注:

    • 路由:要通过哪个URI进行数据操作,例如要从哪里去GET数据、把数据POST到哪里(一般必须处理)
    • 状态码:返回状态的设置和捕捉(不一定要捕捉或设置)

    这里就不做太多展开,在 2 个框架下各给出 1 个例子并做简要解释,更多情况请参阅对应的文档,或等我日后填坑(然后可能就不知不觉弃坑了?)

    [Flask]

    更多有关 Flask 的测试方法,见

    https://pythonhosted.org/Flask-Testing/
    http://flask.pocoo.org/docs/0.12/testing/

    原程序

    @app.route('/userapi/get_phone_number', methods=['POST'])
    def get_phone_number():
        # some code to get phone number
    

    对应测试程序中如何调用该程序:

    # request_data 是已经处理过的要 POST 的 JSON
    rv = self.client.post('/userapi/get_phone_number',  data=request_data)
    

    注意到这里的 '/userapi/get_phone_number' 就是调用路由

    [Tornado]

    更多有关 Tornado 的测试方法,见 http://www.tornadoweb.org/en/stable/testing.html

    原项目中由这个程序指定了程序路由:

    # server.py
    
    class Application(tornado.web.Application):  
    
        def __init__(self):
            handlers = [
                # some other handlers ...
                (r"/qaapi/qa", RESTfulAPIHandler)  # 已有 RESTfulAPIHandler.py 是对应应用
            ]
    
            tornado.web.Application.__init__(self, handlers)
            
            # some other codes ...
    
    

    要测试时:

    uri = '/qaapi/qa?userid={}&token={}'.format(userid, token)
    data = get_data()
    response = self.fetch(uri, method="POST", body=data)
    
    self.assertEqual(400, response.code)
    self.assertEqual('expected_output', response.buffer.getvalue())
    

    相关文章

      网友评论

        本文标题:指南与踩坑:Python 单元测试

        本文链接:https://www.haomeiwen.com/subject/xdbkmxtx.html