From 99ee889708f3a6313bf64ea014e4f48f5d9d0303 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 26 Jun 2018 10:21:30 +0100 Subject: [PATCH] Make JUnit Jupiter extension stateless so it can be used in parallel Previously, REST Docs' JUnit Jupiter extension, RestDocumentationExtension, stored its own state. Since the code was written I have learned that this isn't recommended and that the extension context's store should be used instead. Up until now, we'd got away with the extension being stateful, but the introduction of parallel test execution has highlighted the problem. This commit updates RestDocumentationExtension to use the store from the execution context rather than storing its own state. An unwanted, but necessary, side-effect of this is that RestDocumentationExtension itself no longer implements RestDocumentationContextProvider and, instead, returns a new type rather than itself when asked to resolve the provider. This is technically a breaking API change, but users will be unaffected if they have followed the recommended and documented approach of injecting RestDocumentationContextProvider. Closes gh-520 --- .../junit5/SampleJUnit5ApplicationTests.java | 5 ++- .../restdocs/RestDocumentationExtension.java | 37 +++++++++---------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/samples/junit5/src/test/java/com/example/junit5/SampleJUnit5ApplicationTests.java b/samples/junit5/src/test/java/com/example/junit5/SampleJUnit5ApplicationTests.java index d7a558a3..9328e67f 100644 --- a/samples/junit5/src/test/java/com/example/junit5/SampleJUnit5ApplicationTests.java +++ b/samples/junit5/src/test/java/com/example/junit5/SampleJUnit5ApplicationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2017 the original author or authors. + * Copyright 2014-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; @@ -42,7 +43,7 @@ public class SampleJUnit5ApplicationTests { private MockMvc mockMvc; @BeforeEach - public void setUp(RestDocumentationExtension restDocumentation) { + public void setUp(RestDocumentationContextProvider restDocumentation) { this.mockMvc = MockMvcBuilders.webAppContextSetup(context) .apply(documentationConfiguration(restDocumentation)).build(); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationExtension.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationExtension.java index 683390ca..f384255a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationExtension.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2017 the original author or authors. + * Copyright 2014-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,11 @@ package org.springframework.restdocs; -import java.lang.reflect.Method; - import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.Extension; import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolver; @@ -31,28 +30,18 @@ import org.junit.jupiter.api.extension.ParameterResolver; * * @author Andy Wilkinson */ -public class RestDocumentationExtension implements Extension, BeforeEachCallback, - AfterEachCallback, RestDocumentationContextProvider, ParameterResolver { - - private final ManualRestDocumentation delegate = new ManualRestDocumentation(); +public class RestDocumentationExtension + implements BeforeEachCallback, AfterEachCallback, ParameterResolver { @Override public void beforeEach(ExtensionContext context) throws Exception { - Class testClass = context.getTestClass().orElseThrow( - () -> new IllegalStateException("No test class was available")); - Method testMethod = context.getTestMethod().orElseThrow( - () -> new IllegalStateException("No test method was available")); - this.delegate.beforeTest(testClass, testMethod.getName()); + this.getDelegate(context).beforeTest(context.getRequiredTestClass(), + context.getRequiredTestMethod().getName()); } @Override public void afterEach(ExtensionContext context) throws Exception { - this.delegate.afterTest(); - } - - @Override - public RestDocumentationContext beforeOperation() { - return this.delegate.beforeOperation(); + this.getDelegate(context).afterTest(); } @Override @@ -64,8 +53,16 @@ public class RestDocumentationExtension implements Extension, BeforeEachCallback @Override public Object resolveParameter(ParameterContext parameterContext, - ExtensionContext extensionContext) { - return this; + ExtensionContext context) { + return (RestDocumentationContextProvider) () -> getDelegate(context) + .beforeOperation(); + } + + private ManualRestDocumentation getDelegate(ExtensionContext context) { + Namespace namespace = Namespace.create(getClass(), context.getUniqueId()); + return context.getStore(namespace).getOrComputeIfAbsent( + ManualRestDocumentation.class, (key) -> new ManualRestDocumentation(), + ManualRestDocumentation.class); } }