如何写单元测试(C++/Python)
单测是什么
单元测试是由开发者编写的,用于测试代码里面单个函数、方法或者类的行为。单元测试旨在验证这些单元是否按照预期工作,是否产生正确的输出,是否在各种输入情况下表现正常。写单元测试应该先于开发,好处就是能够明确行为的输入输出,明确预期行为,从而使得编写代码更加规范。
单元测试具有以下特点和相关概念:
- 独立性:单元测试应当独立,不应依赖于其他测试的结果;
- 自动化:无需手动干预即可自动化完成测试;
- 重复性:每次运行测试应当有相同的结果;
- 快速:测试应该在很段时间完成,以便在开发过程中频繁运行;
- 隔离:隔离于其他模块和外部依赖,以便专注于测试当前单元的功能;
- 断言:通常使用断言来检验代码输出是否符合预期;
- 测试框架:python有unittest和pytest等框架,c++有GoogleTest等框架。
为什么要写单测
- 错误检测和定位: 单元测试可以帮助在早期阶段捕获代码中的错误,使得修复成本更低。当添加新功能或修改代码时,单元测试可以帮助及时发现问题,并准确定位问题的根本原因。
- 保障代码质量: 编写单元测试可以确保代码符合预期功能,有助于保证代码的质量和正确性。
- 代码重构: 当需要重构代码时,单元测试可以提供一个安全网,确保重构不会破坏现有功能。如果单元测试仍然通过,可以更加自信地进行重构。
- 文档和示例: 单元测试实际上也是代码的使用文档和示例,展示了如何使用代码,以及预期的行为。
- 提高开发效率: 单元测试允许快速验证代码的功能,从而避免手动测试的繁琐。这可以大大提高开发速度。
- 持续集成和部署: 在持续集成和持续部署流程中,自动化的单元测试可以帮助您确保每次更改不会破坏主要功能,从而确保代码稳定。
- 减少回归测试: 单元测试有助于减少回归测试的工作量。当代码发生变化时,只需要运行受影响的单元测试,而不必运行整个测试套件。
- 洞察力: 在编写单元测试时,您可能会发现更多情况和边界条件,从而更全面地了解您的代码行为。
如何写单元测试——以C++和Python为例
C++单元测试(使用GoogleTest)
-
下载安装GoogleTest
git clone https://github.com/google/googletest.git -b v1.14.0 cd googletest # Main directory of the cloned repository. mkdir build # Create a directory to hold the build output. cd build cmake .. # Generate native build scripts for GoogleTest. make sudo make install # Install in /usr/local/ by default
测试GoogleTest有没有安装成功
新建一个test.cpp文件,然后输入下面的测试代码:
#include <gtest/gtest.h> int add(int a,int b){ return a+b; } TEST(TestCase,TestCaseAddTest){ EXPECT_EQ(add(2,4),4); } int main(){ testing::InitGoogleTest(); return RUN_ALL_TESTS(); }
编译可执行文件
g++ test.cpp -lgtest -lpthread -v -o test
运行可执行文件test
./test
运行结果如下:
[==========] Running 1 test from 1 test suite. [----------] Global test environment set-up. [----------] 1 test from testCase [ RUN ] TestCase.TestCaseAddTest hello.cpp:7: Failure Expected equality of these values: add(2,4) Which is: 6 4 [ FAILED ] TestCase.TestCaseAddTest (0 ms) [----------] 1 test from testCase (0 ms total) [----------] Global test environment tear-down [==========] 1 test from 1 test suite ran. (0 ms total) [ PASSED ] 0 tests. [ FAILED ] 1 test, listed below: [ FAILED ] TestCase.TestCaseAddTest 1 FAILED TEST
表明安装成功。
-
使用GoogleTest来进行单元测试
断言类似于函数调用的宏,可以对要测试的行为作出断言来测试类或者函数。断言失败的时候,GTest会打印断言的源文件和行号位置以及失败消息,可以自定义失败消息。如果想在发生故障时立刻终止函数,不继续测试运行,使用
ASSERT_*
;当不想终止测试函数而是想在测试中生成多个失败,可以使用EXPECT_*
。ASSERT_EQ(x.size(), y.size()) << "Vectors x and y are of unequal length"; for (int i = 0; i < x.size(); ++i) { EXPECT_EQ(x[i], y[i]) << "Vectors x and y differ at index " << i; } //列出一些常见的断言 //大多数宏都是EXPECT_和ASSERT_成对存在,可以根据需要选择合适的断言 //EXPECT_THAT(value,matcher) //ASSERT_THAT(value,matcher) //验证value是否和匹配器 //需要包含头文件 gmock/gmock.h #include <gmock/gmock.h> using ::testing::AllOf; using ::testing::Gt; using ::testing::Lt; using ::testing::MatchesRegex; using ::testing::StartsWith; EXPECT_THAT(value1, StartsWith("Hello"));//匹配以Hello蔚开头的字符串 EXPECT_THAT(value2, MatchesRegex("Line \\d+"));//与正则表达式匹配 ASSERT_THAT(value3, AllOf(Gt(5), Lt(10)));//value3介于5和10之间 //验证布尔条件 EXPECT_TRUE(condition); EXPECT_FALSE(condition); ASSERT_TRUE(condition); ASSERT_FALSE(condition); //验证两个值是否相同或者不同,当用于c字符串时应当使用STREQ,EQ会测试是否在同一内存位置(const char*) //string对象可以使用 EXPECT_EQ(str1,str2); EXPECT_NE(str1,str2); ASSERT_EQ(str1,str2); ASSERT_NE(str1,str2); //主要用于验证C字符串是否相同 EXPECT_STREQ(str1,str2); ASSERT_STREQ(str1,str2); EXPECT_STRNE(str1,str2); ASSERT_STRNE(str1,str2); EXPECT_STRCASEEQ(str1,str2);//忽略大小写 ASSERT_STRCASEEQ(str1,str2); EXPECT_STRCASENE(str1,str2) ASSERT_STRCASENE(str1,str2) //验证两个值大小 EXPECT_LT(value1,value2);//value1<value2 ASSERT_LT(value1,value2); EXPECT_LE(value1,value2);//value1<=value2 ASSERT_LE(value1,value2); EXPECT_GT(value1,value2);//value1>value2 ASSERT_GT(value1,value2); EXPECT_GE(value1,value2);//value1>=value2 ASSERT_GE(value1,value2); //浮点数的比较 //由于存在误差,浮点数可能不会完全匹配, EXPECT_FLOAT_EQ(val1,val2);//是否近似相等,相差在4个ULP以内 ASSERT_FLOAT_EQ(val1,val2); EXPECT_DOUBLE_EQ(val1,val2);//是否近似相等,相差在4个ULP以内 ASSERT_DOUBLE_EQ(val1,val2); EXPECT_NEAR(val1,val2,abs_error);//两个值的误差范围不超过abs_error ASSERT_NEAR(val1,val2,abs_error); //谓词断言 //将给定值作为参数传递时pred是否返回true EXPECT_PRED1(pred,val1); EXPECT_PRED2(pred,val1,val2); ...... ASSERT_PRED1(pred,val1); ASSERT_PRED2(pred,val1,val2); ......
创建测试:
-
使用
TEST()
宏定义和命名测试函数 -
使用各种断言来测试函数
//第一个参数是测试套件名,第二个是套件中测试的名称,命名不要带"_"。 //测试全名由测试套件+测试名称组成。不同的测试套件里面的测试名称可以相同 TEST(TestSuiteName, TestName) { ... test body ... } //例子 int Factorial(int n); // 待测试函数 // 测试1 TEST(FactorialTest, HandlesZeroInput) { EXPECT_EQ(Factorial(0), 1); } // 测试2 TEST(FactorialTest, HandlesPositiveInput) { EXPECT_EQ(Factorial(1), 1); EXPECT_EQ(Factorial(2), 2); EXPECT_EQ(Factorial(3), 6); EXPECT_EQ(Factorial(8), 40320); }
-
测试夹具:
-
它是一种用于在多个测试用例之间共享设置和清理代码的机制。测试夹具提供了一种将测试用例放置在一致环境中执行的方式,有助于确保测试的可靠性和可重复性。
-
创建测试夹具:
- 派生自
::testing::Test
,并且从protected
开始 - 在类中声明使用的所有对象
- 编写默认构造函数/SetUp()函数来申请资源准备对象,析构函数/TearDown()函数来释放资源
- 使用
TEST_F()
//待测试的队列 template <typename E> class Queue { public: Queue(); void Enqueue(const E& element); E* Dequeue(); size_t size() const; ... }; //定义的fixture类,继承自::testing::Test //使用SetUp来做前序工作,使用TearDown来处理后续工作 class QueueTest : public ::testing::Test { protected: void SetUp() override { // q0_ remains empty q1_.Enqueue(1); q2_.Enqueue(2); q2_.Enqueue(3); } // void TearDown() override {} Queue<int> q0_; Queue<int> q1_; Queue<int> q2_; }; //测试部分 TEST_F(QueueTest, IsEmptyInitially) { EXPECT_EQ(q0_.size(), 0); } TEST_F(QueueTest, DequeueWorks) { int* n = q0_.Dequeue(); EXPECT_EQ(n, nullptr); n = q1_.Dequeue(); ASSERT_NE(n, nullptr); EXPECT_EQ(*n, 1); EXPECT_EQ(q1_.size(), 0); delete n; n = q2_.Dequeue(); ASSERT_NE(n, nullptr); EXPECT_EQ(*n, 2); EXPECT_EQ(q2_.size(), 1); delete n; }
运行测试。
int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }
- 派生自
Python单元测试(使用pytest)
首先安装pytest单元测试框架
pip install pytest
之后测试查看pytest版本
pytest --version
一个简单的测试用例test_add.py
:
def my_add(a,b):
return a+b
def test_my_add():
assert my_add(3,4) == 7
assert my_add(3,4) == 3
pytest的运行规则就是查找当前目录以及子目录所有test_*.py
和*_test.py
文件,然后执行文件中test开头的函数并执行。
之后在terminl打开当前文件夹,键入命令pytest
,得到结果:
$ pytest
=== ================================= test session starts ===========================================================
platform linux -- Python 3.10.12, pytest-7.4.0, pluggy-1.2.0
rootdir: /home/nio/code/pythonTest
collected 1 item
test_my_add.py F [100%]
========================================== FAILURES=================================================================
________________________________________________________________________________________________ test_my_add _________________________________________________________________________________________________
def test_my_add():
assert my_add(3,4) == 7
> assert my_add(3,4) == 3
E assert 7 == 3
E + where 7 = my_add(3, 4)
test_my_add.py:6: AssertionError
============================================ short test summary info ================================================
FAILED test_my_add.py::test_my_add - assert 7 == 3
======================================================== 1 failed in 0.01s ==========================================