python 单元测试 mock patch

随着软件开发规模变大,单元测试对确保代码可靠性十分必要。本文以Python为例,介绍了简单的单元测试例子,包括待测试模块、编写加法测试单元等,还阐述了测试命令的可选项。此外,重点讲解了mock模拟测试,如屏蔽外部干扰、指定返回值及side_effect拓展用法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

单元测试的概念

随着软件开发规模的不断变大,代码体积的膨胀,路径覆盖,以及整体测试会变得十分困难
所以错误定位,以及 代码单元可靠 十分必要。

单元测试,简单讲,就是对 一部分代码编写测试用例,以确保被测试的代码单元代码的可靠性。

下边举一个很简单的单元测试例子

待测试单元(模块)

my_math.py

很简单的模块,他只包含了,加法,乘法,阶乘

#!/usr/bin/python
# -*- coding:utf-8 -*-
'''
@File    :   my_math.py
@Time    :   2020/12/24 16:38:24
@Author  :   lmk
@Version :   1.0
'''


def my_add(a, b):
    return a+b


def my_multiply(a, b):
    return a*b


def my_factorial(a, b):
    return a**b


if __name__ == '__main__':
    pass


编写一个 加法 测试单元


#!/usr/bin/python
# -*- coding:utf-8 -*-
'''
@File    :   test_my_math.py
@Time    :   2020/12/24 16:26:49
@Author  :   lmk
@Version :   1.0
'''


from testtools import TestCase
import my_math


class Test_my_math(TestCase):
    def setUp(self):
        super(Test_my_math, self).setUp()
        print("this is set_up")

    def test_my_add_case_1(self):
        self.assertEqual(6, my_math.my_add(1, 5))

    def test_my_add_case_2(self):
        self.assertEqual(7, my_math.my_add(1, 5))


if __name__ == '__main__':
    pass


执行这个测试单元


python3 -m testtools.run test_my_math.py

RuntimeWarning: 'testtools.run' found in sys.modules after import of package 'testtools', but prior to execution of 'testtools.run'; this may result in unpredictable behaviour
  warn(RuntimeWarning(msg))
Tests running...
this is set_up
this is set_up
======================================================================
FAIL: test_my_math.Test_my_math.test_my_add_case_2
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/lmk/codes/learn_testtools/test_my_math.py", line 25, in test_my_add_case_2
    self.assertEqual(7, my_math.my_add(1, 5))
  File "/home/lmk/.local/lib/python3.6/site-packages/testtools/testcase.py", line 415, in assertEqual
    self.assertThat(observed, matcher, message)
  File "/home/lmk/.local/lib/python3.6/site-packages/testtools/testcase.py", line 502, in assertThat
    raise mismatch_error
testtools.matchers._impl.MismatchError: 7 != 6

Ran 2 tests in 0.002s
FAILED (failures=1)

Ran 2 tests in 0.002s
FAILED (failures=1)

输出结果表明 两项 测试 有一项失败了
assertEqual 断言函数,接受三个参数

def assertEqual(self, expected, observed, message=’’):

expected 是 逻辑上期待 的正确结果
observed 是 代码实际 的运行结果
message 是 额外的 错误信息

测试命令的其他可选项

python3 -m testtools.run -h

  -h, --help            show this help message and exit
  -v, --verbose         Verbose output
  -q, --quiet           Quiet output
  --locals              Show local variables in tracebacks
  -f, --failfast        Stop on first fail or error
  -c, --catch           Catch ctrl-C and display results so far
  -b, --buffer          Buffer stdout and stderr during tests
  -l, --list            List tests rather than executing them
  --load-list LOAD_LIST
                        Specifies a file containing test ids, only tests
                        matching those ids are executed
  -s START, --start-directory START
                        Directory to start discovery ('.' default)
  -p PATTERN, --pattern PATTERN
                        Pattern to match tests ('test*.py' default)
  -t TOP, --top-level-directory TOP
                        Top level directory of project (defaults to start
                        directory)

进阶1 - mock 模拟测试

  1. 在开发工作中,难免有一些函数,项目是 并行开发的。
  2. 这个func 依赖其他项目的 输出。

那么此时想要 进行 单元测试。屏蔽外部干扰。

如果不进行模拟而是进行,真正的依赖,那么很有可能导致错误的产生。

  1. 上游错误,导致下游所有测试失败。
  2. 多个错误发生了叠加,使得错误更难被追踪。

所以我们尽量采用 mock 来屏蔽外部的干扰,让程序得到预期的输入

在 Test_my_math 添加 模拟 测试函数指定返回 固定值

添加一个@patch 装饰器 来 模拟函数返回值



@patch("my_math.my_add", new=Mock(return_value=7))
def test_my_add_case_3(self):
    res = my_math.my_add(1, 5)
    self.assertEqual(7, res)

python3 -m testtools.run test_my_math.py 

Ran 2 tests in 0.002s
OK

指定 模拟函数 多次调用的 不同的返回值


from cinder import test
from cinder.volume import ceph_api
from cinder.tests.unit.volume.volume_constant import val
import mock


class TestCephApi(test.TestCase):
    def setUp(self):
        super(TestCephApi, self).setUp()
        self.api = ceph_api.API()
        self.context = None

    # return list when then input is right
    @mock.patch("cinder.volume.ceph_api.API._handle_ceph_cmd", mock.Mock(side_effect = [[_, val.RIGHT_POOL_INFO_OUTBUF, _], [_, val.RIGHT_POOL_DF_OUTBUF, _]]))
    def test_get_all_pools_with_right_input(self):
        res = self.api.get_all_pools(self.context)
        self.assertIsInstance(res, list)

    # return  AttributeError when input is err
    @mock.patch("cinder.volume.ceph_api.API._handle_ceph_cmd", mock.Mock(side_effect = [[_, val.ERR_POOL_INFO_OUTBUF, _], [_, val.ERR_RIGHT_POOL_DF_OUTBUF, _]]))
    def test_get_all_pools_with_err_input(self):
        self.assertRaises(AttributeError, self.api.get_all_pools, self.context)

    # return dict when then input is right
    @mock.patch("cinder.volume.ceph_api.API.get_all_mons", mock.Mock(side_effect = [""]))
    @mock.patch("cinder.volume.ceph_api.API.get_all_pools", mock.Mock(side_effect = [""]))
    @mock.patch("cinder.volume.ceph_api.API._handle_ceph_cmd", mock.Mock(side_effect = [[_, val.RIGHT_POOL_STATUS_OUTBUF, _]]))
    def test_get_all_status_with_right_input(self):
        res = self.api.get_all_status(self.context)
        self.assertIsInstance(res, dict)

    # return keyerr when input is err
    @mock.patch("cinder.volume.ceph_api.API.get_all_mons", mock.Mock(side_effect = [""]))
    @mock.patch("cinder.volume.ceph_api.API.get_all_pools", mock.Mock(side_effect = [""]))
    @mock.patch("cinder.volume.ceph_api.API._handle_ceph_cmd", mock.Mock(side_effect = [[_, val.ERR_POOL_STATUS_OUTBUF, _]]))
    def test_get_all_status_with_err_input(self):
        self.assertRaises(KeyError, self.api.get_all_status, self.context)

side_effect 拓展用法,采用函数替换,patch 目标函数

如果希望更加动态的 指定 mock 返回值。
可以考虑采用一个 简单函数 来 mock 原有函数。


mock = Mock(return_value=3)
def side_effect_func(*args, **kwargs):
    return DEFAULT

mock.side_effect = side_effect_func
mock()

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yuemake999

请我喝茶呗

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值