一种适用于 Android Native 层的代码功能测试框架
Qidi 2023.10.16
0. Android Native 层的代码功能测试场景
参与过 Android Native 层代码编写的朋友都了解,这个层面的开发任务,最终产物大部分情况下不外乎以下几种:
.so
动态库.a
静态库.xml
或.json
等配置文件- 可执行文件
为了验证代码功能是否满足需求,开发人员往往会编写一个测试工具,再通过测试工具去调用各个库实现的接口,或加载配置文件等。
1. 常见测试工具的不足
通常,一个测试工具的工作流程是:main --> showMenu --> chooseTestCase --> prepareTestCaseParameters --> runTestCase
我们可以编写独立的源文件来实现上述不同的工作流程。但很多开发人员在编写测试工具的时候,往往认为测试工具不属于正式代码,从而将上述整个工作流程写在同一个源文件里,甚至是写在同一个函数里,导致了一些问题,比如:
-
耦合严重,复用性差
不同项目的需求不同,代码所依赖的部件也不同。
有的时候,一个项目上要求实现 接口A,但另一个项目却不要求。这时就发现,为了在后一个项目上适配测试工具,需要在一个巨大的源文件头部、中部、尾部等各处散乱地删除 接口A 相关的代码。
还有的时候,两个项目上都要求实现 接口A,但一个要依赖于特定 Binder服务 才能工作,而另一个项目上 接口A 不仅不依赖于那个服务,而且项目上根本连这个服务相关的文件都不存在。这时就会出现测试工具代码在后一个项目上编译都不通过的问题。 -
不可配置,扩展性差,可读性差
即使是同一个项目,随着开发进行,也会添加新接口。
由于测试工具前期设计上的不重视,测试流程相关的代码和各个具体测试用例的代码交叉在一起。带来的结果是,为了测试新增接口,不得不在流程相关的代码里进行改动。比如修改main()
函数、修改switch...case...
、if...else if...else...
语句等。越来越多的接口导致代码里出现越来越多的分支、且函数越来越长,可读性也随之快速下降。
2. 理想的测试工具代码是什么样
理想的测试工具代码应该具备以下特点:
- 测试流程代码和测试用例代码分离
- 与项目需求相关的代码位于测试用例代码中
- 与项目依赖相关的代码也位于测试用例代码中
- 测试函数接口稳定,函数名及参数不因项目不同而变化
- 测试用例代码可以方便地进行替换
3. 使用可变参函数和转换表实现代码功能测试框架
核心逻辑是使用链表实现可变参函数,继而实现转换表,将测试流程和测试用例分离。感兴趣的朋友可以阅读《使用链表而不是 stdarg 实现可变参数函数》
话不多说,直接贴上完整代码。
testtool_main.c:
/**
* Source code of main.
*
* Technically, for meeting different project requirements,
* you may need to implement gTestCaseActionMap per project.
*
* 2023/10/14, Qidi Huang <huang_qi_di@hotmail.com>
*
*/
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include <stdlib.h>
#include "testtool_frameworks.h"
#include "testtool_cases.h"
/////////////////////////////////////////////////////////////////
extern const TestCaseAction gTestCaseActionMap[];
extern const int gTestCaseActionMapSize;
/////////////////////////////////////////////////////////////////
void showMenu()
{
for (int i=0; i<gTestCaseActionMapSize; i++) {
printf("%d. %s\n", gTestCaseActionMap[i].mTestCaseNum, gTestCaseActionMap[i].mTestCaseStr);
}
}
void showArgDescription(char **argDesc, char *argType)
{
printf("Specify a value for %s (%s): \n", *argDesc, argType);
*argDesc = strchr(*argDesc, '\0');
if (*((*argDesc)+1) != '\0') {
(*argDesc)++;
}
}
/////////////////////////////////////////////////////////////////
int main()
{
int ret = RET_ERROR;
showMenu();
int testCaseChoice = -1;
do {
printf("\nSelect a test choice: ");
scanf("%d", &testCaseChoice);
} while (testCaseChoice < 0 || testCaseChoice >= gTestCaseActionMapSize);
FuncArgChainItem_t *argChainHead = NULL, *argChainCurrent = NULL;
char *origArgList = gTestCaseActionMap[testCaseChoice].mTestCaseFuncArg;
int origArgListSize = strlen(origArgList) + 1;
char *copiedArgList = (char *)malloc(origArgListSize);
memcpy(copiedArgList, origArgList, origArgListSize);
char *nextArgDesc = gTestCaseActionMap[testCaseChoice].mTestCaseFuncArgDesc;
char *subArgType, *subArgType_saved;
subArgType = strtok_r(copiedArgList, ",", &subArgType_saved);
while (subArgType != NULL) {
showArgDescription(&nextArgDesc, subArgType);
if (strcmp(subArgType, "ll") == 0) {
long long input_ll;
scanf("%lld", &input_ll);
NEW_ARG_ITEM(input_ll, ll, argChainHead, argChainCurrent);
} else if (strcmp(subArgType, "i") == 0) {
int input_i;
scanf("%d", &input_i);
NEW_ARG_ITEM(input_i, i, argChainHead, argChainCurrent);
} else if (strcmp(subArgType, "ui") == 0) {
unsigned int input_ui;
scanf("%u", &input_ui);
NEW_ARG_ITEM(input_ui, ui, argChainHead, argChainCurrent);
} else if (strcmp(subArgType, "c") == 0) {
char input_c;
scanf("%c", &input_c);
NEW_ARG_ITEM(input_c, c, argChainHead, argChainCurrent);
} else if (strcmp(subArgType, "uc") == 0) {
unsigned char input_uc;
scanf("%hhu", &input_uc);
NEW_ARG_ITEM(input_uc, uc, argChainHead, argChainCurrent);
} else if (strcmp(subArgType, "b") == 0) {
int input_tmp;
scanf("%d", &input_tmp);
bool input_b = !!input_tmp;
NEW_ARG_ITEM(input_b, b, argChainHead, argChainCurrent);
} else if (strcmp(subArgType, "f") == 0) {
float input_f;
scanf("%f", &input_f);
NEW_ARG_ITEM(input_f, f, argChainHead, argChainCurrent);
} else if (strcmp(subArgType, "s") == 0) {
char input_s[MAX_KVPAIR_SIZE] = {0};
LIMITED_STR_SCANF(MAX_KVPAIR_SIZE-1, input_s);
NEW_ARG_ITEM(input_s, s, argChainHead, argChainCurrent);
} else if (strcmp(subArgType, "v") == 0) {
// void argument, do nothing
} else {
printf("illegal argument type \'%s\'\n", subArgType);
return RET_ERROR;
}
subArgType = strtok_r(NULL, ",", &subArgType_saved);
}
free(copiedArgList);
ret = gTestCaseActionMap[testCaseChoice].mTestCaseFunc(argChainHead);
return ret;
}
testtool_frameworks.h:
/**
* Declaration of testtool frameworks functions, as well as
* macros and data types.
*
* 2023/10/14, Qidi Huang <huang_qi_di@hotmail.com>
*
*/
#pragma once
#include <stdbool.h>
#define RET_ERROR -1
#define RET_OK 0
typedef union {
long long m_ll;
unsigned char m_uc;
unsigned int m_ui;
char m_c;
char *m_s;
int m_i;
bool m_b;
float m_f;
} FuncArgItem;
struct FuncArgChainItem {
FuncArgItem mArgVal;
struct FuncArgChainItem *mpNext;
};
typedef struct FuncArgChainItem FuncArgChainItem_t;
typedef int(*TestCaseActionFuncPtr)(FuncArgChainItem_t *);
/////////////////////////////////////////////////////////////////
typedef struct {
int mTestCaseNum; // TestCase enumerations
char *mTestCaseStr;
TestCaseActionFuncPtr mTestCaseFunc;
char *mTestCaseFuncArg;
char *mTestCaseFuncArgDesc;
} TestCaseAction;
#define MAX_KVPAIR_SIZE 50
#define LIMITED_STR_SCANF(max_len, buf) \
char fmt[10] = {0}; \
sprintf(fmt, "%%%ds", max_len); \
scanf(fmt, buf)
#define NEW_ARG_ITEM(argVal, argShortType, argChainHead, argChainCurrent) \
\
FuncArgChainItem_t *argChainItem_##argShortType = (FuncArgChainItem_t *)malloc(sizeof(FuncArgChainItem_t)); \
memset(argChainItem_##argShortType, 0, sizeof(FuncArgChainItem_t)); \
argChainItem_##argShortType->mArgVal.m_##argShortType = argVal; \
\
if (argChainHead == NULL) { \
argChainHead = argChainItem_##argShortType; \
} else { \
argChainCurrent->mpNext = argChainItem_##argShortType; \
} \
argChainCurrent = argChainItem_##argShortType;
#define READ_ARG(arg_chain_head, use_arg_idx, new_arg, arg_type, arg_short_type) \
\
FuncArgChainItem_t *curArg_##new_arg = arg_chain_head; \
FuncArgChainItem_t *useArg_##new_arg = curArg_##new_arg; \
int argReadCount_##new_arg = use_arg_idx; \
while (argReadCount_##new_arg--) { \
curArg_##new_arg = curArg_##new_arg->mpNext; \
useArg_##new_arg = curArg_##new_arg; \
} \
arg_type new_arg = useArg_##new_arg->mArgVal.m_##arg_short_type;
/////////////////////////////////////////////////////////////////
void freeArgChain(FuncArgChainItem_t *argChainHead);
testtool_frameworks.c:
/**
* Source code of testtool frameworks functions.
*
* 2023/10/14, Qidi Huang <huang_qi_di@hotmail.com>
*
*/
#include <stdlib.h>
#include "testtool_frameworks.h"
/**
* Function for releasing memory occupied by argument chain.
*
* @argChainHead chain for storing varadic arguments of
* each test case function.
* @return void.
*/
void freeArgChain(FuncArgChainItem_t *argChainHead)
{
if (argChainHead == NULL)
return;
FuncArgChainItem_t *currentArgItem, *nextArgItem;
currentArgItem = argChainHead;
nextArgItem = argChainHead->mpNext;
while(currentArgItem != NULL) {
free(currentArgItem);
currentArgItem = nextArgItem;
if (nextArgItem != NULL) {
nextArgItem = nextArgItem->mpNext;
}
}
}
testtool_cases.h:
/**
* Declaration of detailed test case functions.
*
* 2023/10/14, Qidi Huang <huang_qi_di@hotmail.com>
*
*/
#pragma once
#include "testtool_frameworks.h"
typedef enum {
CASE_INIT,
CASE_DEINIT,
CASE_CONNECT,
CASE_DISCONNECT,
CASE_SETVOLUME,
CASE_SETMUTE,
CASE_SETPARAMETERS,
CASE_GETPARAMETERS
} TestCase;
int init(FuncArgChainItem_t *argChainHead); // "s"
int deinit(FuncArgChainItem_t *argChainHead); // "v"
int connect(FuncArgChainItem_t *argChainHead); // "ll"
int disconnect(FuncArgChainItem_t *argChainHead); // "ll"
int setVolume(FuncArgChainItem_t *argChainHead); // "ll,i"
int setMute(FuncArgChainItem_t *argChainHead); // "ll,b"
int setParameters(FuncArgChainItem_t *argChainHead); // "s"
int getParameters(FuncArgChainItem_t *argChainHead); // "s"
testtool_cases.c:
/**
* Source code of detailed test case functions.
*
* Implementation of each case could be replaced with
* other implementations to meet different project requirements.
*
* 2023/10/14, Qidi Huang <huang_qi_di@hotmail.com>
*
*/
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include "testtool_frameworks.h"
#include "testtool_cases.h"
int init(FuncArgChainItem_t *argChainHead) // "s"
{
if (argChainHead == NULL) {
return RET_ERROR;
}
char *tuningSet = argChainHead->mArgVal.m_s;
printf("%s got args: tuningSet(%s)\n", __func__, tuningSet);
// doInit(tuningSet);
freeArgChain(argChainHead);
return RET_OK;
}
int deinit(FuncArgChainItem_t *argChainHead) // "v"
{
printf("%s got args: void\n", __func__);
// doDeInit();
return RET_OK;
}
int connect(FuncArgChainItem_t *argChainHead) // "ll,i,f"
{
if (argChainHead == NULL) {
return RET_ERROR;
}
#if 0
long long audioStream = argChainHead->mArgVal.m_ll;
int channelMask = argChainHead->mpNext->mArgVal.m_i;
float sourceGain = argChainHead->mpNext->mpNext->mArgVal.m_f;
#else
READ_ARG(argChainHead, 0, audioStream, long long, ll);
READ_ARG(argChainHead, 1, channelMask, int, i);
READ_ARG(argChainHead, 2, sourceGain, float, f);
#endif
printf("%s got args: audioStream(%lld), channelMask(%d), sourceGain(%f)\n", __func__, audioStream, channelMask, sourceGain);
// doConnect(audioStream, channelMask, sourceGain);
freeArgChain(argChainHead);
return RET_OK;
}
int disconnect(FuncArgChainItem_t *argChainHead) // "ll,i"
{
if (argChainHead == NULL) {
return RET_ERROR;
}
long long audioStream = argChainHead->mArgVal.m_ll;
int channelMask = argChainHead->mpNext->mArgVal.m_i;
printf("%s got args: audioStream(%lld), channelMask(%d)\n", __func__, audioStream, channelMask);
// doDisconnect(audioStream, channelMask);
freeArgChain(argChainHead);
return RET_OK;
}
int setVolume(FuncArgChainItem_t *argChainHead) // "ll,i"
{
if (argChainHead == NULL) {
return RET_ERROR;
}
long long audioStream = argChainHead->mArgVal.m_ll;
int volumeStep = argChainHead->mpNext->mArgVal.m_i;
printf("%s got args: audioStream(%lld), volumeStep(%d)\n", __func__, audioStream, volumeStep);
// doSetVolume(audioStream, volumeStep);
freeArgChain(argChainHead);
return RET_OK;
}
int setMute(FuncArgChainItem_t *argChainHead) // "ll,b"
{
if (argChainHead == NULL) {
return RET_ERROR;
}
long long audioStream = argChainHead->mArgVal.m_ll;
bool muteState = argChainHead->mpNext->mArgVal.m_b;
printf("%s got args: audioStream(%lld), muteState(%d)\n", __func__, audioStream, (int)muteState);
// doSetMute(audioStream, muteState);
freeArgChain(argChainHead);
return RET_OK;
}
int setParameters(FuncArgChainItem_t *argChainHead) // "s"
{
if (argChainHead == NULL) {
return RET_ERROR;
}
char kvPairs[MAX_KVPAIR_SIZE] = {0};
memcpy(kvPairs, argChainHead->mArgVal.m_s, MAX_KVPAIR_SIZE);
printf("%s got args: kvPairs(%s)\n", __func__, kvPairs);
// doSetParameters(kvPairs);
freeArgChain(argChainHead);
return RET_OK;
}
int getParameters(FuncArgChainItem_t *argChainHead) // "s"
{
if (argChainHead == NULL) {
return RET_ERROR;
}
char kvPairs[MAX_KVPAIR_SIZE] = {0};
memcpy(kvPairs, argChainHead->mArgVal.m_s, MAX_KVPAIR_SIZE);
printf("%s got args: kvPairs(%s)\n", __func__, kvPairs);
// doGetParameters(kvPairs);
freeArgChain(argChainHead);
return RET_OK;
}
/////////////////////////////////////////////////////////////////
const TestCaseAction gTestCaseActionMap[] = {
// CAUTION: argument descriptions must be extra '\0' terminated, else program may crash!!!
{CASE_INIT, "init", init, "s", "tuningSet\0"},
{CASE_DEINIT, "deInit", deinit, "v", "\0"},
{CASE_CONNECT, "connect", connect, "ll,i,f", "audioStream\0channelMask\0sourceGain\0"},
{CASE_DISCONNECT, "disconnect", disconnect, "ll,i", "audioStream\0channelMask\0"},
{CASE_SETVOLUME, "setVolume", setVolume, "ll,i", "audioStream\0volumeStep\0"},
{CASE_SETMUTE, "setMute", setMute, "ll,b", "audioStream\0muteState\0"},
{CASE_SETPARAMETERS, "setParameters", setParameters, "s", "kvPairs\0"},
{CASE_GETPARAMETERS, "getParameters", getParameters, "s", "kvPairs\0"},
};
const int gTestCaseActionMapSize = sizeof(gTestCaseActionMap)/sizeof(TestCaseAction);
4. 测试框架代码说明
testtool_main.c
、testtool_frameworks.c/h
- 测试流程代码testtool_cases.c/h
- 测试用例代码
使用此测试框架,针对不同项目的测试需求,只需要修改或替换 testtool_cases.h
和 testtool_cases.c
即可。