美文网首页
开发如何进行单元测试?

开发如何进行单元测试?

作者: DayDayUpppppp | 来源:发表于2021-07-24 22:10 被阅读0次

    知乎上面一个讨论很多的问题,如何进行单元测试?同样,知乎上各路科学家讲了各种思路。大概的思路是:

    1. 使用某个单测的库(比如gtest)
    2. 设计一些科学或伪科学的单元测试case
    3. mock各种接口
    4. 提高单元测试覆盖率,尽可能的覆盖各种分支和边界

    单元测试是通过在开发阶段进行白盒测试,把问题在开发阶段就发现,不至于传递到测试阶段,或者正式环境。通过单元测试,把一些琐碎且容易错的事情交给单测框架。让开发人员在修改代码之后能感到安心,踏实。只要跑一把单元测试,就能自动化验证程序逻辑的正确性,而无需在提交代码之前提心吊胆、担心会漏掉什么情况没有处理或者自己新加入的逻辑制造了bug。


    1. 使用gtest进行单元测试
    // 安装
    $ git clone [https://github.com/google/googletest.git](https://github.com/google/googletest.git)
    $ cd googletest
    $ mkdir build && cd build
    $ cmake ..
    $ make && sudo make install
    
    • 一个gtest简单的例子
    #include <gtest/gtest.h>
    
    int sum(int a, int b) {
        return a+b;
    }
    int multi(int a, int b) {
        return a*b;
    }
    
    // 测试集为 MyTest,测试案例为 Sum
    TEST(MytestSet, sum_func) {
        EXPECT_EQ(sum(10,20), 30);
        EXPECT_EQ(sum(0,0), 0);
    }
    TEST(MytestSet, multi_func) {
        EXPECT_EQ(multi(10,20), 200);
        EXPECT_EQ(multi(0,0), 0);
    }
    
    int main(int argc, char *argv[]) {
        ::testing::InitGoogleTest(&argc, argv);
        return RUN_ALL_TESTS();
    }
    
    • 编译完成之后,使用make test执行单元测试:

    gtest的框架还是比较简单的,使用了一个test的宏。交给用户定义单测的名称。定义如下:

    TEST(test_suite_name, test_case_name) 
    {
        // test body ...
    }
    

    在测试函数中,调用被测试的函数 对其返回的结果进行assert。gtest提供了EXPECT_和ASSERT_这两种风格的断言,区别在于ASSERT_在失败之后,会立刻返回,不会执行后面的逻辑。gtest在执行的时候,会提供一个比较直观的输出。可以看到那些case通过了,那些case没有通过。

    TEST(MyTest, Add) 
    {
        EXPECT_EQ(1 + 1, 2);
        ASSERT_EQ(1 + 1, 2);
    }
    
    gtest 提供了8 个ASSERT_断言分别是:
    ASSERT_TRUE()、ASSERT_FALSE()、ASSERT_EQ()、
    ASSERT_NE()、ASSERT_LT()、ASSERT_LE()、ASSERT_GT()和ASSERT_GE()
    
    EXPECT_的断言同样也有 8 个分别是:
    ASSERT_TRUE()、ASSERT_FALSE()、EXPECT_EQ()、
    EXPECT_NE()、EXPECT_LT()、EXPECT_LE()、EXPECT_GT()和EXPECT_GE()
    

    2. mock测试
    void do_something()
    {
         db_req();
         // todo
    }
    

    db_req()在某些场景里面是一个很重的操作,或者有上下文的状态依赖(比如依赖本地有db,db中的数据是正确的等)。在这些场景中使用单测的话,就需要伪造一个db_req(),这个就是mock。

    gmock是gtest中的一个模块,使用gmock可以实现对各种接口的mock。比如,下面的例子对一个get_from_rpc()的接口进行mock,指定一个调用的结果。

    // FooInterface.h
    #include <string>
    namespace seamless {
    class FooInterface {
    public:
            virtual ~FooInterface() {}
    public:
            virtual std::string get_from_rpc() = 0;
            std::string get_result()
            {
                std::string str = "reuslt is ";
                return str + get_from_rpc();
            }
    };
    }  
    
    // FooMock.h
    #include <gmock/gmock.h>
    #include <string>
    #include "FooInterface.h"
    
    namespace seamless {
    class MockFoo: public FooInterface 
    {
        public:
            MOCK_METHOD0(get_from_rpc, std::string());
    };
    }  
    
    // FooMain.cc
    TEST(mock_test, get_result)
    {  
        MockFoo mockFoo;
        string value = "Hello World!";
        EXPECT_CALL(mockFoo, get_from_rpc())
                .WillRepeatedly(Return(value));
        
        string result1 = mockFoo.get_from_rpc();
        string result2 = mockFoo.get_result();
    
        cout << "get_result() return : " << result1 << endl;
        cout << "get_result() return : " << result2 << endl;
        
        EXPECT_EQ(value, result1);
        EXPECT_EQ("reuslt is " + value, result2);
    }
    
    int main(int argc, char** argv) 
    {
        ::testing::InitGoogleMock(&argc, argv);
        return RUN_ALL_TESTS();
    }
    
    // g++ FooMain.cc -lpthread -lgtest -lgmock
    

    更多的使用方法和api可以参考 gmock手册gmock是通过纯虚函数 + 继承的方式实现mock的,对于非虚函数和普通函数gmock就不太好使了。

    代码地址:
    https://github.com/zhaozhengcoder/CoderNoteBook/tree/master/example_code/test/gmock

    3. 查看代码覆盖率coverage

    编译的时候,添加gcc --coverage参数

    # 编译
    gcc --coverage xxx_mian.c  module.c
    
    # 执行
    ./main
    
    # 生成某个文件的代码覆盖结果
    [linux@ src]~$gcov main.c 
    File 'main.c'
    Lines executed:100.00% of 5
    Creating 'main.c.gcov'
    
    File '/usr/include/c++/8/iostream'
    No executable lines
    Removing 'iostream.gcov'
    
    [linux@ src]~$gcov module_a.c 
    File 'module_a.c'
    Lines executed:80.00% of 5
    Creating 'module_a.c.gcov'
    
    File '/usr/include/c++/8/iostream'
    No executable lines
    Removing 'iostream.gcov'
    
    也可以具体查看文件覆盖的结果:

    测试代码打包地址:
    https://github.com/zhaozhengcoder/CoderNoteBook/tree/master/example_code/test/coverage


    小结

    写单测的原则:
    1)测试用例能验证函数的正确性;
    2)测试用例尽可能涵盖边界条件(例如遍历一个链表,头指针是空,只有一个节点,链表有N个节点,N是问题描述下允许的最大节点数等等);
    3)一些异常和错误处理(例如往一个函数里传入空指针,传入空串,这个函数能否打印一些log,返回错误码,实现加法的Add函数如何检测和处理溢出等等)最理想的情况下,应该尽量多写测试用例,以保证代码功能的正确性符合预期,具有良好的容错性。如果代码较复杂,条件分支较多,测试用例最好能覆盖所有的分支路径。
    4)单测持续集成,自动化运行


    参考:
    https://blog.jetbrains.com/rscpp/2015/09/01/unit-testing-google-test/
    https://paul.pub/gtest-and-coverage/
    https://blog.jetbrains.com/rscpp/2015/09/01/unit-testing-google-test/
    https://www.zhihu.com/question/27313846/answer/130954707
    http://senlinzhan.github.io/2017/10/08/gtest/
    gmock:
    https://github.com/google/googletest/blob/v1.8.x/googlemock/docs/CheatSheet.md
    https://zhuanlan.zhihu.com/p/393954237

    相关文章

      网友评论

          本文标题:开发如何进行单元测试?

          本文链接:https://www.haomeiwen.com/subject/awmvmltx.html