单元测试是开发过程的关键环节。它们允许以可重复执行、可维护的方式对代码进行快速、简单的测试。具体来说,单元测试有以下优点:
在开发期间通过测试捕获缺陷。较小的工作单元更容易编码和调试。
对代码更改进行轻松的测试。因为单元测试是设计来测试业务用例的,如果技术实现发生了变化,那么单元测试可以重新运行以确保程序的结果没有发生变化。
验证在产品测试过程中可能没有被覆盖的罕见边角情况。
向同伴、主管和客户展示质量的程度。
单元测试的一个重要特点是方法必须得单独测试,这与主要测试方法与类之间交互的组装或集成测试不同,单元测试应该独立地测试方法。这有助于查明问题,并大大提高问题的修复速度。
单元测试应该尽可能频繁地运行。这将确保缺陷被立即发现并修复。自动化单元测试执行的一种方法是使用持续集成系统,该系统在每次源代码做出更改时运行所有单元测试。
该指南的对象将是:
技术架构师
数据架构师
应用程序设计师
开发人员
01
构建一个好的单元测试
为了确保类被适当地测试,所以在构建测试场景时必须小心:
通过一次只测试一个业务路径来保持单个测试的简单性。如果只有一条路径的测试失败,则更容易找到错误并纠正它。较短的测试也更容易阅读和维护,更容易作为新测试的构建块重用。
当测试“搜索”方法时,应该考虑多种数据情况。通常,应该创建单独的测试条件来测试返回零条、一条和多条记录以及使用零个、一个和多个搜索条件组合的场景。
使用“无关的”数据,以确保程序使用正确的数据,并忽略其他数据。例如,如果一个方法打算处理所有状态为“A”的记录,那么数据应该包括一些状态不为“A”的记录,以确保它们没有被处理。
测试边角案例非常重要,可以验证它们是否被发现。要特别注意错误条件和异常情况,因为它们通常在单元测试期间比在后面的测试阶段更容易模拟。
02
单元测试的惯例
单元测试应该是可重复执行的,并且独立于执行它们的运行时环境。
在设计和构建代码时考虑可测试性。遵循设计最佳实践,如“对接口编程”或“依赖注入”,因为这些惯例可能会产生容易测试的代码。
编写单元测试的一个关键思维模式是“编写一点,测试一点,编写一点,测试一点……”
尽可能频繁地运行测试。
为最复杂的区域编写测试。尽早运行这些测试,它们可能会失败,但是通过不断优化直到测试通过,您可以对测试的有效性获得信心。
有效的单元测试不仅调用一些应用程序逻辑,而且要彻底检查输出和对象状态。使用断言自动比较实际结果与预期结果,并为每个断言提供有意义的消息。通过这种方式,测试执行报告将准确地显示什么测试失败以及为什么失败。
对于更复杂的逻辑,也要编写自动化的组装/集成测试。遵循与单元测试相同的原则和工具,但同时关注测试多个单元的行为,同时保持范围狭窄,例如controller类如何与底层的服务层交互,或者同一个类中的不同方法在一起工作时行为是如何的。
单元测试不应该依赖于外部系统,如数据库或容器(web服务器、应用服务器)。
组装/集成测试可能依赖于外部系统,但最佳实践是尽可能使用嵌入式的runtimes以确保可执行性和测试的独立性。例如,利用嵌入式或内存数据库和嵌入式容器来运行组装/集成测试。这些类型的嵌入式runtimes在需要时创建,根据需要填充测试数据,并在测试套件完成后丢弃。
如果您发现自己正在编写System.out.println()来调试方法,或者编写main()方法来启动某个测试,请考虑使用合适的测试脚本来替换。
当一个缺陷被报告时,马上为它写一个测试用例。
单元测试中在所有测试用例没有通过之前请不要关闭它。
将单元测试代码提交到源代码配置库中,这样就可以被其他开发人员和持续集成系统利用。这允许在开发人员每次提交代码时确保开发人员A的工作不会破坏开发人员B的工作。
03
保持测试用例简单
使每个单独的测试尽可能简单。编写单一方法的测试用例,而不是间接地测试它的所有交互者。测试应该简单,这意味着可以单独的测试某个功能区域。如果需要测试更大的功能区域,那么可以编写测试用例组来运行。Java模块是离散设计的,它们的测试用例应该反映这一点。
类应该是可测试的,不仅可以在相互隔离的情况下,还可以在运行时环境之外。单元测试只能在应用程序服务器之外有效地执行。好的单元测试可以帮助您简化设计。
单元测试脚本应该是自包含的,并且不依赖于任何外部系统。可以使用Mock对象表示任何外部系统的行为。
04
保持测试小而快
为整个系统执行每个测试不应该花费超过数小时,这样开发人员就可以快速执行测试。如果没有定期运行完整的测试集,当发生更改时,将很难验证整个系统。错误会慢慢地回来,单元测试的好处也会丢失。
单个类或小型框架类的压力测试和负载测试不应作为单元测试套件的一部分运行;它们应该分开执行。
05
使测试易于运行
易于运行的测试将被更经常地使用。它们应该是独立的,并且应该小心确保它们不涉及人工干预。如果测试使用了数据存储或一组文件,那么测试应该在测试开始时初始化资源,并在测试结束时释放它们。在将任何内容签入源代码控制系统之前,应该运行所有测试。
06
选择有意义的测试方法名称
所有测试方法都应该以“test”开头。这是JUnit 3等旧的单元测试框架的需求。它被广泛接受为测试方法命名的最佳实践。
您必须能够通过阅读方法名称来理解测试的是什么。一个好的规则是从testXxx命名方案开始,其中Xxx是要测试的方法的名称。当您针对同一方法添加其他测试时,请使用testXxxYyy方案,其中Yyy描述了测试的不同之处。例如:
testloggingemptymessage ()
testloggingnullmessage ()
testloggingwarningmessage ()
testloggingerrormessage ()
07
文档化单元测试
单元测试是作为代码编写的,应该使用Javadoc对其进行适当的文档化。好的测试用例文档将包括被测试的内容、测试中使用的输入数据和预期结果的简短描述。当需要解释困难的或非典型的测试方法时,使用实现注释,例如如何创建、填充和释放内存中的数据库。
当采用测试驱动的开发方法时,测试将在被测组件设计完成之后立即设计和创建。如果使用UML符号来设计测试,那么您可以利用UML工具的代码生成功能(就像IBM Rational Software Architect那样)来自动生成测试文档和代码框架。利用这一点来加速测试创建过程。
08
解释Assert调用中失败的原因
当您在JUnit框架或其他测试框架中使用assertEquals、assertTrue、assertFalse、assertNull和assertNotNull方法时,请确保您提供了一个有意义的文本描述,可以在测试运行器中显示,或者在断言失败时报告。如果没有提供描述,那么当故障发生时,就很难理解故障的原因。例如:
assertNotNull("Must not return a null response", response); |
如果使用了assertNotNull(Object),JUnit运行程序就会显示一个堆栈跟踪,显示一个没有消息的AssertionFailedError异常,这将更加难以诊断。
如果断言失败,上面的例子为测试人员提供了更多的信息。
使用assertEquals()来测试两个相等的对象。使用assertSame()来测试指向同一对象的两个引用。
09
一个单元测试等于一个测试方法
不要试图将多个测试压缩为一个方法。
结果将是更复杂的测试方法,将变得越来越难以阅读和理解。
这让人们更难以精确地看到哪里出了问题。
如果需要在多个测试中使用相同的代码块,请将其提取到每个测试方法都可以调用的utility方法中。更好的是,如果所有方法都可以共享代码,那么将其放到fixture中。为了获得最好的结果,您的测试方法应该和您的领域方法一样简洁和集中。
10
避免在单元测试中使用配置文件
避免为测试数据使用配置文件。通过编程将数据硬编码到setUp()方法、@BeforeClass注释方法或测试方法本身中。如果需要配置(例如Spring context文件),根据需要为每个测试类或测试方法创建一个配置文件。
对于可能受runtime环境影响的配置数据,请务必使用配置文件。例如,数据库位置和凭证信息应该保存在配置文件中。
通常,这将使配置文件的使用最小化,并且很少有测试脚本实际需要使用它们。
11
不要只是为了测试而创建方法
开发人员不应该仅仅为了测试而创建方法。如果设计的类无法测试,那么开发人员应该与团队主管/架构师讨论,以找出根本原因,并根据需要调整设计。
12
数据管理
编写单元测试的一个重要问题是如何设置代码使用的数据。测试用例所需的所有数据应该在测试方法开始时创建,并在测试完成后销毁。如果测试依赖于数据库中已经存在的数据,那么如果数据库记录被更新或删除,该测试将失败。由于单元测试的目标之一是能够重新执行测试,因此不需要任何人手动检查或修复数据库中的数据,以确保测试能够运行。
单元测试不应该依赖于现有数据库或任何其他外部系统。这不仅意味着测试一个组件和其他组件(例如数据访问对象或底层持久性框架),而且会对运行时环境施加很大的依赖性。当多个开发人员和/或持续集成引擎在运行测试时试图访问相同的数据库时,也会造成麻烦。
因此,为单元测试创建mock或stub对象,并使用它们来隔离被测试的单元。例如,在测试依赖数据访问对象查询数据存储的服务时,为数据访问对象构造一个模拟对象,该对象将为特定查询返回预期的数据。从服务的角度来看,这与让数据存储返回此数据是一样的,测试可以集中于验证服务逻辑。考虑使用EasyMock框架来动态创建模拟对象并根据预期定义它们的行为。EasyMock和类似的工具可以减少设置这种测试所需的工作量,从而减少创建测试方法所需的工作量。
13
代码覆盖率
代码覆盖率是指在单元测试期间执行的代码的百分比。虽然它是测试范围的有用指标,但代码覆盖率本身不应该被视为目的。例如,如果90%的一个特定的类的代码中包含的getter和setter,只有10%是业务逻辑,然后测试覆盖只有getter和setter方法将使代码覆盖率很高(90%),但完全毫无用处,因为业务逻辑至关重要的领域。相反,开发人员必须专注于编写涵盖关键代码的测试,并避免花费时间编写简单代码(如getter和setter)的测试。
代码覆盖率可以通过与单元测试一起运行的工具来测量,这些工具可以自动确定执行了多少代码。可以使用这些工具轻松地查看代码中哪些路径没有被测试,这样就可以创建额外的测试场景来覆盖它们。一个流行的工具是Cobertura,它可以作为一个Eclipse插件执行,与Ant或Maven构建集成,使用一个持续集成引擎(Jenkins)和一个持续质量保证工具(Sonar)。它提供了按项目、包、类和方法划分的覆盖率百分比。它还用绿色高亮显示源代码以显示已执行的代码,用红色显示未执行的代码。
14
使用JUnit
JUnit是一个Java的单元测试框架。它易于使用,并可与Eclipse、Ant或Maven构建、Jenkins等持续集成引擎和Sonar等持续质量保证工具集成。
15
Basic JUnit
从JUnit 4开始,框架提供了用于指定单元测试的注释,并简化了测试方法的创建:
public class MyTests { |
注意:从JUnit 4开始,测试类不必扩展任何JUnit类,JUnit框架也不要求任何方法名称约定。但是,以“test”开头命名测试方法是一种最佳实践。
带有@BeforeClass注释的方法将在执行任何测试之前运行。这对于初始化测试数据、Mock或Stub非常有用。
注释了@AfterClass的方法将在所有测试执行之后运行。使用它来释放任何资源。
每个@Before方法将在每个测试方法之前运行。
每个@After方法将在每个测试方法之后运行。
每个@Test方法将作为单独的测试运行。
16
预期某些异常条件的测试方法
JUnit提供了一种机制,可以方便地验证被测方法中是否出现了异常情况。它基于新的规则机制,需要三个简单的步骤:
在方法的开始定义预期的异常。这是通过用@org.junit注释的类org.junit.rules.ExpectedException的实例完成的。您可以定义预期的异常和/或预期的错误消息或模式。
编写已知会导致异常的测试代码。
JUnit将封装调用并验证是否引发了实际的异常。否则,测试方法将失败。
下面是如何创建这种测试方法的示例:
import org.junit.Rule; public class MyClassTestCase { @Rule /** |
17
Spring 中的JUnit
测试Spring应用程序时,最佳实践是利用Spring框架本身附带的特定JUnit运行器SpringJUnit4ClassRunner。在许多有用的功能中,这个专门的JUnit运行器允许测试类使用不同的应用程序上下文。Spring为此提供了类级注释:
@RunWith(SpringJUnit4ClassRunner.class) |
当指定这些注释时,Spring将从@ContextConfiguration注释中列出的文件加载ApplicationContext。可以使用@Autowired或@Resource属性来访问ApplicationContext中定义的bean。
@Autowired @Resource @Test |
@Autowired会按类型注入bean,这意味着bean的名字不需要匹配变量的名字,但是如果有两个相同类型的bean,注解就会失败。
@Resource将按名称注入bean,这意味着变量名必须与bean名匹配。使用@Resource,你可以使用默认的注释值来选择一个特定的bean,例如@Resource(“theBeanName”)。
18
事务性测试
在为Spring应用程序创建组装/集成测试时,使它们具有事务性通常很有用。例如,如果测试对数据库进行了更改,那么可能需要在测试结束时删除这些更改。通过用@Transactional注释测试方法,Spring将确保整个测试被包装在一个事务中,并且当方法结束时事务被回滚:
@RunWith(SpringJUnit4ClassRunner.class) |
@Transactional表示测试方法应该是事务性的。
可以选择使用@TransactionConfiguration覆盖事务测试的默认设置。它有两个可选参数:
transactionManager:指定要使用的事务管理器的bean名称(默认为“transactionManager”)。
defaultRollback:确定默认情况下事务测试是否应该回滚(默认为“true”)。
3. @BeforeTransaction注释了在每个事务开始之前应该执行的方法。
4. @Before将在每个测试方法之前执行,并在事务内部执行。
5. @Rollback可用于覆盖类级别的“defaultRollback”设置。
6. @After将在每个测试方法之后执行,并在事务内部执行。
7. @AfterTransaction注释了在每个事务开始后应该执行的方法。
8. @NotTransactional表示不应该在事务中执行带注释的方法。
19
构建单元测试
与所有代码一样,单元测试的结构应该易于阅读和维护。如果其他开发人员对被测试的代码进行更改,他们可能需要更新单元测试。此外,由于单元测试应该涵盖业务用例,而不是关注实现细节,因此其他开发人员应该能够阅读单元测试,以了解被测试代码的业务功能。
适当的文档对于测试代码的可维护性是至关重要的。每个测试方法都应该明确地定义它的测试条件和预期的结果。测试条件应该用功能术语解释,用简单的英语。它们不应涉及设计的技术实现。
一个分解的实现将有助于可维护的测试。如果技术设计被分解为小部分的工作,那么可以为每一部分编写简单、可维护的测试。
20
建立数据
单元测试的第一阶段应该设置代码将要使用的数据。这可能包括将数据插入到数据库中,或者创建要传递到被测试方法中的数据。
创建的数据将构成单个测试条件。因此,初始化数据的代码应该是可读的,并且应该使用注释,以便读者或维护人员确切地知道要测试的条件是什么。
实例变量不应该在测试类中使用;所有的变量都应该包含在测试方法中。使用实例变量可以使各个测试方法相互依赖。测试方法应该是完全独立的,以便它们可以独立运行或以任何顺序运行。
更多信息请参见指南前面的“数据管理”部分。
@Test //... |
21
调用要测试的方法
在设置测试数据之后,应该调用要测试的方法。
22
验证得到了预期的结果
在执行了subject方法之后,测试必须验证被测试的方法完成了它应该做的事情。结果主要通过写“断言”来验证。
如果subject方法返回一些东西,那么测试应该验证返回的内容是正确的。
测试应该只包含关于业务逻辑的断言。这将允许测试具有灵活性。如果代码在稍后更改,但没有更改特定情况下的最终结果,则不需要重新访问测试。
如果程序包含抛出异常的代码,并且确定单元测试应该验证抛出的异常,使用规则机制,如本指南前面“预期某些异常条件的测试方法”部分所示:
测试一个搜索方法时,测试应该断言不仅检索到正确的条目数量,而且这些条目包含正确的值。例如:
List results = mySearchMethodToTest(); |
23
测试替身
test double是给定接口的虚假或默认实现。它被称为“测试替身”,因为它在测试期间代表真实的实现(就像特技替身在特技期间代表演员)。测试重复使用提供了一种方法来隔离单元测试中的依赖项。这允许测试针对特定的方法,而不依赖于其他组件的功能。
具体来说,测试替身:
通过测试单一方法来提供更好的聚焦。
允许测试,即使依赖没有编码,只要有API存在。
防止编写测试的人不得不创建数据,以使依赖行为的特定方式。
将测试与依赖项实现中的更改隔离。
为了说明如何使用测试替身,让我们考虑以下场景(基于MartinFowler在“Mock不是Stub”一文中给出的场景)。假设我们有一个OrderServiceImpl,我们的应用程序使用它来下订单。OrderServiceImpl的工作方式是验证订单至少包含一项,然后通过电子邮件将订单发送到仓库。订单由order类表示,该类包含产品名称和数量。电子邮件是由EmailService发送的,它是OrderServiceImpl的一个依赖项。如果电子邮件成功发送,则EmailService返回“true”。
在测试时,我们不希望必须发送实际的电子邮件来验证OrderServiceImpl的功能,因此我们将在测试期间使用测试double来代替EmailService。
public class OrderServiceImpl implements OrderService { |
24
Stubs
Stub是一种测试替身,它提供测试所期望的某些特定行为的默认实现。Stub还可以用于在测试期间存储信息。Stub的定义特征是它使用状态验证。状态验证是指测试正在验证服务是否以特定状态结束。在我们的例子中,我们的测试断言服务以特定的返回类型结束,并且已经发送了特定的订单(或者没有)。
我们的单元测试类包含EmailServiceStub的Stub实现,它让我们非常容易地指定send方法的行为。我们的测试类包含三个条件的测试:有效数量和成功电子邮件,有效数量和不成功电子邮件,以及无效数量。前两次测试的唯一区别是,邮件在第一次测试中“成功”,而在第二次测试中“不成功”。通过使用stub,我们可以通过success属性将我们的电子邮件服务“编程”为成功或不成功。我们的stub还通过存储每次接收到订单的产品名称来跟踪它接收到的订单。在前两个测试中,由于数量是有效的,因此将订单交给EmailService。但是,在第三个测试中,我们可以验证没有调用EmailService。
private static class EmailServiceStub implements EmailService { |
25
Mocks
mock是一种测试替身类型,可以使用预期的行为预编程,并在遇到该行为时返回值。因此,mock使用行为验证。行为验证意味着测试不仅断言服务在特定状态下结束,而且断言在执行测试期间发生的特定事件序列。
有几个框架可以方便使用Mock进行测试。一个流行的mock框架是EasyMock。EasyMock可用于包装接口并定义测试的预期行为,包括调用方法的顺序、方法的参数和方法的返回类型。
对于我们的三个测试条件,我们使用一个Mock EmailService,它是用createMock静态方法创建的。createMock返回一个EmailService以来,我们可以通过简单地调用服务的方法与参数来“编程”我们预计订单的期望结果(注意,应该实现equals方法中使用任何类参数,以便EasyMock在测试期间可以验证参数是正确的)。要指定期望返回值,使用expect和和return方法的组合。一旦定义了行为,就必须调用replay以准备使用Mock。最后,在测试完成之后,验证方法应该用于验证预期的行为确实发生了。
@Test |
26
测试方法
级别1 -单元测试
单元测试是最细粒度的测试级别。单元测试隔离被测试的类,并将对所有协作类使用测试替身。这允许测试专门关注被测试的类,而不用担心协作类中的问题或更改会影响测试。
这种级别的测试在开发的早期阶段和团队级别的构建中非常有用。即使协作类尚未编写,也可以编写和执行单元测试。此外,单元测试不需要连接到环境数据库或外部资源。
除了DAO之外,架构和应用程序中的所有类都应该进行单元测试。
级别2 -组装/集成测试
组装/集成测试用于测试不同类之间的交互。与单元测试不同,在单元测试中使用测试替身来隔离类,集成测试依赖于协作类的实际实现。集成测试可用于测试整个流(例如,从controller到service和DAO再到数据库)或流的任何子集。
与单元测试不同,组装/集成测试应该从Spring的ApplicationContext中检索要测试的bean。这将确保对配置文件中的连接进行测试。此外,组装/集成测试可以与环境数据库或外部资源交互。但是,最佳实践是尽可能地依赖嵌入式runtime,例如利用内存数据库而不是应该在测试时访问的外部数据库。
相关链接:
关于作者
本文分享自微信公众号 - 测试技术圈(SoftwareTesters)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。
来源:oschina
链接:https://my.oschina.net/u/41205/blog/4399754