使用旧版测试代码

旧版代码和测试气味

使用单元测试可以在许多方面帮助改善代码库。
我最喜欢的方面之一是,测试可以使我们指出生产代码中的代码味道。

例如,如果测试需要大型设置或声明许多输出,则可能表明被测单元未遵循良好的设计,例如SRP和其他OOD。

但是有时测试本身的结构或设计不佳。
在这篇文章中,我将针对这种情况给出两个示例,并说明如何解决。

测试类型

(或图层)

有几种类型的测试。

  • 单元测试
    单元测试应该易于描述和理解。 这些测试应该运行得很快 。 他们应该测试一件事。 一个单元(方法?)。
  • 整合测试
    集成测试的定义更加模糊。 他们检查哪种模块? 几个模块集成在一起? 依赖注入器接线? 使用真实数据库进行测试?
  • 行为测试
    这些测试将验证功能。 它们可能是PM / PO与开发团队之间的接口。
  • End2End /接受/分期/功能
    高水平的测试。 可以在生产或类似生产的环境上运行。

测试的复杂性

基本上,测试的“级别越高”,它越复杂。 而且,每个测试级别的可能测试数量与生产代码之间比率会急剧增加。 单元测试将随着代码的增长而线性增长。 但是从集成测试和更高级别的测试开始,选项开始以指数级增长。

计算简单:如果两个类相互交互,并且每个类都有2个方法,那么我们应该检查多少个选项才能覆盖所有选项? 并设想这些方法具有某些控制流程,例如if

偶尔失败的测试

有很多原因使测试“有问题”。 最糟糕的情况之一是测试有时会失败且通常会通过。 团队将忽略CI的邮件。 它会在系统中产生噪音。 您永远无法确定是否有错误或损坏的东西或错误的警报。 最终,我们将禁用配置项,因为“它不起作用并且不值得花时间”。

集成测试和虚警

如果我们不遵守基本规则,则任何类型的测试均会产生误报。 测试级别越高,错误警报的可能性就越大。 在集成测试中,由于外部资源问题而导致误报的可能性更高:没有互联网连接,没有数据库连接,随机丢失等等。

我们的测试环境

我们的系统是“准遗产”。 它不是完全遗留的,因为它具有测试。 那些测试甚至具有良好的覆盖率。 由于它的(非)结构化方式和测试的构建方式,它是传统的。 它过去仅由集成测试覆盖。 在过去的几个月中,我们开始实施单元测试。 特别是在新代码和新功能上。

我们所有的集成测试都继承自BaseTest ,后者继承了Spring的AbstractJUnit4SpringContextTests 。 测试的上下文连接了所有内容。 大约95%的生产代码。 它需要时间,但更糟的是,它连接到实际的外部资源,例如MongoDB和连接到Internet的服务。

为了提高测试速度,几周前,我将MongoDB更改为嵌入式。 它将测试的运行时间缩短了一个数量级。

这种设置会使测试更加困难。 模拟服务非常困难。 环境不是与Internet和DB等隔离的。

在漫长的介绍之后,我想描述两个有问题的测试以及修复它们的方式。 他们共同的失败属性是他们有时会失败并且通常会通过。 但是,每个失败的原因都不相同。

案例研究1 –在构造函数中创建Internet连接

第一个示例显示了一个测试,该测试有时由于连接问题而失败。 棘手的部分是,在构造函数中创建了一个服务。 该服务获得了HttpClient,它也是在构造函数中创建的。

另一个问题是,我无法修改测试以使用模拟代替Spring接线。 这是原始的构造函数(针对示例进行了修改):

private HttpClient httpClient;
private MyServiceOne myServiceOne;
private MyServiceTwo myServiceTwo;

public ClassUnderTest(PoolingClientConnectionManager httpConnenctionManager, int connectionTimeout, int soTimeout) {
	HttpParams httpParams = new BasicHttpParams();
	HttpConnectionParams.setConnectionTimeout(httpParams, connectionTimeout);
	HttpConnectionParams.setSoTimeout(httpParams, soTimeout);
	HttpConnectionParams.setTcpNoDelay(httpParams, true);
	httpClient = new DefaultHttpClient(httpConnenctionManager, httpParams);

	myServiceOne = new MyServiceOne(httpClient);
	myServiceTwo = new MyServiceTwo();
}

经测试的方法使用了myServiceOne 。 由于该服务中的连接问题,测试有时会失败。 另一个问题是,它并不总是确定性的(来自网络的结果),因此失败了。

编写代码的方式无法使我们模拟服务。

在测试代​​码中,使用@Autowired批注注入了被测类。

解决方案–

想法取自有效地使用遗留代码

  1. 确定我需要解决的问题。
    为了使测试具有确定性,并且无需真正连接到Internet,我需要访问服务以创建服务。
  2. 我将介绍创建这些服务的受保护方法。
    而不是在构造函数中创建服务,我将调用这些方法。
  3. 在测试环境中,我将创建一个类来扩展被测类。
    此类将覆盖这些方法,并将返回假的(模拟的)服务。

解决方案的代码

被测课程

public ClassUnderTest(PoolingClientConnectionManager httpConnenctionManager, int connectionTimeout, int soTimeout) {
	HttpParams httpParams = new BasicHttpParams();
	HttpConnectionParams.setConnectionTimeout(httpParams, connectionTimeout);
	HttpConnectionParams.setSoTimeout(httpParams, soTimeout);
	HttpConnectionParams.setTcpNoDelay(httpParams, true);
	
	this.httpClient = createHttpClient(httpConnenctionManager, httpParams);
	this.myserviceOne = createMyServiceOne(httpClient);
	this.myserviceTwo = createMyServiceTwo();
}

protected HttpClient createHttpClient(PoolingClientConnectionManager httpConnenctionManager, HttpParams httpParams) {
	return new DefaultHttpClient(httpConnenctionManager, httpParams);
}

protected MyServiceOne createMyServiceOne(HttpClient httpClient) {
	return new MyServiceOne(httpClient);
}

protected MyServiceTwo createMyServiceTwo() {
	return new MyServiceTwo();
}

测试班

private MyServiceOne mockMyServiceOne = mock(MyServiceOne.class);
private MyServiceTwo mockMyServiceTwo = mock(MyServiceTwo.class);
private HttpClient mockHttpClient = mock(HttpClient.class);

private class ClassUnderTestForTesting extends ClassUnderTest {

	private ClassUnderTestForTesting(int connectionTimeout, int soTimeout) {
		super(null, connectionTimeout, soTimeout);
	}
	
	@Override
	protected HttpClient createHttpClient(PoolingClientConnectionManager httpConnenctionManager, HttpParams httpParams) {
		return mockHttpClient;
	}

	@Override
	protected MyServiceOne createMyServiceOne(HttpClient httpClient) {
		return mockMyServiceOne;
	}

	@Override
	protected MyServiceTwo createMyServiceTwo() {
		return mockMyServiceTwo;
	}
}

现在,不用在测试中连接类,而是在@Before方法中创建它。 它接受其他服务(此处未描述)。 我使用@Autowire获得了这些服务。

另一个注意事项:在创建特殊的测试类之前,我运行了该类的所有集成测试,以验证重构没有破坏任何内容。 我还重新启动了本地服务器,并验证了一切正常。 使用旧版代码时,进行这些验证很重要。

案例研究2 –随机输入的统计检验

第二个示例描述了由于随机结果和统计断言而失败的测试。 该代码在具有相似属性的对象之间进行了随机选择(我在这里简化了场景)。 Random对象是在类的构造函数中创建的。

简化示例:

private Random random;

public ClassUnderTest() {
	random = new Random();
	// more stuff
}

//The method is package protected so we can test it
MyPojo select(List<MyPojo> pojos) {
	// do something
	int randomSelection = random.nextInt(pojos.size());
	// do something
	return pojos.get(randomSelection);
}

原始测试进行了统计分析。 我将对其进行解释,因为它太复杂且冗长,无法编写。 它有1万次迭代的循环。 每次迭代都称为被测方法。 它具有一个Map,该Map计算每个MyPojo的出现次数(返回结果)。 然后,它检查每个MyPojo是否在(10K / Number-Of-MyPojo)处选择了某种偏差0.1。

例:

假设我们在列表中有4个MyPojo实例。 然后该断言验证了每个实例在2400到2600次(10K / 4)之间被选择,偏差为10%。

您当然可以期望有时测试会失败。 增加偏差只会减少错误的失败测试的次数。

解决方案–

  1. 重载被测方法。
    在重载方法中,添加一个与全局字段相同的参数。
  2. 将代码从原始方法移到新方法。
    确保使用方法的参数而不是类的字段。 不同的名称可以在这里提供帮助。
  3. 使用模拟测试新创建的方法。

解决方案代码

private Random random;

// Nothing changed in the constructor
public ClassUnderTest() {
	random = new Random();
	// more stuff
}

// Overloaded method
private select(List<MyPojo> pojos) {
	return select(pojos, this.random);
}

//The method is package protected so we can test it
MyPojo select(List<MyPojo> pojos, boolean inRandom) {
	// do something
	int randomSelection = inRandom.nextInt(pojos.size());
	// do something
	return pojos.get(randomSelection);
}

结论

使用遗留代码可能会充满挑战并且很有趣。 使用遗留测试代码也可能很有趣。 停止接收失败的测试令人讨厌的邮件真的很不错。 这也增加了团队对CI流程的信任。

翻译自: https://www.javacodegeeks.com/2015/03/working-with-legacy-test-code.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值