【单元测试】CUnit用户手册(中文)

声明:本文是对 CUnit Users Guide 的中文翻译。网上也有看到一些中文翻译,但看起来更像是机器翻译的,有很多地方不通顺,为此,自己花了几天重新翻译了一遍,以供他人查阅。如有翻译不妥之处,欢迎留言指出。

版本:截止书稿时,CUnit 的最新版本是CUnit-2.1-3,本文正是基于 CUnit-2.1-3 版本的 CUnit Users Guide 的中文翻译,原版 Users Guide 在 CUnit-2.1-3 源码目录 doc/index.html 中。注意:未来官方版本随时可能变更,此中文翻译可能跟官网英文会有差异,务必注意版本区别。

1. CUnit单元测试简介

1.1. 描述

CUnit是一个在C语言中编写、管理和运行单元测试的系统,它往往被编译成库(静态库或动态库)的形式提供给用户测试代码进行链接。

CUnit使用一个简单的框架作为构建测试结构,它提供了一组丰富的断言来测试常见的数据类型。此外,它还提供了多种不同的接口用来运行测试和报告结果。这包括由代码控制的测试和报告的自动化接口,以及允许由用户动态运行测试和查看结果的交互式接口。

以下头文件对常见用户会用到的数据类型和函数做了声明:

Header FileDescription
#include <CUnit/CUnit.h>ASSERT(断言)宏在测试案例中使用,包括其他框架的头文件。
#include <CUnit/CUError.h>错误处理函数和数据类型。
被CUnit.h文件自动包含。
#include <CUnit/TestDB.h>数据类型的定义和测试套件、测试用例的注册功能接口。
被CUnit.h文件自动包含。
#include <CUnit/TestRun.h>数据类型的定义和运行测试、检索结果的功能接口。
被CUnit.h文件自动包含。
#include <CUnit/Automated.h>自动XML输出接口。
#include <CUnit/Basic.h>一个非交互的输出到标准输出(stdout)的基本接口。
#include <CUnit/Console.h>交互式控制台界面。
#include <CUnit/CUCurses.h>交互式控制台接口(* nix平台)。
#include <CUnit/Win.h>Windows界面(尚未实施)。

1.2. 结构

CUnit是平台无关的框架与各种用户接口的组合。核心框架为管理测试注册表,套件和测试用例提供了基本支持。用户接口便于与框架交互以运行测试和查看结果。

CUnit的组织结构与传统的单元测试框架类似:

                  Test Registry
                        |
         ------------------------------
         |                            |
      Suite '1'      . . . .       Suite 'N'
         |                            |
   ---------------             ---------------
   |             |             |             |
Test '11' ... Test '1M'     Test 'N1' ... Test 'NM'

单独的测试用例(Test)被打包到套件(Suite)中,这些套件又被注册到活动测试注册表(Test Registry)里。每个套件都有自己的构造和析构函数,这两个函数将在运行套件测试之前和之后被自动调用。注册表中的所有套件/测试用例,可以通过调用一个函数执行全部测试,也可以有选择性地执行部分测试。

1.3. 通常用法

使用CUnit框架的典型步骤是:

  1. 编写待测函数(必要时,还得编写套件的 init/cleanup 函数)。
  2. 初始化测试注册表(Test Registry) - CU_initialize_registry()
  3. 将套件(Suite)添加到测试注册表 - CU_add_suite()
  4. 将测试用例(Test)加到套件里 - CU_add_test()
  5. 调用合适的接口函数执行测试,例如CU_console_run_tests
  6. 清除测试注册表 - CU_cleanup_registry

1.4. CUnit第2版API变更

在Cunit中所有公共函数名现在都以 ‘CU_’ 作为前缀。这有利于避免与用户代码中的函数名发生冲突。需要注意的是,早期版本的CUnit使用的是不同的函数名,而且没有这个前缀。旧的API名称已弃用但仍受支持。要使用旧的名称,用户代码必须使用USE_DEPRECATED_CUNIT_NAMES的宏定义进行编译。

那些已弃用的API函数在文档相应部位都有进行描述。

2. 编写CUnit测试用例

2.1. 测试函数

CUnit测试用例是一个C函数,其函数签名(语法格式)为:

void test_func(void)

测试函数除了不应该修改CUnit框架(比如:添加套件或测试用例,修改测试注册表,或启动测试执行)之外,它的内容没有任何限制。测试函数可以调用其他函数(当然也不可以修改框架)。当执行测试时,已注册的测试用例对应的测试函数将会被调用。

以下例子是对「返回2个整数中的最大数值的函数」进行测试的例程:

int maxi(int i1, int i2)
{
  return (i1 > i2) ? i1 : i2;
}

void test_maxi(void)
{
  CU_ASSERT(maxi(0,2) == 2);
  CU_ASSERT(maxi(0,-2) == 0);
  CU_ASSERT(maxi(2,2) == 2);
}

2.2. CUnit断言

CUnit提供了一组用于测试逻辑条件的断言。这些断言的成功或失败会被框架跟踪记录,当测试执行结束后可以对记录的结果进行查看。

每个断言测试一个单一的逻辑条件,如果条件的计算结果为CU_FALSE则失败。断言失败后,测试函数将继续执行,除非用户选择 xxx_FATAL 版本的断言。这种情况下,测试函数将被中止执行,并立即返回。FATAL版本的断言应谨慎使用,一旦 FATAL 版本断言失败,测试函数就没有机会执行自身的清理工作。但是,普通套件的清理函数不受影响,两个版本的断言都会执行套件的清理函数。

还有一些特殊的断言,用于在不执行逻辑测试的情况下,向框架注册通过或失败。这对于测试控制流或其他不需要逻辑测试的情况都非常有用:

void test_longjmp(void)
{
  jmp_buf buf;
  int i;

  i = setjmp(buf);
  if (i == 0) {
    run_other_func();
    CU_PASS("run_other_func() succeeded.");
  }
  else
    CU_FAIL("run_other_func() issued longjmp.");
}

那些被已注册的测试函数调用的其他函数,可以自由地使用CUnit断言,这些断言将被计算到调用它们的测试函数中,也可以使用FATAL版本的断言 - 如果断言失败将中止原始测试函数和整个调用链。

CUnit定义的断言:

#include <CUnit/CUnit.h>

断言描述
CU_ASSERT(int expression)
CU_ASSERT_FATAL(int expression)
CU_TEST(int expression)
CU_TEST_FATAL(int expression)
Assert that expression is TRUE (non-zero)
CU_ASSERT_TRUE(value)
CU_ASSERT_TRUE_FATAL(value)
Assert that value is TRUE (non-zero)
CU_ASSERT_FALSE(value)
CU_ASSERT_FALSE_FATAL(value)
Assert that value is FALSE (zero)
CU_ASSERT_EQUAL(actual, expected)
CU_ASSERT_EQUAL_FATAL(actual, expected)
Assert that actual = = expected
CU_ASSERT_NOT_EQUAL(actual, expected))
CU_ASSERT_NOT_EQUAL_FATAL(actual, expected)
Assert that actual != expected
CU_ASSERT_PTR_EQUAL(actual, expected)
CU_ASSERT_PTR_EQUAL_FATAL(actual, expected)
Assert that pointers actual = = expected
CU_ASSERT_PTR_NOT_EQUAL(actual, expected)
CU_ASSERT_PTR_NOT_EQUAL_FATAL(actual, expected)
Assert that pointers actual != expected
CU_ASSERT_PTR_NULL(value)
CU_ASSERT_PTR_NULL_FATAL(value)
Assert that pointer value == NULL
CU_ASSERT_PTR_NOT_NULL(value)
CU_ASSERT_PTR_NOT_NULL_FATAL(value)
Assert that pointer value != NULL
CU_ASSERT_STRING_EQUAL(actual, expected)
CU_ASSERT_STRING_EQUAL_FATAL(actual, expected)
Assert that strings actual and expected are equivalent
CU_ASSERT_STRING_NOT_EQUAL(actual, expected)
CU_ASSERT_STRING_NOT_EQUAL_FATAL(actual, expected)
Assert that strings actual and expected differ
CU_ASSERT_NSTRING_EQUAL(actual, expected, count)
CU_ASSERT_NSTRING_EQUAL_FATAL(actual, expected, count)
Assert that 1st count chars of actual and expected are the same
CU_ASSERT_NSTRING_NOT_EQUAL(actual, expected, count)
CU_ASSERT_NSTRING_NOT_EQUAL_FATAL(actual, expected, count)
Assert that 1st count chars of actual and expected differ
CU_ASSERT_DOUBLE_EQUAL(actual, expected, granularity)
CU_ASSERT_DOUBLE_EQUAL_FATAL(actual, expected, granularity)
Assert that (actual - expected) <= (granularity)
Math library must be linked in for this assertion.
CU_ASSERT_DOUBLE_NOT_EQUAL(actual, expected, granularity)
CU_ASSERT_DOUBLE_NOT_EQUAL_FATAL(actual, expected, granularity)
Assert that (actual - expected) > (granularity)
Math library must be linked in for this assertion.
CU_PASS(message)Register a passing assertion with the specified message. No logical test is performed.
CU_FAIL(message)
CU_FAIL_FATAL(message)
Register a failed assertion with the specified message. No logical test is performed.

2.3. 已弃用的V1版本的断言

从版本2开始,不推荐使用以下断言。要使用这些断言,用户代码必须使用USE_DEPRECATED_CUNIT_NAMES宏定义进行编译。需要注意的是,它们的行为与版本1中的行为是相同的(失败时发出’return’语句)。

#include <CUnit/CUnit.h>

弃用名称等效新名称
ASSERTCU_ASSERT_FATAL
ASSERT_TRUECU_ASSERT_TRUE_FATAL
ASSERT_FALSECU_ASSERT_FALSE_FATAL
ASSERT_EQUALCU_ASSERT_EQUAL_FATAL
ASSERT_NOT_EQUALCU_ASSERT_NOT_EQUAL_FATAL
ASSERT_PTR_EQUALCU_ASSERT_PTR_EQUAL_FATAL
ASSERT_PTR_NOT_EQUALCU_ASSERT_PTR_NOT_EQUAL_FATAL
ASSERT_PTR_NULLCU_ASSERT_PTR_NULL_FATAL
ASSERT_PTR_NOT_NULLCU_ASSERT_PTR_NOT_NULL_FATAL
ASSERT_STRING_EQUALCU_ASSERT_STRING_EQUAL_FATAL
ASSERT_STRING_NOT_EQUALCU_ASSERT_STRING_NOT_EQUAL_FATAL
ASSERT_NSTRING_EQUALCU_ASSERT_NSTRING_EQUAL_FATAL
ASSERT_NSTRING_NOT_EQUALCU_ASSERT_NSTRING_NOT_EQUAL_FATAL
ASSERT_DOUBLE_EQUALCU_ASSERT_DOUBLE_EQUAL_FATAL
ASSERT_DOUBLE_NOT_EQUALCU_ASSERT_DOUBLE_NOT_EQUAL_FATAL

3. 测试注册表

3.1. 摘要

#include <CUnit/TestDB.h> (included automatically by <CUnit/CUnit.h>)

  typedef struct CU_TestRegistry
  typedef CU_TestRegistry*  CU_pTestRegistry

  CU_ErrorCode     CU_initialize_registry(void)
  void             CU_cleanup_registry(void)
  CU_pTestRegistry CU_get_registry(void)
  CU_pTestRegistry CU_set_registry(CU_pTestRegistry pTestRegistry)
  CU_pTestRegistry CU_create_new_registry(void)
  void             CU_destroy_existing_registry(CU_pTestRegistry* ppRegistry)

3.2. 注册表内部结构

测试注册表是套件和相关测试用例的仓库。CUnit维护着这个活动的注册表,当用户添加套件或测试用例时会更新该注册表。活动注册表中的套件就是用户选择运行所有测试用例时运行的套件。

CUnit测试注册表是<CUnit/TestDB.h>中声明的数据结构CU_TestRegistry。它包括存储在注册表中的套件和测试用例的总数,以及一个指向已注册的套件链表的头指针。

typedef struct CU_TestRegistry
{
  unsigned int uiNumberOfSuites;
  unsigned int uiNumberOfTests;
  CU_pSuite    pSuite;
} CU_TestRegistry;

typedef CU_TestRegistry* CU_pTestRegistry;

用户通常只需要在使用前初始化注册表,使用后进行清理。但是,在必要时也可以使用其他一些函数操作注册表。

3.3. 注册表初始化

  • CU_ErrorCode CU_initialize_registry(void)

    CUnit测试注册表在使用之前必须先初始化。用户在调用任何其他CUnit函数之前应先调用CU_initialize_registry()。不这样做可能会导致系统崩溃。如果多次调用此函数,已存在的注册表都将被清除(即销毁),然后才创建一个新的注册表。在执行测试期间(即测试函数或套件初始化/清除函数中)不应该调用此函数。

    返回值:

    返回值描述
    CUE_SUCCESS初始化成功
    CUE_NOMEMORY内存分配失败
  • CU_BOOL CU_registry_initialized(void)

    此函数可用于检查注册表是否已初始化。如果注册表设置分布在多个文件上,为了确保注册表已准备好进行测试注册,这时会很有用。

3.4. 注册表清除

  • void CU_cleanup_registry(void)

    当测试完成后,用户应调用此函数来清理和释放框架使用的内存。这应该是最后一个调用的CUnit函数(除非使用CU_initialize_registry()或CU_set_registry()恢复测试注册表)。

    未调用 CU_cleanup_registry() 将导致内存泄漏。它可以被多次调用,且不会报错。注意,这个函数会销毁注册表中的所有套件(以及相关联的测试用例)。清理注册表之后,指向已注册的套件和测试用例的指针不应该再被引用。在执行测试期间(即测试函数或套件初始化/清除函数中)不应该调用此函数。

    调用 CU_cleanup_registry() 只会影响CUnit框架维护的内部CU_TestRegistry。那些隶属于用户的的测试注册表,用户有责任自己销毁。可以显式地通过调用CU_destroy_existing_registry()来完成,也可以隐式地通过先调用CU_set_registry()激活注册表,再调用CU_cleanup_registry()来完成。

3.5. 其他注册表函数

其他注册表函数主要用于内部和测试目的。但是,一般用户可以了解它们的用途,并且应该知道它们。

其中包括:

  • CU_pTestRegistry CU_get_registry(void)

    返回一个指向活动测试注册表的指针。注册表是一个CU_TestRegistry数据类型的变量。不建议直接操作内部测试注册表,应该使用API函数操作。框架维护注册表的所有权,因此当调用CU_cleanup_registry()或CU_initialize_registry()函数之后,该函数返回的指针将会失效。

  • CU_pTestRegistry CU_set_registry(CU_pTestRegistry pTestRegistry)

    用指定的注册表替换活动注册表,返回指向之前注册表的指针。调用者有责任销毁旧的注册表,这可以通过为返回的指针调用CU_destroy_existing_registry()来显式地完成。或者,也可以先使用CU_set_registry()激活注册表,再调用CU_cleanup_registry()隐式地销毁注册表。应注意不要显式地销毁被激活的注册表,这会导致同一个内存块被多次释放,很可能会引起崩溃。

  • CU_pTestRegistry CU_create_new_registry(void)

    创建一个新的注册表并返回一个指向它的指针。新的注册表不包含任何的套件或测试用例。调用者有责任通过前面描述的机制之一销毁新的注册表。

  • void CU_destroy_existing_registry(CU_pTestRegistry* ppRegistry)

    销毁并释放指定测试注册表的所有内存,包括所有已注册的套件和测试用例。对于被设置为激活状态的注册表(例如,CU_get_registry()返回的CU_pTestRegistry指针),不应调用此函数。这会导致在调用CU_cleanup_registry()时多次释放相同的内存。ppRegistry可能不为NULL,但指针内容可以为空。在这种情况下,该函数不起作用。请注意,本函数返回时会将 *ppRegistry 设置为NULL。

3.6. 已弃用的V1版本的数据类型和函数

从版本2开始,不推荐使用以下数据类型和函数。要使用这些已弃用的名称,用户代码必须使用USE_DEPRECATED_CUNIT_NAMES宏定义进行编译。

#include <CUnit/TestDB.h> (included automatically by CUnit/CUnit.h>).

弃用名称等效新名称
_TestRegistryCU_TestRegistry
_TestRegistry.uiNumberOfGroups
PTestRegistry->uiNumberOfGroups
CU_TestRegistry.uiNumberOfSuites
CU_pTestRegistry->uiNumberOfSuites
_TestRegistry.pGroup
PTestRegistry->pGroup
CU_TestRegistry.pSuite
CU_pTestRegistry->pSuite
PTestRegistryCU_pTestRegistry
initialize_registry()CU_initialize_registry()
cleanup_registry()CU_cleanup_registry()
get_registry()CU_get_registry()
set_registry()CU_set_registry()

4. 管理测试用例和套件

为了让CUnit运行一个测试用例,必须将其添加到由测试注册表注册的测试集(套件)中。

4.1. 摘要

#include <CUnit/TestDB.h> (included automatically by <CUnit/CUnit.h>)

  typedef struct CU_Suite
  typedef CU_Suite*  CU_pSuite

  typedef struct CU_Test
  typedef CU_Test*  CU_pTest

  typedef void (*CU_TestFunc)(void)
  typedef int  (*CU_InitializeFunc)(void)
  typedef int  (*CU_CleanupFunc)(void)

  CU_pSuite CU_add_suite(const char* strName,
                         CU_InitializeFunc pInit,
                         CU_CleanupFunc pClean);

  CU_pTest  CU_add_test(CU_pSuite pSuite,
                        const char* strName,
                        CU_TestFunc pTestFunc);

  typedef struct CU_TestInfo
  typedef struct CU_SuiteInfo

  CU_ErrorCode CU_register_suites(CU_SuiteInfo suite_info[]);
  CU_ErrorCode CU_register_nsuites(int suite_count, ...);

  CU_ErrorCode CU_set_suite_active(CU_pSuite pSuite, CU_BOOL fNewActive)
  CU_ErrorCode CU_set_test_active(CU_pTest, CU_BOOL fNewActive)

  CU_ErrorCode CU_set_suite_name(CU_pSuite pSuite, const char *strNewName)
  CU_ErrorCode CU_set_suite_initfunc(CU_pSuite pSuite, CU_InitializeFunc pNewInit)
  CU_ErrorCode CU_set_suite_cleanupfunc(CU_pSuite pSuite, CU_CleanupFunc pNewClean)

  CU_ErrorCode CU_set_test_name(CU_pTest pTest, const char *strNewName)
  CU_ErrorCode CU_set_test_func(CU_pTest pTest, CU_TestFunc pNewFunc)

  CU_pSuite CU_get_suite(const char* strName)
  CU_pSuite CU_get_suite_at_pos(unsigned int pos)
  unsigned int CU_get_suite_pos(CU_pSuite pSuite)
  unsigned int CU_get_suite_pos_by_name(const char* strName)

  CU_pTest CU_get_test(CU_pSuite pSuite, const char *strName)
  CU_pTest CU_get_test_at_pos(CU_pSuite pSuite, unsigned int pos)
  unsigned int CU_get_test_pos(CU_pSuite pSuite, CU_pTest pTest)
  unsigned int CU_get_test_pos_by_name(CU_pSuite pSuite, const char *strName)

4.2. 往注册表中添加套件

  • CU_pSuite CU_add_suite(const char* strName, CU_InitializeFunc pInit, CU_CleanupFunc pClean)

    创建一个新的测试用例集(套件),它具有制定的名称,初始化函数(或称为构造函数)和清理函数(或称为析构函数)。新的套件注册在(隶属于)测试注册表,因此在添加套件之前,必须先对注册表进行初始化。目前的实现并不支持创建独立于注册表的套件。在执行测试期间(即测试函数或套件初始化/清除函数中)不应该调用此函数。

    建议注册表中的每个套件的名称是唯一的。这有助于按名称查找套件,仅查找符合给定名称的第一个套件。初始化和清理函数是可选的,并作为指针传递给这些函数,即运行套件中的测试用例之前和之后要调用的函数。这使得套件可以 setUp 和 tearDown 临时 fixtures(固定环境),以支持运行测试用例。这些函数没有参数,如果他们执行成功应返回零(否则返回非零值)。如果套件不需要这些函数中的一个或两个,可以传递NULL给CU_add_suite()。

    解释下什么是 setUp、tearDown 和 fixtures(本段文字原文中没有)。Test Fixture 是指一个测试运行所需的固定环境,准确的定义:The test fixture is everything we need to have in place to exercise the SUT. 在进行测试时,我们通常需要把环境设置成已知状态(如创建对象、获取资源等)来创建测试,每次测试开始时都处于一个固定的初始状态(通过setUp实现);测试结果后需要将测试状态还原(通过tearDown实现)。setUp用于测试用例执行前的初始化工作,tearDown与之对应,用于测试用例执行后的善后工作。

    该函数返回一个指向新套件的指针,这是向套件添加测试用例时所需要的。如果发生错误,则返回NULL,框架错误代码被设置为下列之一:

    返回值描述
    CUE_SUCCESS创建成功
    CUE_NOREGISTRY注册表未初始化
    CUE_NO_SUITENAME套件名(strName)为空NULL
    CUE_DUP_SUITE套件名不唯一
    CUE_NOMEMORY内存申请失

4.3. 往套件中添加测试用例

  • CU_pTest CU_add_test(CU_pSuite pSuite, const char* strName, CU_TestFunc pTestFunc)

    创建一个具有指定名称和测试功能的新测试用例,并将其注册到指定的套件中。所指定的套件必须是已经调用CU_add_suite()创建好了的。目前的实现并不支持创建独立于套件的测试用例。在执行测试期间(即测试函数或套件初始化/清除函数中)不应该调用此函数。

    建议同一个套件中的每个测试用例的名称是唯一的。这有助于按名称查找测试用例,仅查找符合给定名称的第一个测试用例。测试函数不能为空,须指向一个运行测试时被调用的函数。测试函数既没有参数也没有返回值。

    该函数返回一个指向新测试用例的指针。如果创建测试用例过程中发生错误,则返回NULL,框架错误代码被设置为下列之一:

    返回值描述
    CUE_SUCCESS创建成功
    CUE_NOREGISTRY注册表未初始化
    CUE_NOSUITE指定的套件无效
    CUE_NO_TESTNAME测试用例名称(strName)为空NULL
    CUE_NO_TEST测试函数指针无效
    CUE_DUP_TEST测试用例名不唯一
    CUE_NOMEMORY内存申请失败

4.4. 管理测试用例的快捷方法

  • #define CU_ADD_TEST(suite, test) (CU_add_test(suite, #test, (CU_TestFunc)test))

    这个宏会根据测试函数名自动生成唯一的测试用例名称,并将其添加到指定的套件中。用户应检查返回值以验证是否成功。

  • CU_ErrorCode CU_register_suites(CU_SuiteInfo suite_info[])
    CU_ErrorCode CU_register_nsuites(int suite_count, ...)

    对于有着许多测试用例和套件的大型的测试结构,管理测试用例/套件之间的关系及注册工作是是繁琐且容易出错的。CUnit提供了一个特殊的注册系统来帮助管理套件和测试用例。它的主要优点是统一对套件及其关联的测试用例进行注册,并最大程度地减少用户需要编写的错误检查代码量。

    测试用例首先分组为CU_TestInfo实例数组(在<CUnit/TestDB.h>中定义):

    CU_TestInfo test_array1[] = {
      { "testname1", test_func1 },
      { "testname2", test_func2 },
      { "testname3", test_func3 },
      CU_TEST_INFO_NULL,
    };
    

    每个数组元素包含一个测试用例的(唯一)名称和测试函数。该数组的结束元素必须为NULL,宏CU_TEST_INFO_NULL很方便地定义了该元素。单个CU_TestInfo数组中包含的测试用例形成了一个测试集,这个测试集将被注册到同一个套件中。

    然后在一个或多个CU_SuiteInfo实例数组中定义套件信息(在<CUnit/TestDB.h>中定义):

    CU_SuiteInfo suites[] = {
      { "suitename1", suite1_init-func, suite1_cleanup_func, test_array1 },
      { "suitename2", suite2_init-func, suite2_cleanup_func, test_array2 },
      CU_SUITE_INFO_NULL,
    };
    

    每个数组元素包含一个套件的(唯一)名称、套件初始化函数、套件清理函数和测试用例集CU_TestInfo数组。同理,如果给定的套件不需要初始化或清除函数则可以设置为NULL。该数组必须以全NULL的元素作为结束,可以使用宏CU_SUITE_INFO_NULL。

    然后,在CU_SuiteInfo数组中定义的所有套件都可以在一条语句中注册:

    CU_ErrorCode error = CU_register_suites(suites);

    如果在注册任何套件或测试用例期间发生错误,则返回错误码。错误码和普通的套件注册和添加测试用例操作返回的错误码是一样的。用户希望在一条语句中注册多个CU_SuiteInfo数组的情况下,可以使用CU_register_nsuites()函数:

    CU_ErrorCode error = CU_register_nsuites(2, suites1, suites2);

    此函数接受可变数量的CU_SuiteInfo数组。第一个参数表示传递的数组的实际数量。

4.5. 激活套件和测试用例

  • CU_ErrorCode CU_set_suite_active(CU_pSuite pSuite, CU_BOOL fNewActive)
    CU_ErrorCode CU_set_test_active(CU_pTest pTest, CU_BOOL fNewActive)

    这些函数可用于禁用或激活各个套件和测试用例。除非套件或测试用例处于激活状态,否则在执行测试期间将不会被执行。所有套件和测试用例在创建时默认是激活的。当前的激活状态分别保存在 pSuite->fActive 和 pTest->fActive 数据结构成员中。如果为激活状态该标志为CU_TRUE,否则为CU_FALSE。这使用户能够动态地选择要执行的部分测试用例集。注意,禁用一个套件或测试用例然后又指定执行它将产生框架错误。这些函数如果执行成功返回CUE_SUCCESS,如果相应的套件(或测试用例)为空的则返回 CUE_NOSUITE (或CUE_NOTEST)。

4.6. 修改套件和测试用例的其他属性

通常,套件和测试用例的属性是在创建时就设置好的。在某些情况下,用户希望可以通过操纵这些属性动态地修改测试结构。为此提供了以下函数,应该使用这些函数,而不是直接设置数据结构成员的值。所有这些函数如果执行成功返回CUE_SUCCESS,失败返回指示的错误代码。查找函数可以帮助用户定位特定的套件和测试用例。

  • CU_ErrorCode CU_set_suite_name(CU_pSuite pSuite, const char *strNewName)
    CU_ErrorCode CU_set_test_name(CU_pTest pTest, const char *strNewName)

    这两个函数用于更改已注册套件和测试用例的名称。当前的名称保存在 pSuite->pName 和 pTest->pName 数据结构成员中。如果 pSuite 或 pTest 为 NULL,将分别返回 CUE_NOSUITE 或 CUE_NOTEST。如果 strNewName 为 NULL,则分别返回 CUE_NO_SUITENAME 或 CUE_NO_TESTNAME。

  • CU_ErrorCode CU_set_suite_initfunc(CU_pSuite pSuite, CU_InitializeFunc pNewInit)
    CU_ErrorCode CU_set_suite_cleanupfunc(CU_pSuite pSuite, CU_CleanupFunc pNewClean)

    这两个函数用于更改已注册套件的初始化函数和清理函数。当前的函数保存在 pSuite->pInitializeFunc 和 pSuite->pCleanupFunc 数据结构成员中。如果 pSuite 为 NULL 则返回 CUE_NOSUITE。

  • CU_ErrorCode CU_set_test_func(CU_pTest pTest, CU_TestFunc pNewFunc)

    该函数用于更改已注册测试用例的测试函数。当前测试函数保存在 pTest->pTestFunc 数据结构成员中。如果 pTest 或者 pNewFunc 为NULL 则返回 CUE_NOTEST。

4.7. 查找单个套件和测试

在大多数情况下,用户将通过 CU_add_suite() 和 CU_add_test() 返回的指针来引用已注册的套件和测试用例。有时,用户可能需要重新获取套件和测试用例的引用。如果用户拥有关于实体的一些信息(注册名称或顺序),下面的函数将协助用户完成此操作。在对套件或测试用例一无所知的情况下,用户需要迭代内部数据结构来枚举套件和测试用例。API不直接支持此功能。

  • CU_pSuite CU_get_suite(const char* strName)
    CU_pSuite CU_get_suite_at_pos(unsigned int pos)
    unsigned int CU_get_suite_pos(CU_pSuite pSuite)
    unsigned int CU_get_suite_pos_by_name(const char* strName)

    这些函数有助于查找在活动注册表中已注册的套件。前2个函数可以通过名称或位置查找套件,找不到则返回NULL。位置是从1开始的索引,范围为[1 … CU_get_registry()->uiNumberOfSuites]。当注册有重名的套件时,这可能很有帮助,在这种情况下,按名称查找只能检索到第一个符合该名称的套件。后2个函数帮助用户识别已注册套件的位置。如果找不到套件,则返回0。此外,如果注册表未初始化,所有这些函数都会将CUnit错误状态设置为CUE_NOREGISTRY。如果strName为NULL,则设置为CUE_NO_SUITENAME,如果pSuite为NULL,则设置为CUE_NOSUITE。

  • CU_pTest CU_get_test(CU_pSuite pSuite, const char *strName)
    CU_pTest CU_get_test_at_pos(CU_pSuite pSuite, unsigned int pos)
    unsigned int CU_get_test_pos(CU_pSuite pSuite, CU_pTest pTest)
    unsigned int CU_get_test_pos_by_name(CU_pSuite pSuite, const char *strName)

    这些函数有助于查找在套件中已注册的测试用例。前2个函数可以通过名称或位置查找测试用例,找不到则返回NULL。位置是从1开始的索引,范围为[1 … pSuite->uiNumberOfSuites]。当注册有重名的测试用例时,这可能很有帮助,在这种情况下,按名称查找只能检索到第一个符合该名称的测试用例。后2个函数帮助用户识别测试用例在套件中的位置。如果找不到测试用例,则返回0。此外,如果注册表未初始化,所有这些函数都会将CUnit错误状态设置为CUE_NOREGISTRY。如果pSuite为NULL,则设置为CUE_NOSUITE,如果strName为NULL,则设置为CUE_NO_TESTNAME,如果pTest为NULL,则设置为CUE_NOTEST。

4.8. 已弃用的V1版本的数据类型和函数

从版本2开始,不推荐使用以下数据类型和函数。要使用这些已弃用的名称,用户代码必须使用USE_DEPRECATED_CUNIT_NAMES宏定义进行编译。

#include <CUnit/TestDB.h> (included automatically by CUnit/CUnit.h>).

弃用名称等效新名称
TestFuncCU_TestFunc
InitializeFuncCU_InitializeFunc
CleanupFuncCU_CleanupFunc
_TestCaseCU_Test
PTestCaseCU_pTest
_TestGroupCU_Suite
PTestGroupCU_pSuite
add_test_group()CU_add_suite()
add_test_case()CU_add_test()
ADD_TEST_TO_GROUP()CU_ADD_TEST()
test_case_tCU_TestInfo
test_group_tCU_SuiteInfo
test_suite_tno equivalent - use CU_SuiteInfo
TEST_CASE_NULLCU_TEST_INFO_NULL
TEST_GROUP_NULLCU_SUITE_INFO_NULL
test_group_registerCU_register_suites()
test_suite_registerno equivalent - use CU_register_suites()

5. 执行测试

5.1. 摘要

#include <CUnit/Automated.h>

  void         CU_automated_run_tests(void)
  CU_ErrorCode CU_list_tests_to_file(void)
  void         CU_set_output_filename(const char* szFilenameRoot)

#include <CUnit/Basic.h>

  typedef enum    CU_BasicRunMode
  CU_ErrorCode    CU_basic_run_tests(void)
  CU_ErrorCode    CU_basic_run_suite(CU_pSuite pSuite)
  CU_ErrorCode    CU_basic_run_test(CU_pSuite pSuite, CU_pTest pTest)
  void            CU_basic_set_mode(CU_BasicRunMode mode)
  CU_BasicRunMode CU_basic_get_mode(void)
  void            CU_basic_show_failures(CU_pFailureRecord pFailure)

#include <CUnit/Console.h>

  void CU_console_run_tests(void)

#include <CUnit/CUCurses.h>

  void CU_curses_run_tests(void)

#include <CUnit/TestRun.h> (included automatically by <CUnit/CUnit.h>)

  unsigned int CU_get_number_of_suites_run(void)
  unsigned int CU_get_number_of_suites_failed(void)
  unsigned int CU_get_number_of_tests_run(void)
  unsigned int CU_get_number_of_tests_failed(void)
  unsigned int CU_get_number_of_asserts(void)
  unsigned int CU_get_number_of_successes(void)
  unsigned int CU_get_number_of_failures(void)

  typedef struct CU_RunSummary
  typedef CU_Runsummary* CU_pRunSummary
  const CU_pRunSummary CU_get_run_summary(void)

  typedef struct CU_FailureRecord
  typedef CU_FailureRecord*  CU_pFailureRecord
  const CU_pFailureRecord CU_get_failure_list(void)
  unsigned int CU_get_number_of_failure_records(void)

  void CU_set_fail_on_inactive(CU_BOOL new_inactive)
  CU_BOOL CU_get_fail_on_inactive(void)

5.2. 在CUnit中运行测试

CUnit支持运行所有已注册套件中的所有测试用例,同时也可以单独运行套件和测试用例。在每次运行期间,框架会跟踪记录执行的套件、测试用例和执行通过或失败的断言数。注意,每次启动测试运行时(即使失败)都会清除先前的结果。如果用户端希望将单个套件或测试用例排除在特定的测试运行之外,则可以禁用它们。但是,禁用一个套件或测试用例然后又明确请求执行它将产生框架错误。

虽然CUnit为运行套件和测试用例提供了基本函数,但大多数用户都希望使用简化的用户接口。这些接口处理与框架交互的细节,并为用户提供测试详细信息和结果的输出。

CUnit库中包含以下接口:

InterfacePlatformDescription
Automatedall非交互式,输出到xml文件
Basicall非交互式,可选输出到stdout
Consoleall交互式,控制台模式
CursesLinux/Unix交互式,curses模式

如果这些接口无法满足需要,用户还可以使用<CUnit/TestRun.h>中定义的原始框架API函数。有关如何直接与原语API交互的示例,请参见各种接口的源代码。

5.3. 自动模式

自动模式的接口是非交互式的。用户启动测试运行,结果输出到XML文件。已注册的套件和测试用例的列表也可以输出到XML文件中。

自动接口API由以下函数组成:

  • void CU_automated_run_tests(void)

    运行所有已注册的(且为激活的)套件中的所有测试用例。测试结果输出到一个文件名为ROOT-Results.xml.的文件中。可以使用CU_set_output_filename()设置文件名ROOT,或者使用默认的CUnitAutomated-Results.xml。注意,如果在每次运行之前没有设置一个有别于ROOT的文件名,则结果文件将被覆盖。

    结果文件支持两种类型,文件类型定义文件(CUnit Run.dtd)和XSL样式表(CUnit Run.xsl)。这些文件在源码和安装路径的Share子目录中都有提供。

  • CU_ErrorCode CU_list_tests_to_file(void)

    列举出已注册套件及相关测试用例到文件中。列表文件名为ROOT-Listing.xml。可以使用CU_set_output_filename()设置文件名ROOT,或者使用默认的CUnitAutomated-Listing.xml。注意,如果在每次运行之前没有设置一个有别于ROOT的文件名,则列表文件将被覆盖。

    列表文件支持两种类型,文件类型定义文件(CUnit Run.dtd)和XSL样式表(CUnit Run.xsl)。这些文件在源码和安装路径的Share子目录中都有提供。

    还要注意的是,列表文件不是由CU_automated_run_tests()自动生成的。当用户需要列表信息时,用户代码必须调用该接口作出明确的请求。

  • void CU_set_output_filename(const char* szFilenameRoot)

    Sets the output filenames for the results and listing files. szFilenameRoot is used to construct the filenames by appending -Results.xml and -Listing.xml, respectively.

    设置结果和列表文件的输出文件名。通过szFilenameRoot并分别附加-Results.xml和-Listing.xml来构成文件名。

5.4. 基本模式

基本模式的接口也是非交互式的,结果输出到标准输出(stdout)。此接口支持运行单独的套件或测试用例,并允许用户代码控制每次运行期间显示的输出类型。此接口为希望简化使用CUnit API的用户提供了最大的灵活性。

提供以下公共函数:

  • CU_ErrorCode CU_basic_run_tests(void)

    运行所有已注册套件中的所有测试用例。仅执行激活的套件,如果遇到非激活的套件,不会将其视为错误,而是跳过。返回测试运行期间发生的第一个错误码。输出类型由当前的运行模式控制,该模式可以使用CU_basic_set_mode()进行设置。

  • CU_ErrorCode CU_basic_run_suite(CU_pSuite pSuite)

    运行指定单一套件中的所有测试用例。返回测试运行期间发生的第一个错误码。如果pSuite为NULL,则返回CUE_NOSUITE,如果pSuite未激活,则返回CUE_SUITE_INACTIVE。输出类型由当前的运行模式控制,该模式可以使用CU_basic_set_mode()进行设置。

  • CU_ErrorCode CU_basic_run_test(CU_pSuite pSuite, CU_pTest pTest)

    运行指定套件中的一个测试用例。返回测试运行期间发生的第一个错误码。如果pSuite为NULL,则返回CUE_NOSUITE,如果pTest为NULL,则返回CUE_NOTEST,如果pSuite未激活,则返回CUE_SUITE_INACTIVE,如果pTest不是套件中已注册的测试用例,则返回CUE_TEST_NOT_IN_SUITE,如果pTest未激活,则返回CUE_TEST_INACTIVE。输出类型由当前的运行模式控制,该模式可以使用CU_basic_set_mode()进行设置。

  • void CU_basic_set_mode(CU_BasicRunMode mode)

    设置运行模式,该模式在测试运行期间控制输出。选择如下:

    模式描述
    CU_BRM_NORMAL打印故障和运行结果
    CU_BRM_SILENT除错误消息外不打印输出
    CU_BRM_VERBOSE最大程度地输出运行的详细信息
  • CU_BasicRunMode CU_basic_get_mode(void)

    获取当前的运行模式。

  • void CU_basic_show_failures(CU_pFailureRecord pFailure)

    将所有失败汇总打印到stdout。不依赖于运行模式。

5.5. 交互式控制台模式

控制台模式的接口是交互式的。用户需要做的就是启动控制台会话,并且用户以交互方式控制测试运行。这些操作包括选择和运行套件和测试用例,以及查看测试结果。要启动控制台会话,使用:

  • void CU_console_run_tests(void)

5.6. 交互式curses模式

curses模式的接口是交互式的。用户需要做的就是启动控制台会话,并且用户以交互方式控制测试运行。这些操作包括选择和运行套件和测试用例,以及查看测试结果。使用此接口需要将ncurses库链接到应用程序中。要启动控制台会话,使用:

  • void CU_curses_run_tests(void)

5.7. 修改常规运行时行为

以下函数允许用户在测试运行期间修改框架的行为:

  • void CU_set_fail_on_inactive(CU_BOOL new_inactive)
    CU_BOOL CU_get_fail_on_inactive(void)

    默认运行时行为是,将非激活的套件和测试用例报告为失败。这样客户就能知道他们的测试结构已经部分被停用。如果用户希望忽略非激活套件和测试用例,可以使用这些函数修改行为。CU_FALSE表示框架将忽略非激活实体; CU_TRUE会将它们视为失败。

5.8. 获得测试结果

以上接口都会呈现测试运行结果,但有时用户代码需要直接访问结果。这些结果包括各种运行计数,以及保存故障详细信息的故障记录链接列表。注意,每次启动新的测试运行或初始化或清理注册表时,都会覆盖测试结果。

访问测试结果的函数如下:

  • unsigned int CU_get_number_of_suites_run(void)
    unsigned int CU_get_number_of_suites_failed(void)
    unsigned int CU_get_number_of_tests_run(void)
    unsigned int CU_get_number_of_tests_failed(void)
    unsigned int CU_get_number_of_asserts(void)
    unsigned int CU_get_number_of_successes(void)
    unsigned int CU_get_number_of_failures(void)

    这些函数报告在上一次运行期间已运行或失败的套件、测试用例和断言的数量。如果套件的初始化或清理函数返回非NULL,或者套件处于非激活状态并且框架设置为将非激活套件/测试用例视为失败,则该套件被认为失败。一个测试用例中任何一个断言失败,或者在相同条件下处于非激活状态,则该测试用例被认定为失败。最后3个函数返回的是对应情况的断言数目。非激活套件(或测试用例)不计入已运行套件(测试用例)数量中。这样做的结果是,套件(或测试用例)即使没有报告已经运行也会失败。

    要获取已注册套件和测试用例的总数,请分别使用CU_get_registry()−>uiNumberOfSuites和CU_get_registry()−>uiNumberOfTests。

  • const CU_pRunSummary CU_get_run_summary(void)

    一次性获取所有测试结果计数。返回值是一个指向包含计数结果值的存储结构的指针。此数据类型在<CUnit/TestRun.h>中定义(由<CUnit/CUnit.h>自动包含):

    typedef struct CU_RunSummary
    {
      unsigned int nSuitesRun;
      unsigned int nSuitesFailed;
      unsigned int nTestsRun;
      unsigned int nTestsFailed;
      unsigned int nAsserts;
      unsigned int nAssertsFailed;
      unsigned int nFailureRecords;
    } CU_RunSummary;
    
    typedef CU_Runsummary* CU_pRunSummary;
    

    返回的指针的结构变量为框架所拥有,因此用户不应该释放或以其他方式更改它。注意,一旦启动另一个测试运行,指针可能失效。

  • const CU_pFailureRecord CU_get_failure_list(void)

    获取一个链表,该链表记录着最后一次测试运行期间发生的所有失败(NULL代表没有失败)。返回值的数据类型在<CUnit/TestRun.h> 中定义(由<CUnit/CUnit.h>自动包含)。每个失败记录都包含有关失败位置和性质的信息:

    typedef struct CU_FailureRecord
    {
      unsigned int  uiLineNumber;
      char*         strFileName;
      char*         strCondition;
      CU_pTest      pTest;
      CU_pSuite     pSuite;
    
      struct CU_FailureRecord* pNext;
      struct CU_FailureRecord* pPrev;
    
    } CU_FailureRecord;
    
    typedef CU_FailureRecord*  CU_pFailureRecord;
    

    返回的指针的结构变量为框架所拥有,因此用户不应该释放或以其他方式更改它。注意,一旦启动另一个测试运行,指针可能失效。

  • unsigned int CU_get_number_of_failure_records(void)

    获取由CU_get_failure_list()返回的失败链表中CU_FailureRecord结点的个数。请注意,这个数值可能比失败的断言数量多,因为套件的初始化和清除的失败也计算在内。

5.9. 已弃用的V1版本的数据类型和函数

从版本2开始,不推荐使用以下数据类型和函数。要使用这些已弃用的名称,用户代码必须使用USE_DEPRECATED_CUNIT_NAMES宏定义进行编译。

弃用名称等效新名称
automated_run_tests()CU_automated_run_tests() plus
CU_list_tests_to_file()
set_output_filename()CU_set_output_filename()
console_run_tests()CU_console_run_tests()
curses_run_tests()CU_curses_run_tests()

6. 错误处理

6.1. 摘要

#include <CUnit/CUError.h> (included automatically by <CUnit/CUnit.h>)

  typedef enum CU_ErrorCode
  CU_ErrorCode   CU_get_error(void);
  const char*    CU_get_error_msg(void);

  typedef enum CU_ErrorAction
  void           CU_set_error_action(CU_ErrorAction action);
  CU_ErrorAction CU_get_error_action(void);

6.2. CUnit错误处理

CUnit大多数函数设置错误代码来表示的框架错误状态。有些函数返回错误代码,而有些函数只是设置错误代码,并返回一些其他值。以下两个函数用于检查框架错误状态:

CU_ErrorCode CU_get_error(void)
const char* CU_get_error_msg(void)

第一个函数返回错误代码本身,而第二个函数返回描述错误状态的消息。错误代码是在<CUnit/CUError.h>文件中定义的一个CU_ErrorCode枚举类型。

Error ValueDescription
CUE_SUCCESS无错误状态
CUE_NOMEMORY内存申请失败
CUE_NOREGISTRY注册表未初始化
CUE_REGISTRY_EXISTS在没有CU_cleanup_registry()前尝试CU_set_registry()
CUE_NOSUITE所需的CU_pSuite指针为NULL
CUE_NO_SUITENAME未提供所需CU_Suite名
CUE_SINIT_FAILED套件初始化失败
CUE_SCLEAN_FAILED套件清除失败
CUE_DUP_SUITE不允许套件重名
CUE_NOTEST所需的CU_pTest指针为NULL
CUE_NO_TESTNAME未提供所需的CU_Test名
CUE_DUP_TEST不允许测试用例重名
CUE_TEST_NOT_IN_SUITE测试用例未注册在指定的套件中
CUE_TEST_INACTIVE测试用例未激活
CUE_FOPEN_FAILED打开一个文件时发生错误
CUE_FCLOSE_FAILED关闭一个文件时发生错误
CUE_BAD_FILENAME无效的文件名(NULL,空的,不存在等)
CUE_WRITE_ERROR写入文件时发生错误

6.3. 框架错误时的行为

遇到一个错误条件时的默认行为是设置错误代码并继续执行。在这种情况下,失败的断言不被认为是“框架错误”。其他错误条件还包括套件初始化或者清理失败,非激活套件或测试用例被显示地执行,等等。有时,用户可能更希望执行测试时停在框架发生错误的地方,甚至让测试程序退出。这种行为可以由用户设置,提供以下函数:

  • void CU_set_error_action(CU_ErrorAction action)
    CU_ErrorAction CU_get_error_action(void)

    action是一个定义在<CUnit/CUError.h>文件中的CU_ErrorAction枚举型变量。定义如下:

Error ValueDescription
CUEA_IGNORE运行时发生错误,继续运行(默认)
CUEA_FAIL运行时发生错误,停止
CUEA_ABORT运行时发生错误,退出应用程序

6.4. 已弃用的V1版本的数据类型和函数

从版本2开始,不推荐使用以下数据类型和函数。要使用这些已弃用的名称,用户代码必须使用USE_DEPRECATED_CUNIT_NAMES宏定义进行编译。

Deprecated NameEquivalent New Name
get_error()CU_get_error_msg()
error_code没用. 使用 CU_get_error()
  • 48
    点赞
  • 65
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: CUnit是一个用于开发和执行C程序的单元测试框架。开发者可以使用CUnit来编写测试用例,以验证程序的各个模块的功能是否正确。以下是CUnit单元测试用例开发的一般流程: 1. 安装CUnit:首先需要从CUnit的官方网站下载并安装CUnit库。 2. 引入头文件:在测试用例的C文件中引入CUnit的头文件,以便使用CUnit的相关函数和宏。 3. 定义测试用例:在测试用例的C文件中,可以使用CUnit提供的宏来定义测试用例及其相关的测试函数。测试函数应该包含一系列测试断言,用于验证被测程序的输出是否符合预期。 4. 初始化测试套件和测试注册:使用CUnit提供的宏和函数,初始化测试套件并将测试用例注册到测试套件中。 5. 执行测试用例:使用CUnit提供的函数,执行测试套件中的所有测试用例。 6. 生成测试报告:CUnit会自动记录测试结果,包括测试通过和测试失败的情况,还可以生成详细的测试报告。 7. 分析和修复错误:根据测试报告,开发者可以分析测试失败的原因,并修改被测程序中的错误。 通过CUnit单元测试用例开发,可以有效地提高程序的质量和稳定性。测试用例可以覆盖程序的各个功能模块,验证其正确性和健壮性。同时,CUnit还能提供详细的测试报告,让开发者更容易发现并修复错误。 ### 回答2: CUnit是一个用于C语言项目的单元测试框架。在软件开发过程中,为了保证代码的质量和稳定性,需要对不同的函数模块进行单元测试CUnit可以帮助开发人员编写和执行这些测试用例。 用CUnit进行单元测试用例开发需要以下几个步骤: 第一步是创建测试用例。测试用例是一段测试代码,用于验证功能模块的正确性。开发人员需要根据功能要求和预期结果,编写一系列测试用例。 第二步是编写测试代码。测试代码中包含了一系列宏和函数,用于定义测试集合、测试套件和测试用例。开发人员需要定义不同的测试集合,并将测试用例添加到相应的集合中。 第三步是执行测试。通过调用CUnit提供的函数,开发人员可以执行之前定义的测试集合。CUnit将自动执行测试用例,并记录测试结果。开发人员可以查看测试结果,以确定功能模块的正确性。 第四步是分析测试结果。通过查看测试结果,开发人员可以了解哪些测试用例通过了,哪些失败了。通过分析失败的测试用例,可以找到代码中的问题,并进行修复。 最后一步是反复迭代测试过程。在软件开发过程中,需要不断进行单元测试,以确保代码的质量和稳定性。开发人员可以修改测试用例和测试代码,并重复执行测试过程,直到代码满足预期结果为止。 通过CUnit单元测试用例开发,开发人员可以更好地验证和调试功能模块,提高代码的质量和稳定性,从而提高整个软件项目的可靠性。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值