Skip to content

Latest commit

 

History

History
393 lines (336 loc) · 13 KB

测试框架.md

File metadata and controls

393 lines (336 loc) · 13 KB

为了不额外的第三方库,EasyNN内部实现了一个简单的单元测试框架,用来实现单元测试的功能。

单元测试介绍

单元测试是一种软件测试方法,主要关注软件中的最小可测试单元,通常是单个函数、方法或类。单元测试的作用主要包括:

  1. 提升代码质量:通过在编写代码的过程中发现并修复错误,有助于提高代码的质量和减少bug的数量。
  2. 提升反馈速度:可以快速地提供反馈,让开发人员知道他们的代码是否按照预期工作,从而减少重复工作,提高开发效率。
  3. 保护代码:当对代码进行修改或添加新功能时,单元测试可以帮助确保这些更改没有破坏现有的功能。
  4. 简化代码维护:作为代码的一种文档,单元测试可以帮助开发人员理解代码的功能和工作方式,从而使代码的维护变得更容易。
  5. 改进代码设计:因为单元测试强制开发人员编写可测试的代码,这通常也意味着代码的设计更好。

单元测试的使用

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()