当前位置: 首页 > 科技观察

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

时间:2023-03-20 00:47:54 科技观察

我们知道,在软件工程中,单元测试是保证软件质量的重要手段之一。对于一个优秀的代码,单元测试的代码量往往会超过被测试的代码本身。一个理想化的开发团队可能会花三分之二的时间写测试,剩下三分之一的时间写业务代码。如果你从项目一开始就编写单元测试,那么你应该轻松??愉快地编写它们,因为单元测试将使你的代码本身成为可测试的代码。但是如果你接手的是一个几十万行代码的大项目,在这些代码中加入单元测试会让你知道什么叫不知所措。你会发现有一些函数让你想知道如何编写测试代码。但是不能随便修改代码的结构,谁知道会引起什么样的连锁反应呢?我们来看一个例子:我想测试下当check_data_dup在business_code中返回True或False时代码的逻辑。也就是说,我只关心第18-27行的逻辑。这个时候不用管MySQL和Redis。但是每次测试都要从他们那里读取数据,这会导致测试代码依赖于外部环境。如果MySQL或Redis挂了,测试代码将无法运行。而且,即使Redis和MySQL都没有故障,你怎么知道你的data_id和pk在数据库中对应的是什么数据呢?为了分别到具体的分支,还需要检测数据库中具体数据的id。万一是测试环境,别人修改了里面的数据,你的测试也有可能失败。如果直接用Pytest写测试用例,代码是这样的:大家可以看到,我运行Pytest后,一个成功,一个失败。这里我模拟数据库没有数据的情况让check_data_dup返回True逻辑。我是否必须去数据库构造特定的数据才能继续进行单元测试?这只是一个单元测试,而不是集成测试。为了解决这个问题,我们可以使用mock模块。这是Python自带的一个模块,可以动态替换函数。它的写法很简单:我们只需要使用@mock.patch装饰器来装饰测试函数即可。这个装饰器接收两个参数,第一个参数是mocked函数的路径,用点隔开;第二个参数是你希望它返回的值。从上图可以看出,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了,如下图。在check_data_dup函数的逻辑中,如果data参数包含字符x且user_id为偶数,则返回True,否则返回False。通过分别mock读取数据和设置不同返回值这两个函数,我们就可以满足check_data_dup返回不同值的条件。mock.path有个小坑,一定要注意。我们看一下下面的文件结构:read_data_from_redis和read_data_from_mysql这两个函数分布在不同的文件中。它们被导入并在runner.py中使用。在test_runner.py中,我们使用@mock.patch对这两个函数定义的路径进行patch,以替换它们。但是替换之后,运行Pytest,你会发现这两个函数运行正常。换句话说,我们的替换失败了。之所以会出现这种情况,是因为我们要修补的不是这两个函数定义的地方,而是使用它们的地方。在runner.py中,我们使用了如下两条语句:frommysql_util.SqlUtilimportread_data_from_mysqlfromcontroller.lib.redis.RedisUtilimportread_data_from_redis导入了这两个函数,我们在runner.py中也使用了这两个函数。所以@mock.patch的第一个参数应该还是runner.read_data_from_redis和runner.read_data_from_mysql。正确的做法如下图所示:mock.patch还有更高级的用法,比如替换类,替换实例方法等等。它可以在unittest.mock中找到。从Python3.3开始,官方自带了unittest.mock,和直接导入mock效果一样。