草庐IT

【嵌入式单元测试】C语言单元测试框架搭建

知否,知否 2023-12-30 原文

cmocka

单元测试框架是一个软件包,它能够让开发者比较方便的表达产品代码需要表现出什么样的行为。单元测试框架提供了一个自动化单元测试的解决方案,让开发者把更多的精力放在测试用例的设计的编写上,而不用花精力考虑如何对测试用例进行组织。

cmocka是一个优雅的C语言单元测试框架,支持模拟对象。它只需要标准的C库,适用于各种计算平台(Linux、windows,以及嵌入式)。
理论上来说,cmocka可以支持任何使用标准C库的交叉编译器。

本文将介绍如何在嵌入式环境(交叉编译)搭建cmocka单元测试环境,以及cmocka的简单使用示例。

cmocka交叉编译

源码下载

目前最新的1.1.5版本,对于嵌入式环境,我们需要下载源码进行交叉编译

cmocka1.1源码下载地址

这里以cmocka-1.1.5.tar.xz为例。

编译准备

将上述源码在linux环境中解压,并在源码同级目录新建编译目录build_dir

cmocka-1.1.5内容如下:

源码修改

进入cmocka-1.1.5源码目录,修改顶层CMakeLists.txt,将如下行注释掉。
doc组件需要特定的库支持,嵌入式环境一般没有这个库,而且这个只是生成代码注释,对功能没有影响,所以将其注释。

# add_subdirectory(doc)

指定编译器

接下来在build_dir目录的同级目录新建配置文件arm64_setup.cmake(文件名随意)

文件内容如下:

set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm64)

set(tools /mnt/opt/compile_tools/bin/)
set(CMAKE_C_COMPILER ${tools}/aarch64-openwrt-linux-gcc)
set(CMAKE_CXX_COMPILER ${tools}/aarch64-openwrt-linux-g++)

  • CMAKE_SYSTEM_NAME 指定嵌入式系统类型
  • CMAKE_SYSTEM_PROCESSOR 指定嵌入式平台
  • tools 交叉编译器路径(以实际路径为准)
  • CMAKE_C_COMPILER C交叉编译器
  • CMAKE_CXX_COMPILER C++交叉编译器

编译

编译需要进入build_dir目录,先执行如下命令生成必要的配置和makefile文件

$ cd build_dir
$ cmake -DCMAKE_TOOLCHAIN_FILE=../arm64_setup.cmake -DBUILD_STATIC_LIB=ON ../cmocka-1.1.5/
  • -DCMAKE_TOOLCHAIN_FILE是指定刚刚创建的编译器配置文件
  • -DBUILD_STATIC_LIB=ON 是编译生成静态库,去掉这句只会生成动态库
  • ../cmocka-1.1.5/指定cmocka源码目录

如果上述步骤没有错误,那么在build_dir应该会生成若干目录和文件,其中就包括makefile,接下来执行make 编译即可。

$ make

出现下列提示表示编译成功:

Scanning dependencies of target cmocka
[  4%] Building C object src/CMakeFiles/cmocka.dir/cmocka.c.o
......
[100%] Linking C executable test_uptime
[100%] Built target test_uptime

如果需要clean,直接在build_dir目录执行make clean是不行的,因为Cmake不支持。最简单的方式就是把build_dir目录手动清空即可。

编译完成后查看build_dir/src,会生成我们需要的动态库或者静态库.
使用file指令可以看到这个动态库是ARM aarch64平台的,表明我们交叉编译成功了。

$ build_dir/src$ ls
CMakeFiles           libcmocka.so    libcmocka.so.0.7.0  Makefile
cmake_install.cmake  libcmocka.so.0  libcmocka-static.a

$ file libcmocka.so.0.7.0 
libcmocka.so.0.7.0: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, with debug_info, not stripped

另外,build_dir/example目录下也会生成一些示例demo,可以直接在开发板上运行。

$ build_dir/example$ ls
allocate_module_test  CMakeFiles           Makefile
assert_macro_test     cmake_install.cmake  mock
assert_module_test    CTestTestfile.cmake  simple_test
$ file simple_test 
simple_test: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-aarch64.so.1, with debug_info, not stripped

有了libcmocka,在加上源码中的cmocka.h,我就可以利用cmocka提供的API编译我们自己的单元测试代码了。

cmocka.h位于cmocka源码的include目录

cmocka使用示例

测试代码如下:

$ tree hello/ 
hello/
├── inc
│   └── cmocka.h
├── libs
│   ├── libcmocka.so
│   └── libcmocka-static.a
├── makefile
└── src
    └── hello_cmocka.c
  • libs文件夹主要存放库文件,推荐使用libcmocka静态库,使用静态库的好处是编译出的二进制文件在开发板上可以直接运行,如果是动态库还需要开发板上也安装这个动态库,静态库的缺点就是编译产物体积较大。

示例makefile文件如下:

#source file
SOURCE  += $(wildcard ./src/*.c)
OBJS    := $(patsubst %.c,%.o,$(patsubst %.cpp,%.o,$(SOURCE)))

TARGET  := hello_cmocka

#compile and lib parameter
CC      := aarch64-openwrt-linux-gcc
LIBS    := -lcmocka-static
LDFLAGS := -L./libs  
DEFINES :=
INCLUDE := -I./inc/ 
CFLAGS  := -g -Wno-unused-variable -O3 $(DEFINES) $(INCLUDE)
CXXFLAGS:= $(CFLAGS) 
  
.PHONY: 

all : $(TARGET)
objs : $(OBJS)
             
clean :
	rm -fr ./src/*.o
	rm -fr $(TARGET)
  
$(TARGET) : $(OBJS)
	$(CC) $(CXXFLAGS) -o $@ $(OBJS) $(LDFLAGS) $(LIBS)

示例hello_cmocka.c如下:

#include <stdarg.h>
#include <stdio.h>
#include <setjmp.h>
#include <stddef.h>
#include <stdint.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <cmocka.h>

static int add(int a ,int b)
{
	return a+b;
}

static char* print_string(int num)
{
	switch(num)
	{
		case 1: return "CASE1";
		case 2: return "CASE2";
		default: return "NOT SUPPORT";
	}

	return "NOT SUPPORT";
}

static void test_demo1(void **state)
{
    int ret = add(3,2); 
	assert_int_equal(ret, 5);

	(void) state;
}
static void test_demo2(void **state)
{
  int ret = add(3,2);
	assert_int_equal(ret, 0);

	(void) state;
}

static void test_demo3(void **state)
{
  char *p = print_string(1);
	assert_string_equal(p, "CASE1");

	(void) state;
}

int main(void)
{
	const struct CMUnitTest tests[] = {
		cmocka_unit_test(test_demo1),
		cmocka_unit_test(test_demo2),
		cmocka_unit_test(test_demo3),
	};

	return cmocka_run_group_tests(tests, NULL, NULL);
}

运行结果如下:

# ./hello_cmocka
[==========] Running 3 test(s).
[ RUN      ] test_demo1
[       OK ] test_demo1
[ RUN      ] test_demo2
[  ERROR   ] --- 0x5 != 0
[   LINE   ] --- src/hello_cmocka.c:46: error: Failure!
[  FAILED  ] test_demo2
[ RUN      ] test_demo3
[       OK ] test_demo3
[==========] 3 test(s) run.
[  PASSED  ] 2 test(s).
[  FAILED  ] 1 test(s), listed below:
[  FAILED  ] test_demo2

 1 FAILED TEST(S)

提示test_demo2测试失败,原因是0x5 != 0,test_demo2的预期结果是0,实际返回5,不符合预期,故测试失败。
assert_int_equal()是判断int类型结果,assert_string_equal()是判断const char *类型的结果。除此之外,cmocka还提供了更多其他的测试API,请参考cmocka.h.

常见问题

  1. 编译错误
/cmocka-1.1.5/include/cmocka.h:132:28: error: conflicting types for 'uintptr_t'
typedef unsigned int uintptr_t;
/mnt/opt/include/bits/alltypes.h:109:24: note: previous declaration of 'uintptr_t' was here
 typedef unsigned _Addr uintptr_t;

cmocka源码中uintptr_t定义和我们的交叉编译器中的定义冲突了,这里只需要暂时把编译器中的定义注释掉,编译完cmocka再改回来即可。

  1. 其他编译问题

cmocka仅使用了标准C库,它的跨平台兼容性很好。因此,对于绝大多数的交叉编译器应该都是支持的(除非你的编译器版本很低,对C库的支持不全)。
因此,交叉编译cmocka源码中遇到的编译问题基本上都是C代码问题,与平台无关。

参考

有关【嵌入式单元测试】C语言单元测试框架搭建的更多相关文章

  1. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  2. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  3. ruby - 使用 C 扩展开发 ruby​​gem 时,如何使用 Rspec 在本地进行测试? - 2

    我正在编写一个包含C扩展的gem。通常当我写一个gem时,我会遵循TDD的过程,我会写一个失败的规范,然后处理代码直到它通过,等等......在“ext/mygem/mygem.c”中我的C扩展和在gemspec的“扩展”中配置的有效extconf.rb,如何运行我的规范并仍然加载我的C扩展?当我更改C代码时,我需要采取哪些步骤来重新编译代码?这可能是个愚蠢的问题,但是从我的gem的开发源代码树中输入“bundleinstall”不会构建任何native扩展。当我手动运行rubyext/mygem/extconf.rb时,我确实得到了一个Makefile(在整个项目的根目录中),然后当

  4. ruby - Ruby 的 Hash 在比较键时使用哪种相等性测试? - 2

    我有一个围绕一些对象的包装类,我想将这些对象用作散列中的键。包装对象和解包装对象应映射到相同的键。一个简单的例子是这样的:classAattr_reader:xdefinitialize(inner)@inner=innerenddefx;@inner.x;enddef==(other)@inner.x==other.xendenda=A.new(o)#oisjustanyobjectthatallowso.xb=A.new(o)h={a=>5}ph[a]#5ph[b]#nil,shouldbe5ph[o]#nil,shouldbe5我试过==、===、eq?并散列所有无济于事。

  5. ruby - RSpec - 使用测试替身作为 block 参数 - 2

    我有一些Ruby代码,如下所示:Something.createdo|x|x.foo=barend我想编写一个测试,它使用double代替block参数x,这样我就可以调用:x_double.should_receive(:foo).with("whatever").这可能吗? 最佳答案 specify'something'dox=doublex.should_receive(:foo=).with("whatever")Something.should_receive(:create).and_yield(x)#callthere

  6. ruby - Sinatra:运行 rspec 测试时记录噪音 - 2

    Sinatra新手;我正在运行一些rspec测试,但在日志中收到了一堆不需要的噪音。如何消除日志中过多的噪音?我仔细检查了环境是否设置为:test,这意味着记录器级别应设置为WARN而不是DEBUG。spec_helper:require"./app"require"sinatra"require"rspec"require"rack/test"require"database_cleaner"require"factory_girl"set:environment,:testFactoryGirl.definition_file_paths=%w{./factories./test/

  7. ruby-on-rails - 迷你测试错误 : "NameError: uninitialized constant" - 2

    我遵循MichaelHartl的“RubyonRails教程:学习Web开发”,并创建了检查用户名和电子邮件长度有效性的测试(名称最多50个字符,电子邮件最多255个字符)。test/helpers/application_helper_test.rb的内容是:require'test_helper'classApplicationHelperTest在运行bundleexecraketest时,所有测试都通过了,但我看到以下消息在最后被标记为错误:ERROR["test_full_title_helper",ApplicationHelperTest,1.820016791]test

  8. ruby - 即使失败也继续进行多主机测试 - 2

    我已经构建了一些serverspec代码来在多个主机上运行一组测试。问题是当任何测试失败时,测试会在当前主机停止。即使测试失败,我也希望它继续在所有主机上运行。Rakefile:namespace:specdotask:all=>hosts.map{|h|'spec:'+h.split('.')[0]}hosts.eachdo|host|begindesc"Runserverspecto#{host}"RSpec::Core::RakeTask.new(host)do|t|ENV['TARGET_HOST']=hostt.pattern="spec/cfengine3/*_spec.r

  9. ruby-on-rails - 如何使辅助方法在 Rails 集成测试中可用? - 2

    我在app/helpers/sessions_helper.rb中有一个帮助程序文件,其中包含一个方法my_preference,它返回当前登录用户的首选项。我想在集成测试中访问该方法。例如,这样我就可以在测试中使用getuser_path(my_preference)。在其他帖子中,我读到这可以通过在测试文件中包含requiresessions_helper来实现,但我仍然收到错误NameError:undefinedlocalvariableormethod'my_preference'.我做错了什么?require'test_helper'require'sessions_hel

  10. ruby - 寻找通过阅读代码确定编程语言的ruby gem? - 2

    几个月前,我读了一篇关于ruby​​gem的博客文章,它可以通过阅读代码本身来确定编程语言。对于我的生活,我不记得博客或gem的名称。谷歌搜索“ruby编程语言猜测”及其变体也无济于事。有人碰巧知道相关gem的名称吗? 最佳答案 是这个吗:http://github.com/chrislo/sourceclassifier/tree/master 关于ruby-寻找通过阅读代码确定编程语言的rubygem?,我们在StackOverflow上找到一个类似的问题:

随机推荐