如何写单元测试(C++/Python)

如何写单元测试(C++/Python)

单测是什么

单元测试是由开发者编写的,用于测试代码里面单个函数、方法或者类的行为。单元测试旨在验证这些单元是否按照预期工作,是否产生正确的输出,是否在各种输入情况下表现正常。写单元测试应该先于开发,好处就是能够明确行为的输入输出,明确预期行为,从而使得编写代码更加规范。

单元测试具有以下特点和相关概念:

  1. 独立性:单元测试应当独立,不应依赖于其他测试的结果;
  2. 自动化:无需手动干预即可自动化完成测试;
  3. 重复性:每次运行测试应当有相同的结果;
  4. 快速:测试应该在很段时间完成,以便在开发过程中频繁运行;
  5. 隔离:隔离于其他模块和外部依赖,以便专注于测试当前单元的功能;
  6. 断言:通常使用断言来检验代码输出是否符合预期;
  7. 测试框架:python有unittest和pytest等框架,c++有GoogleTest等框架。

为什么要写单测

  1. 错误检测和定位: 单元测试可以帮助在早期阶段捕获代码中的错误,使得修复成本更低。当添加新功能或修改代码时,单元测试可以帮助及时发现问题,并准确定位问题的根本原因。
  2. 保障代码质量: 编写单元测试可以确保代码符合预期功能,有助于保证代码的质量和正确性。
  3. 代码重构: 当需要重构代码时,单元测试可以提供一个安全网,确保重构不会破坏现有功能。如果单元测试仍然通过,可以更加自信地进行重构。
  4. 文档和示例: 单元测试实际上也是代码的使用文档和示例,展示了如何使用代码,以及预期的行为。
  5. 提高开发效率: 单元测试允许快速验证代码的功能,从而避免手动测试的繁琐。这可以大大提高开发速度。
  6. 持续集成和部署: 在持续集成和持续部署流程中,自动化的单元测试可以帮助您确保每次更改不会破坏主要功能,从而确保代码稳定。
  7. 减少回归测试: 单元测试有助于减少回归测试的工作量。当代码发生变化时,只需要运行受影响的单元测试,而不必运行整个测试套件。
  8. 洞察力: 在编写单元测试时,您可能会发现更多情况和边界条件,从而更全面地了解您的代码行为。

如何写单元测试——以C++和Python为例

C++单元测试(使用GoogleTest)

  1. 下载安装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
    

    表明安装成功。

  2. 使用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);
    ......
    
    

    创建测试:

    1. 使用TEST()宏定义和命名测试函数

    2. 使用各种断言来测试函数

      //第一个参数是测试套件名,第二个是套件中测试的名称,命名不要带"_"。
      //测试全名由测试套件+测试名称组成。不同的测试套件里面的测试名称可以相同
      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);
      }
      

测试夹具:

  1. 它是一种用于在多个测试用例之间共享设置和清理代码的机制。测试夹具提供了一种将测试用例放置在一致环境中执行的方式,有助于确保测试的可靠性和可重复性。

  2. 创建测试夹具:

    • 派生自::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 ==========================================

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值