一日一技:如何正确为历史遗留代码补充单元测试?

网站建设4年前发布
35 0 0

我们知道,在软件工程中,单元测试是保证软件质量的重要手段之一。一个优秀的代码,单元测试的代码量,经常会超过被测试的代码本身。一个理想化的开发团队,可能有三分之二的时间是在写测试,剩下的三分之一时间才是写业务代码。,如果你的项目是从一开始就写单元测试,那么你写起来应该轻松又愉快,因为单元测试会促使你的代码自身变成可测试的代码。,但如果你接手了一个大项目,里面已经有几十万行代码了,那么给这些代码补单元测试会让你知道什么叫做痛不欲生。你会发现有一些函数,它让你不知道怎么写测试代码。但你又不能随便修改代码的结构,谁知道会引起什么连锁反应?,我们来看一个例子:,20230306120544c7d56b072a0f776c8435517cea0ae1be5dd046263,我想测试的是business_code​里面,check_data_dup分别返回True或者False的时候,下面代码的逻辑。也就是说,我只关心第18-27行的逻辑。这个时候不关心MySQL和Redis。但是每次测试都要从他们里面读取数据,这样就会导致测试代码依赖外部环境。如果MySQL或者Redis挂了,那么测试代码就会运行失败。,而且,就算Redis和MySQL没有故障,你怎么知道你的data_id和pk,在数据库中对应的是什么数据?为了分别走到特定的分支,你还需要去检测数据库中特定数据的id。万一是测试环境,别人修改了里面的数据,你的测试也可能会挂掉。,如果直接使用Pytest来写测试案例,代码是这样的:,20230306101017e56da1a895626f825bc0741924903f9d60a329757,可以看到,我运行Pytest以后,成功了一个,失败了一个。这里我模拟出数据库中没有数据能够让check_data_dup​走到返回True逻辑的情况。,难道为了让单元测试进行下去,我还要去数据库构造一条特定的数据?这只是单元测试,又不是集成测试。,为了解决这个问题,我们就可以使用mock模块。这是Python自带的一个模块,可以动态替换函数。,它的写法非常简单:,2023030610101867d91bf5872a26a1a7e5067075d4b10b4a629a984,我们只需要使用@mock.patch装饰器,装饰测试函数就可以了。这个装饰器接收两个参数,第一个参数是被模拟的函数的路径,以点分割;第二个参数是你想让它返回的值。,从上图可以看到,test_runner.py​运行以后,原本在read_data_from_redis和read_data_from_mysql中打印的两段文字都没有打印,说明这两个函数已经被动态替换了,他们内部的代码不会运行。只会直接返回我们预设的这个返回值。这样一来就跟数据库解耦了。,注意,在上图中,由于我们已经mock了check_data_dup​,因此read_data_from_redis和read_data_from_mysql​两个函数随便返回什么值都可以。如果你想顺带也测试一下check_data_dup,那么可以不mock它,如下图所示。,202303061010187331118855e61b5f5db2291d61efcd2c0af191643,在check_data_dup​函数的逻辑中,如果data​参数含有字符x​,并且user_id​是偶数,就返回True​,否则返回False​。我们通过mock两个读数据的函数,分别设置不同的返回值,就能满足让check_data_dup返回不同值的条件。,mock.path有一个小坑,一定要注意。我们来看看下面这个文件结构:,2023030610101948023e052955d72434554939500a3074e97ef1260,read_data_from_redis和read_data_from_mysql​两个函数分布在了不同的文件里面。在runner.py​中导入并使用了他们。test_runner.py​中,我们使用@mock.patch对这两个函数定义的路径打补丁进行替换。可是替换了以后,运行Pytest,会发现这两个函数竟然正常运行了。也就是说我们的替换失败了。,之所以会出现这种情况,是因为我们要打补丁的并不是这两个函数定义的地方,而是使用的地方。我们在runner.py中,分别使用如下两个语句:,导入了这两个函数,我们也是在runner.py​中使用他们的。因此,@mock.patch​的第一个参数,依然应该是runner.read_data_from_redis和runner.read_data_from_mysql。,正确的做法如下图所示:,2023030610123131592b884bb348e285c943e21fb95540fee773467,mock.patch​还有更多高级用法,例如替换类,替换实例方法等等。可以在unittest.mock中找到他。从Python 3.3开始,官方自带了unittest.mock​,它跟直接import mock的效果是一样的。,

© 版权声明

相关文章