微服务架构下的自动化测试全链路设计_第1页
微服务架构下的自动化测试全链路设计_第2页
微服务架构下的自动化测试全链路设计_第3页
微服务架构下的自动化测试全链路设计_第4页
微服务架构下的自动化测试全链路设计_第5页
已阅读5页,还剩20页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

微服务架构下的自动化测试全链路设计

目录1. 背景 32. 被忽视的软件工程环节——DevTestOps 33. 开发阶段unitTestmock外部依赖 64. 连调阶段mock外部依赖 75. 自动化测试阶段mock需求 126. autoTestMockGateway浮出水面 147. 轻量级版本实现 158. 总结 25

背景从SOA架构到现在大行其道的微服务架构,系统越拆越小,整体架构的复杂度也是直线上升,我们一直老生常谈的微服务架构下的技术难点及解决方案也日渐成熟(包括典型的数据一致性,系统调用带来的一致性问题,还是跨节点跨机房复制带来的一致性问题都有了很多解决方案),但是有一个环节我们明显忽略了。在现在的微服务架构趋势下,微服务在运维层面和自动化部署方面基本上是比较完善了。从我个人经验来看,上层的开发、测试对微服务架构带来的巨大变化还在反应和学习中。

开发层面讨论微服务的更多是框架、治理、性能等,但是从完整的软件工程来看我们严重缺失分析、设计知识,这也是我们现在的工程师普遍缺乏的技术。我们经常会发现一旦你想重构点东西是多么的艰难,就是因为在初期构造这栋建筑的时候严重缺失了通盘的分析、设计,最终导致这个建筑慢慢僵化最后人见人怕,因为他逐渐变成一个怪物。(比如,开发很少写unitTest,我们总是忽视单元测试背后产生的软件工程的价值。)被忽视的软件工程环节——DevTestOps我们有没有发现一个现象,在整个软件过程里,测试这个环节容易被忽视。任何一种软件工程模型都有QA环节,但是这个环节似乎很薄很弱,目前我们绝大多数工程师、架构师都严重低估了这个环节的力量和价值,还停留在无技术含量,手动功能测试低级效率印象里。

这主要是测试这个角色整个技术体系、工程化能力偏弱,一部分是客观大环境问题,还有一部分自身问题,没有让自己走出去,多去学习整个工程化的技术,多去了解开发的技术,生产上的物理架构,这会有助于测试放大自己的声音。

导致测试环节在国内整个设计创新薄弱的原因还有一个主要原因就是,开发工程师普遍没有完整的工程基础。在国外IT发达国家,日本、美国等,一个合格的开发工程师、测试工程师都是边界模糊的,自己开发产品自己测试,这需要切换思维模式,需要同时具备这两种能力,但是这才是整个软件工程的完整流程。

我们有没有想过一个问题,为什么现在大家都在谈论DevOps,而不是DevTestOps,为什么偏偏跳过测试这个环节,难道开发的系统需要具备良好的可运维性就不需要可测试性吗,开发需要具备运维能力,运维需要具备开发能力,为什么测试环节忽略了。

我们对QA环节的轻视,对测试角色的不重视其实带来的副作用是非常大的。

微服务架构下测试复杂度和效率问题微服务的拆分粒度要比SOA细了很多,从容器化镜像自动部署来衡量,是拆小了之后很方便,但是拆小了之后会给整个开发、测试环节增加很大的复杂度和效率问题。

在SOA时期,契约驱动这个原则在微服务里也一样适用,跨部门需求定义好契约你就可以先开发上线了。但是这个里面最大的问题就是当前系统的部分连调问题和自动化回归问题,如果是新系统上线还需要做性能压测,这外部的依赖如何解决。

也许我们会说,不是应该依赖方先ready,然后我们紧接着进行测试、发布吗。如果是业务、架构合理的情况下,这种场景最大的问题就是我们的项目容易被依赖方牵制,这会带来很多问题,比如,研发人员需要切换出来做其他事情,branch一直挂着,不知道哪天突然来找你说可以对接了,也许这已经过去一个月或者更久,这种方式一旦养成习惯性研发流程就很容易产生线上BUG。

还有一种情况也是合理的情况就是平台提供方需要调用业务方的接口,这里面有一般调用的callback接口、交易链路上的marketing接口、配送routing接口等。

这里给大家分享我们目前正在进行中的marketing-cloud(营销云)规则引擎项目。

marketing-cloud提供了一些营销类业务,有团购、优惠券、促销等,但是我们的业务方需要有自己个性化的营销活动玩法,我们需要在marketing-cloud规则引擎中抽象出业务方营销活动的返回信息,同时打通个性化营销活动与公共交易、结算环节,形成一个完整的业务流。

这是一个marketing-cloud逻辑架构图,跟我们主题相关的就是营销规则引擎,他就是我们这里所说的合理的业务场景。在整个正向下单过程中,营销规则引擎要肩负起既要提供marketing-cloud内的共用营销活动,还需要桥接外部营销中心的各类营销玩法,外部的营销中心会有多个,目前我们主要有两个。由于这篇文章不是介绍营销平台怎么设计,所以这里不打算扩展话题。主要是起到抛砖引玉的目的,平台型的业务会存在各种各样的对外系统依赖的业务场景。文章接下来的部分将展开marketing-cloud规则引擎在打通测试链路上的实践。开发阶段unitTestmock外部依赖在开发阶段,我们会经常性的编写单元测试来测试我们的逻辑,在编写unitTest的时候都需要mock周边的依赖,mock出来的对象分为两种类型,一种是不具有Assert逻辑的stub桩对象,还有一种就是需要支持Assert的mocker模拟对象。

但是我们也不需要明显区分他们,两者的区别不是太明显,在编码规范内可能需要区分。

我们关心的是如何解决对象之间的依赖问题,各种mock框架其实提供了很多非常好用的工具,我们可以很轻松的mock周边的依赖。

given(marketingService.mixMarketingActivity(anyObject())).willReturn(stubResponse);

RuleCalculateResponse

response

=

this.ruleCalculatorBiz.ruleCalculate(request);这里我们mock了marketingService.mixMarketingActivity()方法。

Java世界里提供了很多好用的mock框架,比较流行好用的框架之一mockito可以轻松mockService层的依赖,当然除了mockito之外还有很多优秀的mock框架。

这些框架大同小异,编写unitTest最大的问题就是如何重构逻辑使之更加便于测试,也就是代码是否具备很好的可测试性,是否已经消除了绝大多数private方法,private方法是否有某些指责是我们没有捕捉到业务概念。连调阶段mock外部依赖在我们完成了所有的开发,完善的单元测试保证了我们内部的逻辑是没有问题的(当然这里不讨论unitTest的case的设计是否完善情况)。

现在我们需要对接周边系统开发进行连调了,这个周边系统还是属于本平台之类的其他支撑系统。比如我们的marketing-cloud规则引擎系统与下单系统之间的关系。在开发的时候我们编写unitTest是顺利的完成了开发解决的验证工作,但是现在面对连调问题。

系统需要正式的跑起来,但是我们缺乏对外部营销中心的依赖,我们怎么办。其实我们也需要在连调阶段mock外部依赖,只不过这个mock的技术和方法不是通过unitTest框架来支持,而是需要我们自己来设计我们的整个服务的开发架构。

首先要能识别本次request是需要mock的,那就需要某种mockparameter参数来提供识别能力。

我们来看下marketing-cloud营销规则引擎在这块的一个初步尝试。

public

interface

CCMarketingCentralFacade

{

CallResponse

callMarketingCentral(CallRequest

request);

}

public

interface

ClassMarketingCentralFacade

{

CallResponse

callMarketingCentral(CallRequest

request);

}营销规则引擎使用RestEasyclientapi作为rest调用框架。这两个Facade是营销平台对CCTalk、沪江网校沪江两大子公司营销中心发起调用的Facade。

为了尽量还原我们的工程实践干货同时需要消除一些敏感信息的情况下,整篇文章所有的代码实例,我都删除了一些不影响阅读且和本文无关的代码,同时做了一些伪编码和省略,使代码更精简更便于阅读。在正常逻辑下,我们会根据营销路由key来决定调用哪个公司的营销中心接口,但是由于我们在开发这个项目的时候暂时业务方还没有存在的地址让我们对接,所以我们自己做了mockfacade,来解决连调问题。public

class

CCMarketingCentralFacadeMocker

implements

CCMarketingCentralFacade

{

@Override

public

CallResponse

callMarketingCentral(CallRequest

request)

{

CallResponse

response

=

...

MarketingResultDto

marketingResultDto

=

...

marketingResultDto.setTotalDiscount(new

BigDecimal("90.19"));

marketingResultDto.setUseTotalDiscount(true);

response.getData().setMarketingResult(marketingResultDto);

return

response;

}

}public

class

ClassMarketingCentralFacadeMocker

implements

ClassMarketingCentralFacade

{

@Override

public

CallResponse

callMarketingCentral(CallRequest

request)

{

CallResponse

response

=

...

MarketingResultDto

marketingResultDto

=

...

marketingResultDto.setUseCoupon(true);

marketingResultDto.setTotalDiscount(null);

marketingResultDto.setUseTotalDiscount(false);

List<MarketingProductDiscountDto>

discountDtos

=

...

request.getMarketingProductTagsParameter().getMarketingTags().forEach(item

->

{

MarketingProductDiscountDto

discountDto

=

...

discountDto.setProductId(item.getProductID());

...

discountDtos.add(discountDto);

});

...

return

response;

}

}我们定义了两个mock类,都是一些测试数据,就是为了解决在连调阶段的问题,也就是在DEV环境上的依赖问题。

有了mockfacade之后就需要request定义mockparameter参数了。

public

abstract

class

BaseRequest

implements

Serializable

{

public

MockParameter

mockParameter;

}public

class

MockParameter

{

/**

*

mock

cc

营销调用接口

*/

public

Boolean

mockCCMarketingInterface;

/**

*

mock

class

营销调用接口

*/

public

Boolean

mockClassMarketingInterface;

/**

*

是否自动化测试

mock

*/

public

Boolean

useAutoTestMock;

/**

*

测试mock参数

*/

public

String

testMockParam;

}我们暂且忽略通用型之类的设计,这里只是我们在赶项目的情况下做的一个迭代尝试,等我们把这整个流程都跑通了再来考虑重构提取框架。有了输入参数,我们就可以根据参数判断来动态注入mockfacade。自动化测试阶段mock需求我们继续向前推进,过了连调阶段紧接着就进入测试环节,现在基本上大多数互联网公司都是自动化的测试,很少在有手动的,尤其是后端系统。

那么在autoTest阶段面临的一个问题就是,我们需要一个公共的autoTest地址,这个测试地址是不变的,我们在自动化测试下mock的facadebean的地址就是这个地址,这个地址输出的值需要能够对应到每次自动化脚本执行的上下文中。

我们有很多微服务系统来组成一个平台,每个服务都有依赖的第三方接口,原来在自动化测试这些服务的时候都需要去了解业务方系统的接口、DB、前台入口等,因为在编写自动化脚本的时候需要同步创建测试数据,最后才能Assert。

这个跨部门的沟通和协作效率严重低下,而且人员变动、系统变动都会直接影响上线周期,这里绝对值得创新来解决这个效率严重阻塞问题。

@Value("${marketing.cloud.business.access.url.mock}")

private

String

mockUrl;

/**

*

自动化测试

mocker

bean

*/

@Bean("CCMarketingCentralFacadeTestMock")

public

CCMarketingCentralFacade

CCMarketingCentralFacadeTestMock()

{

RestClientProxyFactoryBean<CCMarketingCentralFacade>

restClientProxyFactoryBean

...

restClientProxyFactoryBean.setBaseUri(this.mockUrl);

...

}

/**

*

自动化测试

mocker

bean

*/

@Bean("ClassMarketingCentralFacadeTestMock")

public

ClassMarketingCentralFacade

ClassMarketingCentralFacadeTestMock()

{

RestClientProxyFactoryBean<ClassMarketingCentralFacade>

restClientProxyFactoryBean

...

restClientProxyFactoryBean.setBaseUri(this.mockUrl);

...

}这里的mockUrl就是我们抽象出来的统一的autoTest地址,在前面的mockparameter中有一个useAutoTestMockBoolean类型的参数,如果当前请求此参数为true,我们将动态注入自动化测试mockbean,后续的所有调用都会走到mockUrl指定的地方。autoTestMockGateway浮出水面到目前为止,我们遇到了自动化测试统一的mock地址要收口所有微服务在这方面的需求。现在最大的问题就是,所有的微服务对外依赖的response都不相同,自动化脚本在执行的时候预先创建好的response要能适配到当前测试的上下文中。

比如,营销规则引擎,我们的自动化脚本在创建一个订单的时候需要预先构造好当前商品(比如,productID:101010),在获取外部营销中心提供的活动信息和抵扣信息的response,最后才能去Assert订单的金额和活动信息记录是否正确,这就是一次autoTestcontext。

有两种方式来识别当前autoTestcontext,一种是在case执行的时候确定商品ID,最后通过商品ID来获取mock的response。还有一种就是支持传递autoTestmock参数给到mockUrl指定的服务,可以使用这个参数来识别当前测试上下文。

一个测试case可能会穿过很多微服务,这些所有的依赖服务可能都需要预设mockresponse,这基本上是一劳永逸的。

所以,我们抽象出了autoTestMockGateway(自动化测试mock网关服务),在整个自动化测试环节还有很多需要支持的工作,服务之间的鉴权,鉴权key的mock,加解密,加解密key的mock,自动化测试case交替并行执行等。

作为工程师的我们都希望用系统化、工程化的方式来解决整体问题,而不是个别点状问题。有了这个mockgateway我们可以做很多事情,也可以普惠所有需要的其他部门。

在一次autoTestcontext里构造好mockresponse,然后通过mockparameter来动态识别具体的来源服务进行路由、鉴权、加解密等操作。MockGateway是一个支点,我相信这个支点可以撬动很多测试空间和创新能力。轻量级版本实现接下来我们将展示在marketing-cloud营销规则引擎中的初步尝试。

整体逻辑架构

自动化脚本在每跑一个case的时候会创建当前case对应的autoTestContext,这里面都是一些metadata,用来表示这个case中所有涉及到的微服务系统哪些是需要走mockgateway的。

在mockGateway中所有的配置都是有一个autoTestContext所对应,如果没有autoTestContext说明是所有case共用。

将mockparameter纳入服务框架标准requestcontract

要想打通整个微服务架构中的所有通道,就需要在标准requestcontract定义mockParameter,这是这一切的前提。

服务与服务之间调用走标准微服务requestcontract,服务与外部系统的依赖可以选择走HTTPHeader,也可以选择走标准request,就要看我们的整个服务框架是否已经覆盖所有的产线及一些遗留系统的问题。

public

abstract

class

BaseRequest

implements

Serializable

{

public

MockParameter

mockParameter;

}BaseRequest是所有request的基类,这样才能保证所有的请求能够正常的传递。

使用AOP+RestEasyHttpClientRequestSPI初步实现Mock

整个系统的开发架构分层依赖是:facade->biz->service,基本的所有核心逻辑都是在service中,请求的requestdto最多不能越界到service层,按照规范讲requestdto顶多滞留在biz层,但是在互联网的世界中一些都是可以快速迭代的,并不是多么硬性规定,及时重构是偿还技术债务的主要方法。

前面我们已经讲过,我们采用的RPC框架是RestEasy+RestEasyclient,我们先来看下入口的地方。

@Component

@Path("v1/calculator/")

public

class

RuleCalculatorFacadeImpl

extends

BaseFacade

implements

RuleCalculatorFacade

{

@MockFacade(Setting

=

MockFacade.SETTING_REQUEST_MOCK_PARAMETER)

public

RuleCalculateResponse

ruleCalculate(RuleCalculateRequest

request)

{

...

}

}再看下service对象。

@Component

public

class

MarketingServiceImpl

extends

MarketingBaseService

implements

MarketingService

{

@MockFacade(Setting

=

MockFacade.SETTING_FACADE_MOCK_BEAN)

public

MarketingResult

onlyExtendMarketingActivity(Marketing..Parameter

tagsParameter)

{

...

}我们重点看下@MockFacadeannotation声明。

@Target({ElementType.METHOD})

@Retention(RetentionPolicy.RUNTIME)

public

@interface

MockFacade

{

String

SETTING_REQUEST_MOCK_PARAMETER

=

"setting_request_mock_parameter";

String

SETTING_FACADE_MOCK_BEAN

=

"setting_facade_mock_bean";

String

Setting();

}通过这个annotation我们的主要目的就是将mockParameter放到ThreadLocal中去和请求处理完时的清理工作。还有一个功能就是service层的mockbean处理。

@Aspect

@Component

@Slf4j

public

class

MockMarketingFacadeInterceptor

{

@Before("@annotation(mockFacade)")

public

void

beforeMethod(JoinPoint

joinPoint,

MockFacade

mockFacade)

{

String

settingName

=

mockFacade.Setting();

if

(MockFacade.SETTING_REQUEST_MOCK_PARAMETER.equals(settingName))

{

Object[]

args

=

joinPoint.getArgs();

if

(args

==

null)

return;

List<Object>

argList

=

Arrays.asList(args);

argList.forEach(item

->

{

if

(item

instanceof

BaseRequest)

{

BaseRequest

request

=

(BaseRequest)

item;

if

(request.getMockParameter()

!=

null)

{

MarketingBaseService.mockParameterThreadLocal.set(request.getMockParameter());

("setting

mock

parameter:{}",

JSON.toJSONString(request.getMockParameter()));

}

}

});

}

else

if

(MockFacade.SETTING_FACADE_MOCK_BEAN.equals(settingName))

{

MarketingBaseService

marketingBaseService

=

(MarketingBaseService)

joinPoint.getThis();

marketingBaseService.mockBean();

("setting

mock

bean.");

}

}

@After("@annotation(mockFacade)")

public

void

afterMethod(JoinPoint

joinpoint,

MockFacade

mockFacade)

{

if

(MockFacade.SETTING_FACADE_MOCK_BEAN.equals(mockFacade.Setting()))

{

MarketingBaseService

marketingBaseService

=

(MarketingBaseService)

joinpoint.getThis();

marketingBaseService.mockRemove();

("remove

mock

bean.");

}

if

(MockFacade.SETTING_REQUEST_MOCK_PARAMETER.equals(mockFacade.Setting()))

{

MarketingBaseService.mockParameterThreadLocal.remove();

("remove

ThreadLocal.

ThreadLocal

get

{}",

MarketingBaseService.mockParameterThreadLocal.get());

}

}

}这些逻辑完全基于一个约定,就是MarketingBaseService,不具有通用型,只是在逐步的重构和提取中,最终会是一个plugin框架。

public

abstract

class

MarketingBaseService

extends

BaseService

{

protected

ClassMarketingCentralFacade

classMarketingCentralFacade;

protected

CCMarketingCentralFacade

ccMarketingCentralFacade;

public

static

ThreadLocal<MockParameter>

mockParameterThreadLocal

=

new

ThreadLocal<>();

public

void

mockBean()

{

MockParameter

mockParameter

=

mockParameterThreadLocal.get();

if

(mockParameter

!=

null

&&

mockParameter.mockClassMarketingInterface)

{

if

(mockParameter.useAutoTestingMock)

{

this.setClassMarketingCentralFacade(SpringContextHolder.getBean("ClassMarketingCentralFacadeTestMock",

ClassMarketingCentralFacade.class));

}

else

{

this.setClassMarketingCentralFacade(SpringContextHolder.getBean("ClassMarketingCentralFacadeMocker",

ClassMarketingCentralFacadeMocker.class));

}

}

else

{

this.setClassMarketingCentralFacade(SpringContextHolder.getBean("ClassMarketingCentralFacade",

ClassMarketingCentralFacade.class));

}

if

(mockParameter

!=

null

&&

mockParameter.mockCCMarketingInterface)

{

if

(mockParameter.useAutoTestingMock)

{

this.setCcMarketingCentralFacade(SpringContextHolder.getBean("CCMarketingCentralFacadeTestMock",

CCMarketingCentralFacade.class));

}

else

{

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论