第一章 Wings 单元测试自动编码引擎

随着科技的飞速发展,软件系统越来越复杂,在系统测试阶段不断遇到的瓶颈,迫使行业逐步追根溯源到了单元测试阶段。软件缺陷发现得越晚,其处理费用就越呈几何激增,因此测试左移概念已经成为趋势。

单元测试面临的最大问题是单元测试用例编写工作量巨大,极端情况下与开发工作量比达到 1:1,甚至更高,这使大部分开发团队要么主动忽视单元测试,要么象征性的走个流程。

如果可以让计算机先对被测试程序进行全局分析和深度理解,再由计算机进行全自动的完成单元测试编码,同时还能确保自动编写的代码无语法、语义错误的直接运行起来,这种用计算机智能算法全自动产生的测试编码去验证开发人员编写的源代码逻辑输入输出对错的高端测试模式,无疑是未来软件测试领域最为璀璨的 “明珠” 技术。

国外软件诸如 c++ test 完成了这个领域的初步技术探索,星云测试 Wings(目前支持 c/c++ 程序),则大踏步完成了整体技术跨越。

Wings 可以对程序参数进行深度解析,比如 c++ 类、模板类、数组、结构体、指针、链表以及任意复杂结构的层级嵌套,同时对于面向对象的程序特性以及常用的容器库,能够完美识别和支持。对于一些 void*、函数指针、模板类等无法直接静态分析进行类型确定的特殊情况,均有基于人工智能的程序分析辅助进行类型确定。

Wings 在基于深度参数解析的基础上,对于全局范围的程序进行理解分析后,第一步 按照内置规则,自动化构建被测程序的输入用例代码;第二步 构建测试代码用于调用被测程序的源代码;第三步 构建被测程序输出断言,完成调用被测试程序单元的全部环境准备。这个构建速度非常快,可以达到每分钟 100 万行左右的生成速度,编写的代码比程序开发人员手工编写的规范度高出一截,并确保 100% 的语法语义正确,免去大量的调试时间。

在驱动数据上,Wings 实现了驱动代码和数据的分离。Wings 基于深度参数解析基础上,可以根据参数的结构自动生成层级嵌套的测试数据结构,用图形界面可视化的展示给用户。用户只需要根据 Wings 提供的界面向导对测试数据进行填充即可,驱动程序会自动识别并读取这些数据,完成对被测试程序的调用。

Wings 还可以全自动生成参数捕获程序,并自动插装在被测试程序中。当被测试程序运行后,可以通过专用软件捕获程序中每个函数模块运行的具体参数值。Wings 的测试代码驱动自动生成和参数捕获,相当于完成了一种全智能的闭环测试验证体系。Wings 使测试数据不需要人工准备,只需要在前序轮次中通过参数捕获自动存储。若前序测试用例运行正常,那么这些数据都可以作为后续测试输入和进行校验的基础数据。

第二章 单元测试自动生成技术

2.1 测试左移后单元测试面临的问题

测试左移后,传统的单元测试一般面临很多问题,主要如下:

(1) 传统程序级测试用例的编写会耗费开发人员大量的工时,比如 TDD 测试驱动开发里面的单元测试无法有效实施,导致所有测试几乎全部依赖于系统级黑盒测试。程序级测试用例的开发成本是功能实现代码本身时间至少为 1:1,绝大部分企业选择放弃开发程序级测试,而采用系统级测试方法。

(2) 需求发生变化导致程序实现发生变化后,程序集用例也需要发生变化,和自动化面临的问题一样,单元测试本身的可维护性问题导致投入是持续的而不是一次性的,会打消其企业应用的热情。

(3) 很多单元在未组装成系统的时候切入,如果需要进行测试需要进行大量的 mock 操作或者桩模拟,这个过程会造成单元测试的不精确性。

(4) 程序集测试数据量很大,全部需要用户来进行准备无法全自动从前序系统测试过程中获取。

针对以上问题,很多业内人士提出了很多办法,例如自动生成测试用例、自动构建测试驱动、模糊测试方法等诸多方式,但是实际开发中程序输入参数十分复杂,简单的输入已经不能满足,如何能够构建复杂的参数输入,是要解决的重要问题。因此,星云测试研发了 Wings 产品 -- 单元级的自动编码引擎,它不仅能够自动构建测试驱动,还能处理十分复杂的大型程序参数,帮助企业能够更好的进行单元测试。

2.2 完成单元测试要素

构成单元测试的要素如下:

a. 测试数据

b. 测试驱动代码

例如针对以下程序,需要准备的测试输入以及测试代码

① 全局变量:c

② 参数:a、b

③ 预期输出:320 30

int c = 100;
int sum(int a, int b)
{
    return a + b + c;
}
int driver_sum()
{
    int a = 100, b = 20;
    c = 200;
    return sum(a, b);
}
TEST(test, driver_sum)
{
    EXPECT_EQ(320, driver_sum);
    EXPECT_EQ(30, driver_sum);
}

2.3 构建测试数据和测试代码遇到的技术瓶颈

编写测试代码比较繁琐

开发编写驱动不难,但是浪费大量的时间,并且没有创造性。

编写代码是否有良好的代码规范

单元测试也需要一些规范支持,例如代码规范,注释规范等,有了这些规范能够为测试人员提供很好的功能测试用例设计的逻辑思考,也为其他开发熟悉代码提供极大的便利。

数据类型比较复杂

通常情况下,输入参数比较复杂,嵌套层析结构比较深入,例如类包含其他类等。

能否支持多组测试数据

一般代码中每个循环、分支判断以及输入输出都有可能产生缺陷。因此单元测试需要很多用例,满足不同的条件分支,达到满足覆盖率。

第三章 Wings 的基本架构介绍

3.1 Wings 测试用例驱动自动生成技术的特性

(1)Wings 是智能的全自动测试用例驱动构建系统,能够在很短时间内完成对大型复杂程序的自动解析、构建。每分钟达到 100 万行代码的生成速率。

(2)可以将任意复杂类型逐步分解为基本数据类型,例如结构体嵌套,复杂类对象,c++ 标准容器,自定义模板类等。

(3)支持多层次的可视化的数据表格来对变量类型进行赋值,无需关注驱动程序本身。数据表格可以表达任意深度和多层次的数据关系,用户只需要关注表格数据,完成对输入数据的校对。

(4)能够区分系统数据类型和用户自定义类型,对于复杂的约定俗成系统类型可由用户自定义扁平式赋值模板,例如 std::vector 类型等,内部集成常用系统类型的模板。

3.2 Wings 的技术架构介绍

Wings 的技术架构:首先 Wings 利用代码静态分析技术,提取被测程序的主干信息并保存到 Program Structure Description(PSD)结构中,PSD 结构中主要存储函数的参数、全局变量以及返回值的信息,类的成员变量,成员函数,以及访问权限等信息。利用存储的信息,依据一定的编写规则,自动构建测试驱动、googletest 期望断言、测试输入、参数捕获等代码和值。

图 3.2 Wings 总体架构图

上述 Wings 的总体架构图,说明了 Wings 构建代码与测试输入的具体过程,整个核心技术是通过编译底层技术获取程序的信息,然后按照一定的规则构建需要的数据。

第四章 程序结构信息描述

程序结构信息(Program Structure Description)是指对源程序进行提取后的描述信息,主要包括类名、命名空间、类的成员变量信息、类的函数信息,以及各种类型信息等(程序结构信息,下文简称 “PSD”)。描述信息的保存 C 语言是以一个文件为单元存储,C++ 提取所有的类信息存储在一个文件中。

Wings 的主要特性是能够对复杂的类型(结构体、类、模板类等)进行逐层展开

分解到最基本数据类型(char、int、string 等)。

PSD 结构存储在 XML 文件中,不同的 XML 文件存储不同的描述信息。Wings 提取的程序结果都存储在 Wingsprojects 文件下的 funxml 与 globalxml 文件夹中,其中 funxml 文件中主要存储的文件以及作用如下:

RecordDecl.xml: 结构体,联合体与类描述信息。

ClassTemplateDecl.xml:模板类描述信息

EnumDecl.xml:枚举描述信息

funcPoint.txt: 存储函数参数为函数指针的分析结果。

funcCount.txt: 存储分析到的全部函数,参数个数,参数类型信息。

void.txt: 存储函数参数为 void* 的分析结果。

filename.xml:存储 c 语言文件的信息。

注:具体文件的描述信息,参看附录 A。

针对复杂类型,例如结构体类型 location_s,成员变量中除了基本数据类型之外,还存在包含结构体类型的情况,如下所示的代码中,location_s 中包含 coordinate_s 结构体,以及 FILE 等类型的信息,针对不同的类型进行标记区分。

源代码如下:


typedef struct coordinate_s{
    void(*setx)(double, int);
    int mInt;
    char *mPoi;
    int mArr[2][3];
    void *vo;
} coordinate;

typedef struct location_s {
    int **mPoi;
    coordinate *coor;
    FILE *pf;
    struct location_s *next;
}location;

coordinate *coordinate_create(void);
int coordinate_destroy(location *loc,size_t length,char *ch);
void func_point(double param1, int param2);

生成对应的 PSD 结构信息如下图 4:


图 4:PSD 描述信息

C++ 的主要表示类型是类,因此测试是 C++ 以一个类为单元做测试,类主要包括类的成员变量名以及类型信息,成员变量的访问权限信息。类的成员函数分为构造函数、内联函数、虚函数等,成员函数的参数信息以及类型信息等。具体描述信息可以登陆 www.codeWings.net 下载 Wings 试用版本进行学习。

第五章 Wings 构建程序描述

Wings 通过获取到的存储信息,依据一定的编码规则,构建不同的代码程序,如驱动程序、期望断言程序、参数捕获程序等。

5.1 驱动程序构建

驱动程序指单元测试代码,Wings 针对函数参数的类型,自动完成单元测试代码的编写。为了更详细的说明驱动程序,以一个 C++ 类作为具体的例子说明。

类的声明如下:

class BlockBuilder {
public:
    explicit BlockBuilder(const Options* options);
    BlockBuilder(const BlockBuilder&) = delete;
    BlockBuilder& operator=(const BlockBuilder&) = delete;
    void Reset();
    void Add(const Slice& key, std::string & value);
    Slice Finish();
    size_t CurrentSizeEstimate() const;
private:
    const Options* options_;
    std::string buffer_;
    int counter_;
    bool finished_;
};

针对如上 BlockBuilder 类,构建一个对应的驱动类 DriverBlockBuilder,驱动类中主要包含构造函数、析构函数、每个成员函数的驱动函数以及返回值函数。为了避免类中重名函数,对写入 PSD 需要对应生成驱动的函数进行顺序编号。

驱动类声明:

class DriverBlockBuilder {
public:
    DriverBlockBuilder(Json::Value Root, int times);
    ~DriverBlockBuilder();
    int DriverBlockBuilderReset1(int times);
    int Reset1Times;
    int DriverBlockBuilderAdd2(int times);
    int Add2Times;
    int DriverBlockBuilderFinish3(int times);
    void ReturnDriver_Finish3(class Wings::Slice returnType);
    int Finish3Times;
    int DriverBlockBuilderCurrentSizeEstimate4(int times);
    void ReturnDriver_CurrentSizeEstimate4(size_t returnType);
    int CurrentSizeEstimate4Times;
private:
    Wings::BlockBuilder* _BlockBuilder;
};

在上述驱动类中,构造函数的作用是构造 BlockBuilder 的对象,用构造的对象,调用测试 BlockBuilder 中的成员函数。析构函数的作用是释放构建的对象。

构造函数与析构函数:

DriverBlockBuilder::DriverBlockBuilder(Json::Value Root, int times){
  Json::Value _BlockBuilder_Root = Root["BlockBuilder" + std::to_string(times)];
  /* options_ */
  Json::Value _options__Root = _BlockBuilder_Root["options_"];
  int _options__len = _options__Root.size();
  Wings::Options* _options_ = DriverstructOptionsPoint(_options__Root, _options__len);
  /* buffer_ */
  std::string _buffer_= _BlockBuilder_Root["buffer_"].asString();
  /* counter_ */
  int _counter_ = _BlockBuilder_Root["counter_"].asInt();
  /* finished_ */
  bool _finished_;
  int _finished__value_ = _BlockBuilder_Root["finished_"].asInt();
  if (_finished__value_ == 0) {
    _finished_ = true;
   }
  else {
    _finished_ = false;
  }
  _BlockBuilder = new Wings::BlockBuilder(_options_, _buffer_, _counter_, _finished_, false);
}
DriverBlockBuilder::~DriverBlockBuilder()
{
    if (_BlockBuilder != nullptr) {
        delete _BlockBuilder;
}
}

每个成员函数对应生成自己的驱动函数。类中的 Add 函数对应的驱动函数如下。

Add 驱动函数:

int DriverBlockBuilder::DriverBlockBuilderAdd2(int times)
{
    Add2Times = times;
    const char* jsonFilePath = "drivervalue/BlockBuilder/Add2.json";
    Json::Value Root;
    Json::Reader _reader;
    std::ifstream _ifs(jsonFilePath);
    _reader.parse(_ifs, Root);
    Json::Value _Add2_Root = Root["Add2" + std::to_string(times)];
    /*It is the 1 global variable: count    Add */
    int _count = _Add2_Root["count"].asInt();
    count = _count;
    /*It is the 1 parameter: key Add2
     * Parameters of the prototype:const Wings::Slice &key */
    Json::Value _keykey_Root = _Add2_Root["key"];
    /* data_ */
    char* _keydata_;
    {
        std::string _keydata__str = _keykey_Root["data_"].asString();
        _keydata_ = new char[_keydata__str.size()];
        memcpy(_keydata_, _keydata__str.c_str(), _keydata__str.size());
    }
    /* size_ */
    unsigned int _keysize_ = _keykey_Root["size_"].asUInt();
    Wings::Slice _key(_keydata_, _keysize_, false);
    /*It is the 2 parameter: value    Add2
     * Parameters of the prototype:const std::string &value  */
    string _value = _Add2_Root["value"].asString();

    //The Function of Class    Call
    _BlockBuilder->Add(_key, _value);
    return 0;
}

构成以上驱动函数的主要包括全局变量、参数、调用被测函数的构建。

以上是针对 BlockBuilder 类的驱动类的主要信息,而构建的程序遵守 google 的编码规范。一些命名规则如下:

Wings 生成的驱动代码,存储在 drivercode 文件夹中。

(1)driver.cc 与 driver.h,针对程序中使用到的一些公共函数以及头文件。

(2)同一个结构体或者联合体,可能会作为多个函数的参数使用,为了避免代码的重复,Wings 针对所有的结构体和联合体的不同类型,封装成不同的驱动函数或者参数捕获函数。driver_structorunion.cc 存储结构体驱动函数的代 driver_structorunion.h 对应的头文件。

结构体实现函数的命名规则为:DriverStruct+ 结构体名字 + 类型,其中 Point 代表一级指针或者一维数组,PointPoint 代表二级指针或者二维数组使用。

源文件的命名规则为:driver_+ 源文件名/类名 +.cc

例如:driver_nginx.cc 或 driverBlockBuilder.cc

驱动函数的命名规则:Driver_+ 函数名 +(编号)

例如:Driver_ngx_show_version_info(void);

DriverBlockBuilderAdd2(int times)

(3)返回值的打印输出

返回值的打印输出函数命名规则:Driver+Return+Print_+ 函数名。

例如:DriverReturnPrint_ngx_show_version_info();

(4)用户源代码中的 main 函数自动进行注释,重新生成一个 main 函数文件,来进行测试。Wings 会生成驱动 main 的主函数文件为:gtest_auto_main.cc

Wings 主要针对参数进行逐层展开,解析到最底层为基本类型进行处理。驱动的赋值部分,主要就是针对基本类型进行处理。(注:特殊类型,比如 FILE 等,后面会详细讲解如何赋值)

以 int 类型举例,一般程序构成 int 的主要组成大概包括以下情况:

int p; int p; int **p; int **p;

int p[1]; int p[2][3]; int p[1][2][3];

int(p)[]; int(*p)[][3]; int *(*p)[]; int (*p)[];

int *a[]; int **a[]; int *a[][3]; int (*a[])[];

Wings 会针对基本类型的以上 15 种类型,进行不同的赋值。

构建完驱动程序之后,要对驱动程序进行运行,Wings 构建 googletest 的框架,来进行测试。下面我们将针对期望断言的 googletest 程序进行构建。

5.2 googletest 程序的构建

在构建完单元测试驱动程序之后,Wings 将调用 googletest 的框架,完成对返回值的期望值验证代码,并且输出测试结果。

Wings 运行单元测试过程中,会构建返回值的保存代码,将程序的返回值结果进行存储,然后读取用户输入的预期值,进行结果对比,判断是否通过。

针对具体的类,对应生成 gtest 类,每个 gtest 类中对每个函数构建期望代码。例如针对 BlockBuilder 类,生成的 gtest 类为 GtestBlockBuilder。

GtestBlockBuilder 类的声明:

class GtestBlockBuilder : public testing::Test {
protected:
    virtual void SetUp()
    {
        const char* jsonFilePath = "../drivervalue/RecordDecl.json";
        Json::Value Root;
        Json::Reader _reader;
        std::ifstream _ifs(jsonFilePath);
        _reader.parse(_ifs, Root);
        driverBlockBuilder = new DriverBlockBuilder(Root, 0);
    }
    virtual void TearDown()
    {
        delete driverBlockBuilder;
    }

    DriverBlockBuilder* driverBlockBuilder;
};

BlockBuilder 类中的每个函数对应一个 gtest 函数,每个函数负责调用 DriverBlockBuilder 编写的驱动函数,对于包含返回值信息的函数,则对应生成具体的期望值对比。

期望对比函数 CurrentSizeEstimate:

TEST_F(GtestBlockBuilder, DriverBlockBuilderCurrentSizeEstimate4)
{
    const char* jsonFilePath = "drivervalue/BlockBuilder/CurrentSizeEstimate4.json";
    Json::Value Root;
    Json::Reader _reader;
    std::ifstream _ifs(jsonFilePath);
    _reader.parse(_ifs, Root);
    for (int i = 0; i < BLOCKBUILDER_CURRENTSIZEESTIMATE4_TIMES; i++) {
        driverBlockBuilder->DriverBlockBuilderCurrentSizeEstimate4(i);
        Json::Value _CurrentSizeEstimate4_Root = Root["CurrentSizeEstimate4" + std::to_string(i)];
        /* return */
        unsigned int _return_actual = _CurrentSizeEstimate4_Root["return"].asUInt();
        /* return */
        unsigned int _return_expected = _CurrentSizeEstimate4_Root["return"].asUInt();
        /* return_expected */
        EXPECT_EQ(_return_expected, _return_actual);
    }
}

最后调用自动构建的 main 函数运行整个单元测试过程。

5.3 参数捕获程序构建

参数捕获是指在程序运行过程中获取程序的变量信息,主要包括函数的参数、全局变量、返回值等。Wings 自动构建获取参数的程序,利用插装技术,将构建的捕获函数插入源代码中对应的位置,将获取的具体信息,写入值文件,可以将获取的数据作为单元测试的输入,在代码发生变更后,利用相同的输入判断是否得到相同的输出,进行回归测试。

Wings 的参数捕获代码存储在 paramcaputrecode 文件夹中。其中命名规则同驱动格式一样,将所有的 driver 替换为 param 即可。Wings 针对每个类生成一个对应的参数捕获类,而参数捕获类中针对每个函数生成对应的捕获参数、全局变量以及返回值的函数。c++ 中类的成员变量是私有,无法从外部获取,Wings 利用插桩技术,对每个类插入一个捕获函数,来获取类的私有成员变量。

参数捕获类 ParamCaptureBlockBuilder:

class ParamCaptureBlockBuilder
{
public:
  ParamCaptureBlockBuilder();
  ~ParamCaptureBlockBuilder();
  void ParamCapture_Reset1();
  void GlobalCapture_Reset1();
  void ReturnCapture_Reset1();
  void ParamCapture_Add2(const Wings::Slice &key, const std::string &value);
  void GlobalCapture_Add2();
  void ReturnCapture_Add2();
  void ParamCapture_Finish3();
  void GlobalCapture_Finish3();
  void ReturnCapture_Finish3(class Wings::Slice returnType);
  void ParamCapture_CurrentSizeEstimate4();
  void GlobalCapture_CurrentSizeEstimate4();
  void ReturnCapture_CurrentSizeEstimate4(size_t returnType);
};

具体的捕获函数不再详细说明,具体信息可以在 Wings 官网下载试用版本查看。

第六章 Wings 类型以及面向对象语法特性的支持

Wings 能够支持任意的类型以及面向对象的语法特性。

类型支持:

特殊模板:

语法支持:

6.1 链表

针对链表类型,采用比较灵活的赋值方式,考虑到实际应用中的一些因素,针对链表类型,默认赋值两层结构,在实际测试过程中,用户可依据需要自动添加节点。

6.2 标准库容器

Wings 能够支持 c++ 的标准库容器,能够对容器进行逐层展开,利用不同容器的标准赋值函数进行赋值以取值。

其他类似的容器例如 QT 中的容器以及 boost 库中的相关容器,我们在继续支持。

6.3 自定义模板类

一些用户自定义的模板类类型,Wings 能够识别是用户自定义的模板类,Wings 依据实际程序中的赋值类型,进行处理。

6.4 void* 与函数指针

Void* 与函数指针在实际程序中可以作为函数参数或者结构体与类的成员变量,针对不确定的赋值类型,Wings 提供了具体的解决办法:

① 利用编译底层技术,对源程序静态分析,获取真实类型,对真实类型进行赋值

② 由用户在数据表格界面配置实际类型

6.5 特殊模板

在实际的代码程序中,会存在一些类型无法使用通用的模式全部展开,如一些系统变量(FILE、iostream)、第三方库以及一些用户需要特殊赋值。

Wings 是如何针对以上特殊类型进行赋值,举例如下:

struct sockaddr_in {
        short   sin_family;
        u_short sin_port;
        struct  in_addr sin_addr;
        char    sin_zero[8];
};

步骤如下:

a. 识别 sockaddr_in 为系统变量类型,特殊标记

b. 检测程序中的所有系统变量类型,显示在模板界面

c. 用户配置特殊变量,如 sin_family

d. 构建 sockaddr_in 对象

e. 调用模板,生成驱动

模板配置如下:

图:6.5 模板配置

第七章 数据表格

Wings 目前测试用例数据采用随机生成的方式,支持 int、char、double、float、bool、char* 类型。数据表格可以任意编辑以上类型的数值。

(1) Wings 数据表格将会针对参数进行展开,假如参数类型为结构类型,数据表格将分层展开结构的类型,到基本类型。

(2) 针对基本类型的指针类型,例如 int *p;Wings 处理为不定长度的一维数组类型,int **p;处理为不定长度的二维数组类型,默认长度为 3,数据表格界面可以点击进行添加和删除数据。

(3) 针对不定长度的数组作为函数参数,例如 int p[];Wings 默认长度为 1,用户依据需求,在数据表格界面进行任意添加和修改即可。

图 7-1 数据表格展示

附录 A
表一:type 属性

ZOA_CHAR_S/ZOA_UCHAR/ZOA_INT/ZOA_UINT/ZOA_LONG/ZOA_ULONG/ZOA_FLOAT/ZOA_UFLOAT/ZOA_SHOTR/ZOA_USHORT/ZOA_DOUBLE/ZOA_UDOUBLE 基本类型
StructureOrClassType 结构体类型
ZOA_FUNC 函数指针类型
ZOA_UNION 联合体类型
ZOA_ENUM 枚举类型
ClassType 类类型


表二:basetype 属性
| BuiltinType | 基本类型 |
| -- |--|
| ArrayType | 数组类型 |
| PointerType | 指针类型 |
| StructureOrClassType | 结构体类型 |
| UnionType | 联合体类型 |
| EnumType | 枚举类型 |
| FunctionPointType | 函数指针类型 |

表三:其他属性
| Name | 代表结构体、类、联合体名字 |
| -- |--|
| NodeType | 代表链表类型 |
| parmType | 代表函数参数类型 |
| parNum | 代表函数参数个数 |
| SystemVar | 代表此类型为系统头文件类型 |
| value | 代表枚举类型的值 |
| bitfield | 代表位域类型所占字节 |
| returnType | 代表返回值类型 |
| Field | 类成员变量 |
| Method | 类构造函数 |
| paramName | 类构造函数参数名 |
| paramType | 类构造函数参数类型 |
| TemplateArgumentType | STL 结构参数类型 |
| WingsTemplateArgument | STL 结构嵌套参数名字 |
| TemplateArgumentValue | STL 结构中参数为具体值 |
| FunctionModifiers | 函数访问权限 |
| FunctionAttribute | 函数是 extern 或者 static 函数 |
| FuncClassName | 函数所属类 |
| OperatorFundecl | 重载运算符函数 |
| Operator | 重载运算符类型 |


↙↙↙阅读原文可查看相关链接,并与作者交流