Yu's Blog


  • 首页

  • 归档

  • 标签

http1.1里的keepalive

发表于 2018-11-27   |  

http是现在web领域极其普遍的应用层传输协议, 目前常见的使用版本则是http1.1, 当然最先版本是http2.0。

传统的Http应用里都是一次TCP连接一次request。

这种情况下效率有点低:

  • 服务端负载增加,每个请求过来都得占用端口
  • 客户端或服务端对客户端连接数的限制(chrome 限制是6个)
    这种情况很多,比如网页加载对于这个case的处理就是使用将静态资源放置到不同Domain或者压缩打包减少数量来提高效率

http1.1 协议里增加了 keepalive的支持, 并且默认开启。

客户端和服务端在建立连接并完成request后并不会立即断开TCP连接,而是在下次request来临时复用这次TCP连接。但是这里也必须要有TCP连接的timeout时间限制。不然会造成服务端端口被长期占用释放不了。

对于不适用keepalive的request来说,不管是客户端还是服务端都是通过TCP的链接的断开知道request的结束(TCP 挥手时会check 数据包的 seq, 保证数据完整性)。
支持keepalive后,如何知道request结束了呢?
在Http1.1的版本里, 解决方案是request 和reponse里使用contentLength来帮助确认是否收到全部数据。

另一个问题就是在使用keepalive的情况,客户端依然有同时发送多个请求的情况,比如网页加载是需要同时load多个静态资源。比如 浏览器默认最大连接数是6,现在有十个资源同时加载,那么这十个里会有6个并行,4个与前6个串行。

在keepalive里有个问题就是如果能知道每个repose与其对应的request的话,并发的请求可以只需要一次TCP连接,这也就是http2.0实现的多路复用。

分布式事务一致性

发表于 2018-09-22   |  

二阶段提交(2 phase commit)

二阶段提交是将事务分成两个阶段进行处理,阶段一 提交事务请求,阶段二执行事务请求。
整个过程中含有两个角色协调者和参与者,协调者发起事务请求,参与者执行。
二阶段提交的核心就是对每个事务请求都采取先尝试后提交的方式。

一 提交事务请求

  • 事务询问

    协调者向所有参与者发送事务内容,询问是否可以执事务,然后等待所有参与的回应。

  • 事务执行

    参与者收到事务内容后,执行事务内容, 并将Undo和Redo的信息记入事务日志。

  • 事务询问

    若参与者成功执行了事务,就反馈给协调者Yes,如果参与者没有成功执行事务则反馈No。

    整个第一阶段得到的结果就是 事务是否可以被执行。
    Case有三种:

  • 参与者全部回复Yes
  • 存在参与者返回No
  • 存在参与者没有回应

二 执行事务请求

  • 参与者全部回复Yes, 那么久Happy的可以通知参与者执行事务了
    • 发送commit请求 到所以参与者
    • 参与者接收到commit 请求的时候,正式执行事务提交操作
    • 执行完了,发送Ack给协调者
    • 协调者收到Ack,事务结束
  • 有参与者没有回复Yes
    协调者在收到参与者返回的No或者参与者超时都没有返回时,就会直接触发事务中断的操作

    • 发送Rollback请求
    • 参与者执行Undo
    • 反馈Ack
    • 收到Ack,事务结束

二阶段提交示意图
2-phase-commit

二阶段提交是一个很简单容易理解的协议,但是也存在一些问题。

  • 在两个阶段都可能出现各种原因导致服务器之间的通讯障碍问题:

    • 第一阶段的参与者出现问题
      协调者会等待直到请求超时认为请求失败,这也会导致这次事务提交的失败。这里体现的问题就是容错性,二阶段提交没有很好的容错性机制,只能这种方式来保证。

    • 第二阶段的参与者出现问题
      事务提交的时候个别参与者没有收到commit请求,这也就意味着只有部分参与者成功执行事务。产生的问题就是各个参与者之间的数据不一致。

    • 协调者出现问题
      协调者是整个二阶段提交最重要的角色,协调者出现问题整个二阶段请求都没办法继续操作下去。特别是在阶段二出现问题的话会导致参与者处于事务锁定状态。

  • 二阶段提交的过程中,所有和此次事务相关的逻辑都处于阻塞状态,各个参与者处于相互等待状态下也无法进行其他操作。这也会带来事务处理的性能问题。

三阶段提交(3 phase commit)

三阶段请求是对二阶段提交协议的改进,将事务操作分成三个阶段

一阶段

  • Can Commit
    • 协调者发起事务询问
    • 参与者回馈是否可以执行事务内容(在二阶段提交里,参与者在接到事务内容后先尝试执行,并保存Undo和Redo)

二阶段

根据 一阶段的阶段来进行后续操作,可能为Pre commit 或 中断事务

  • Pre Commit
    • 发起预提交请求 协调者发起请求并进入prepared状态
    • 预提交事务 尝试执行事务内容,记录Undo和Redo到事务日志
    • 向协调者发送事务执行响应 同时等待协调者后续操作命令(Commit or Abort)
  • 中断事务
    有参与者一阶段返回了No或者没有响应

    • 发送Abort请求
    • 参与者中断事务 无论是收到协调者的Abort请求还是在等待协调者的过程中超时都会中断事务

三阶段

这个阶段才会真正进行事务的提交

  • Do Commit

    • 发送提交请求, 进入这个阶段,假设协调者处于正常工作状态并且接收到了所有参与者的预提交阶段的Ack消息。那么就向参与者发送 do commit请求。
    • 执行 事务提交操作,并在完成后释放事务占用资源。
    • 事务执行返程,向协调者发送Ack
    • 协调者收到Ack消息,完成事务
  • 中断事务
    协调者收到参与发送No或者在时限内没有收到所以参与者发送的反馈响应那么就中断事务

    • 发送中断请求, 向所有参与者发送Abort请求
    • 事务回滚 参与者收到Abort后,会执行事务的Undo操作,并在完成后释放事务占用资源
    • 反馈事务回滚结果 向协调者发送Ack
    • 中断事务 协调者收到所有参与者的Ack后,完成中断事务

在阶段二完成后有可能出现一下问题:

  • 协调者出现问题
  • 协调者和参与者之间通信障碍

无论是哪个问题都会导致参与者无法及时收到协调者发送的 do commit 或 abort请求。
对于这种情况在三阶段提交里,参与者会在超时后继续事务的提交。
所以有可能会出现数据不一致的情况,但是也解决了协调者单点故障的问题。

Paxos协议

Paxos是一种基于消息传递的并具有高度容错性的一致性算法。

那么Paxos到底解决了什么问题,这个问题可以用拜占庭将军作为场景描述:

拜占庭帝国

ZAB协议(Zookeeper Automic Brodcast)

Mysql事务隔离和加锁处理

发表于 2017-08-31   |  

前言

前几天项目上碰到一个坑,事务先插入一条数据,接着在再读取数据确认数据插入但是发现还是读不到,结果又插入了一次。
经过各种猜测,发现最终问题竟然是生成数据库的事务隔离级别设成了Repeatable read。最后还是通过将Spring事务隔离从ISOLATION_DEFAULT 改成了

jackson多态反序列化浅谈

发表于 2017-08-09   |  

前一天在项目里使用了mongo的多态支持,十分好用,但是后台的问题是解决了,伴生的前端多态支持也是个问题。mongo基于morphia的多态支持是在写入mongo的entity时,在entity上补上了相应的className字段,jackson也有类似的处理。本文描述的主题就是基于jackson的反序列化多态问题。

基于jackson 的JsonTypeInfo实现的反序列化多态

jackson本身提供了对多态反序列化的支持JacksonPolymorphicDeserialization,具体实现可以参考上述文档。

这种方式在使用时和morphia的多态实现非常相似。但是不同的是morphia在将数据持久化到db中是就已经附带定义了实体数据的类型,而对于api之类而言是无法知道数据类型的,client端在数据传输时只能指定数据而无法给出具体数据类型。所以这种方式无法适用于依赖客户端创建的数据,纯粹由服务端管理创建的资源更加适用,因为服务端在跟client端交互时jackson会自动附加上类型值(只在序列化和反序列化中存在),这个值并不影响实体的使用。

自定义JsonDeserializer

另外一种稍微傻点的方式就是,自定义json Deserializer方式,好处就是可以随意管理反序列化和序列化方式。可以解决的第一种方法无法支持的创建时多态。恶心的点在于如何界定json属于指定的反序列化方式,第一种方式是在json中制定了类型,但是抱歉的时这里什么都没有。当然最不济的就是根据具体的属性名类反推类型,但是这样强行多态真的好么,直接依赖属性名,依赖一个还是多个,又新加了一个类,特有属性和别的一样怎么办?判断属性全不全来分辨具体类型?
一个讨巧的做法就是在创建资源是给定类型字符,这是没办法避免的一个步骤,不然就真的只能属性值判断了。还可以将deserilizer method使用多态定义到具体类里,这样使用起来也相对方便一点。

基于jacoco的集成测试及回归测试覆盖率

发表于 2017-07-05   |  

前言

代码覆盖率是用来干什么的?得到之后呢?

这是昨天隔壁小伙问我的问题,仔细想了一想。代码覆盖率只是对于测试质量评估的某一个维度而已。并不是什么指标等硬性的要求。大多数项目其实都用上了单元测试的代码覆盖率,java项目的jacoco, js下的karma等工具也也来越容易使用。在我这有限的工作经验中也经历过多次对于代码覆盖率的讨论,尤其是UT的test coverage。在java项目中,尤其是事务脚本类的web结构代码,代码覆盖率就更加无法代表代码质量。

换个角度去看,测试覆盖率可以帮助找到没有被测试到case,这样反而意义更大。这也是本文提到集成测试和回归测试的覆盖率的起因。

代码覆盖率工具

Jacoco是现在java项目常用的覆盖率工具,jacoco的代码覆盖粒度包括了 类,方法,行,分支,指令 和圈。
jacoco插庄

这是几种java代码覆盖率工具(jacoco, emma, Cobertura)使用的插庄方式,
本文要说的是基于byte code的植入方式。jacoco使用的是基于javaagent 的on-the-fly模式。
这种方式相比于offline无需修改代码,并且可以在程序运行时dump覆盖率报告,但是需要开启额外的代理。

JacocoAgent

jacoco的on-the-fly 模式是基于javaagent。需要在启动程序的jvm参数中指定-javaagent 的jar文件,也就是存在机器某个位置的jacocoAgent.jar 文件。

jacocoAgent的报告dump方式有三种:

  • local file
    在jvm参数值配置output路径
    -javaagent:[yourpath/]jacocoagent.jar=output=/user/path/jacoco.exec

  • TCP server
    配置address和port,然后使用jacocoant,dump jacoco报告。

  • TCP client
    配置address和port,然后使用jacocoant,数据直接sync到remote端口。

dump报告

在server使用jacocoAgent启动后,jacoco会一直检测启动后这段时间内的server运行锁执行过的程序生成覆盖率报告。
jacocoAgent默认会在jvm stop是自动dump报告数据。但是也可以通过Ant Tasks 或 jacoco mvn plugin 来dump。
具体使用可参照文档。

Refrence:

  • 浅谈代码覆盖率
  • Jacoco Documentation

ThreadLocal杂谈

发表于 2017-05-10   |  

ThreadLocal 是为多线程服务的。本质上ThreadLocal是一个关于创建线程局部变量的类。
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。而使用ThreadLocal创建的变量只能被当前线程访问,其他线程则无法访问和修改。举个例子, 服务器同时被100个用户访问,访问的时候需要对用户信息进行操作,这里threadLocal就可以用来对这100个用户信息进行thread级别的隔离。

ThreadLocal是如何做到变量只能被当前线程访问呢?

首先需要声明的是thread local是通过给thread创建变量副本的方式来存储变量的。这些需要被线程使用到的变量就是存储在thread对象的threadLocals属性上。

这里又要提到ThreadLocalMap,这个是threadlocals的类型。从这个角度来看ThreadLocal类更像是一个util类,主要的职责就是帮助thread管理threadLocals属性。

变量存储

利用threadlocal对象 set 变量到thread上时调用的set方法源码是通过获取当前thread上绑定的threadlocals属性的值来进行操作,最终所有的值真正存储的位置其实都在thread本身与threadlocal对象并无直接关系。

1
2
3
4
5
6
7
8
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

再来看get方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

这里的设计就有点意思了,首先先从thread上拿到了threadlocals属性,这是一个ThreadLocalMap类型的值,也是数据真实的存储位置。thread所关联的所有threadlocal其实都存在这个map的entry里,也就是说不管这个thread里持有了多少个threadlocal实例,变量副本存储的地方有且只有一个map的entry。

每个Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal 实例本身,value 是真正需要存储的 Object。
也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。值得注意的是图中的虚线,表示 ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。

threadlocal的存在是为了帮助thread进行threadlocals属性的关于制定key(threadllocal其实就是threadlocalMap的key)的赋值和取值。多线程之间的threadlocal实例本身其实是共享的,所以使用的时候一般定义成static变量,达到thread间共享实例的目的。

如何正确理解url,uri以及urn

发表于 2016-05-27   |  

今天碰到这个问题就好好研究了一下,其实在平时使用中我们很少去关注这几个概念之间具体的区别在哪里。尤其是URL和URI。

在使用API时更不会去关注这些。可是当我们需要处理http request的路径时就不得不面对这个问题。

Google Analystics 基础概念

发表于 2016-05-24   |  

作为移动应用开发者,关注的是用户的痛点乃至用户在应用中的页面上花费的时间,这可以帮助你创建和调整应用,使其被更多的人分享和使用.

默认情况下,当应用集成了Google Analytics后,Google Analytics可以提供如下的信息:

User and session count
Session duration
Operating system information
Device model
Geographic location
除了包括统计信息外,还可以创建和使用以下操作来增强您的应用分析:

Events
Goals
Ecommerce
Custom timings
Custom dimensions
Custom metrics
每一个这些附加功能都有助于更深入的了解应用程序。

Events(事件)

事件其实就是为对用户的客观行为的量化。例如用户点击了某个按钮这就是一个典型的事件。

在Google Analystics中,事件由四个组件组成:

Category
Action
Label (optional)
Value (optional)
Category 就是将需要被跟踪的行为归类后的集合。实际使用的时候,将被跟踪的actions归类更加容易处理和返回数据尤其是在现实report的时候。在使用Event时候必须要注意去track真正需要关心的东西,也就是说Event的集合必须是有具体含义的概念,假如只使用通用的集合,例如Visits,那么最后很可能只能得到大量没有衡量价值的信息。当然,如果想捕获更详细的事件可以使用类似“Visit-help”这种命名方式加以区分。

action其实就是对于Category的细分,但同样属于事件核心概念的一部分,action的命名是基于用户正在执行的行为或用户开始或完成的任务。例如,一个action可以命名为“started,” “paused,” 或 “saving.”。需要注意的是因为action是对Category的细分,所以可以在不同的category下使用相同的action命名。这种灵活性允许在两个category下发生同一个action,但是在同一个category下不能发生重复的action。

Label是一个用于要与类别和动作追踪附加信息的可选组件。Label可以用来传递系统信息,下载的信息,或者是触发事件的说明都可以。

Value也是一个可选组件,与其它组件不同的是它是一个正整数值。这个值可以是任何你想要的整数值。例如,可以使用它作为一个计数器值,一个时间渲染值,或跟踪用户对一个特定的屏幕,视图停留的时间的值。

下面是在游戏中跟踪事件的代码片段:

// Event to track coin gathering mTracker.send(new HitBuilders.EventBuilder()
.setCategory(“Brackety Bricks”) .setAction(“Collect”) .setLabel(“coin”)
.setValue(1)
.build());
Goals

目标是对于一种期望得到的客观结果。目标是自定义的,可能是包括页面访问,购买的行为,目标以及目标的完成期限紧密联系在一起的。 目标可以分为以下几种类型:

Destination:用户已加载或访问一个特定的位置。
Duration:用户花费在一次会话中的最短时间。
Pages/Screens per session:用户访问或查看的页面的具体数量.
Event:一个特定的事件触发。
在Google Analystics 后台管理页面,提供了漏斗图去跟踪用户如何完成目标。漏斗图显示了网站和应用的用户流量和用户流出。所有的用户开始在漏斗的顶部,然后他们滤除,因为它们不能达到特定的目标,经过这层过滤得到的是最终完成目标的用户样本,而不是只给出一个“漏斗”形状,在报告中也显示了流量如何漏斗,通过转换成目标完成。

仔细想想怎么去创建目标,因为Google Analystics只支持最多20个目标的设置。可以通过创建目标组的形式,将五个相关联的目标设置成一组。目标在Google Analystics上一旦创建就无法删除,但是可以通过修改的形式重新利用。需要注意的是,如果通过修改的方式可能会导致已生成的报告中的数据产生错乱。修改过的目标与过去是代表不同含义的,但是Google Analystics暂时不支持这种区分,在修改目标内容前,请再三确认。

Ecommerce(电子商务)

Ecommerce的功能加强包括对于商品的熟知度,促销,结账,退款以及其他采购过程的跟踪。由于生成Ecommerce报告的数据必须是客观存在的事实,因此Ecommerce必须要与Goals和Events联系在一起。 要在应用里跟踪购买产品的行为,须先创建一个产品并分配一个名称和价格,设置和ProductAction和分配事务ID,然后将建立的跟踪事件发送给谷歌Analytics(分析)。示例代码如下:

// create the Product
Product product = new Product()
.setName(“Rocket Fuel”) .setPrice(10.00);
// set the ProductAction
ProductAction productAction = new ProductAction(ProductAction.ACTION_PURCHASE)
.setTransactionId(“T01701”);
// add the transaction to an Event
HitBuilders.EventBuilder builder = new HitBuilders.EventBuilder()
.setCategory(“In-Game Store”) .setAction(“Purchase”) .addProduct(product) .setProductAction(productAction);
// send the transaction data with the event mTracker.send(builder.build());
Custom Timings(自定义计时)

Custom Timings 是用来衡量在应用里完成特定任务所需要的时间,Custom Timings和events从创建方式来说很类似,但是从时间角度来说又是不同的。下面的代码片段展示了如何创建跟踪用户需要多长时间在游戏中完成任务的Custom Timings的例子:

// build and send a custom timing mTracker.send(new HitBuilders.TimingBuilder()
.setCategory(“Brackety Bricks”) .setValue(42000) // 42 seconds .setVariable(“First Stage”) .setLabel(“Race”)
.build());

和创建跟踪Event非常类似,需要设置类别,价值和标签。不同的地方在于用Variable代替了Event中的action。正整数Value就是需要被记录的时间信息。

Custom Dimensions

Custom Dimensions 可以在创建的报告中进行一些维度划分,包括用户特征,标签,或者是满足设定条件的数据等。这种自定维度的数据跟踪和采集对于界定使用者的技能水平,及其选择的难度级别设置时大多数使用者参与游戏使用设备的型号很有帮助。 设置一个Custom Dimensions 需要在Google Analystics网站上配置一些基本的维度。这和设置Goals很类似,同样只有最多20个可设置的数量。 下面代码片段是增加一个显示当前级别’Brackety Bricks’的示例:

// set a custom dimension to track level and difficulty mTracker.setScreen(“BracketyBricks”);
mTracker.send(new HitBuilders.ScreenViewBuilder()
.setCustomDimension(3, “Brackety Bricks”)
.build() );
Custom Metrics

Custom Metrics 和 Custom dimensions很相似,都支持使用不同的域。Custom Metrics最好在创建难以在其他地方跟踪而不产生过多的错误数据的报告时使用。

创建Custom Metrics是在管理设置中, 位于Google Analystics 网站的属性设置下的Custom Dimensions(自定义维度)的可选菜单里面,定义Custom Metrics 需要以下的参数:

A name
A scope set to either Hit or Product
A formatting type of Integer, Currency, or Time
当然,在设置Custom Metrics时,可以选择性的设置想要收集值区间。下面的代码片段展示了 在’Brtackety Bricks’的一个级别下从一个view到一个提示页面的Custom Metrics报告.

mTracker.setScreen(“BracketyBricks”); mTracker.send(new HitBuilders.ScreenViewBuilder()
.setCustomMetric(1, “Hint Page”)
.build() );
Summary

在app中添加google analystic

发表于 2016-05-17   |  

Google Analystic 是一个对mobile app和网站进行统计和分析的平台,对于产品来说,不管是功能的测试还是调整完善都与用户的使用和反馈直接相关。Google Analystic数据统计和分析的结果是用来发现和产品的不足的利器。

如何集成 Google Analytics到android 应用

Google Analystics的集成过程可以概略为两部分:

  • 创建账户

    Google Analystics可以被视为数据收集平台,既然收集了数据,必然是给人看的,少不了的就是相应的管理页面。当然第一步就是要在Google Analystics平台上注册管理账户。 接着就可以在自己账户下配置自己想要跟踪的app。

    整个过程需要的产出只有两个:

    • google-services.json 文件
    • trackID
  • 集成Api:

    • 将 下载下来的google-services.json 文件拷贝到需要被跟踪的app,项目文件下的/app目录
    • 修改build.gradle文件,添加相关依赖

      • 项目外层的build.gradle文件(版本号已更新很多):

        1
        classpath 'com.google.gms:google-services:1.4.0-beta3'
      • app module 下的build.gradle修改:

        1
        2
        3
        4
        5
        6
        // other plugins
        apply plugin: 'com.google.gms.google-services'
        dependencies {
        // other dependencies
        compile 'com.google.android.gms:play-services-analytics:8.1.0'
        }
      • AndroidManifest.xml 给app配置INTERNET和ACCESS_NETWORK_STATE权限

        1
        2
        3
        4
        5
        6
        7
        8
        	<manifest xmlns:android="http://schemas.android.com/apk/res/android" 		package="com.example.analytics">
        <uses-permission android:name="android.permission.INTERNET"/>
        <uses-permission
        android:name="android.permission.ACCESS_NETWORK_STATE"/>
        <application android:name="AnalyticsApplication">
        <!-- The rest of your application manifest -->
        </application>
        </manifest>

在代码中跟踪想要获取的信息

  • 使用Application的子类获取track实例

    1
    2
    3
    4
    5
    6
    7
    synchronized public Tracker getDefaultTracker() { 
    if (mTracker == null) {
    GoogleAnalytics analytics = GoogleAnalytics.getInstance(this);
    mTracker = analytics.newTracker(R.xml.global_tracker);
    }
    return mTracker;
    }

跟踪Activity

  • 在Activity中进行跟踪
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// set up the shared Tracker
AnalyticsApplication application = (AnalyticsApplication) getApplication();
mTracker = application.getDefaultTracker();
}
@Override
public void onStart() {
super.onResume(); // Always call the superclass method first
// add log to make sure that GA is being called... Log.i(TAG, "Setting screen name: " + name);
mTracker.setScreenName("Image~" + name);
// send the "hit" to GA
mTracker.send(new HitBuilders.ScreenViewBuilder().build());
}

上面的示例中,包含了两部分,其一是获取tracker的实例,tracker的实例化过程都交给application去做,这样的带来好处的就是对于tracker的统一管理,其二便是通过tracker把screen name 发送到GA,当然更建议的做法就是把tracker的发送screen的这部分功能也放到application类或其余便于管理的地方。对于应用的代码来说,只需要知道自己使用了tracker的tracker screen方法便可,并不需要在每个页面实现同样的方法。

使用stub破除external dependency

发表于 2016-03-17   |  

在测试中,如果因为代码对外部资源存在依赖的行为,尽管代码的逻辑是正确的也会存在测试失败的可能性,这也被称为test-inhibiting

  • 常见的external dependency是系统中的一个对象,被测试的代码需要与这个对象进行交互,但是你并不能去控制这个对象的行为。

  • 而stub则是对于系统的external dependency的可控制的替代物。stub带来的效果就是可以在测试代码中无需对external dependency进行直接处理。

提高代码的可测试性

破除dependency最直接的方式就是引入seam,当然这比如是需要refactoring配合的

A型方法:把具体类抽象成接口

  • 抽取接口以便对实现进行替换

B型方法:注入委托和接口(fake implementation)

  • 在被测试类注入stub
  • 在构造函数注入伪对象
  • 利用属性注入的方式注入伪对象
  • 在方法调用前注入问对象
抽取接口方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ExternalManager:IExternalManager
{
public bool IsExternal (string name)
{
......
}
}

public interface IExternalManager
{
bool IsExternal(string name);
}

//测试单元
public bool IsExternalManager(string name)
{
IexternalManager mgr = new ExternalManager();
return mgr.IsexternalManager(name);
}
  • 返回值为true的stub
1
2
3
4
5
6
7
public class AlwaysTrueExternalManager:IExternalManager
{
public bool IsExternal (string name)
{
return true;
}
}
在构造函数中注入伪对象

这种方式需要给测试类添加新的构造函数或者是个其构造函数添加新的参数,传入抽取出来的接口类型的对象,通过field或var 供被测试方法或其它相关区域的调用。

通过这种方式,测试需要先进行stub的配置,然后传入被测试对象。这种方式可以提高测试代码的可读性,将需要被了解的信息集中在一点地方。

同时,利用构造函数添加参数的方式实现注入,实际上也使得这些参数称为不可选的依赖项,也就是说类的使用者需要为每个所需的特定依赖传入参数。

利用构造函数注入伪对象也可能会带来一些问题,比如拥有多个external dependency 。这时候难道要添加多个构造函数或是添加多个参数?(明显是不明智的选择)

这个问题可以通过创建特殊的类,拥有可以初始化一个类所需要的所有值,这样构造函数就只需要拥有这个类作为唯一参数,当然更好的方式是通过IoC去实现。

属性注入

这个case需要为每个要注入的dependency添加一个属性,包括get和set。然后只需要在被测函数中相应的地方使用这些依赖。
与构造函数的依赖注入方式一样,需要指明必需的和可选的依赖项,因此也会影响到api的设计。通过使用属性,也同时说明了使用这个函数,那些依赖项是不需要的(可能这也是需要使用的属性注入的场景之一)。

方法调用前注入伪对象

这个case很明显的的意思就是我在对这个对象进行操作之前才可以得到其实例,而不是在构造函数或属性中就已经准备好,这个case的不同之处就是发起stub请求的对象就是被测试代码。

这样的话,可以通过使用工厂类来作为实例的提供者,当然必须在构造函数中初始化这个实例提供者。在这个工厂中,可以使用静态方法返回实例了接口的对象实例,同时采用别的方式(如扩展名)返回stub(这种方法会破坏类的设计封装)。

返回stub的层次

  • 第一层:在被测试类中伪造一个成员
    • 添加构造函数,在构造函数中设置类,从测试代码中设置构造参数,还要考虑对api的影响。
    • 这种方式会改变被测试类的语义,在没有充分理由情况下一般不采用这种方式。
  • 第二层:在工厂类伪造一个成员

    • 上述方法调用前注入伪对象中提到的就是这一层,把工厂类的属性设置成伪依赖项,这种方式并不会改变语义,一切都是原样,代码也较为简洁。但是使用这种方式,必须要充分了解实例的使用时间。
  • 第三层:伪造工厂类

    • 这种方式需要实现自己专有的伪工厂类,同时这个类也能有或者可能没有接口,若没有,则需要为这个工厂类创建接口,然后再创建伪工厂实例,让其返回依赖项。这种层次可以理解为利用一个伪对象去返回一个伪对象,这个实在是很难想象的事情。

伪造方法

这种方法的层次,不同于上述几种,相比而言,其更接近于被测试代码(一般而言,约接近代码,越少需要更改依赖项)。很重要的一点就是这种方式将被测试的代码也作为一个依赖项。

  • 使用方式:

    • 在测试类中:
      * 添加返回真是实例的虚工厂方法
      * 在代码中正常的使用工厂方法
      
    • 在测试项目中:
      • 创建新的类
      • 声明这个新类继承被测试类
      • 创建需要替换的接口类型的公共字段(field)
      • 重写虚工厂方法
      • 返回公共字段
    • 测试代码中
      • 创建一个stub类的实例(实现相应接口)
      • 创建新的派生类而非被测试类的实例
      • 配置新实例的公共字段(设置成stub实例)

    在测试的派生类中,通过重写工厂方法,产品代码将使用配置的伪对象
    这种抽取和重写的方式,可以直接替换依赖项,实现方法避免了大量和接口和虚函数。

  • 这种方式比较适合用于模拟提供给被测试代码的输入,但是不适合验证被测试代码到依赖项的调用。
    如果被测试代码是web服务时,得到一个返回值,这是如果想要模拟自己的返回值的话,这种方法就很适合。但是如果想要测试代码对于web服务的调用时是否正确就显得捉襟见肘。
    当代码中以及存在可以伪造的接口的时候,有明显的可以使用seam的位置,便不需要使用这种方式。当然如果面对一个密封类,这种方法也显得很无力,无从下手。。。。。

Yu Zhou

Yu Zhou

10 日志
21 标签
© 2018 Yu Zhou
由 Hexo 强力驱动
主题 - NexT.Pisces