第 12 章 测试模块

Chapter 12. Testing Modules

Building modular codebases also involves testing. The Java community has always fostered a strong culture of automated testing. Unit tests play a large role in Java software development.

构建模块化代码库也涉及测试,Java 社区一直在提倡强大的自动化测试文化。单元测试在 Java 软件开发中起着重要的作用。

What effect does the module system have on existing testing practices? We want to be able to test code inside modules. In this chapter, we look at two common scenarios:

模块系统对现有测试实践有什么影响呢?我们希望能够测试模块内的代码。在本章中,将会介绍两种常见的测试方案:

Blackbox testing Test modules from the outside. Blackbox tests exercise the public API of a module, without knowledge of internals (hence, the box is opaque). The tests can test either a single module or several modules at once. As such, you can also characterize these tests as integration tests for modules.

1)黑盒测试(blackbox testing):从外部测试模块。黑盒测试主要使用模块的公共 API,而不需要了解模块内部情况(因此,盒子是不透明的),可以一次测试一个模块或多个模块。因此,也可以将这些测试描述为模块的集成测试。

Whitebox testing Test modules from the inside. Instead of taking the outside view, whitebox tests assume knowledge of the internals of a module. These tests are typically unit tests, testing a single class or method in isolation.

2)白盒测试(whitebox testing):从内部测试模块。白盒测试不是采用外部视图,而是假定了解模块的内部。这些测试通常是单元测试,单独测试一个类或一个方法。

Although other testing scenarios are possible, these two cover a wide range of existing practices. Blackbox tests are more restricted in what they can test, but are also more stable because they exercise public APIs. Conversely, whitebox tests can test internal details more easily, at the risk of needing more maintenance.

虽然还有其他的测试方案,但以上两种方案涵盖了大量现行的做法。黑盒测试在测试中受到更多限制,但是由于它们使用公共 API,所以也更加稳定。而白盒测试可以更容易地测试内部细节,但需要承担更多的维护风险。

The focus of this chapter is to highlight the interplay between testing and the module system. It is expected that build tools and IDEs will take care of a lot of the details described in this chapter. Still, it’s important to get a feeling for how testing scenarios play out with the module system in place.

本章的重点是突出介绍测试和模块系统之间的相互影响。接下来将使用构建工具和 IDE 完成本章中所描述的许多细节。不过,重要的是要了解测试方案如何与模块系统配合使用。

For the remainder of this chapter, we assume the following module is under test:

本章将会使用如下所示的待测试模块:

  1. easytext.syllablecounter
  2. ├── javamodularity
  3. └── easytext
  4. └── syllablecounter
  5. ├── SimpleSyllableCounter.java
  6. └── vowel
  7. └── VowelHelper.java
  8. └── module-info.java

The module descriptor of easytext.syllablecounter is as follows:

easytext.syllablecounter 的模块描述符如下所示:

  1. module easytext.syllablecounter {
  2. exports javamodularity.easytext.syllablecounter;
  3. }

The package containing VowelHelper is not exported, whereas SimpleSyllableCounter is in an exported package. Internally, SimpleSyllableCounter uses VowelHelper to implement the syllable-counting algorithm. This distinction becomes important when moving from blackbox to whitebox testing.

包含 VowelHelper 的包不会被导出,而 SimpleSyllableCounter 位于导出的包中。在内部,SimpleSyllableCounter 使用 VowelHelper 来实现音节计数算法。从黑盒测试转到白盒测试时,这个区别就显得非常重要了。

In the following sections, we are going to look at what it takes to run both types of tests with modules.

在下面的章节中,将看看如何对模块运行两种类型的测试。

12.1 Blackbox Tests 黑盒测试

Let’s say we want to test the easytext.syllablecounter module. Rather than unit testing all internal details, we want to test the functional behavior of the module’s API. In this case, that means testing the public API as exposed by SimpleSyllableCounter. It has one public method, countSyllables.

假设对 easytext.syllablecounter 模块进行测试。此时并不是对所有内部细节进行单元测试,而是测试模块 API 的功能行为。在这种情况下,就意味着测试 SimpleSyllable Counter 公开的公共 API。它有一个公共的方法:countSyllables。

The easiest way to do so is to create another module that requires easytext.syllablecounter, as shown in Figure 12-1. It is also possible to just put the module and its tests on the classpath. We don’t pursue this option here because we want the module to be tested as a module (i.e., including its module descriptor with requires and uses clauses), not just as some code on the classpath. In “Whitebox Tests”, where we look at testing the internals of modules, the classpath testing approach is shown.

最简单的方法是创建另一个需要 easytext.syllablecounter 的模块,如图 12-1 所示,也可以将模块及其测试放在类路径中。此时并不使用后一种方法,因为我们希望将 easytext.syllablecounter 作为一个模块进行测试(即包括带有 requires 和 uses 子句的模块描述符),而不仅仅是作为类路径上的一些代码。在 12.3 节中将会测试模块内部,并介绍类路径测试方法。

Testing module easytext.syllablecounter with a separate easytext.syllablecounter.test module

To start things slowly, we’re going to create a test without a testing framework first. Later, we’ll adapt the test to use JUnit, a popular unit-testing framework. Example 12-1 presents the code that performs the test, using standard Java asserts for verification purposes.

为了慢慢深入,将首先创建一个没有测试框架的测试。然后,调整测试以使用流行的单元测试框架 JUnit。示例 12-1 给出了执行测试的代码,其中使用了标准的 Java 断言来进行验证。

Example 12-1. Blackbox test for SimpleSyllableCounter (➥ chapter12/blackbox)

示例 12-1:针对 SimpleSyllableCounter 的黑盒测试(chapter12/blackbox)

  1. package javamodularity.easytext.test;
  2. import javamodularity.easytext.syllablecounter.SimpleSyllableCounter;
  3. public class TestSyllableCounter {
  4. public static void main(String... args) {
  5. SimpleSyllableCounter sc = new SimpleSyllableCounter();
  6. assert sc.countSyllables("Bike") == 1;
  7. assert sc.countSyllables("Motor") == 2;
  8. assert sc.countSyllables("Bicycle") == 3;
  9. }
  10. }

This code is as simple as it can get: the main method instantiates the publicly exported SimpleSyllableCounter class and verifies its behavior by using asserts. The test class is placed in its own module with the following descriptor:

上述代码非常简单:main 方法实例化公共导出的 SimpleSyllableCounter 类,并通过使用断言来验证其行为。测试类被放置在它自己的模块中,且具有以下描述符:

  1. module easytext.syllablecounter.test {
  2. requires easytext.syllablecounter;
  3. }

Then, it can be compiled and run as usual. We assume the module under test is already compiled in the out directory:

然后,像往常一样编译和运行代码。假设待测模块已经被编译到 out 目录中:

  1. $ javac --module-path out \ 1
  2. --module-source-path src-test -d out-test -m easytext.syllablecounter.test
  3. $ java -ea --module-path out:out-test \ 2
  4. -m easytext.syllablecounter.test/javamodularity.easytext.test.TestSyllableCounter 3
  5. Exception in thread "main" java.lang.AssertionError
  6. at easytext.syllablecounter.test/javamodularity.easytext.test.
  7. TestSyllableCounter.main(TestSyllableCounter.java:12)
  1. Compile the test with the module under test on the module path.
  2. Run with assertions enabled (-ea) and with both the module under test and the test module on the module path.
  3. Start the test module’s class containing the main method.

  1. 使用模块路径上的待测试模块编译测试。
  2. 在启用断言的情况下(-ea)运行,并使用模块路径上的待测试模块和测试模块。
  3. 启动测试模块中包含 main 方法的类。

An AssertionError is thrown, because the naive syllable–counting algorithm chokes on the word Bicycle. That’s good, because it means we have a working blackbox test. Before we move on to introducing a test framework to run the test, let’s reflect on this blackbox-testing approach.

此时抛出了 AssertionError,因为“幼稚的”音节计数算法在单词 Bicycle 上卡住了。这很好,因为这意味着正在进行黑盒测试。在开始介绍测试框架并运行测试之前,先回忆一下黑盒测试方法。

Testing a module this way has several advantages but also some drawbacks. An advantage of doing blackbox testing is that you can test a module in its natural habitat. You’re testing the module just as other modules would use it in the application. If, for example, the module under test would offer a service, this can be tested as well in this setup. Just add a uses constraint to the module descriptor of the test module, and load the service with ServiceLoader in the test.

以这种方式测试模块有几个优点,但也有一些缺点。黑盒测试的一个优点是可以在自然环境下测试模块。在测试模块时,就像其他模块在应用程序中使用它一样。例如,如果待测模块提供了服务,则也可以对服务进行测试。只需在测试模块的模块描述符中添加一个使用约束,然后在测试中使用 ServiceLoader 加载服务。

On the other hand, testing a module this way means that only the exported parts can be tested directly. There’s no way to directly test, for example, the encapsulated VowelHelper class. Nor would any nonpublic parts of SimpleSyllableCounter be accessible.

另一方面,以黑盒测试方式测试模块意味着只能对导出的部分进行直接测试,而无法对封装类进行测试,如 VowelHelper 类。同时,也不能访问 SimpleSyllableCounter 的任何非公共部分。

NOTE

You could run the tests by using, for example, —add-exports or —add-opens flags to give the test module access to encapsulated parts.

例如,可以使用—add-exports 或—add-opens 标志来运行测试,以便测试模块可以访问封装部分。

Another limitation is that the test classes need to be in a different package than the classes under test. For unit tests in Java, it’s customary to put test classes in a different source folder but under the same package name. The goal of this setup is to be able to test elements that are package-private (e.g., a class without the public modifier). In the classpath situation, this was no problem. The packages would, as it were, merge when running the tests. However, two modules containing the same package cannot be loaded in the boot layer (as discussed in “Classloading in Layers”).

另一个限制是测试类与待测试类需要位于不同的包中。对于 Java 中的单元测试,习惯上把测试类放在不同的源文件夹中,但是在相同的包名下,这样设置的目的是测试包私有(package-priviate)的元素(例如,没有 public 修饰符的类)。在使用类路径的情况下,这样做是没有问题的;运行测试时,包就会合并。但是,包含相同包的两个模块不能在引导层中加载(如 6.3.2 节所述)。

Last, all dependencies of the module under test and the test module itself must be satisfied. If easytext.syllablecounter had other modules it required, those would need to be on the module path as well. Of course, it’s possible to create mock modules in those situations. Instead of putting the actual module dependency on the module path, you can create a new module with the same name, containing just enough code to get the test running. Whether you want to do this depends on the scope of the test. Running it with actual modules makes it more of an integration test, whereas running the test with mock modules offers more isolation and control.

最后,必须满足待测模块和测试模块自身所有的依赖关系。如果 easytext. syllablecounter 需要其他模块,那么也需要将这些模块放在模块路径中。当然,在这些情况下可以创建模拟模块(mock module)。可以创建一个具有相同名称的新模块(仅包含了运行测试所需的代码),而不是将实际的模块依赖项放在模块路径上。是否要这样做取决于测试的范围。使用实际模块运行测试更像是一个集成测试,而使用模拟模块运行测试则提供了更多的隔离和控制。

12.2 Blackbox Tests with JUnit 使用 JUnit 进行黑盒测试

At this point, we can run test code against easytext.syllablecounter from a separate test module. Writing tests using plain asserts in a main method isn’t really what you’d expect from tests in Java, though. To fix that, we’re going to rewrite the test to use JUnit 4, as shown in Example 12-2.

此时,可以由单独的测试模块对 easytext.syllablecounter 运行测试代码。然而,通过在主方法中使用普通的断言来编写测试代码并不是所期望的。为了解决这个问题,需要重写测试代码,以便使用 JUnit 4,如示例 12-2 所示。

Example 12-2. JUnit test for SimpleSyllableCounter (➥ chapter12/blackbox)

示例 12-2:针对 SimpleSyllableCounter 的 JUnit 测试(chapter12/blackbox)

  1. package javamodularity.easytext.test;
  2. import org.junit.Test;
  3. import javamodularity.easytext.syllablecounter.SimpleSyllableCounter;
  4. import static org.junit.Assert.assertEquals;
  5. public class JUnitTestSyllableCounter {
  6. private SimpleSyllableCounter counter = new SimpleSyllableCounter();
  7. @Test
  8. public void testSyllableCounter() {
  9. assertEquals(1, counter.countSyllables("Bike"));
  10. assertEquals(2, counter.countSyllables("Motor"));
  11. assertEquals(3, counter.countSyllables("Bicycle"));
  12. }
  13. }

The test module now has a dependency on JUnit. At run-time, the JUnit test runner will reflectively load our test class to execute the unit-test methods. For this to work, the test package must be exported or opened. An open module suffices:

现在,测试模块依赖于 JUnit。在运行时,JUnit 测试运行器将以反射的方式加载测试类来执行单元测试方法。为此,必须导出或开放测试包。一个开放式模块就足够了:

  1. open module easytext.syllablecounter.junit {
  2. requires easytext.syllablecounter;
  3. requires junit;
  4. }

Figure 12-2 shows the new situation, adding JUnit to the mix.

图 12-2 显示了将 JUnit 添加到组合中的新情况。

The easytext.syllablecounter.junit module depends on the (automatic) module junit

To make this work, we put the JUnit JAR (and its Hamcrest dependency) in a lib folder:

为此,必须将 JUnit JAR(以及 Hamcrest 依赖项)放到 lib 文件夹中:

  1. lib
  2. ├── hamcrest-core-1.3.jar
  3. └── junit-4.12.jar

Then, JUnit can be used as an automatic module by putting the lib folder on the module path. The derived module names are junit and hamcrest.core.

然后,将 lib 文件夹放在模块路径上,从而将 JUnit 用作自动模块。派生模块名称是 junit 和 hamcrest.core。

As before, we assume the easytext.syllablecounter module is available in the out folder:

与以前一样,假设 out 文件夹中有 easytext.syllablecounter 模块:

  1. $ javac --module-path out:lib \ 1
  2. --module-source-path src-test -d out-test -m easytext.syllablecounter.junit
  3. $ java --module-path out:lib:out-test \
  4. -m junit/org.junit.runner.JUnitCore \ 2
  5. javamodularity.easytext.test.JUnitTestSyllableCounter
  6. JUnit version 4.12
  7. .E
  8. Time: 0,002
  9. There was 1 failure:
  10. 1) initializationError(org.junit.runner.JUnitCommandLineParseResult)
  11. java.lang.IllegalArgumentException: Could not find class
  12. [javamodularity.easytext.test.JUnitTestSyllableCounter]
  1. Compile the test with the module under test and JUnit (as an automatic module) on the module path.
  2. Start the JUnitCore test runner from the JUnit (automatic) module.

  1. 使用模块路径中的待测模块和 JUnit 来编译测试代码。
  2. 从 JUnit(自动)模块启动 JUnitCore 测试运行器。

Running the test is done using the JUnitCore runner class, part of JUnit. It’s a simple console-based test runner, where we provide the classname of the test class as a command-line argument. Unfortunately, running the unit test ends in an unexpected exception, saying the JUnitTestSyllableCounter class can’t be found by JUnit. Why can it not find the test class? We made the module open so that JUnit can access it at run-time, after all.

运行测试是使用 JUnitCore 运行器类(JUnit 的一部分)完成的。这是一个简单的基于控制台的测试运行器,需要提供测试类的类名作为命令行参数。但不幸的是,运行单元测试后却得到了一个意想不到的异常:JUnit 找不到 JUnitTestSyllableCounter 类。为什么找不到测试类?此时需要开放模块,以便 JUnit 可以在运行时访问它。

The problem is that the test module is never resolved. JUnit is used as the root module for starting the runner. Because it is an automatic module, the other automatic module (hamcrest.core) on the module path gets resolved as well. However, junit does not require easytext.syllablecounter.junit, the module containing our test. Only at run-time does JUnit try to load the test class from the test module using reflection. That doesn’t work, because the easytext.syllablecounter.junit module is never resolved by the module system at startup, even though it is on the module path.

问题出在测试模块从未被解析。JUnit 被用作启动运行器的根模块。由于它是自动模块,因此模块路径上的其他自动模块(hamcrest.core)也会被解析。但是,junit 不需要 easytext.syllablecounter.junit,而该模块包含了测试代码。只有在运行时,JUnit 才会尝试使用反射从测试模块加载测试类。但这样做是行不通的,因为即使 easytext. syllablecounter.junit 模块在模块路径上,在启动时也不会被模块系统解析。

To resolve the test module as well, we can define an —add-modules statement when starting the test runner:

为了解析测试模块,可以在启动测试运行器时定义一个—add-modules 语句:

  1. $ java --module-path out:lib:out-test \
  2. --add-modules easytext.syllablecounter.junit \
  3. -m junit/org.junit.runner.JUnitCore \
  4. javamodularity.easytext.test.JUnitTestSyllableCounter
  5. JUnit version 4.12
  6. .E
  7. Time: 0,005
  8. There was 1 failure:
  9. 1) testSyllableCounter(javamodularity.easytext.test.JUnitTestSyllableCounter)
  10. java.lang.AssertionError: expected:<3> but was:<1>

Now we get a legitimate test failure, meaning JUnit was able to run our test class.

现在得到了一个合法的测试失败提示,这意味着 JUnit 能够运行测试类。

We’ve extended our blackbox test to use an external test framework. Because JUnit 4.12 is not modularized, it has to be used as an automatic module. Running the test through JUnit is possible only if the test module is resolved and our test class is accessible at run-time.

现在,我们已经将黑盒测试扩展为使用外部测试框架。因为 JUnit 4.12 没有模块化,所以它必须被用作自动模块。只有在测试模块被解析且测试类在运行时可访问时,通过 JUnit 运行测试才是可能的。

12.3 Whitebox Tests 白盒测试

Now, what if we want to test VowelHelper as well? It’s a public, but nonexported class with a public method isVowel and a package-private method getVowels. Unit testing this class means crossing from blackbox testing to whitebox testing.

现在,如果想要测试 VowelHelper,应该怎么做呢?它是一个公共但非导出类,带有一个公共方法 isVowel 以及包私有的方法 getVowels。对该类进行单元测试意味着从黑盒测试切换到白盒测试。

Access to the encapsulated VowelHelper class is necessary. Also, when we want to test package-private functionality, we need our test classes to be in the same package. How can we get these capabilities in a modular setup? Using —add-exports or —add-opens to expose types for testing works to a limited extent. If test classes need to be in the same package as the classes under test, a package clash between the test module and the module to be tested arises.

访问封装的 VowelHelper 类是必要的。另外,在测试包私有的功能时,需要测试类位于同一个包中。如何在模块化设置中获得这些功能呢?使用—add-exports 或—add-opens 可以在一定程度上公开用于测试的类型。如果测试类需要与被测试类处于同一个包中,那么测试模块与待测试模块之间会发生包冲突。

There are two major approaches to solving this problem. Which one will be the best in practice remains to be seen. Again, it’s mostly up to the build tools and IDEs to lead the way here. We’ll examine both approaches, to get a feeling for the underlying mechanisms at play:

解决这个问题主要有两种方法,但哪一种方法在实践中是最好的还有待观察。此外,构建工具和 IDE 决定了使用哪种方法。接下来研究这两种方法,以便对底层机制有一个直观的了解:

  • Using the classpath for tests
  • Patching modules by injecting tests

  • 使用类路径进行测试。
  • 通过注入测试来修补模块。

The first approach is the most straightforward, because it mostly builds on what was always happening:

第一种方法是最直接的,因为需要使用该方法的事情经常发生:

  1. package javamodularity.easytext.syllablecounter.vowel;
  2. import org.junit.Test;
  3. import javamodularity.easytext.syllablecounter.vowel.VowelHelper;
  4. import static org.junit.Assert.assertEquals;
  5. import static org.junit.Assert.assertTrue;
  6. public class JUnitTestVowelHelper {
  7. @Test
  8. public void testIsVowel() {
  9. assertTrue(VowelHelper.isVowel('e'));
  10. }
  11. @Test
  12. public void testGetVowels() {
  13. assertEquals(5, VowelHelper.getVowels().size());
  14. }
  15. }

We can test encapsulated code in a compiled module by not treating it as a module at test-time. Modules placed on the classpath behave as if they have no module descriptor in the first place. Furthermore, split packages between JARs on the classpath do not pose any issues. If the test class is then compiled outside a module as well, things work remarkably well:

可以在已编译模块中测试封装的代码,而不是在测试时将其作为模块处理。放在类路径上的模块的行为就好像它们没有模块描述符一样。此外,类路径上 JAR 之间的拆分包不会造成任何问题。如果测试类也是在模块之外编译的,那么事情就非常简单了:

  1. $ javac -cp lib/junit-4.12.jar:out/easytext.syllablecounter \
  2. -d out-test $(find . -name '*.java')
  3. $ java -cp lib/junit-4.12.jar:lib/hamcrest-core-1.3.jar:\
  4. out/easytext.syllablecounter:out-test \
  5. org.junit.runner.JUnitCore \
  6. javamodularity.easytext.syllablecounter.vowel.JUnitTestVowelHelper
  7. JUnit version 4.12
  8. ..
  9. Time: 0,004
  10. OK (2 tests)

Of course, using the classpath means we don’t benefit from automatic resolving of modules. Dependencies declared in the module descriptor are not resolved, because a module on the classpath behaves as a regular, nonmodular JAR. The classpath needs to be constructed manually. Also, any service provides/uses clauses in the module descriptor of the module under test are ignored. So, even though the classpath-based testing approach works, it has several drawbacks. Ultimately, it seems better to test a module as a module, and not as though it were a nonmodular JAR.

当然,使用类路径意味着无法从模块的自动解析中受益。模块描述符中声明的依赖项没有被解析,因为类路径上的模块表现为常规的非模块 JAR。类路径需要手动构建。而且,被测模块的模块描述符中任何服务的 provides/uses 子句都被忽略。所以,即使基于类路径的测试方法可以使用,也存在几个缺点。最终,将一个模块作为模块进行测试似乎更好,而不是将其视为非模块 JAR。

There’s a way to keep the module structure intact, while at the same time creating a test class in the same package. Through a feature called module patching, new classes can be added to an existing module.

有一种方法既可以保持模块结构的完整,又可以在同一个包中创建测试类。通过使用称为模块修补(module patching)的功能,可以将新类添加到现有模块中。

Creating a whitebox unit test within the same module and package works as follows. First, the test class needs to be compiled with the —patch-module flag:

在同一模块和包中创建白盒单元测试的过程如下所示。首先,需要使用—patch-module 标志编译测试类:

  1. $ javac --patch-module easytext.syllablecounter=src-test \ 1
  2. --module-path lib:out \
  3. --add-modules junit \ 2
  4. --add-reads easytext.syllablecounter=junit \ 3
  5. -d out-test $(find src-test -name '*.java')
  1. The test sources are compiled as if they were part of the easytext.syllablecounter module. There’s no module descriptor for the test code.
  2. Because there’s no test module descriptor to require junit, it must be added explicitly.
  3. With the test class added to easytext.syllablecounter, this module must now read junit. There’s no requires clause in the original module descriptor for that, so —add-reads is necessary.

  1. 测试源代码被编译,就好像它们是 easytext.syllablecounter 模块的一部分。请注意,这些测试代码没有模块描述符。
  2. 由于没有测试模块描述符以表达需要 junit,因此必须显式添加。
  3. 随着测试类被添加到 easytext.syllablecounter,该模块现在必须读取 junit。因为原始模块描述符中没有 requires 子句,所以—add-reads 是必需的。

By patching a module, we can compile the unit-test class as part of the already compiled easytext.syllablecounter module. Because the test lives in the same package, it can invoke the package-private getVowels method.

通过修补模块,可以将单元测试类编译为已编译 easytext.syllablecounter 模块的一部分。由于测试代码位于同一个包中,因此可以调用包私有的 getVowels 方法。

Making the test class part of the easytext.syllablecounter module on the fly does pose some challenges. Steps must be taken to ensure that the module now reads junit. At run-time, junit must be able to access the test class in a nonexported package. That leads to the following, quite impressive, java invocation:

动态地使测试类成为 easytext.syllablecounter 模块的一部分会带来一些挑战。必须采取措施确保模块可以读取 junit。在运行时,junit 必须能够访问非导出包中的测试类。这样一来就会导致以下 Java 调用:

  1. $ java --patch-module easytext.syllablecounter=out-test \ 1
  2. --add-reads easytext.syllablecounter=junit \ 2
  3. --add-opens \ 3
  4. easytext.syllablecounter/javamodularity.easytext.syllablecounter.vowel=junit \
  5. --module-path lib:out \
  6. --add-modules easytext.syllablecounter \ 4
  7. -m junit/org.junit.runner.JUnitCore \
  8. javamodularity.easytext.syllablecounter.vowel.JUnitTestVowelHelper
  9. JUnit version 4.12
  10. ..
  11. Time: 0,004
  12. OK (2 tests)
  1. At run-time, the module must be patched with the compiled test class.
  2. As during compilation, the module needs to read junit, which is not expressed in the module descriptor.
  3. Because junit reflectively instantiates JUnitTestVowelHelper, its containing package needs to be opened or exported to junit.
  4. As before, junit is the initial module and doesn’t require easytext.syllablecounter, so it must be added explicitly.

  1. 在运行时,必须使用已编译测试类来修补模块。
  2. 在编译期间,模块需要读取 junit(在模块描述符中并没有表示 junit)。
  3. 因为 junit 以反射的方式实例化 JUnitTextVowelHelper,所以其包含的包必须是开放的或导出到 junit。
  4. 同以前一样,junit 是初始模块,不需要 easytext.syllablecounter,因此必须显式添加。

Unlike with the compiler invocation, there is no need to have —add-modules junit. JUnit is used as root module to run, and is therefore already resolved.

与编译器调用不同,不需要使用—add-modules junit。JUnit 作为根模块运行,因此已经解析了。

In Figure 12-3, you can see the whole picture of what patching a module to run unit tests entails.

在图 12-3 中,可以看到如何修补一个模块以运行单元测试。

The JUnitTestVowelHelper class (in the same package as VowelHelper) is patched into the easytext.syllablecounter module

This second approach has quite a few moving parts. On the upside, all features of the module are taken into account at test-time as well. The module’s dependencies are resolved, and uses/provides clauses are taken into account. On the downside, setting up all the correct command-line flags seems like a lot of wizardry. Even though we could explain why everything needs to be done this way, it’s still a lot to take in.

第二种方法有很多可移动部件。有利的一面是,在测试时可以考虑模块的所有功能。在解析模块的依赖项的同时也会考虑 uses/provides 子句。而不利的一面是,设置所有正确的命令行标志似乎需要很多技巧。尽管可以解释为什么所有事情都需要这样做,但仍然需要付出很多努力。

In practice, setting up all the intricate details for unit-testing scenarios shouldn’t be up to developers. Build tools and development environments offer their own test runners for most of the popular testing frameworks. These runners should take care of setting up the environment such that tests are automatically patched into modules.

在实践中,为单元测试场景设置所有复杂的细节不应该由开发人员来决定。构建工具和开发环境针对大多数流行的测试框架提供了自己的测试运行器,这些运行器应该负责建立环境,以便将测试代码自动修补到模块中。

PATCHING MODULES FOR OTHER REASONS 出于其他原因而修补模块

There are other scenarios besides running unit tests for which patching modules is convenient. For example, for debugging purposes, you may want to add or replace classes in an existing module. Both classes and resources can be patched into a module. The only thing that cannot be replaced is the module descriptor.

除了运行单元测试以外,在其他一些场景中使用修补模块也是很方便的。例如,出于调试目的,可能希望添加或替换现有模块中的类。类和资源都可以修补到模块中,唯一不能被替换的是模块描述符。

The —patch-module flag also works for platform modules. So, technically, you can (re)place classes in the package java.lang in the java.base module. Module patching replaces -Xbootclasspath:/p, a feature which is dropped in JDK 9.

—patch-module 标志也适用于平台模块。所以,从技术上讲,可以(重新)在 java. base 模块的 java.lang 包中放置类。模块修补将替换-Xbootclasspath:/ p,这是在 JDK 9 中已删除的功能。

Using the module-patching feature for production scenarios is highly discouraged.

不鼓励在生产场景中使用模块修补功能。

12.4 Test Tooling 测试工具

Test frameworks such as JUnit and TestNG are well supported by IDEs as well as build tools. Typically, they are used to write and run whitebox unit-tests. Most tools are currently taking the classpath route to run tests, even when code includes a module descriptor. This way, the module descriptor is basically ignored, as is strong encapsulation. Because of this, tests run the same way you’re used to. This also keeps compatibility with existing project structures, where the application code and test code are in different folders but in the same package structure.

诸如 JUnit 和 TestNG 之类的测试框架得到了 IDE 以及构建工具的很好支持。通常,它们用于编写和运行白盒单元测试。即使代码包含模块描述符,大多数工具也都使用类路径来运行测试。这样一来,就像强封装一样,模块描述符基本上被忽略了。正因如此,测试的运行方式与以往相同。这也保持与现有项目结构的兼容性,其中应用程序代码和测试代码位于不同的文件夹中,但处于相同的包结构中。

If code is relying on services using the new keywords in module descriptors, this will not automatically work on the classpath. Then, the blackbox testing scenario described in this chapter is more appropriate. There is no specific support for these scenarios in the current versions of test frameworks. It is to be expected that new tools, or support in existing tools, will be created to help with testing modules following the strategies in this chapter.

如果代码依赖于模块描述符中使用新关键字的服务,那么就不会自动在类路径上工作。此时,本章所介绍的黑盒测试场景更为合适。在当前版本的测试框架中对这些场景没有具体的支持。但可以预料的是,将会出现新的工具或现有工具的支持来帮助按照本章中的策略进行模块测试。