junit 继承
You write Java code, hence you write JUnit tests. Easy. But you write Java code with Spring, and testing is now getting complicated. However, you can work it out: you’re not the only one, there are good resources out there. It takes a bit of an effort, but you succeed in writing JUnit tests for your Spring services. Say you want to package this effort, to avoid duplicate code, and for next time. So you write a base class that all your Spring-based test classes inherit from. In this class, you perform the required Spring configuration, you install the JUnit hooks to execute before each test, and you add a few utility methods, too. And then your colleague tells you that inheritance in JUnit test classes is a code smell…
您编写Java代码,因此编写JUnit测试。 简单。 但是,您使用Spring编写Java代码,现在测试变得越来越复杂。 但是,您可以解决问题:您不是唯一的人,那里有很多资源。 这需要一些努力,但是您成功为Spring服务编写了JUnit测试。 假设您要打包此工作,以避免重复的代码,并且下次再使用。 因此,您编写了一个基类,您所有基于Spring的测试类都将继承该基类。 在此类中,您将执行必需的Spring配置,安装JUnit挂钩以在每次测试之前执行,并且还添加了一些实用程序方法。 然后您的同事告诉您,JUnit测试类中的继承是代码的味道……
Side note: To be fair, Spring is not the cause of the complexity here. The culprit would rather be dependency injection, that is, the ability to define beans (components, controllers, services, repositories, etc.) declaratively, and have them reference each other without having to manage their instantiation and wiring. In this picture, Spring is just one way (and admittedly a good one) to implement dependency injection, and the efforts to configure that would be required with other dependency injection frameworks, too.
旁注:公平地说,Spring不是造成此处复杂性的原因。 罪魁祸首是依赖注入,即以声明方式定义bean(组件,控制器,服务,存储库等)并使其相互引用而不必管理其实例化和连接的能力。 在这张图中,Spring只是实现依赖注入的一种方法(当然是一种很好的方法),并且其他依赖注入框架也需要进行配置。
使用JUnit 5编写基于Spring的测试 (Writing Spring-based Tests With JUnit 5)
Imagine that you are responsible for a Spring-based µ-service that manages a stock of books. The Spring components in your µ-service can roughly be divided into:
想象一下,您负责管理基于Spring的µ服务,该服务管理书籍的库存。 µ服务中的Spring组件可以大致分为:
- The REST controllers that implement the endpoints called by the UI and/or other µ-services; 实现由UI和/或其他µ服务调用的端点的REST控制器;
- The services that implement your business logic; 实现您的业务逻辑的服务;
- The repositories that provide persistence of your entities to the database of your choice; 提供实体持久性到您选择的数据库的存储库;
- Additional out-bound components, such as a REST client to access your supplier’s inventory, or a web socket or information bus client to notify sibling µ-services. 其他出站组件,例如用于访问供应商库存的REST客户端,或用于通知同级µ服务的Web套接字或信息总线客户端。
Among your JUnit tests, you have some simple ones that check the low-level functions of your business logic implementation, such as VAT computation. But you also have some more complex ones that aim at checking your service from a more global viewpoint. For example, that when a customer buys a book, there is one less in stock after than before; that if it was the last in stock, an alarm is raised; etc. You can recognise those tests by the fact that you probably had debates with your teammates about whether they could legitimately be called “unit tests”.
在您的JUnit测试中,您有一些简单的测试可以检查业务逻辑实现的低级功能,例如增值税计算。 但是您还有一些更复杂的工具,旨在从更全球化的角度检查您的服务。 例如,当客户购买一本书时,之后的存货比以前少了; 如果是最后存货,则会发出警报; 您可以通过与队友就是否可以合法地称为“单元测试”进行辩论来认识这些测试。
The challenge for these complex tests is that you want the code of your Spring services to execute, including when they call each other, as if they had been stimulated by a user interaction. However, you do not want to set up a real database, you have nowhere to send notifications, and you do not want to really connect to your supplier’s API. This is where the SpringRunner
of JUnit 4, and the SpringExtension
of JUnit 5, come into play. (Actually, the latter is specific to JUnit Jupiter, one of the engines for JUnit 5.)
这些复杂测试的挑战在于,您希望Spring服务的代码能够执行,包括它们何时相互调用,就好像它们是由用户交互所激发的一样。 但是,您不想建立一个真实的数据库,您无处发送通知,也不想真正连接到供应商的API。 这就是SpringRunner
JUnit 4中,并且SpringExtension
的JUnit 5,开始发挥作用。 (实际上,后者特定于JUnit Jupiter,它是JUnit 5的引擎之一。)
In short, the two axes that you have to work along are: ensure that the Spring machinery runs, including bean instantiation and wiring; and ensure that it stops at the boundaries of your code under test. The first point is usually achieved by annotating your test class with
简而言之,您必须遵循的两个轴是:确保Spring机械运行,包括bean实例化和连线; 并确保它停止在被测代码的边界。 第一点通常是通过以下方式注释测试类来实现的:
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = ComplexTestConfiguration.class)
where ComplexTestConfiguration
is a Spring configuration class in which you install the services under test and you mock the rest. It may look as follows
其中, ComplexTestConfiguration
是一个Spring配置类,您可以在其中安装要测试的服务,然后模拟其余的服务。 可能如下
@Configuration
@ComponentScan(basePackages = "com.bookstore.stock.services")
@DataMongoTest
@EnableMongoRepositories(basePackages = "com.bookstore.stock.repository")
public class UnitTestConfiguration {
@MockBean
public SupplierInventoryClient supplierInventoryClient;
@MockBean
public SimpMessagingTemplate websocket;
}
On line 2, we install the Spring services that we want to test. Lines 3–4 configure our MongoDB repositories to be used with an embedded implementation of the database, such as Flapdoodle’s one. Lines 6–7 provide a stub to be used in lieu of the REST client to our supplier’s inventory. Similarly, lines 9–10 ensure that our notification service will run, although no message will be sent anywhere. Still, we will be able to check what this service sent to the web socket, using Mockito’s verify
methods.
在第2行中,我们安装了要测试的Spring服务。 第3至4行将MongoDB存储库配置为与数据库的嵌入式实现(例如Flapdoodle的之一)一起使用。 第6-7行提供了一个存根,以代替REST客户使用我们的供应商的库存。 同样,第9-10行确保我们的通知服务将运行,尽管不会在任何地方发送消息。 不过,我们仍可以使用Mockito的verify
方法来检查此服务发送到Web套接字的内容。
为测试类引入基类(错!) (Introducing a Base Class for Test Classes (Wrong!))
With the ComplexTestConfiguration
configuration class (tailored to your needs) and the two annotations on the test class, you can write and run some tests that check the behaviour of the services that implement your business logic from end to end. Still, as you write more and more such complex tests, you will soon notice that you are duplicating code between test classes and methods, be it set-up code to install a starting environment for your tests, or some code patterns that you would like to factor.
使用ComplexTestConfiguration
配置类(根据您的需求)和测试类上的两个注释,您可以编写并运行一些测试,以检查从头到尾实现您的业务逻辑的服务的行为。 但是,随着编写越来越多的此类复杂测试,您很快就会注意到,您正在测试类和方法之间复制代码,无论是设置代码以安装测试的起始环境,还是想要的某些代码模式因素。
In my case, for example, one of the repositories had been mocked in memory and needed to be emptied before each test. Also, I had written a couple of utility methods to decode the messages sent to the web socket and check that they matched my expectations. This resulted in some code that I wanted to share between all my test classes, do I ended up with having these test classes extend the ComplexTestBase
class.
以我的情况为例,其中一个存储库已在内存中被模拟,需要在每次测试之前清空。 另外,我编写了一些实用程序方法来解码发送到Web套接字的消息,并检查它们是否符合我的期望。 这产生了一些我想在所有测试类之间共享的代码,我是否最终让这些测试类扩展了ComplexTestBase
类。
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = ComplexTestConfiguration.class)
public abstract class ComplexTestBase {
@Autowired
public ShoppingCartRepository shoppingCartRepository;
@Autowired
public SimpMessagingTemplate websocket;
@BeforeEach
public void resetInMemoryRepositories() {
shoppingCartRepository.clear();
}
protected void checkNotificationEmitted(String messageType) {
// capture arguments of calls to websocket.convertAndSend()
// check that it matches the expected 'messageType'
}
}
As a result, the test classes could read as follows:
结果,测试类可能如下所示:
class BookstoreNotificationTest extends ComplexTestBase {
@Autowired
StockService stockService;
@Test
void testSomething() {
// Set up initial state
shoppingCartRepository.save(...);
// Launch the business logic
stockService.processCart(...);
// Check that it worked as expected
assertEquals(...);
checkNotificationEmitted(...);
}
}
The ComplexTestBase
class above gathers the Spring configuration annotations, the definition of JUnit hooks, and the utility methods. It is ready for use as a superclass by test classes that want to benefit from all these. Yet, it is not the recommended way to go. Why that?
上面的ComplexTestBase
类收集Spring配置批注,JUnit挂钩的定义以及实用程序方法。 希望从所有这些中受益的测试类可以将其用作超类。 但是,这不是推荐的方法。 为什么?
To be honest, in simple cases, gathering the boilerplate in a superclass is not a problem in my opinion. It gets the job done, in a clean way, it is readable, maintainable, etc. However, as soon as you no longer are in a simple case, you hit the single inheritance wall. Then, if you want to stick to the inheritance model, you will start piling up superclasses in an arbitrary order, introducing unwanted dependencies, and basically ruining the encapsulation and maintainability properties just mentioned.
老实说,在简单的情况下,我认为收集超类中的样板并不是问题。 它以一种干净的方式完成了工作,它是可读性,可维护性等。但是,一旦您不再处于简单的情况下,您就会碰到单个继承墙。 然后,如果要坚持继承模型,将开始以任意顺序堆积超类,引入不必要的依赖关系,并且基本上破坏了刚才提到的封装和可维护性属性。
In my case, the situation occurred when we introduced an access control system to our services. We rapidly wanted to provide testing utilities that would allow the developers of all our µ-services to write tests against various permission set-ups. Without surprise, these testing utilities included: Spring configuration, JUnit hooks, and a handful of utility methods. This is precisely why JUnit 5 switched to the extension model.
就我而言,这种情况是在我们为服务引入访问控制系统时发生的。 我们Swift希望提供测试实用程序,以允许我们所有µ服务的开发人员针对各种权限设置编写测试。 毫不奇怪,这些测试实用程序包括:Spring配置,JUnit挂钩和少数实用程序方法。 这就是为什么JUnit 5切换到扩展模型的原因。
切换到扩展名(和注释) (Switching to Extensions (and Annotations))
两个词的JUnit扩展 (JUnit Extensions in Two Words)
Technically speaking, the role of an extension is to provide JUnit hooks. This includes the typical @BeforeEach
, but also hooks for other points of the testing life cycle, including the injection of parameters to test methods, which is quite powerful and elegant. In addition, extensions introduce the extension context, a clean mechanism to store and convey state information across the tests life-cycle, and even between extensions, as we will see further below.
从技术上讲,扩展的作用是提供JUnit挂钩。 这不仅包括典型的@BeforeEach
,而且还挂钩了测试生命周期的其他点,包括向测试方法中注入参数,这是非常强大而优雅的。 另外,扩展引入了扩展上下文,这是一种干净的机制,用于在测试生命周期甚至扩展之间存储和传递状态信息,这将在下文中进一步介绍。
Here are three links that helped me:
以下三个链接对我有帮助:
https://www.baeldung.com/junit-5-extensions: the basics
https://blog.codefx.org/design/architecture/junit-5-extension-model: a detailed explanation and example
https://blog.codefx.org/design/architecture/junit-5-extension-model :详细说明和示例
https://stackoverflow.com/questions/56904620/junit-5-inject-spring-components-to-extension-beforeallcallback-afterallcall: how to connect your extension with Spring
https://stackoverflow.com/questions/56904620/junit-5-inject-spring-components-to-extension-beforeallcallback-afterallcall :如何将扩展与Spring连接
播放扩展程序(Putting Extensions Into Play)
The goal here is no different from above: package and deliver Spring configuration, JUnit hooks, and utility methods to test classes. What is new is that we are going to do it in a composable way.
这里的目标与上面没有什么不同:打包并交付Spring配置,JUnit挂钩和用于测试类的实用程序方法。 新的是,我们将以一种可组合的方式进行操作。
We start by introducing an annotation that will replace the inheritance:
我们首先介绍一个将替代继承的注释:
@Target({ TYPE, ANNOTATION_TYPE })
@Retention(RUNTIME)
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = ComplexTestConfiguration.class)
public @interface ConfigureComplexTest {}
This is nothing revolutionary: we simply moved the two annotations from previous section into the @ConfigureComplexTest
annotation (lines 3–4). Lines 1–2 are standard in annotation definitions, except perhaps the inclusion of ANNOTATION_TYPE
in the targets. This is to leverage the meta-annotation mechanism in JUnit.
这并不是革命性的:我们只需将上一节中的两个注释移至@ConfigureComplexTest
注释(第3–4行)即可。 第1至第2行是注释定义中的标准行,除了可能在目标中包含ANNOTATION_TYPE
。 这是为了利用JUnit中的元注释机制。
Using this annotation achieves the Spring configuration just as the base class, through to the unchanged ComplexTestConfiguration
class. To also provide the JUnit hooks and our utility methods, we introduce a specific JUnit extension, which we include in the annotation (look at line 3):
使用此批注就可以像基本类一样实现Spring配置,直到未更改的ComplexTestConfiguration
类。 为了还提供JUnit挂钩和我们的实用程序方法,我们引入了一个特定的JUnit扩展,该扩展包含在注释中(请看第3行):
@Target({ TYPE, ANNOTATION_TYPE })
@Retention(RUNTIME)
@ExtendWith({ SpringExtension.class, ComplexTestExtension.class })
@ContextConfiguration(classes = ComplexTestConfiguration.class)
public @interface ConfigureComplexTest {}
In our case, all of the hooks in our extension will be dealing with Spring components. The first item in our agenda is thus to grasp a link to Spring’s application context. To this end, we add a hook that is called just after the test class instance is initialised, in which we retrieve the application context from the Spring extension.
在我们的例子中,扩展中的所有钩子都将处理Spring组件。 因此,我们议程中的第一项是掌握与Spring应用程序上下文的链接。 为此,我们添加一个在初始化测试类实例之后立即调用的钩子,在其中我们从Spring扩展中检索应用程序上下文。
public class ComplexTestExtension implements TestInstancePostProcessor {
private ApplicationContext applicationContext;
/*
* This method is executed after the test class has been instantiated. It fetches the application context from
* the SpringExtension.
*/
@Override
public void postProcessTestInstance(Object testInstance, ExtensionContext extensionContext) {
applicationContext = SpringExtension.getApplicationContext(extensionContext);
}
}
By having our extension class also implement BeforeEachCallback
, we can add the hook that will execute before each test. Similar life-cycle hooks are added by implementing the corresponding interfaces.
通过让我们的扩展类也实现BeforeEachCallback
,我们可以添加将在每次测试之前执行的钩子。 通过实现相应的接口,可以添加类似的生命周期挂钩。
To provide our utility methods, we will gather them in a Spring service and put this service at the disposal of test methods through a parameter. Let’s first introduce our service class. It is not very different from the base class of previous section:
为了提供我们的实用方法,我们将它们收集在Spring服务中,并通过参数将该服务置于测试方法的处理范围内。 让我们首先介绍我们的服务类。 它与上一节的基类没有太大区别:
@Service
public class ComplexTestHelper {
public final ShoppingCartRepository shoppingCartRepository;
public final SimpMessagingTemplate websocket;
@Autowired
public ComplexTestHelper(ShoppingCartRepository shoppingCartRepository, SimpMessagingTemplate websocket) {
this.shoppingCartRepository = shoppingCartRepository;
this.websocket = websocket;
}
public void reset() {
shoppingCartRepository.clear();
}
public void checkNotificationEmitted(String messageType) {
// capture arguments of calls to websocket.convertAndSend()
// check that it matches the expected 'messageType'
}
}
The last step is to augment our extension class so that it triggers the reset()
method above before each test, and it provides the service as a parameter to methods that request them.
最后一步是扩展我们的扩展类,以便在每次测试之前触发上面的reset()
方法,并将其作为参数提供给请求它们的方法。
public class ComplexTestExtension implements TestInstancePostProcessor, BeforeEachCallback, ParameterResolver {
private ApplicationContext applicationContext;
/*
* This method is executed after the test class has been instantiated. It fetches the application context from
* the SpringExtension.
*/
@Override
public void postProcessTestInstance(Object testInstance, ExtensionContext extensionContext) {
applicationContext = SpringExtension.getApplicationContext(extensionContext);
}
/**
* Returns the {@code ComplexTestHelper} instance.
*/
public ComplexTestHelper getComplexTestHelper() {
return applicationContext.getBean(ComplexTestHelper.class);
}
/*
* This method is executed before each test method is called.
*/
@Override
public void beforeEach(ExtensionContext context) {
getComplexTestHelper().reset();
}
/*
* This method is executed as part of the resolution of test method parameters. It indicates that we know how to
* provide a value for ComplexTestHelper parameters.
*/
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
return parameterContext.getParameter().getType() == ComplexTestHelper.class;
}
/*
* This method is executed as part of the resolution of test method parameters. It provides the value for
* ComplexTestHelper parameters.
*/
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
return getComplexTestHelper();
}
}
For line 17 above to actually return our helper service, you will need to augment ComplexTestConfiguration
so that it scans the ComplexTestHelper
class, typically by adding its package to @ComponentScan
.
为了使上面的第17行真正返回我们的帮助服务,您将需要增强ComplexTestConfiguration
以便它扫描ComplexTestHelper
类,通常是将其包添加到@ComponentScan
。
As a result of the above, here is how our test class would now read:
由于上述原因,下面是我们的测试类的内容:
@ConfigureComplexTest
class BookstoreNotificationTest {
@Autowired
StockService stockService;
@Test
void testSomething(ComplexTestHelper test) {
// Set up initial state
test.shoppingCartRepository.save(...);
// Launch the business logic
stockService.processCart(...);
// Check that it worked as expected
assertEquals(...);
test.checkNotificationEmitted(...);
}
}
The main differences with the superclass version are:
与超类版本的主要区别是:
- The inheritance is replaced with an annotation. Obviously, this can be repeated ad lib, while inheritance could not. 继承被注释替换。 显然,可以随意重复此操作,而继承则不能重复。
- The inherited fields and methods are now accessed though the helper parameter. 现在可以通过helper参数访问继承的字段和方法。
结论 (Conclusion)
While the base class pattern is ok in simple situations, it is limited by the single inheritance model of Java. With annotations and extensions, JUnit Jupiter provides a composable solution to factor Spring configuration, JUnit hooks, and utility methods. (However, the hard part remains to properly configure dependency injection, in both cases.)
虽然在简单情况下可以使用基类模式,但它受Java的单个继承模型的限制。 通过注释和扩展,JUnit Jupiter提供了可组合的解决方案来分解Spring配置,JUnit挂钩和实用程序方法。 (但是,在两种情况下,仍然很难正确配置依赖项注入。)
On our project, we applied the above pattern to the complex tests that address the business logic of each µ-service. We also applied it to package some testing utilities of our access control system. As a result, we could easily write tests that check the business logic of a µ-service under various permission settings: the test class simply bears two @Configure...
annotations, and the test method takes to helper parameters.
在我们的项目中,我们将上述模式应用于处理每个µ服务的业务逻辑的复杂测试。 我们还将其应用于打包访问控制系统的一些测试实用程序。 结果,我们可以轻松地编写测试来在各种权限设置下检查µ服务的业务逻辑:测试类仅带有两个@Configure...
批注,而测试方法采用辅助参数。
Special thanks to my colleague Adrien Estran for pointing out the subject, for helping me in its implementation, and for proof-reading this story.
特别感谢我的同事Adrien Estran指出了这个主题,为我的实现提供了帮助,并证明了这个故事。
junit 继承