为了不额外的第三方库,EasyNN内部实现了一个简单的单元测试框架,用来实现单元测试的功能。
单元测试是一种软件测试方法,主要关注软件中的最小可测试单元,通常是单个函数、方法或类。单元测试的作用主要包括:
- 提升代码质量:通过在编写代码的过程中发现并修复错误,有助于提高代码的质量和减少bug的数量。
- 提升反馈速度:可以快速地提供反馈,让开发人员知道他们的代码是否按照预期工作,从而减少重复工作,提高开发效率。
- 保护代码:当对代码进行修改或添加新功能时,单元测试可以帮助确保这些更改没有破坏现有的功能。
- 简化代码维护:作为代码的一种文档,单元测试可以帮助开发人员理解代码的功能和工作方式,从而使代码的维护变得更容易。
- 改进代码设计:因为单元测试强制开发人员编写可测试的代码,这通常也意味着代码的设计更好。
TEST(Mat, refcount)
{
{
easynn::Mat m1(10,10);
EXPECT_EQ(*m1.refcount, 1);
easynn::Mat m2(m1);
easynn::Mat m3=m1;
EXPECT_EQ(*m2.refcount, 3);
EXPECT_EQ(*m3.refcount, 3);
}
easynn::Mat m1(10,10);
{
EXPECT_EQ(*m1.refcount, 1);
easynn::Mat m2(m1);
easynn::Mat m3=m1;
EXPECT_EQ(*m2.refcount, 3);
EXPECT_EQ(*m3.refcount, 3);
}
EXPECT_EQ(*m1.refcount, 1);
{
easynn::Mat m2;
easynn::Mat m3=m2;
EXPECT_EQ((long)m2.refcount, 0);
EXPECT_EQ((long)m3.refcount, 0);
}
}
如上所示,就是一个单元测试的示例,TEST(Mat, refcount)
,表示这是Mat类的单元测试,且测试的是引用计数的方法。
EXPECT_EQ()
是一个宏,表示期望两个数值相等,如果不相等的话,则refcount这个单元测试会报错。
相关的代码在test/test_ulti.h下
#define QTEST_EXPECT(x, y, cond) \
if (!((x)cond(y))) \
{ \
printf("%s:%u: Failure\n", __FILE__, __LINE__); \
if (strcmp(#cond, "==") == 0) \
{ \
printf("Expected equality of these values:\n"); \
printf(" %s\n", #x); \
qtest_evaluate_if_required(#x, x); \
printf(" %s\n", #y); \
qtest_evaluate_if_required(#y, y); \
} \
else \
{ \
printf("Expected: (%s) %s (%s), actual: %s vs %s\n", #x, #cond, #y, std::to_string(x).c_str(), std::to_string(y).c_str()); \
} \
*qtest_current_fail_cnt = *qtest_current_fail_cnt + 1; \
}
#define EXPECT_EQ(x, y) (x, y, ==)
#define EXPECT_NE(x, y) QTEST_EXPECT(x, y, !=)
#define EXPECT_LT(x, y) QTEST_EXPECT(x, y, <)
#define EXPECT_LE(x, y) QTEST_EXPECT(x, y, <=)
#define EXPECT_GT(x, y) QTEST_EXPECT(x, y, >)
#define EXPECT_GE(x, y) QTEST_EXPECT(x, y, >=)
#define EXPECT_TRUE(x) \
if (!((x))) \
{ \
printf("%s:%u: Failure\n", __FILE__, __LINE__); \
printf("Value of: %s\n", #x); \
printf(" Actual: false\n"); \
printf("Expected: true\n"); \
*qtest_current_fail_cnt = *qtest_current_fail_cnt + 1; \
}
#define EXPECT_FALSE(x) \
if (((x))) \
{ \
printf("%s:%u: Failure\n", __FILE__, __LINE__); \
printf("Value of: %s\n", #x); \
printf(" Actual: true\n"); \
printf("Expected: false\n"); \
*qtest_current_fail_cnt = *qtest_current_fail_cnt + 1; \
}
template<typename T>
void qtest_evaluate_if_required(const char* str, T value)
{
if (strcmp(str, std::to_string(value).c_str()) != 0)
{
std::cout << " Which is: " << value << std::endl;
}
}
#define ASSERT_EQ(x, y) \
if ((x)!=(y)) \
{ \
printf("%s:%u: Failure\n", __FILE__, __LINE__); \
printf("Expected equality of these values:\n"); \
printf(" %s\n", #x); \
qtest_evaluate_if_required(#x, x); \
printf(" %s\n", #y); \
qtest_evaluate_if_required(#y, y); \
*qtest_current_fail_cnt = *qtest_current_fail_cnt + 1; \
return; \
}
上面的代码是用来实现判断值想不想等的宏,例如EXPECT_EQ表示希望两个值相等,EXPECT_NE表示希望两个值不相等,如果输入的值与所期望的判断条件不等的话,那么就会相对应的输出错误的信息,同时记录下错误。
class TestEntity
{
private:
TestEntity() { };
~TestEntity() { };
public:
std::string make_proper_str(size_t num, const std::string str, bool uppercase = false)
{
std::string res;
if (num > 1)
{
if (uppercase)
res = std::to_string(num) + " " + str + "S";
else
res = std::to_string(num) + " " + str + "s";
}
else
{
res = std::to_string(num) + " " + str;
}
return res;
}
public:
TestEntity(const TestEntity& other) = delete;
TestEntity operator=(const TestEntity& other) = delete;
static TestEntity& get_instance()
{
static TestEntity entity;
return entity;
}
int add(std::string test_set_name, std::string test_name, std::function<void(int*)> f, const char* fname)
{
TestItem item(f, fname);
test_sets[test_set_name].test_items.emplace_back(item);
return 0;
}
int set_filter(std::string _filter)
{
filter = _filter;
return 0;
}
int run_all_test_functions()
{
std::map<std::string, TestSet>::iterator it = test_sets.begin();
for (; it != test_sets.end(); it++)
{
std::string test_set_name = it->first;
TestSet& test_set = it->second;
std::vector<TestItem>& test_items = test_set.test_items;
int cnt = 0;
for (int i = 0; i < test_items.size(); i++)
{
const std::string fname = test_items[i].fname;
if (filter.length() == 0 || (filter.length() > 0 && strmatch(fname, filter)))
{
cnt++;
}
}
if (cnt == 0) continue;
matched_test_set_count++;
const std::string test_item_str = make_proper_str(cnt, "test");
printf("%s[----------]%s %s from %s\n", QTEST_ESCAPE_COLOR_GREEN, QTEST_ESCAPE_COLOR_END, test_item_str.c_str(), it->first.c_str());
for (int i = 0; i < test_items.size(); i++)
{
auto f = test_items[i].f;
std::string fname = test_items[i].fname;
if (filter.length() > 0 && !strmatch(fname, filter))
{
continue;
}
matched_test_case_count++;
int qtest_current_fail_cnt = 0;
printf("%s[ RUN ]%s %s\n", QTEST_ESCAPE_COLOR_GREEN, QTEST_ESCAPE_COLOR_END, fname.c_str());
f(&qtest_current_fail_cnt);
if (qtest_current_fail_cnt == 0)
{
printf("%s[ OK ]%s %s (0 ms)\n", QTEST_ESCAPE_COLOR_GREEN, QTEST_ESCAPE_COLOR_END, fname.c_str());
}
else
{
printf("%s[ FAILED ]%s %s (0 ms)\n", QTEST_ESCAPE_COLOR_RED, QTEST_ESCAPE_COLOR_END, fname.c_str());
qtest_fail_cnt++;
}
test_items[i].success = (qtest_current_fail_cnt == 0);
}
printf("%s[----------]%s %s from %s (0 ms total)\n", QTEST_ESCAPE_COLOR_GREEN, QTEST_ESCAPE_COLOR_END, test_item_str.c_str(), it->first.c_str());
printf("\n");
}
printf("%s[----------]%s Global test environment tear-down\n", QTEST_ESCAPE_COLOR_GREEN, QTEST_ESCAPE_COLOR_END);
std::string tests_str = make_proper_str(matched_test_case_count, "test");
std::string suite_str = make_proper_str(matched_test_set_count, "test suite");
printf("%s[==========]%s %s from %s ran. (0 ms total)\n",
QTEST_ESCAPE_COLOR_GREEN, QTEST_ESCAPE_COLOR_END,
tests_str.c_str(),
suite_str.c_str()
);
int passed_test_count = matched_test_case_count - qtest_fail_cnt;
std::string how_many_test_str = make_proper_str(passed_test_count, "test");
printf("%s[ PASSED ]%s %s.\n", QTEST_ESCAPE_COLOR_GREEN, QTEST_ESCAPE_COLOR_END, how_many_test_str.c_str());
if (qtest_fail_cnt)
{
std::string failed_test_str = make_proper_str(qtest_fail_cnt, "test");
printf("%s[ FAILED ]%s %s, listed below:\n", QTEST_ESCAPE_COLOR_RED, QTEST_ESCAPE_COLOR_END, failed_test_str.c_str());
std::map<std::string, TestSet>::iterator it = test_sets.begin();
for (; it != test_sets.end(); it++)
{
std::string test_set_name = it->first;
TestSet test_set = it->second;
std::vector<TestItem> test_items = test_set.test_items;
for (int i = 0; i < test_items.size(); i++)
{
if (!test_items[i].success)
{
printf("%s[ FAILED ]%s %s\n", QTEST_ESCAPE_COLOR_RED, QTEST_ESCAPE_COLOR_END, test_items[i].fname.c_str());
}
}
}
}
if (qtest_fail_cnt > 0)
{
std::string failed_test_str = make_proper_str(qtest_fail_cnt, "FAILED TEST", true);
printf("\n %s\n", failed_test_str.c_str());
}
return 0;
}
private:
// https://leetcode.cn/problems/wildcard-matching/solutions/315802/tong-pei-fu-pi-pei-by-leetcode-solution/
/// @param s string
/// @param p pattern
bool strmatch(std::string s, std::string p)
{
auto allStars = [](const std::string& str, int left, int right) {
for (int i = left; i < right; ++i) {
if (str[i] != '*') {
return false;
}
}
return true;
};
auto charMatch = [](char u, char v)
{
return u == v || v == '?';
};
while (s.size() && p.size() && p.back() != '*')
{
if (charMatch(s.back(), p.back())) {
s.pop_back();
p.pop_back();
}
else {
return false;
}
}
if (p.empty()) {
return s.empty();
}
int sIndex = 0;
int pIndex = 0;
int sRecord = -1;
int pRecord = -1;
while (sIndex < s.size() && pIndex < p.size()) {
if (p[pIndex] == '*') {
++pIndex;
sRecord = sIndex;
pRecord = pIndex;
}
else if (charMatch(s[sIndex], p[pIndex])) {
++sIndex;
++pIndex;
}
else if (sRecord != -1 && sRecord + 1 < s.size()) {
++sRecord;
sIndex = sRecord;
pIndex = pRecord;
}
else {
return false;
}
}
return allStars(p, pIndex, p.size());
}
public:
struct TestItem
{
std::function<void(int*)> f;
std::string fname;
bool success;
TestItem(std::function<void(int*)> _f, std::string _fname):
f(_f), fname(_fname), success(true)
{}
};
struct TestSet
{
std::vector<TestItem> test_items;
};
std::map<std::string, TestSet> test_sets;
public:
int matched_test_case_count = 0;
int matched_test_set_count = 0;
private:
int qtest_fail_cnt = 0; // number of failures in one test set
std::string filter;
};
接下来就是TestEntity这个类的实现。总体上看,TestEntity是作为单例模式来使用,当我们在写单元测试时,如下
TEST(Mat, refcount)
{
}
#define TEST(set, name) \
void qtest_##set##_##name(int* qtest_current_fail_cnt); \
int qtest_mark_##set##_##name = TestEntity::get_instance().add(#set, #name, qtest_##set##_##name, #set "." #name); \
void qtest_##set##_##name(int* qtest_current_fail_cnt) \
Test是一个宏,宏展开后变成三行代码,第一行是把名字替换一下声明一个函数,第二行是调用一个函数TestEntity::get_instance().add()
函数
static TestEntity& get_instance()
{
static TestEntity entity;
return entity;
}
int add(std::string test_set_name, std::string test_name, std::function<void(int*)> f, const char* fname)
{
TestItem item(f, fname);
test_sets[test_set_name].test_items.emplace_back(item);
return 0;
}
get_instance
是返回返回类的实例,也是也就是单例模式,只有一个对象。add函数就是将我们的单元测试的指针保存起来。只要把这些单元测试的指针保存下来了,我们只要遍历并执行,做一些对应的处理,就可以了,相对应的函数是run_all_test_functions()
。