1. Overview
1.概述
Unit Testing is a crucial step in software design and implementation.
单元测试是软件设计和实现的一个关键步骤。
It not only improves the efficiency and effectiveness of the code, but it also makes the code more robust and reduces the regressions in future development and maintenance.
它不仅提高了代码的效率和效果,而且还使代码更加健壮,减少了未来开发和维护中的退步现象。
In this tutorial, we’ll discuss a few best practices for unit testing in Java.
在本教程中,我们将讨论Java中单元测试的一些最佳实践。
2. What Is Unit Testing?
2.什么是单元测试?
Unit Testing is a methodology of testing source code for its fitment of use in production.
单元测试是一种测试源代码是否适合在生产中使用的方法。
We start out writing unit tests by creating various test cases to verify the behaviors of an individual unit of source code.
我们在开始编写单元测试时,会创建各种测试用例来验证单个单元的源代码的行为。
Then the complete test suite executes to catch the regressions, either in the implementation phase or while building packages for various stages of deployments such as staging and production.
然后执行完整的测试套件以捕捉回归,无论是在实施阶段还是在为不同阶段的部署构建软件包时,如暂存和生产。
Let’s take a look at a simple scenario.
让我们来看看一个简单的场景。
To start with, let’s create the Circle class and implement the calculateArea method in it:
首先,让我们创建Circle类并在其中实现calculateArea方法。
public class Circle {
public static double calculateArea(double radius) {
return Math.PI * radius * radius;
}
}
Then we’ll create unit tests for the Circle class to make sure the calculateArea method works as expected.
然后我们将为Circle类创建单元测试,以确保calculateArea方法按预期运行。
Let’s create the CalculatorTest class in the src/main/test directory:
让我们在src/main/test目录下创建CalculatorTest类。
public class CircleTest {
@Test
public void testCalculateArea() {
//...
}
}
In this case, we’re using JUnit’s @Test annotation along with build tools such as Maven or Gradle to run the test.
在这种情况下,我们使用JUnit的@Test注解以及诸如Maven或Gradle等构建工具来运行该测试。
3. Best Practices
3.最佳实践
3.1. Source Code
3.1 源代码
It’s a good idea to keep the test classes separate from the main source code. So, they are developed, executed and maintained separately from the production code.
将测试类与主源代码分开是个好主意。因此,它们是与生产代码分开开发、执行和维护的。
Also, it avoids any possibility of running test code in the production environment.
同时,它避免了在生产环境中运行测试代码的任何可能性。
We can follow the steps of the build tools such as Maven and Gradle that look for src/main/test directory for test implementations.
我们可以按照Maven和Gradle等构建工具的步骤,寻找src/main/test目录进行测试实现。
3.2. Package Naming Convention
3.2.包的命名规则
We should create a similar package structure in the src/main/test directory for test classes, this way improving the readability and maintainability of the test code.
我们应该在src/main/test目录下为测试类创建一个类似的包结构,这样可以提高测试代码的可读性和可维护性。
Simply put, the package of the test class should match the package of the source class whose unit of source code it’ll test.
简单地说,测试类的包应该与源类的包相匹配,它将测试源码的单元。
For instance, if our Circle class exists in the com.baeldung.math package, the CircleTest class should also exist in the com.baeldung.math package under the src/main/test directory structure.
例如,如果我们的Circle类存在于com.baeldung.math包中,CircleTest类也应该存在于com.baeldung.math包的src/main/test目录结构之下。
3.3. Test Case Naming Convention
3.3.测试用例命名规则
The test names should be insightful, and users should understand the behavior and expectation of the test by just glancing at the name itself.
测试名称应该是有洞察力的,用户只需扫一眼名称本身,就应该了解测试的行为和期望。
For example, the name of our unit test was testCalculateArea, which is vague on any meaningful information about the test scenario and expectation.
例如,我们的单元测试的名称是testCalculateArea,这对测试场景和期望的任何有意义的信息都是模糊的。
Therefore, we should name a test with the action and expectation such as testCalculateAreaWithGeneralDoubleValueRadiusThatReturnsAreaInDouble, testCalculateAreaWithLargeDoubleValueRadiusThatReturnsAreaAsInfinity.
因此,我们应该用动作和期望来命名一个测试,比如testCalculateAreaWithGeneralDoubleValueRadiusThatReturnsAreaInDouble,testCalculateAreaWithLargeDoubleValueRadiusThatReturnsAreaAsInfinity。
However, we can still improve the names for better readability.
然而,我们仍然可以改进这些名称,以提高可读性。
It’s often helpful to name the test cases in given_when_then to elaborate on the purpose of a unit test:
用given_when_then来命名测试用例,对阐述单元测试的目的往往很有帮助。
public class CircleTest {
//...
@Test
public void givenRadius_whenCalculateArea_thenReturnArea() {
//...
}
@Test
public void givenDoubleMaxValueAsRadius_whenCalculateArea_thenReturnAreaAsInfinity() {
//...
}
}
We should also describe code blocks in the Given, When and Then format. In addition, it helps to differentiate the test into three parts: input, action and output.
我们还应该以Given、When和Then格式描述代码块。此外,有助于将测试区分为三个部分:输入、行动和输出。
First, the code block corresponding to the given section creates the test objects, mocks the data and arranges input.
首先,对应于given部分的代码块创建测试对象,模拟数据并安排输入。
Next, the code block for the when section represents a specific action or test scenario.
接下来,when 部分的代码块代表一个特定的动作或测试场景。
Likewise, the then section points out the output of the code, which is verified against the expected result using assertions.
同样,then部分指出了代码的输出,使用断言对预期结果进行验证。
3.4. Expected vs Actual
3.4.预期与实际
A test case should have an assertion between expected and actual values.
测试案例应该在预期值和实际值之间有一个断言。
To corroborate the idea of the expected vs actual values, we can look at the definition of the assertEquals method of JUnit’s Assert class:
为了证实预期值与实际值的想法,我们可以看看JUnit的Assert类的assertEquals方法的定义。
public static void assertEquals(Object expected, Object actual)
Let’s use the assertion in one of our test cases:
让我们在我们的一个测试案例中使用这个断言。
@Test
public void givenRadius_whenCalculateArea_thenReturnArea() {
double actualArea = Circle.calculateArea(1d);
double expectedArea = 3.141592653589793;
Assert.assertEquals(expectedArea, actualArea);
}
It’s suggested to prefix the variable names with the actual and expected keyword to improve the readability of the test code.
建议在变量名前加上实际和预期的关键字,以提高测试代码的可读性。
3.5. Prefer Simple Test Case
3.5.倾向于简单的测试案例
In the previous test case, we can see that the expected value was hard-coded. This is done to avoid rewriting or reusing actual code implementation in the test case to get the expected value.
在前面的测试案例中,我们可以看到,预期值是硬编码的。这样做是为了避免在测试案例中重写或重复使用实际的代码实现来获得预期值。
It’s not encouraged to calculate the area of the circle to match against the return value of the calculateArea method:
不鼓励计算圆的面积与calculateArea方法的返回值匹配。
@Test
public void givenRadius_whenCalculateArea_thenReturnArea() {
double actualArea = Circle.calculateArea(2d);
double expectedArea = 3.141592653589793 * 2 * 2;
Assert.assertEquals(expectedArea, actualArea);
}
In this assertion, we’re calculating both expected and actual values using similar logic, resulting in similar results forever. So, our test case won’t have any value added to the unit testing of code.
在这个断言中,我们使用类似的逻辑来计算预期值和实际值,结果永远是类似的结果。所以,我们的测试用例不会对代码的单元测试有任何附加价值。
Therefore, we should create a simple test case that asserts hard-coded expected value against the actual one.
因此,我们应该创建一个简单的测试用例,对硬编码的预期值与实际值进行断言。
Although it’s sometimes required to write the logic in the test case, we shouldn’t overdo it. Also, as commonly seen, we should never implement production logic in a test case to pass the assertions.
虽然有时需要在测试用例中编写逻辑,但我们不应该过度。另外,正如通常所见,我们不应该在测试用例中实现生产逻辑以通过断言。。
3.6. Appropriate Assertions
3.6.适当的断言
Always use proper assertions to verify the expected vs. actual results. We should use various methods available in the Assert class of JUnit or similar frameworks such as AssertJ.
始终使用适当的断言来验证预期与实际的结果。我们应该使用JUnit的Assert类中的各种方法或类似的框架,如AssertJ。
For instance, we’ve already used the Assert.assertEquals method for value assertion. Similarly, we can use assertNotEquals to check if the expected and actual values are not equal.
例如,我们已经使用Assert.assertEquals方法进行了值断言。类似地,我们可以使用assertNotEquals来检查预期值和实际值是否不相等。
Other methods such as assertNotNull, assertTrue and assertNotSame are beneficial in distinct assertions.
其他方法,如assertNotNull、assertTrue和assertNotSame在不同的断言中是有益的。
3.7. Specific Unit Tests
3.7.具体的单元测试
Instead of adding multiple assertions to the same unit test, we should create separate test cases.
我们不应该在同一个单元测试中添加多个断言,而是应该创建单独的测试案例。
Of course, it’s sometimes tempting to verify multiple scenarios in the same test, but it’s a good idea to keep them separate. Then, in the case of test failures, it’ll be easier to determine which specific scenario failed and, likewise, simpler to fix the code.
当然,有时在同一个测试中验证多个场景是很诱人的,但把它们分开是一个好主意。然后,在测试失败的情况下,将更容易确定哪个特定场景失败,同样,也更容易修复代码。
Therefore, always write a unit test to test a single specific scenario.
因此,总是写一个单元测试来测试一个单一的特定场景。
A unit test won’t get overly complicated to understand. Moreover, it’ll be easier to debug and maintain unit tests later.
一个单元测试不会变得过于复杂,难以理解。此外,以后调试和维护单元测试也会更容易。
3.8. Test Production Scenarios
3.8.测试生产情况
Unit testing is more rewarding when we write tests considering real scenarios in mind.
当我们在编写测试时考虑到真实场景时,单元测试会更有价值。
Principally, it helps to make unit tests more relatable. Also, it proves essential in understanding the behavior of the code in certain production cases.
主要是,它有助于使单元测试更有关联性。此外,它还证明了在某些生产案例中理解代码行为的重要性。
3.9. Mock External Services
3.9.模拟外部服务
Although unit tests concentrate on specific and smaller pieces of code, there is a chance that the code is dependent on external services for some logic.
虽然单元测试集中在特定的、较小的代码片断上,但代码有可能在某些逻辑上依赖于外部服务。
Therefore, we should mock the external services and merely test the logic and execution of our code for varying scenarios.
因此,我们应该模拟外部服务,仅仅测试我们代码在不同情况下的逻辑和执行。
We can use various frameworks such as Mockito, EasyMock and JMockit for mocking external services.
我们可以使用各种框架,如Mockito、EasyMock和JMockit来嘲讽外部服务。
3.10. Avoid Code Redundancy
3.10.避免代码冗余
Create more and more helper functions to generate the commonly used objects and mock the data or external services for similar unit tests.
创建越来越多的帮助函数,以生成常用的对象和模拟数据或外部服务,用于类似的单元测试。
As with other recommendations, this enhances the readability and maintainability of the test code.
与其他建议一样,这增强了测试代码的可读性和可维护性。
3.11. Annotations
3.11.注释
Often, testing frameworks provide annotations for various purposes, for example, performing setup, executing code before and tearing down after running a test.
通常,测试框架为各种目的提供注解,例如,执行设置,在运行测试前执行代码,在运行测试后拆解。
Various annotations such as JUnit’s @Before, @BeforeClass and @After and from other test frameworks such as TestNG are at our disposal.
各种注释,如JUnit的@Before, @BeforeClass 和@After以及来自其他测试框架的注释,如TestNG都可供我们使用。
We should leverage annotations to prepare the system for tests by creating data, arranging objects and dropping all of it after every test to keep test cases isolated from each other.
我们应该利用注解来为测试准备系统,创建数据,安排对象,并在每次测试后丢弃所有对象,以保持测试案例相互隔离。
3.12. 80% Test Coverage
3.12.80%的测试覆盖率
More test coverage for the source code is always beneficial. However, it’s not the only goal to achieve. We should make a well-informed decision and choose a better trade-off that works for our implementation, deadlines and the team.
更多的源代码的测试覆盖率总是有益的。然而,这并不是要实现的唯一目标。我们应该做出一个明智的决定,并选择一个更好的权衡,为我们的实施、截止日期和团队服务。
As a rule of thumb, we should try to cover 80% of the code by unit tests.
作为一个经验法则,我们应该尝试用单元测试覆盖80%的代码。
Additionally, we can use tools such as JaCoCo and Cobertura along with Maven or Gradle to generate code coverage reports.
此外,我们可以使用JaCoCo和Cobertura等工具与Maven或Gradle一起生成代码覆盖报告。
3.13. TDD Approach
3.13.TDD方法
Test-Driven Development (TDD) is the methodology where we create test cases before and in ongoing implementation. The approach couples with the process of designing and implementing the source code.
测试驱动开发(TDD)是一种方法,我们在实施前和实施中创建测试案例。该方法与设计和实现源代码的过程相配合。
The benefit includes testable production code from the start, robust implementation with easy refactorings and fewer regressions.
这样做的好处包括:从一开始就可测试的生产代码,稳健的实施,易于重构,减少回归。
3.14. Automation
3.14. 自动化
We can improve the reliability of the code by automating the execution of the entire test suite while creating new builds.
我们可以在创建新的构建时,通过自动执行整个测试套件来提高代码的可靠性。
Primarily, this helps to avoid unfortunate regressions in various release environments. It also ensures rapid feedback before a broken code is released.
主要是,这有助于避免在各种发布环境中出现不幸的回归。它还能确保在破损的代码被发布之前得到快速的反馈。
Therefore, unit test execution should be part of CI-CD pipelines and alert the stakeholders in case of malfunctions.
因此,单元测试的执行应该是CI-CD管道的一部分,并在出现故障时提醒利益相关者。
4. Conclusion
4.总结
In this article, we explored some best practices of Unit Testing in Java. Following best practices can help in many aspects of software development.
在这篇文章中,我们探讨了Java中单元测试的一些最佳实践。遵循最佳实践可以在软件开发的许多方面有所帮助。