一种适用于 Android Native 层的代码功能测试框架

发布时间 2023-10-16 01:33:55作者: Qidi_Huang

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.ctesttool_frameworks.c/h - 测试流程代码
  • testtool_cases.c/h - 测试用例代码

使用此测试框架,针对不同项目的测试需求,只需要修改或替换 testtool_cases.htesttool_cases.c 即可。