蚊章汇 - 单元测试
type: channel
link: https://linuxcn.bwsl.wang/tag/%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95/
items: 使用 GoogleTest 和 CTest 进行单元测试
items: Python 单元测试：assertTrue 是真值，assertFalse 是假值
items: Mock 在 Python 单元测试中的使用

使用 GoogleTest 和 CTest 进行单元测试
type: item
date: 2023-08-02T19:15:50+08:00
description: 进行单元测试可以提高代码质量，并且它不会打断你的工作流。本文是 使用 CMake 和 VSCodium 设置一个构建系统 的后续文章。在上一篇文章中我介绍了基于 VSCodium 和 CMake 配置构建系统。本文我将介绍如何通过 GoogleTest 和 CTest 将单元测试集成到这个构建系统中。首先克隆 这个仓库，用 VSCodium 打开，切换到 devops_2 标签。你可以通过点击 main 分支符号（红框处），然后选择 devops_2 标签（黄框处）来进行切换：或者你可以通过命令行来切换：$ git checkout tags/devops_2 GoogleTestGoogleTest 是一个平台无关的开源 C++ 测试框架。单元测试是用来验证单个逻辑单元的行为的。尽管 GoogleTest 并不是专门用于单元测试的，我将用它对 Generator 库进行单元测试。在 GoogleTest 中，测试用例是通过断言宏来定义的。断言可能产生以下结果：成功: 测试通过。非致命失败: 测试失败，但测试继续。致命失败: 测试失败，且测试终止。致命断言和非致命断言通过不同的宏来区分：ASSERT_*： 致命断言，失败时终止。EXPECT_*： 非致命断言，失败时不终止。谷歌推荐使用 EXPECT_* 宏，因为当测试中包含多个的断言时，它允许继续执行。断言有两个参数：第一个参数是测试分组的名称，第二个参数是测试自己的名称。Generator 只定义了 generate(...) 函数，所以本文中所有的测试都属于同一个测试组：GeneratorTest。针对 generate(...) 函数的测试可以从 GeneratorTest.cpp 中找到。引用一致性检查generate(...) 函数有一个 std::stringstream 的引用作为输入参数，并且它也将这个引用作为返回值。第一个测试就是检查输入的引用和返回的引用是否一致。TEST(GeneratorTest, ReferenceCheck){ const int NumberOfElements = 10; std::stringstream buffer; EXPECT_EQ( std::addressof(buffer), std::addressof(Generator::generate(buffer, NumberOfElements)) ); } 在这个测试中我使用 std::addressof 来获取对象的地址，并用 EXPECT_EQ 来比较输入对象和返回对象是否是同一个。检查元素个数本测试检查作为输入的 std::stringstream 引用中的元素个数与输入参数中指定的个数是否相同。TEST(GeneratorTest, NumberOfElements){ const int NumberOfElements = 50; int nCalcNoElements = 0; std::stringstream buffer; Generator::generate(buffer, NumberOfElements); std::string s_no; while(std::getline(buffer, s_no, ' ')) { nCalcNoElements++; } EXPECT_EQ(nCalcNoElements, NumberOfElements); } 乱序重排本测试检查随机化引擎是否工作正常。如果连续调用两次 generate 函数，应该得到的是两个不同的结果。TEST(GeneratorTest, Shuffle){ const int NumberOfElements = 50; std::stringstream buffer_A; std::stringstream buffer_B; Generator::generate(buffer_A, NumberOfElements); Generator::generate(buffer_B, NumberOfElements); EXPECT_NE(buffer_A.str(), buffer_B.str()); } 求和校验与前面的测试相比，这是一个大体量的测试。它检查 1 到 n 的数值序列的和与乱序重排后的序列的和是否相等。 generate(...) 函数应该生成一个 1 到 n 的乱序的序列，这个序列的和应当是不变的。TEST(GeneratorTest, CheckSum){ const int NumberOfElements = 50; int nChecksum_in = 0; int nChecksum_out = 0; std::vector<in…

Python 单元测试：assertTrue 是真值，assertFalse 是假值
type: item
date: 2016-10-31T16:48:00+08:00
description: 在这篇文章中，我们将介绍单元测试的布尔断言方法 assertTrue 和 assertFalse 与身份断言 assertIs 之间的区别。定义下面是目前单元测试模块文档中关于 assertTrue 和 assertFalse 的说明，代码进行了高亮：assertTrue(expr, msg=None)assertFalse(expr, msg=None)测试该表达式是真值（或假值）。注：这等价于bool(expr) is True而不等价于expr is True（后一种情况请使用 assertIs(expr, True)）。Mozilla 开发者网络中定义 真值 如下：在一个布尔值的上下文环境中能变成“真”的值在 Python 中等价于：bool(expr) is True 这个和 assertTrue 的测试目的完全匹配。因此该文档中已经指出 assertTrue 返回真值，assertFalse 返回假值。这些断言方法从接受到的值构造出一个布尔值，然后判断它。同样文档中也建议我们根本不应该使用 assertTrue 和 assertFalse。在实践中怎么理解？我们使用一个非常简单的例子 - 一个名称为 always_true 的函数，它返回 True。我们为它写一些测试用例，然后改变代码，看看测试用例的表现。作为开始，我们先写两个测试用例。一个是“宽松的”：使用 assertTrue 来测试真值。另外一个是“严格的”：使用文档中建议的 assertIs 函数。import unittest from func import always_true class TestAlwaysTrue(unittest.TestCase): def test_assertTrue(self): """ always_true returns a truthy value """ result = always_true() self.assertTrue(result) def test_assertIs(self): """ always_true returns True """ result = always_true() self.assertIs(result, True) 下面是 func.py 中的非常简单的函数代码：def always_true(): """ I'm always True. Returns: bool: True """ return True 当你运行时，所有测试都通过了：always_true returns True ... ok always_true returns a truthy value ... ok ---------------------------------------------------------------------- Ran 2 tests in 0.004s OK 开心ing~现在，某个人将 always_true 函数改变成下面这样：def always_true(): """ I'm always True. Returns: bool: True """ return 'True' 它现在是用返回字符串 "True" 来替代之前反馈的 True （布尔值）。（当然，那个“某人”并没有更新文档 - 后面我们会增加难度。）这次结果并不如开心了：always_true returns True ... FAIL always_true returns a truthy value ... ok ====================================================================== FAIL: always_true returns True ---------------------------------------------------------------------- Traceback (most recent call last): File "/tmp/assertttt/test.py", line 22, in test_is_true self.assertIs(result, True) AssertionError: 'True' is not True ---------------------------------------------------------------------- Ran 2 tests in 0.004s FAILED (failures=1) 只有一个测试用例失败了！这意味着 assertTrue 给了我们一个 误判 （ false-po…

Mock 在 Python 单元测试中的使用
type: item
date: 2016-08-26T02:22:34+08:00
description: 本文讲述的是 Python 中 Mock 的使用。如何执行单元测试而不用考验你的耐心很多时候，我们编写的软件会直接与那些被标记为“垃圾”的服务交互。用外行人的话说：服务对我们的应用程序很重要，但是我们想要的是交互，而不是那些不想要的副作用，这里的“不想要”是在自动化测试运行的语境中说的。例如：我们正在写一个社交 app，并且想要测试一下 "发布到 Facebook" 的新功能，但是不想每次运行测试集的时候真的发布到 Facebook。Python 的 unittest 库包含了一个名为 unittest.mock 或者可以称之为依赖的子包，简称为 mock —— 其提供了极其强大和有用的方法，通过它们可以 模拟 （ mock ） 并去除那些我们不希望的副作用。注意：mock 最近被收录到了 Python 3.3 的标准库中；先前发布的版本必须通过 PyPI 下载 Mock 库。恐惧系统调用再举另一个例子，我们在接下来的部分都会用到它，这是就是系统调用。不难发现，这些系统调用都是主要的模拟对象：无论你是正在写一个可以弹出 CD 驱动器的脚本，还是一个用来删除 /tmp 下过期的缓存文件的 Web 服务，或者一个绑定到 TCP 端口的 socket 服务器，这些调用都是在你的单元测试上下文中不希望产生的副作用。作为一个开发者，你需要更关心你的库是否成功地调用了一个可以弹出 CD 的系统函数（使用了正确的参数等等），而不是切身经历 CD 托盘每次在测试执行的时候都打开了。（或者更糟糕的是，弹出了很多次，在一个单元测试运行期间多个测试都引用了弹出代码！）同样，保持单元测试的效率和性能意味着需要让如此多的“缓慢代码”远离自动测试，比如文件系统和网络访问。对于第一个例子来说，我们要从原始形式换成使用 mock 重构一个标准 Python 测试用例。我们会演示如何使用 mock 写一个测试用例，使我们的测试更加智能、快速，并展示更多关于我们软件的工作原理。一个简单的删除函数我们都有过需要从文件系统中一遍又一遍的删除文件的时候，因此，让我们在 Python 中写一个可以使我们的脚本更加轻易完成此功能的函数。#!/usr/bin/env python # -*- coding: utf-8 -*- import os def rm(filename): os.remove(filename) 很明显，我们的 rm 方法此时无法提供比 os.remove 方法更多的相关功能，但我们可以在这里添加更多的功能，使我们的基础代码逐步改善。让我们写一个传统的测试用例，即，没有使用 mock：#!/usr/bin/env python # -*- coding: utf-8 -*- from mymodule import rm import os.path import tempfile import unittest class RmTestCase(unittest.TestCase): tmpfilepath = os.path.join(tempfile.gettempdir(), "tmp-testfile") def setUp(self): with open(self.tmpfilepath, "wb") as f: f.write("Delete me!") def test_rm(self): # remove the file rm(self.tmpfilepath) # test that it was actually removed self.assertFalse(os.path.isfile(self.tmpfilepath), "Failed to remove the file.") 我们的测试用例相当简单，但是在它每次运行的时候，它都会创建一个临时文件并且随后删除。此外，我们没有办法测试我们的 rm 方法是否正确地将我们的参数向下传递给 os.remove 调用。我们可以基于以上的测试认为它做到了，但还有很多需要改进的地方。使用 Mock 重构让我们使用 mock 重构我们的测试用例：#!/usr/bin/env python # -*- coding: utf-8 -*- from mymodule import rm import mock import unittest class RmTestCase(unittest.TestCase): @mock.patch('mymodule.os') def test_rm(self, mock_os): rm("any path") # test that rm called os.remove with the right parameters mock_os.…