架构演化中的软件设计原则_创建可维护和可演化测试的原则

架构演化中的软件设计原则

进行[自动化]单元/集成/功能/…测试非常好,但是它们很容易成为障碍,使系统的任何更改都变得痛苦而缓慢-直至您将其丢弃。 如何避免这种僵化的测试诅咒,太脆弱,太交织,太与实施细节耦合?

当然,遵循清洁代码的原则不仅对生产代码而且对测试都将有所帮助,但这是否足够? 不它不是。

根据我们最近与肯特·贝克(Kent Beck)的课程讨论,我认为以下三个原则对于分离,易于发展的测试很重要:

  • 测试讲故事
  • 真实的单元测试+解耦的更高级别的集成测试(-> Mike Cohn的测试自动化金字塔
  • 更多功能组成的处理

(免责声明:这里所有好的想法都来自肯特·贝克和我的同班同学。所有误解都是我的。)

1.测试讲故事

如果您认为测试讲的是故事–单一测试方法讲出有关小功能的简单故事,有关特定大型功能的整个测试类,有关软件的整个测试套件,则最终会更好,更分离,更容易了解测试。 这意味着当有人阅读测验时,他/他感觉就像在读故事。

实际上,这与Gojko Adzic教授的验收测试非常吻合 。 也就是说,它们应该是供业务用户使用的系统的“实时文档 ”,这是他们迄今为止最大的收益(而且,通常认为,这不是您拥有一组回归测试或您拥有可以表明系统正常运行。

你为什么要把测试写成故事?

  • 它迫使您专注于告诉代码应该做什么,而不是应该怎么做。 因此,您的测试将与实现更分离,从而更易于维护,并且更有可能在实现需求的方式中发现缺陷。
  • 讲故事的方法迫使您从不重要的细节中抽象出来。 例如 通过在测试和所测试内容的低级详细信息之间创建一个抽象层(该层可以包括一些简单的辅助方法或更复杂的方法)
  • 测试将更容易理解。 众所周知,代码被读取的次数比编写的次数多,因此可理解性非常重要。 如果您的测试易于阅读和掌握,它们将成为测试代码的很好的文档。 从事此工作的后代程序员会爱上您。
  • 如果您发现很难以类似故事的方式编写测试,则您的API可能存在问题,应进行更改。

您应该如何编写测试来读故事?

  • 命名–不:should_return_20_if_sk_or_ba –这告诉我什么:20是多少? 什么是ba,sk? (出于好奇:航空公司,即SAS和英国航空公司)是:should_give_discount_for_preferred_airlines –这告诉我要执行的操作和原因
  • 适当的抽象级别–如上所述,要从测试中获得真正的好处并使其能够长期使用,您需要将其保持在适当的抽象级别。 将无关紧要的细节从测试中移到辅助方法,对象(例如ObjectMother )或setUp中。 不要混淆测试的主要逻辑。 (当然,如果您需要大量的设置,或者您的测试中有很多低级代码,则您的测试方法,所测试的代码的设计或这两者都存在某些问题。请首先对其进行修复。) 例如,有些人声称测试中的循环太底层了-如果您需要它,那么您的API可能不够用,它的用户也可能需要循环,所以为什么不只是提供一种合适的方法来进行测试呢?您? 顺便说一句,没有人说在正确的抽象级别上编写测试很容易。 但这是有回报的。

一个(设想的)示例:

// GOOD ABSTRACTION LEVEL:
this.employee.setSickFrom(date(2011,DECEMBER,1)).to(date(2011,DECEMBER,31));
assertSalaryType(SalaryType.SICK, this.calculator.salaryFor(employee, DECEMBER));

// BAD ABSTRACTION LEVEL:
Calendar start = Calendar.getInstance();
start.set(Calendar.YEAR, 2011);
start.set(Calendar.MONTH, DECEMBER - 1); // Januar = 0
start.set(Calendar.DAY_OF_MONTH, 1);
Calendar end = Calendar.getInstance();
start.set(Calendar.YEAR, 2011);
start.set(Calendar.MONTH, DECEMBER - 1); // Januar = 0
start.set(Calendar.DAY_OF_MONTH, 31);
this.employee.setSickFrom(start).to(end);

Salary salary = this.calculator.salaryFor(employee, DECEMBER);

assertNotNull(salary);
SalaryType salaryType = salary.getType();
assertNotNull(salaryType);
assertEquals(SalaryType.SICK, salaryType.getName());
  • 与实现细节的最小耦合–测试代码与实现代码越相似,其实用性就越差。 整个测试的重点是,与您的实现相比,做同一件事的方式有所不同(通常更简单),因此您更有可能捕获其中的错误。 因此,从实现中复制和粘贴代码并稍作调整是一件很糟糕的事情。 如果您不能更简单地做事情(确定不能?!),请尝试至少以不同的方式来做。
  • 大多数情况下使用公共API –为了使您的测试尽可能地分离和可维护,而又不损害代码的可扩展性,对于我来说,尝试尽可能多地坚持被测试类的公共API是合理的。 如果您想检查一些底层实现细节,以确保您正确地完成了这些,请为其创建另一个测试用例,以便在实现发生更改时,您可以将其丢弃,而同时测试由以下人员实施的“故事”该代码将能够幸福地生活下去。 (请参阅从不混合公共和私人单元测试!
  • 每个Fixture的测试案例(针对每种不同设置需求的单独测试类)–我希望您已经知道为一个业务类拥有更多测试用例是正常的。 实际上,JUnit强迫您执行此操作,例如通过要求参数化测试的数据在类级别上。 如果将需要相同设置的测试分组,则可以将设置代码移至@Before方法,从而使测试本身更加简单易读。 当然,在测试用例,夹具和测试方法的数量之间找到合适的平衡总是很困难的。

(旁注:《 xUnit模式》一书也将“ 测试作为文档 ”规定为测试的主要目标之一。)

2.真实的单元测试+解耦的更高级别的集成测试

(-> Mike Cohn的“测试自动化金字塔”各层

对于讨论这个JUnit也许不是一个很好的例子–它非常特殊,因为它使用自身来测试其行为,并且即使测试框架中存在缺陷,测试也必须失败–但是我们仍然可以从中学习一些东西。 令我惊讶的是,它有大量的集成测试*,大约占35%。 长期,可演变的测试的配方似乎是这样写的:

  • 真正的单元测试 ,即仅检查一个类并且不依赖于它与其他类的协作的测试。 这样的测试不受那些协作(集成测试所涵盖)的更改的影响,并且如果测试的类本身发生更改,则该测试可能会跟上更改的步伐,或者是大规模的更改,您只需将其丢弃(可能与要测试的类一起),然后为新设计编写新的测试。 (我并不是说这更容易实现,它还需要一种特殊的编码和结构化软件方式(请参见下面的第3点),但这显然是一种方法。)
  • 集成测试可检查多个对象的协作–不一定是整个应用程序或子系统,几乎是任何可定义的功能。 同样,集成测试应该讲一个有关模块功能的故事-即使对象组在内部实现它的方式发展了,这个故事本身也不大可能改变。 因此,在正确的级别进行测试至关重要,在这种级别上,您不会对具体的实现细节感到烦恼,但与想要测试的代码相距不远。 肯特(Kent)提到了一个很好的例子,说明了如何通过使用命令对象替换嵌套的方法调用来重构JUnit 4.5管理和执行测试的各个阶段的方式(这使得引入@Rule成为可能)–感谢集成测试处于“ I”级别给执行子系统一些输入,并期望有一个特定的输出”,它们仍然有效。 如果它们处于较低级别,并且依赖于存在一系列嵌套方法调用的事实,则重构将更加困难。

(*)什么是集成测试? 根据一种可能的定义,单元测试是一种测试,在该测试中,看到失败消息后,您可以立即查明出问题的代码片段甚至行。 与此相反,如果集成测试失败,则通常无法说出原因,而必须深入研究或调试一下。

3.更多功能的处理(即杀死模拟!)

您是否需要模拟框架来编写测试? 然后,您可能会以次佳的方式进行操作。 (免责声明:几乎所有东西都肯定有不同但同样好的方法:-)。)当肯特(Kent)解释他编写程序的方式时,他画了一幅类似的图:

有趣的是什么? 这不是典型的对象网络,在该网络中,一个对象执行某项操作并调用另一个对象以“继续”进行处理或执行其中的一部分。 它由两种类型的对象组成:接收输入并产生输出的工作人员,以及将工作委派给各个工作人员并以自己最小的逻辑将工作从一个工作人员传递到另一个工作人员的集成商。 这些工作人员功能非常强大,因此很容易使用真正的单元测试进行测试(而且,如果说需要不同的实现,您可以将工作人员的测试丢掉并创建一个新的测试),而集成商应该像可能(因此出现缺陷的可能性较小),并由集成测试覆盖。 (一切都很好地融合在一起,不是吗?)

一个示例工作程序是JUnit的Statementf.ex. FailOnTimeout ),它代替了以前使用的一系列嵌套函数调用。 集成者的角色由跑步者承担。

结论

鉴于大多数大型商务软件系统已经存在了很多年,因此编写测试以增强和限制系统的可扩展性至关重要。 这并不容易,但我们必须为此作出努力。

为了获得成功,我们必须以一种特殊的方式来构造我们的代码和测试,并按照测试方法,测试类和测试套件来讲述有关被测代码的功能的故事。

相关链接

参考: JCG合作伙伴 Jakub Holy“ The Holy Java”博客上 创建可维护和可演化测试的原则

相关文章 :

翻译自: https://www.javacodegeeks.com/2011/11/principles-for-creating-maintainable.html

架构演化中的软件设计原则

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值