导语

随着微服务、容器、云计算的发展,近些年 DevOps、CI/CD 等概念越来越多地映入大家的眼帘。许多开发团队都希望应用这些理念来提高软件质量和开发效率,工欲善其事必先利其器,什么样的工具才能够满足开发者的需求?TARS 作为一套优秀的开源微服务开发运营一体化平台,拥有多语言、高性能、敏捷研发、高可用等特点。那么 TARS 是否能够完美支持 DevOps 理念呢?在上一篇文章中,我们了解了如何将开源 CI 工具 Jenkins 与 TARS 集成实现 TARS 服务的自动化构建与部署。而软件测试是软件开发过程中必不可少的一步,本文将在上一篇文章的基础上,以一次完整的实践来展示如何通过 Jenkins 与 TARS 集成实现 TARS 服务的自动化单元测试。

目录

什么是单元测试

随着微服务、容器、云计算的发展,近些年 DevOps、CI/CD 等概念越来越多地映入大家的眼帘。DevOps 是现在流行的一种软件开发方法,将持续开发、持续测试、持续集成、持续部署、持续监控等贯穿到软件开发的生命周期中,用于提高软件的开发质量,被当前几乎所有顶级公司采用。

TARS 作为一套优秀的开源微服务开发运营一体化平台,拥有多语言、高性能、敏捷研发、高可用等特点。通过将开源 CI 工具 Jenkins 与 TARS 集成即可实现针对 TARS 服务开发的自动化测试,减轻开发与测试人员的工作量。由于篇幅所限,本文仅针对自动化单元测试展开。

软件测试是软件开发过程中必不可少的一步,而单元测试是软件测试中最基础的一种形式。单元测试中,单元可以指代码中的一个模块、一个函数或者一个类;单元测试就是为每个单元编写测试用例,对该单元进行正确性检验,测试逻辑是否正确,确保每个单元的行为符合预期。因此单元测试的添加能够很大程度上降低软件或服务上线后出现问题的概率。

环境准备

本文基于前文使用的 TarsCppCIDemo 项目,使用 GoogleTest 作为单元测试框架,实际项目中请根据需求选择测试框架。

安装 GoogleTest

GoogleTest 是 Google 开源的一套 C++ 测试框架,能够很方便的进行单元测试。接下来,我们在部署 Jenkins 的机器上安装这个框架。

GoogleTest 的 GitHub 仓库地址为: https://github.com/google/googletest,可以直接 clone 后构建安装。这里我们安装稳定版,在 GitHub 页面右侧点击 Release 可以查看历史发布的版本。本文截稿前最新版本为 1.10.0,下载安装命令如下


wget https://github.com/google/googletest/archive/release-1.10.0.tar.gz

tar -zxvf release-1.10.0.tar.gz

cd googletest-release-1.10.0

mkdir build

cd build

cmake ..

make

make install

至此,GoogleTest 便安装完成了。

安装 xUnit plugin

xUnit 是一个 Jenkins 平台的插件,可以用于读取单元测试的结果,支持多种测试框架,包括 GoogleTest。

打开 Jenkins 的管理页面,进入 系统管理->插件管理->可选插件,在搜素框中搜索 xUnit,在出现的结果中选择 xUnit plugin,点击 直接安装 后,等待 Jenkins 安装重启即可。

修改项目

现在回到我们之前创建的 Demo 项目,我们为项目的 HelloServer 添加几个接口和一个计数类,实现一个简单的计数服务。

修改 Hello.tars

服务的接口通过 tars 文件定义,我们编辑 Hello.tars,为其添加三个接口,分别为增加计数、减少计数和获取当前的计数值,编辑后的 Hello.tars 如下


module TarsCppCIDemo

{

interface Hello

{

int test();

int increment(out int count);

int decrement(out int count);

int getCount(out int count);

};

};

可以看出,除了自动生成的 test 接口,我们添加了 increment, decrement, getCount 三个接口,三个接口均返回 count,即计数的结果。在项目根目录 TarsCppCIDemo 中,进入 HelloServer/src 目录,我们运行脚本 tars2cppHello.tars 转化为相应的头文件 Hello.h


cd HelloServer/src

/usr/local/tars/cpp/tools/tars2cpp Hello.tars

修改 HelloImp.h

然后我们编辑接口实现文件的头文件 HelloImp.h ,在类 HelloImp 中添加三个接口的声明:


/**

* @param count out 返回计数值

* @return 服务状态码

*/

virtual int increment(int& count, tars::TarsCurrentPtr current);

/**

* @param count out 返回计数值

* @return 服务状态码

*/

virtual int decrement(int& count, tars::TarsCurrentPtr current);

/**

* @param count out 返回计数值

* @return 服务状态码

*/

virtual int getCount(int& count, tars::TarsCurrentPtr current);

新建 Counter 类

接下来我们通过新建 Counter 类来实现计数器的功能。TarsCpp的公共组件中提供了单件类模板组件 TC_Singleton,我们直接继承该类,Counter.h 如下:


#ifndef __COUNTER_H_

#define __COUNTER_H_

#include "util/tc_singleton.h"

#include "util/tc_thread.h"

#include "util/tc_thread_rwlock.h"

// A simple monotonic counter.

class Counter: public tars::TC_Singleton<Counter> {

private:

int counter_;

tars::TC_ThreadRWLocker rwlocker_;

public:

// Creates a counter that starts at 0.

Counter() : counter_(0) {}

// 返回计数值,并对计数执行+1

int Increment();

// 返回计数值,并对计数执行-1

int Decrement();

// 返回当前计数值

int GetCount();

};

#endif // __COUNTER_H_

其中 TC_ThreadRWLocker 为TarsCpp工具组件提供的读写锁类,更多 TarsCpp 公共组件可以在 TarsCpp/util/include/util 中查看其定义和用法。

接下来是 Counter.cpp


#include "Counter.h"

int Counter::Increment() {

tars::TC_ThreadWLock wlock(rwlocker_);

return counter_++;

}

int Counter::Decrement() {

if (counter_ == 0) { // 为0时直接返回

tars::TC_ThreadRLock rlock(rwlocker_);

return counter_;

} else {

tars::TC_ThreadWLock wlock(rwlocker_);

return counter_--;

}

}

int Counter::GetCount() {

tars::TC_ThreadRLock rlock(rwlocker_);

return counter_;

}

这样就完成 Counter 类的创建了。

修改 HelloImp.cpp

接下来我们添加对三个接口的实现


#include "HelloImp.h"

#include "servant/Application.h"

#include "Counter.h"

using namespace std;

void HelloImp::initialize() {

}

void HelloImp::destroy() {

}

int HelloImp::increment(int& count, tars::TarsCurrentPtr current) {

count = Counter::getInstance()->Increment();

return 0;

}

int HelloImp::decrement(int& count, tars::TarsCurrentPtr current) {

count = Counter::getInstance()->Decrement();

return 0;

}

int HelloImp::getCount(int& count, tars::TarsCurrentPtr current) {

count = Counter::getInstance()->GetCount();

return 0;

}

到这里,我们就完成了三个接口逻辑的添加。

建立测试项目

接下来我们创建测试项目,在 HelloServer 目录下新建 test 目录,并在 test 中新建 app_ut.cppCMakeLists.txt,目录结构如下


HelloServer

├── build

├── CMakeLists.txt

├── src

│   ├── CMakeLists.txt

│   ├── Counter.cpp

│   ├── Counter.h

│   ├── Hello.h

│   ├── HelloImp.cpp

│   ├── HelloImp.h

│   ├── HelloServer.cpp

│   ├── HelloServer.h

│   └── Hello.tars

└── test

├── app_ut.cpp

└── CMakeLists.txt

添加测试用例

GoogleTest 包含了丰富的断言,能够方便的进行单元测试,关于 GoogleTest 的使用方法可以阅读其使用文档

我们在 app_ut.cpp 中添加测试流程和测试用例,如下,为 Counter 类添加了三个测试用例,测试的执行顺序是按照定义顺序执行的。其中的 EXPECT_EQ 是用于判断两个值是否相等的断言,不相等触发错误,输出在测试结果中。


#define private public

#include "gtest/gtest.h"

#include "Counter.h"

namespace {

// Tests the Increment() method.

TEST(Counter, Increment) {

Counter* c = Counter::getInstance();

EXPECT_EQ(0, c->Increment());

EXPECT_EQ(1, c->Increment());

EXPECT_EQ(2, c->Increment());

EXPECT_EQ(3, c->Increment());

c->counter_ = 0;

}

// Tests the Decrement() method.

TEST(Counter, Decrement) {

Counter* c = Counter::getInstance();

EXPECT_EQ(0, c->Decrement());

EXPECT_EQ(0, c->Increment());

EXPECT_EQ(1, c->Increment());

EXPECT_EQ(2, c->Decrement());

EXPECT_EQ(1, c->Decrement());

c->counter_ = 0;

}

// Tests the GetCount() method.

TEST(Counter, GetCount) {

Counter* c = Counter::getInstance();

EXPECT_EQ(0, c->GetCount());

EXPECT_EQ(0, c->Increment());

EXPECT_EQ(1, c->GetCount());

EXPECT_EQ(1, c->Increment());

EXPECT_EQ(2, c->GetCount());

EXPECT_EQ(2, c->Decrement());

EXPECT_EQ(1, c->GetCount());

c->counter_ = 0;

}

} // namespace

其中的 #define private public 是单元测试中常用到的宏替换,方便修改私有对象进行测试。Counter 类是单件类,为了不影响其他测试用例,每个测试用例最后将 counter_ 置零。

为测试用例添加 CMakeLists.txt

完成了测试用例的创建,我们需要编译测试项目,生成用于测试的可执行文件。编译框架可以根据自己的偏好选择,本例子中我们使用 cmake 管理代码编译,关于 cmake 的用法可以参照官方文档

首先,我们在 test 目录下添加 CMakeLists.txt 文件,内容如下。


cmake_minimum_required(VERSION 3.10)

find_package(GTest REQUIRED)

set(TARS_INC "/usr/local/tars/cpp/include")

set(TARS_LIB "/usr/local/tars/cpp/lib")

set(TARS_LIB_UTIL "${TARS_LIB}/libtarsutil.a")

set(COUNTER_SRC "${PROJECT_SOURCE_DIR}/src/Counter.cpp")

include_directories(${GTEST_INCLUDE_DIRS}

${TARS_INC}

${PROJECT_SOURCE_DIR}/src )

link_directories(${TARS_LIB})

add_executable(app_ut ${COUNTER_SRC} app_ut.cpp)

target_link_libraries( app_ut

/usr/local/tars/cpp/lib/libtarsutil.a

${GTEST_BOTH_LIBRARIES}

pthread )

gtest_discover_tests(app_ut

XML_OUTPUT_DIR "${PROJECT_SOURCE_DIR}/build/test_result" )

cmake 中在 3.10 之后的版本中添加了对 gtest 的支持,新增了gtest_discover_tests 直接添加测试,但实际使用过程中发现该方法的 XML_OUTPUT_DIR 参数在 3.18 版本才起作用,低于 3.18 的版本都无法在指定路径生成测试结果文件。

因此建议 cmake 版本在 3.18 以下的设备上,通过执行构建的测试可执行文件进行测试用例的运行,在后续部分中会进行详细介绍。

修改项目主 CMakeLists.txt

在使用 TarsCpp 项目生成工具生成项目的时候,默认生成了用于项目编译的 CMakeLists.txt 。接下来我们将修改这个文件,实现在构建项目的同时,编译测试用例。

在上节中我们已经完成了测试用例部分的 CMakeLists.txt 的编写,在项目主 CMakeLists.txt 文件中,只要添加子目录即可,如下,新增了 enable_testing()add_subdirectory(test)


cmake_minimum_required(VERSION 2.8.8)

project(Demo-DemoServer)

option(TARS_MYSQL "option for mysql" ON)

option(TARS_SSL "option for ssl" OFF)

option(TARS_HTTP2 "option for http2" OFF)

if(WIN32)

include (c:tarscppmakefiletars-tools.cmake)

else()

include (/usr/local/tars/cpp/makefile/tars-tools.cmake)

endif()

####you can: cd build; cmake .. -DTARS_WEB_HOST={your web host}

set(TARS_WEB_HOST "" CACHE STRING "set web host")

IF (TARS_WEB_HOST STREQUAL "")

set(TARS_WEB_HOST "http://tars.test.com")

ENDIF ()

include_directories(/usr/local/tars/cpp/thirdparty/include)

link_directories(/usr/local/tars/cpp/thirdparty/lib)

#include_directories(/home/tarsprotol/App/OtherServer)

enable_testing() # 开启测试

add_subdirectory(src)

add_subdirectory(test) # 添加test

#target_link_libraries(mysqlclient ssl crypto nghttp2-static)

接下来按照 TarsCpp 项目的编译方式编译构建项目就可以了。

运行测试用例

有两种运行测试用例的方式,根据要求任选一种即可

  • 直接使用 cmake 集成的测试功能,构建完成后只需要在 build 目录下直接执行 make test 即可,要求 cmake 版本为 3.18
  • 运行测试用例编译构建的可执行文件,在执行完项目构建命令后,会在 build/bin 中生成测试用例可执行文件,在本项目中为 app_ut,直接执行即可,适用于 cmake 2.8.8 以上版本。通常会添加参数 --gtest_output="xml:test*.xml" 用于输出测试结果,如下

./bin/app_ut --gtest_output="xml:testresults.xml"

修改 Jenkins 项目配置

本部分将会介绍如何配置 Jenkins 任务,实现能够自动执行项目中的单元测试,并获取测试的结果。

修改构建shell命令

构建过程的脚本中,我们只需要添加命令运行测试用例即可,根据上节中的 运行测试用例 部分,根据 cmake 版本选择任一命令即可,以执行测试用例可执行文件为例,修改后的构建脚本如下


#!/bin/sh

mkdir -p HelloServer/build

cd HelloServer/build

cmake ..

make -j4

make HelloServer-tar

./bin/app_ut --gtest_output="xml:test_results.xml"

添加构建后操作

点击 增加构建后操作步骤 选择 Publish xUnit test result report,新增构建后步骤,如下

然后在 Report Type 点击 新增 ,选择 GoogleTest

然后在 Pattern 中填写匹配模式,用于匹配前面构建过程中生成的 xml 文件,可以直接使用模式 **/*.xml 匹配所有的 xml 文件,也可以根据命名方式自定义模式匹配,如下

最后点击 保存 就完成了Jenkins任务的配置。

自动化测试

前面我们已经完成了自动化测试所需的配置,同自动化构建与部署一样,接下来只需要 push 项目到 GitHub 仓库即可触发自动化构建与测试流程。

等构建完成后,我们可以查看此次构建的测试结果,如下

到这里,我们就完成了基于 Jenkins 的 TarsCpp 项目自动化单元测试,其他语言大致相同,选择自己熟悉的测试框架和 Jenkins 上对应的插件即可。

总结

本文在前一篇文章的基础上,介绍了如何通过 Jenkins 与 TARS 集成,实现 TARS 服务的自动化单元测试,帮助提升软件开发过程中的软件质量。

TARS可以在考虑到易用性和高性能的同时快速构建系统并自动生成代码,帮助开发人员和企业以微服务的方式快速构建自己稳定可靠的分布式应用,从而令开发人员只关注业务逻辑,提高运营效率。多语言、敏捷研发、高可用和高效运营的特性使 TARS 成为企业级产品。

TARS微服务助您数字化转型,欢迎访问:

TARS官网:https://TarsCloud.org

TARS源码:https://github.com/TarsCloud

Linux基金会官方微服务免费课程:https://www.edx.org/course/bu...

获取《TARS官方培训电子书》:https://wj.qq.com/s2/6570357/...

或扫码获取:

QR


TARS基金会
16 声望7 粉丝

2020年3月10日,Linux基金会正式宣布旗下的TARS开源项目将成立TARS基金会。TARS基金会是一个专注于微服务领域的开源基金会,致力于帮助企业在拓展新领域时拥抱微服务体系架构,解决在使用微服务方面可能出现的问...