ARM学习(35)单元测试框架以及MinGW GCC覆盖率报告

单元测试框架以及MinGW GCC覆盖率报告

1、单元测试与覆盖率简介

随着代码越写越多,越来越需要注意自测的重要性,基本可以提前解决90%的问题,所以就来介绍一下单元测试,单元测试是否测试充分,需要进行评价,覆盖率就是单元测试是否充分的评估工具。

例如跑过单元测试后,所有的代码都覆盖了,代码覆盖率达到100%,虽然不敢保证100%没有问题,但是最起码心里有底,所有的代码都跑到了,且没有出现问题。

如果要记录代码跑过的路径,就需要使用数据变量记录,比如在函数开始以及判断的位置增加变量记录,等代码跑到的时候,就记录下来,代码没跑到的地方,变量就没有值,通过这种方法就可以统计测试代码的覆盖率情况。

当然代码量足够大的时候,人工去修改代码,记录代码执行过程,根本不可能,所以就需要编译器去支持,编译器插桩就是这样的一个过程,在每个函数以及if for语句的地方插桩,最后统计变量的数据,就可以形成覆盖率报告。

MingW的gcc gcov就支持这样的形式。

2、单元测试与覆盖率工具安装

笔者知道几种单元测试框架,比如cppunit,google test等,笔者这里介绍一下cppunit。下载地址。

cppunit测试框架

CppUnit 是一个用于 C++ 的单元测试框架,它是从 JUnit(Java 的单元测试框架)移植而来。CppUnit 允许开发者编写和运行自动化测试,确保程序的正确性,尤其是在开发过程中进行单元测试时非常有用。

  • 测试用例(Test Case): 测试的最小单位,每个测试用例通过测试一个特定的功能或代码段来验证是否正常工作。
  • 测试套件(Test Suite): 测试用例的集合。一个测试套件可以包含多个测试用例,用来运行一组相关的测试。
  • 断言(Assertion): 在测试用例中用来检查代码输出是否符合预期的语句。CppUnit 提供了多种断言方式,例如 assertTrue、assertFalse、assertEquals 等。
  • 运行器(Test Runner): 用于运行测试套件和测试用例的工具。CppUnit 提供了一个测试运行器来执行和报告结果。

下载完CPPUNIT之后,可以开始构建:

  • ./configure
  • ./make
  • 或者通过 VS2019进行构建

覆盖率通过lcov工具去生成。工具下载地址。

因为测试工程最后需要链接cppunit这个库,所以需要把cppunit链接进来,静态库和动态库的方式都可以。

需要注意一点,

  • VS工程编译的dll和gcc编译的dll不能混用,因为对于C++的命名修饰不同。
  • 出覆盖率的需要用gcc编译,VS编译的暂时不支持出覆盖率,gcc编译需要用到编译参数-fprofile-arcs -fprofile-generate=. -ftest-coverage
  • GCC编译完成后,目标文件会多生成两个文件,gcda文件和gcno文件,前者是数据文件,代码执行后才会生成,gcno是编译前就会生成,指示文件被插桩的一些格式等情况,通过这两种数据就可以覆盖率报告。

VS工程构建静态库.lib与动态库.dll



GCC 构建静态库cppunit .lib与动态库.dll

主要动态库以dll结尾,且需要链接选项增加-share -fpic,静态库以lib结尾。

makefile 复制代码
TARGET = ./lib/libcppunit.lib

CFLAGS = 
CFLAGS += -D _DEBUG -D _LIB -D WIN32 -I ./include
CFLAGS += -fprofile-arcs -fprofile-generate=. -ftest-coverage

LFLAGS = 
LFLAGS += -lstdc++

SOURCES = 
SRCDIRS = ./src/cppunit

SOURCES += TestSuite.cpp
SOURCES += Test.cpp
SOURCES += XmlOutputter.cpp
SOURCES += BeOsDynamicLibraryManager.cpp
SOURCES += TestSetUp.cpp
SOURCES += TestFailure.cpp
SOURCES += TestCase.cpp
SOURCES += TestPath.cpp
SOURCES += TextTestRunner.cpp
SOURCES += CompilerOutputter.cpp
SOURCES += Message.cpp
SOURCES += SourceLine.cpp
SOURCES += TestResultCollector.cpp
SOURCES += SynchronizedObject.cpp
SOURCES += PlugInManager.cpp
SOURCES += TestAssert.cpp
SOURCES += AdditionalMessage.cpp
SOURCES += TestRunner.cpp
SOURCES += UnixDynamicLibraryManager.cpp
SOURCES += DynamicLibraryManagerException.cpp
SOURCES += RepeatedTest.cpp
SOURCES += ShlDynamicLibraryManager.cpp
SOURCES += TestSuiteBuilderContext.cpp
SOURCES += TestComposite.cpp
SOURCES += DefaultProtector.cpp
SOURCES += Asserter.cpp
SOURCES += TestFactoryRegistry.cpp
SOURCES += TestPlugInDefaultImpl.cpp
SOURCES += StringTools.cpp
SOURCES += PlugInParameters.cpp
SOURCES += TextOutputter.cpp
SOURCES += DynamicLibraryManager.cpp
SOURCES += XmlDocument.cpp
SOURCES += BriefTestProgressListener.cpp
SOURCES += TextTestProgressListener.cpp
SOURCES += XmlElement.cpp
SOURCES += TestNamer.cpp
SOURCES += TestLeaf.cpp
SOURCES += TestDecorator.cpp
SOURCES += Protector.cpp
SOURCES += Win32DynamicLibraryManager.cpp
SOURCES += XmlOutputterHook.cpp
SOURCES += Exception.cpp
SOURCES += TestResult.cpp
SOURCES += TextTestResult.cpp
SOURCES += ProtectorChain.cpp
SOURCES += TypeInfoHelper.cpp
SOURCES += TestSuccessListener.cpp
SOURCES += TestCaseDecorator.cpp

OBJECTS += $(foreach source, $(SOURCES), obj/$(basename $(source)).o)
GCNOFILES += $(foreach source, $(SOURCES), obj/$(basename $(source)).gcno)

vpath %.c . $(SRCDIRS)
vpath %.cxx . $(SRCDIRS)
vpath %.cpp . $(SRCDIRS)

.PHONY: build
build : init-build $(TARGET)

cppunit.exe : $(OBJECTS)
	echo Link $@ ... 2>&1 | tee -a build.log 
	g++ -fprofile-arcs -fprofile-generate=. -ftest-coverage$(OBJECTS) $(LFLAGS) -o $@ 2>&1 | tee -a build.log 

./lib/libcppunit.a : $(OBJECTS)
	echo Archive $@ ... 2>&1 | tee -a build.log 
	ar rcs $@ $(OBJECTS) 2>&1 | tee -a build.log 

.PHONY: init-build
init-build:
	-mkdir -p obj
	echo 2>&1 | tee build.log 

.PHONY: clean
clean : 
	echo Clean ...
	rm -f $(TARGET)
	rm -f $(OBJECTS)
	rm -f $(GCNOFILES)

obj/%.o : %.c
	echo Compiling $<
	gcc -c $(CFLAGS) $< -o $@

obj/%.o : %.cxx
	echo Compiling $<
	gcc -c $(CFLAGS) $< -o $@

obj/%.o : %.cpp
	echo Compiling $<
	gcc -c $(CFLAGS) $< -o $@

动态库makefile

makefile 复制代码
TARGET = ./lib/libcppunit.dll

CFLAGS = 
CFLAGS += -D _DEBUG -D _LIB -D WIN32 -I ./include
CFLAGS += -fprofile-arcs -fprofile-generate=. -ftest-coverage

LFLAGS = 
LFLAGS += -lstdc++ -fpic -shared

SOURCES = 
SRCDIRS = ./src/cppunit

SOURCES += TestSuite.cpp
SOURCES += Test.cpp
SOURCES += XmlOutputter.cpp
SOURCES += BeOsDynamicLibraryManager.cpp
SOURCES += TestSetUp.cpp
SOURCES += TestFailure.cpp
SOURCES += TestCase.cpp
SOURCES += TestPath.cpp
SOURCES += TextTestRunner.cpp
SOURCES += CompilerOutputter.cpp
SOURCES += Message.cpp
SOURCES += SourceLine.cpp
SOURCES += TestResultCollector.cpp
SOURCES += SynchronizedObject.cpp
SOURCES += PlugInManager.cpp
SOURCES += TestAssert.cpp
SOURCES += AdditionalMessage.cpp
SOURCES += TestRunner.cpp
SOURCES += UnixDynamicLibraryManager.cpp
SOURCES += DynamicLibraryManagerException.cpp
SOURCES += RepeatedTest.cpp
SOURCES += ShlDynamicLibraryManager.cpp
SOURCES += TestSuiteBuilderContext.cpp
SOURCES += TestComposite.cpp
SOURCES += DefaultProtector.cpp
SOURCES += Asserter.cpp
SOURCES += TestFactoryRegistry.cpp
SOURCES += TestPlugInDefaultImpl.cpp
SOURCES += StringTools.cpp
SOURCES += PlugInParameters.cpp
SOURCES += TextOutputter.cpp
SOURCES += DynamicLibraryManager.cpp
SOURCES += XmlDocument.cpp
SOURCES += BriefTestProgressListener.cpp
SOURCES += TextTestProgressListener.cpp
SOURCES += XmlElement.cpp
SOURCES += TestNamer.cpp
SOURCES += TestLeaf.cpp
SOURCES += TestDecorator.cpp
SOURCES += Protector.cpp
SOURCES += Win32DynamicLibraryManager.cpp
SOURCES += XmlOutputterHook.cpp
SOURCES += Exception.cpp
SOURCES += TestResult.cpp
SOURCES += TextTestResult.cpp
SOURCES += ProtectorChain.cpp
SOURCES += TypeInfoHelper.cpp
SOURCES += TestSuccessListener.cpp
SOURCES += TestCaseDecorator.cpp

OBJECTS += $(foreach source, $(SOURCES), obj/$(basename $(source)).o)
GCNOFILES += $(foreach source, $(SOURCES), obj/$(basename $(source)).gcno)

vpath %.c . $(SRCDIRS)
vpath %.cxx . $(SRCDIRS)
vpath %.cpp . $(SRCDIRS)

.PHONY: build
build : init-build $(TARGET)

./lib/libcppunit.dll : $(OBJECTS)
	echo Link $@ ... 2>&1 | tee -a build.log 
	g++ -fprofile-arcs -fprofile-generate=. -ftest-coverage $(OBJECTS) $(LFLAGS) -o $@ 2>&1 | tee -a build.log 


.PHONY: init-build
init-build:
	-mkdir -p obj
	echo 2>&1 | tee build.log 

.PHONY: clean
clean : 
	echo Clean ...
	rm -f $(TARGET)
	rm -f $(OBJECTS)
	rm -f $(GCNOFILES)

obj/%.o : %.c
	echo Compiling $<
	gcc -c $(CFLAGS) $< -o $@

obj/%.o : %.cxx
	echo Compiling $<
	gcc -c $(CFLAGS) $< -o $@

obj/%.o : %.cpp
	echo Compiling $<
	gcc -c $(CFLAGS) $< -o $@

对于静态库lib和动态库dll的区分,可以通过dumpbin工具去区分。

3、单元测试代码编写

话不多说,直接来看例子。

框架代码:

c 复制代码
#include <cppunit/TestRunner.h>
#include <cppunit/TestResult.h>
#include <cppunit/TestResultCollector.h>
#include <cppunit/extensions/HelperMacros.h>
#include <cppunit/BriefTestProgressListener.h>
#include <cppunit/extensions/TestFactoryRegistry.h>
#include <cppunit/CompilerOutputter.h>
int main( int argc, char **argv )
{
  // Create the event manager and test controller
  CPPUNIT_NS::TestResult controller;

  // Add a listener that colllects test result
  CPPUNIT_NS::TestResultCollector result;
  controller.addListener( &result );       

  // Add a listener that print dots as test run.
  CPPUNIT_NS::BriefTestProgressListener progress;
  controller.addListener( &progress );     

  // Add the top suite to the test runner
#if 0
  CPPUNIT_NS::TestRunner runner;
  CPPUNIT_NS::TestFactoryRegistry factoryRegistry("factory registry");
  CPPUNIT_NS::TestSuiteFactory<Test2> factory;
  factoryRegistry.registerFactory(&factory);
  runner.addTest(factoryRegistry.makeTest());
  runner.run( controller );
#else
  CPPUNIT_NS::TestRunner runner;
  runner.addTest( CPPUNIT_NS::TestFactoryRegistry::getRegistry().makeTest() );
  runner.run( controller );
#endif

  // Print test in a compiler compatible format
  CPPUNIT_NS::CompilerOutputter outputter(&result, CPPUNIT_NS::stdCOut());
  outputter.write();
  
  CPPUNIT_NS::OFileStream fs("report.txt");
  CPPUNIT_NS::CompilerOutputter outputter_file(&result, fs);
  outputter_file.write();

  return result.wasSuccessful() ? 0 : 1;
}

被测试代码:LRU算法实现。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define LRU_CAPACITY   4
#define LRU_VALUE_MAX  256
#define ID_VALUE_MAX   32
typedef struct lru_node_struct
{
    int value;
    int update_count;
}lru_node_t;

typedef struct lru_struct
{
    unsigned int first_node_index;
    unsigned int node_num;
    unsigned int update_count_value;
    unsigned int hit_count;
    int *hash_map;
    lru_node_t *lru_node;
}lru_t;

lru_t lru_g;

void lru_init(lru_t* lru, int lru_capacity)
{
    int i;
    lru->lru_node = (lru_node_t *)malloc(lru_capacity*sizeof(lru_node_t));
    lru->hash_map = (int*)malloc(LRU_VALUE_MAX*sizeof(int));
    lru->node_num = lru_capacity;
    lru->first_node_index = 0;
    lru->update_count_value = 0;
    lru->hit_count = 0;

    for(i=0;i<lru->node_num;i++)
    {
        lru->lru_node[i].value = -1;
        lru->lru_node[i].update_count = -1;
    }

    for(i=0;i<LRU_VALUE_MAX;i++)
    {
        lru->hash_map[i] = -1;
    }
}

void lru_node_swap(lru_node_t* node1, lru_node_t* node2)
{
    lru_node_t temp_node;
    temp_node = *node1;
    *node1 = *node2;
    *node2 = temp_node;
}

void lru_node_sort(lru_t* lru)
{
    int i,j;
    for(i=0;i<(lru->node_num-1);i++)
    {
        for(j=i+1;j<(lru->node_num);j++)
        {
            if(lru->lru_node[i].update_count > lru->lru_node[j].update_count)
            {
                lru_node_swap(&lru->lru_node[i], &lru->lru_node[j]);
            }
        }
    }
}

void lru_print(lru_t* lru)
{
    int i;
    printf("LRU ->{ ");
    for(i=0;i<lru->node_num;i++)
    {
        printf(" %2d ", lru->lru_node[i].value);
    }
    printf("}\r\n");
}
signed char lru_value_is_exit(lru_t* lru, int value)
{
    int i;
    signed char res = -1;
    for(i=0;i<lru->node_num;i++)
    {
        if(lru->lru_node[i].value == value)
        {
            res = i;
        }
    }
    return res;
}

void lru_node_value_put(lru_t* lru, int value)
{   
    lru->update_count_value ++;
    signed char index = lru_value_is_exit(lru, value);
    if(index != -1)
    {
        lru->lru_node[index].update_count = lru->update_count_value;
        lru->hit_count ++;
    }
    else
    {
        lru->lru_node[lru->first_node_index].value = value;
        lru->lru_node[lru->first_node_index].update_count = lru->update_count_value;
    }

    lru_node_sort(lru);
    printf("enter node %2d ", value);
    lru_print(lru);
}

void lru_deinit(lru_t* lru)
{
    free(lru->lru_node);
    lru->lru_node = NULL;
    lru->first_node_index = 0;
    lru->node_num = 0;
    lru->update_count_value = 0;
}

cppunit单元测试代码:

c 复制代码
class Test3 : public CPPUNIT_NS::TestFixture
{
  CPPUNIT_TEST_SUITE(Test3);
  CPPUNIT_TEST(test3Case);
  CPPUNIT_TEST_SUITE_END();
 
public:
  void setUp(void) {}
  void tearDown(void) {}
 
protected:
  void test3Case(void) 
  {
    
    lru_init(&lru_g, LRU_CAPACITY);
    srand((unsigned int)time(NULL));  
    int i;
    for(i=0;i<1000;i++)
    {   
        int value = rand()%ID_VALUE_MAX;
        lru_node_value_put(&lru_g,value);
    }
    printf("hit_count=%d lru_g.update_count_value=%d hit ratio=%f\r\n",lru_g.hit_count, lru_g.update_count_value, (float)lru_g.hit_count/(lru_g.update_count_value+1));
  }
};
 
 CPPUNIT_TEST_SUITE_REGISTRATION(Test3);

makefile编译代码,需要注意链接的cppunit库文件。

  • -L 指明库的路径
  • -l 指明库的名称
  • win下面不以lib开头的dll文件也可以被链接,如果被链接的dll库编译时以lib开头,即使修改名字后,链接的还是以lib开头的;同理如果不以lib开头,即使修改成lib开头的,也没有用。

    如上图,如果链接的时cppunit.dll,即使重新连接从cppunit.dll修名为libcppunit.dll,也不行,还是会显示cppunit.dll,所以依次判断,连接的时候以dll里面内容为主,而不是名字。但是运行的时候,会以真实的名字为主。

    同理如下:猜测这样的原因时兼容Windows原来的dll连接,但是linux必须以lib开头。
c 复制代码
TARGET = tester.exe 

CFLAGS = -I ../cppunit-1.12.1/include/cppunit -I ../cppunit-1.12.1/include
CFLAGS += -D _UNICODE -D UNICODE
CFLAGS += -D WIN32 -D _DEBUG -D _CONSOLE
CFLAGS += -fprofile-arcs -fprofile-generate=. -ftest-coverage

LFLAGS = 
LFLAGS += -lstdc++  -L ./lib -lcppunit

SOURCES = 
SRCDIRS = 

SOURCES += tester.cpp

OBJECTS += $(foreach source, $(SOURCES), app_obj/$(basename $(source)).o)
GCNOFILES += $(foreach source, $(SOURCES), app_obj/$(basename $(source)).gcno)

vpath %.c . $(SRCDIRS)
vpath %.cxx . $(SRCDIRS)
vpath %.cpp . $(SRCDIRS)

.PHONY: build
build : init-build $(TARGET)

tester.exe : $(OBJECTS)
	echo Link $@ ... 2>&1 | tee -a build.log 
	g++ -fprofile-arcs -fprofile-generate=cov -ftest-coverage$(OBJECTS) $(LFLAGS) -o $@ 2>&1 | tee -a build.log 



.PHONY: init-build
init-build:
	-mkdir -p app_obj
	echo 2>&1 | tee build.log 

.PHONY: clean
clean : 
	echo Clean ...
	rm -f $(TARGET)
	rm -f $(OBJECTS)
	rm -f $(GCNOFILES)

app_obj/%.o : %.c
	echo Compiling $<
	gcc -c $(CFLAGS) $< -o $@

app_obj/%.o : %.cxx
	echo Compiling $<
	gcc -c $(CFLAGS) $< -o $@

app_obj/%.o : %.cpp
	echo Compiling $<
	gcc -c $(CFLAGS) $< -o $@

结果查询:

4、覆盖率数据生成

分为两步:

  • lcov :负责收集覆盖率数据,
  • genhtml:负责生成覆盖率数据报告
c 复制代码
lcov -c -d app_obj -o cov.info
genhtml -o cov-report cov.info

覆盖率报告说明:

  • 右上角是行覆盖和函数覆盖率
  • 蓝色框住代码 代表执行过,红色则么有执行过
  • 代码开始位置的数字,代表跑了多少次。

相关推荐
丁卯4045 小时前
golang单元测试
开发语言·golang·单元测试
爱上语文14 小时前
Java后端开发单元测试
java·开发语言·单元测试
SomeB1oody21 小时前
【Rust自学】11.9. 单元测试
开发语言·后端·rust·单元测试
SomeB1oody21 小时前
【Rust自学】11.10. 集成测试
开发语言·后端·rust·单元测试·集成测试
昔我往昔1 天前
Spring Boot 支持哪些日志框架
spring boot·后端·单元测试
测试杂货铺2 天前
软件测试之单元测试总结
自动化测试·软件测试·python·测试工具·职场和发展·单元测试·测试用例
不能只会打代码2 天前
32单片机从入门到精通之测试与验证——单元测试(十五)
单片机·嵌入式硬件·单元测试·32单片机
林犀居士2 天前
H2数据库在单元测试中的应用
数据库·单元测试·h2·内存数据库
亦可呀3 天前
Maven核心与单元测试
单元测试·maven·依赖管理
ccmjga6 天前
升级 Spring Boot 3 配置讲解 —— 如何在 Spring Boot 3 中接入生成式 AI?
java·人工智能·spring boot·后端·docker·面试·单元测试