Add support for working with resources in tests

Closes gh-44444
This commit is contained in:
Andy Wilkinson 2025-02-24 10:22:58 +00:00
parent abf320d273
commit 3acea583ad
25 changed files with 1397 additions and 0 deletions

View File

@ -0,0 +1,44 @@
/*
* Copyright 2012-2025 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.testsupport.classpath.resources;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation that indicates that the content of a resource should be injected. Supported
* on parameters of type:
*
* <ul>
* <li>{@link String}</li>
* </ul>
*
* @author Andy Wilkinson
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface ResourceContent {
/**
* The name of the resource whose content should be injected.
* @return the resource name
*/
String value();
}

View File

@ -0,0 +1,48 @@
/*
* Copyright 2012-2025 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.testsupport.classpath.resources;
import java.io.File;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.nio.file.Path;
/**
* Annotation that indicates that the path of a resource should be injected. Supported on
* parameters of type:
*
* <ul>
* <li>{@link File}</li>
* <li>{@link Path}</li>
* <li>{@link String}</li>
* </ul>
*
* @author Andy Wilkinson
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface ResourcePath {
/**
* The name of the resource whose path should be injected.
* @return the resource name
*/
String value();
}

View File

@ -0,0 +1,130 @@
/*
* Copyright 2012-2025 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.testsupport.classpath.resources;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import org.springframework.util.Assert;
import org.springframework.util.FileSystemUtils;
/**
* A collection of resources.
*
* @author Andy Wilkinson
*/
class Resources {
private final Path root;
Resources(Path root) {
this.root = root;
}
Resources addPackage(Package root, String[] resourceNames) {
Set<String> unmatchedNames = new HashSet<>(Arrays.asList(resourceNames));
try {
Enumeration<URL> sources = getClass().getClassLoader().getResources(root.getName().replace(".", "/"));
for (URL source : Collections.list(sources)) {
Path sourceRoot = Paths.get(source.toURI());
for (String resourceName : resourceNames) {
Path resource = sourceRoot.resolve(resourceName);
if (Files.isRegularFile(resource)) {
Path target = this.root.resolve(resourceName);
Path targetDirectory = target.getParent();
if (!Files.isDirectory(targetDirectory)) {
Files.createDirectories(targetDirectory);
}
Files.copy(resource, target);
unmatchedNames.remove(resourceName);
}
}
}
Assert.isTrue(unmatchedNames.isEmpty(),
"Package '" + root.getName() + "' did not contain resources: " + unmatchedNames);
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
catch (URISyntaxException ex) {
throw new RuntimeException(ex);
}
return this;
}
Resources addResource(String name, String content) {
Path resourcePath = this.root.resolve(name);
if (Files.isDirectory(resourcePath)) {
throw new IllegalStateException(
"Cannot create resource '" + name + "' as a directory already exists at that location");
}
Path parent = resourcePath.getParent();
try {
if (!Files.isDirectory(resourcePath)) {
Files.createDirectories(parent);
}
Files.writeString(resourcePath, processContent(content));
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
return this;
}
private String processContent(String content) {
return content.replace("${resourceRoot}", this.root.toString());
}
Resources addDirectory(String name) {
Path directoryPath = this.root.resolve(name);
if (Files.isRegularFile(directoryPath)) {
throw new IllegalStateException(
"Cannot create directory '" + name + " as a file already exists at that location");
}
try {
Files.createDirectories(directoryPath);
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
return this;
}
void delete() {
try {
FileSystemUtils.deleteRecursively(this.root);
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
Path getRoot() {
return this.root;
}
}

View File

@ -0,0 +1,62 @@
/*
* Copyright 2012-2025 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.testsupport.classpath.resources;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
/**
* A {@link ClassLoader} that provides access to {@link Resources resources}.
*
* @author Andy Wilkinson
*/
class ResourcesClassLoader extends ClassLoader {
private final Resources resources;
ResourcesClassLoader(ClassLoader parent, Resources resources) {
super(parent);
this.resources = resources;
}
@Override
protected URL findResource(String name) {
Path resource = this.resources.getRoot().resolve(name);
if (Files.exists(resource)) {
try {
return resource.toUri().toURL();
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
return null;
}
@Override
protected Enumeration<URL> findResources(String name) throws IOException {
URL resourceUrl = findResource(name);
return (resourceUrl != null) ? Collections.enumeration(List.of(resourceUrl)) : Collections.emptyEnumeration();
}
}

View File

@ -0,0 +1,189 @@
/*
* Copyright 2012-2025 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.testsupport.classpath.resources;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
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.ExtensionContext.Store;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.junit.platform.commons.support.AnnotationSupport;
import org.junit.platform.commons.support.SearchOption;
import org.springframework.util.StreamUtils;
/**
* {@link Extension} for managing resources in tests. Resources are made available through
* {@link Thread#getContextClassLoader() thread context class loader}.
*
* @author Andy Wilkinson
* @see WithPackageResources
* @see WithResource
* @see WithResourceDirectory
*/
class ResourcesExtension implements BeforeEachCallback, AfterEachCallback, ParameterResolver {
private static final String RESOURCES_KEY = ResourcesExtension.class.getName() + ".resources";
private static final String TCCL_KEY = ResourcesExtension.class.getName() + ".tccl";
@Override
public void beforeEach(ExtensionContext context) throws Exception {
Store store = context.getStore(Namespace.create(ResourcesExtension.class));
Resources resources = new Resources(Files.createTempDirectory("resources"));
store.put(RESOURCES_KEY, resources);
Method testMethod = context.getRequiredTestMethod();
resourcesOf(testMethod).forEach((resource) -> resources.addResource(resource.name(), resource.content()));
resourceDirectoriesOf(testMethod).forEach((directory) -> resources.addDirectory(directory.value()));
packageResourcesOf(testMethod).forEach((withPackageResources) -> resources
.addPackage(testMethod.getDeclaringClass().getPackage(), withPackageResources.value()));
ResourcesClassLoader classLoader = new ResourcesClassLoader(context.getRequiredTestClass().getClassLoader(),
resources);
store.put(TCCL_KEY, Thread.currentThread().getContextClassLoader());
Thread.currentThread().setContextClassLoader(classLoader);
}
private List<WithResource> resourcesOf(Method method) {
return withAnnotationsOf(method, WithResource.class);
}
private List<WithResourceDirectory> resourceDirectoriesOf(Method method) {
return withAnnotationsOf(method, WithResourceDirectory.class);
}
private <A extends Annotation> List<A> withAnnotationsOf(Method method, Class<A> annotationType) {
List<A> annotations = new ArrayList<>();
AnnotationSupport.findRepeatableAnnotations(method, annotationType).forEach(annotations::add);
Class<?> type = method.getDeclaringClass();
while (type != null) {
AnnotationSupport.findRepeatableAnnotations(type, annotationType).forEach(annotations::add);
type = type.getEnclosingClass();
}
return annotations;
}
private List<WithPackageResources> packageResourcesOf(Method method) {
List<WithPackageResources> annotations = new ArrayList<>();
AnnotationSupport.findAnnotation(method, WithPackageResources.class).ifPresent(annotations::add);
AnnotationSupport
.findAnnotation(method.getDeclaringClass(), WithPackageResources.class,
SearchOption.INCLUDE_ENCLOSING_CLASSES)
.ifPresent(annotations::add);
return annotations;
}
@Override
public void afterEach(ExtensionContext context) throws Exception {
Store store = context.getStore(Namespace.create(ResourcesExtension.class));
store.get(RESOURCES_KEY, Resources.class).delete();
Thread.currentThread().setContextClassLoader(store.get(TCCL_KEY, ClassLoader.class));
}
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
return parameterContext.isAnnotated(ResourcesRoot.class) || parameterContext.isAnnotated(ResourcePath.class)
|| parameterContext.isAnnotated(ResourceContent.class);
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
if (parameterContext.isAnnotated(ResourcesRoot.class)) {
return resolveResourcesRoot(parameterContext, extensionContext);
}
if (parameterContext.isAnnotated(ResourcePath.class)) {
return resolveResourcePath(parameterContext, extensionContext);
}
if (parameterContext.isAnnotated(ResourceContent.class)) {
return resolveResourceContent(parameterContext, extensionContext);
}
throw new ParameterResolutionException(
"Parameter is not annotated with @ResourcesRoot, @ResourceContent, or @ResourcePath");
}
private Object resolveResourcesRoot(ParameterContext parameterContext, ExtensionContext extensionContext) {
Resources resources = getResources(extensionContext);
Class<?> parameterType = parameterContext.getParameter().getType();
if (parameterType.isAssignableFrom(Path.class)) {
return resources.getRoot();
}
else if (parameterType.isAssignableFrom(File.class)) {
return resources.getRoot().toFile();
}
throw new IllegalStateException(
"@ResourcesRoot is not supported with parameter type '" + parameterType.getName() + "'");
}
private Object resolveResourcePath(ParameterContext parameterContext, ExtensionContext extensionContext) {
Resources resources = getResources(extensionContext);
Class<?> parameterType = parameterContext.getParameter().getType();
Path resourcePath = resources.getRoot()
.resolve(parameterContext.findAnnotation(ResourcePath.class).get().value());
if (parameterType.isAssignableFrom(Path.class)) {
return resourcePath;
}
else if (parameterType.isAssignableFrom(File.class)) {
return resourcePath.toFile();
}
else if (parameterType.isAssignableFrom(String.class)) {
return resourcePath.toString();
}
throw new IllegalStateException(
"@ResourcePath is not supported with parameter type '" + parameterType.getName() + "'");
}
private Object resolveResourceContent(ParameterContext parameterContext, ExtensionContext extensionContext) {
Resources resources = getResources(extensionContext);
Class<?> parameterType = parameterContext.getParameter().getType();
Path resourcePath = resources.getRoot()
.resolve(parameterContext.findAnnotation(ResourceContent.class).get().value());
if (parameterType.isAssignableFrom(String.class)) {
try (InputStream in = Files.newInputStream(resourcePath)) {
return StreamUtils.copyToString(in, StandardCharsets.UTF_8);
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
throw new IllegalStateException(
"@ResourceContent is not supported with parameter type '" + parameterType.getName() + "'");
}
private Resources getResources(ExtensionContext extensionContext) {
Store store = extensionContext.getStore(Namespace.create(ResourcesExtension.class));
Resources resources = store.get(RESOURCES_KEY, Resources.class);
return resources;
}
}

View File

@ -0,0 +1,41 @@
/*
* Copyright 2012-2025 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.testsupport.classpath.resources;
import java.io.File;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.nio.file.Path;
/**
* Annotation that indicates that the resources root should be injected. Supported on
* parameters of type:
*
* <ul>
* <li>{@link File}</li>
* <li>{@link Path}</li>
* </ul>
*
* @author Andy Wilkinson
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface ResourcesRoot {
}

View File

@ -0,0 +1,45 @@
/*
* Copyright 2012-2025 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.testsupport.classpath.resources;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.extension.ExtendWith;
/**
* Makes resources in the package of the annotated class available from the root of the
* classpath.
*
* @author Andy Wilkinson
*/
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
@ExtendWith(ResourcesExtension.class)
public @interface WithPackageResources {
/**
* The resources to make available from the root.
* @return the resources
*/
String[] value();
}

View File

@ -0,0 +1,57 @@
/*
* Copyright 2012-2025 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.testsupport.classpath.resources;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.extension.ExtendWith;
/**
* Makes a resource directory available from the thread context class loader.
* <p>
* For cases where one resource needs to refer to another, the resource's content may
* contain the placeholder <code>${resourceRoot}</code>. It will be replaced with the path
* to the root of the resources. For example, a resource with the {@link #name}
* {@code example.txt} can be referenced using <code>${resourceRoot}/example.txt</code>.
*
* @author Andy Wilkinson
*/
@Inherited
@Repeatable(WithResources.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
@ExtendWith(ResourcesExtension.class)
public @interface WithResource {
/**
* The name of the resource.
* @return the name
*/
String name();
/**
* The content of the resource. When omitted an empty resource will be created.
* @return the content
*/
String content() default "";
}

View File

@ -0,0 +1,39 @@
/*
* Copyright 2012-2025 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.testsupport.classpath.resources;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Container annotation for {@link WithResourceDirectory}.
*
* @author Andy Wilkinson
*/
@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface WithResourceDirectories {
WithResourceDirectory[] value();
}

View File

@ -0,0 +1,47 @@
/*
* Copyright 2012-2025 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.testsupport.classpath.resources;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.extension.ExtendWith;
/**
* Makes a resource available from the thread context class loader. Typically used when a
* test requires an empty directory to exist.
*
* @author Andy Wilkinson
*/
@Inherited
@Repeatable(WithResourceDirectories.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
@ExtendWith(ResourcesExtension.class)
public @interface WithResourceDirectory {
/**
* The name of the directory.
* @return the name
*/
String value();
}

View File

@ -0,0 +1,39 @@
/*
* Copyright 2012-2025 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.testsupport.classpath.resources;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Container annotation for {@link WithResource}.
*
* @author Andy Wilkinson
*/
@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface WithResources {
WithResource[] value();
}

View File

@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Custom JUnit extension for testing with resources.
*/
package org.springframework.boot.testsupport.classpath.resources;

View File

@ -0,0 +1,44 @@
/*
* Copyright 2012-2025 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.testsupport.classpath.resources;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link WithPackageResources} on a class.
*
* @author Andy Wilkinson
*/
@WithPackageResources({ "resource-1.txt", "resource-2.txt", "sub/resource-3.txt" })
class OnClassWithPackageResourcesTests {
@Test
void whenWithPackageResourcesIsUsedOnAClassThenResourcesAreAvailable() throws IOException {
assertThat(new ClassPathResource("resource-1.txt").getContentAsString(StandardCharsets.UTF_8)).isEqualTo("one");
assertThat(new ClassPathResource("resource-2.txt").getContentAsString(StandardCharsets.UTF_8)).isEqualTo("two");
assertThat(new ClassPathResource("sub/resource-3.txt").getContentAsString(StandardCharsets.UTF_8))
.isEqualTo("three");
}
}

View File

@ -0,0 +1,89 @@
/*
* Copyright 2012-2025 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.testsupport.classpath.resources;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link WithResource} when used on a class.
*
* @author Andy Wilkinson
*/
@WithResource(name = "on-class", content = "class content")
class OnClassWithResourceTests {
@Test
void whenWithResourceIsUsedOnAClassThenResourceIsAvailable() throws IOException {
assertThat(new ClassPathResource("on-class").getContentAsString(StandardCharsets.UTF_8))
.isEqualTo("class content");
}
@Test
@WithResource(name = "method-resource", content = "method")
void whenWithResourceIsUsedOnClassAndMethodThenBothResourcesAreAvailable() throws IOException {
assertThat(new ClassPathResource("on-class").getContentAsString(StandardCharsets.UTF_8))
.isEqualTo("class content");
assertThat(new ClassPathResource("method-resource").getContentAsString(StandardCharsets.UTF_8))
.isEqualTo("method");
}
@Test
@WithResource(name = "method-resource-1", content = "method-1")
@WithResource(name = "method-resource-2", content = "method-2")
void whenWithResourceIsUsedOnClassAndRepeatedOnMethodThenAllResourcesAreAvailable() throws IOException {
assertThat(new ClassPathResource("on-class").getContentAsString(StandardCharsets.UTF_8))
.isEqualTo("class content");
assertThat(new ClassPathResource("method-resource-1").getContentAsString(StandardCharsets.UTF_8))
.isEqualTo("method-1");
assertThat(new ClassPathResource("method-resource-2").getContentAsString(StandardCharsets.UTF_8))
.isEqualTo("method-2");
}
@Nested
class NestedTests {
@Test
void whenWithResourceIsUsedOnEnclosingClassThenResourceIsAvailable() throws IOException {
assertThat(new ClassPathResource("on-class").getContentAsString(StandardCharsets.UTF_8))
.isEqualTo("class content");
}
}
@Nested
@WithResource(name = "on-nested-class", content = "nested class content")
class WithResourceNestedTests {
@Test
void whenWithResourceIsUsedOnEnclosingClassAndClassThenBothResourcesAreAvailable() throws IOException {
assertThat(new ClassPathResource("on-class").getContentAsString(StandardCharsets.UTF_8))
.isEqualTo("class content");
assertThat(new ClassPathResource("on-nested-class").getContentAsString(StandardCharsets.UTF_8))
.isEqualTo("nested class content");
}
}
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2012-2025 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.testsupport.classpath.resources;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link WithPackageResources} when used on a super-class.
*
* @author Andy Wilkinson
*/
class OnSuperClassWithPackageResourcesTests extends WithPackageResourcesClass {
@Test
void whenWithPackageResourcesIsUsedOnASuperClassThenResourcesAreAvailable() throws IOException {
assertThat(new ClassPathResource("resource-1.txt").getContentAsString(StandardCharsets.UTF_8)).isEqualTo("one");
assertThat(new ClassPathResource("resource-2.txt").getContentAsString(StandardCharsets.UTF_8)).isEqualTo("two");
assertThat(new ClassPathResource("sub/resource-3.txt").getContentAsString(StandardCharsets.UTF_8))
.isEqualTo("three");
}
}

View File

@ -0,0 +1,62 @@
/*
* Copyright 2012-2025 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.testsupport.classpath.resources;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link WithResource} when used on a super-class.
*
* @author Andy Wilkinson
*/
class OnSuperClassWithResourceTests extends WithResourceClass {
@Test
void whenWithResourceIsUsedOnASuperClassThenResourceIsAvailable() throws IOException {
assertThat(new ClassPathResource("on-super-class").getContentAsString(StandardCharsets.UTF_8))
.isEqualTo("super-class content");
}
@Test
@WithResource(name = "method-resource", content = "method")
void whenWithResourceIsUsedOnASuperClassAndMethodThenBothResourcesAreAvailable() throws IOException {
assertThat(new ClassPathResource("on-super-class").getContentAsString(StandardCharsets.UTF_8))
.isEqualTo("super-class content");
assertThat(new ClassPathResource("method-resource").getContentAsString(StandardCharsets.UTF_8))
.isEqualTo("method");
}
@Test
@WithResource(name = "method-resource-1", content = "method-1")
@WithResource(name = "method-resource-2", content = "method-2")
void whenWithResourceIsUsedOnASuperClassAndRepeatedOnMethodThenAllResourcesAreAvailable() throws IOException {
assertThat(new ClassPathResource("on-super-class").getContentAsString(StandardCharsets.UTF_8))
.isEqualTo("super-class content");
assertThat(new ClassPathResource("method-resource-1").getContentAsString(StandardCharsets.UTF_8))
.isEqualTo("method-1");
assertThat(new ClassPathResource("method-resource-2").getContentAsString(StandardCharsets.UTF_8))
.isEqualTo("method-2");
}
}

View File

@ -0,0 +1,132 @@
/*
* Copyright 2012-2025 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.testsupport.classpath.resources;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link Resources}.
*
* @author Andy Wilkinson
*/
class ResourcesTests {
@TempDir
private Path root;
@Test
void whenAddResourceThenResourceIsCreated() {
new Resources(this.root).addResource("test", "test-content");
assertThat(this.root.resolve("test")).hasContent("test-content");
}
@Test
void whenAddResourceHasContentReferencingResourceRootThenResourceIsCreatedWithReferenceToRoot() {
new Resources(this.root).addResource("test", "*** ${resourceRoot} ***");
assertThat(this.root.resolve("test")).hasContent("*** " + this.root + " ***");
}
@Test
void whenAddResourceWithPathThenResourceIsCreated() {
new Resources(this.root).addResource("a/b/c/test", "test-content");
assertThat(this.root.resolve("a/b/c/test")).hasContent("test-content");
}
@Test
void whenAddResourceAndResourceAlreadyExistsThenResourcesIsOverwritten() {
Resources resources = new Resources(this.root);
resources.addResource("a/b/c/test", "original-content");
resources.addResource("a/b/c/test", "new-content");
assertThat(this.root.resolve("a/b/c/test")).hasContent("new-content");
}
@Test
void whenAddPackageThenNamedResourcesFromPackageAreCreated() {
new Resources(this.root).addPackage(getClass().getPackage(),
new String[] { "resource-1.txt", "sub/resource-3.txt" });
assertThat(this.root.resolve("resource-1.txt")).hasContent("one");
assertThat(this.root.resolve("resource-2.txt")).doesNotExist();
assertThat(this.root.resolve("sub/resource-3.txt")).hasContent("three");
}
@Test
void whenAddResourceAndDeleteThenResourceDoesNotExist() {
Resources resources = new Resources(this.root);
resources.addResource("test", "test-content");
assertThat(this.root.resolve("test")).hasContent("test-content");
resources.delete();
assertThat(this.root.resolve("test")).doesNotExist();
}
@Test
void whenAddPackageAndDeleteThenResourcesDoNotExist() {
Resources resources = new Resources(this.root);
resources.addPackage(getClass().getPackage(),
new String[] { "resource-1.txt", "resource-2.txt", "sub/resource-3.txt" });
assertThat(this.root.resolve("resource-1.txt")).hasContent("one");
assertThat(this.root.resolve("resource-2.txt")).hasContent("two");
assertThat(this.root.resolve("sub/resource-3.txt")).hasContent("three");
resources.delete();
assertThat(this.root.resolve("resource-1.txt")).doesNotExist();
assertThat(this.root.resolve("resource-2.txt")).doesNotExist();
assertThat(this.root.resolve("sub/resource-3.txt")).doesNotExist();
assertThat(this.root.resolve("sub")).doesNotExist();
}
@Test
void whenAddDirectoryThenDirectoryIsCreated() {
Resources resources = new Resources(this.root);
resources.addDirectory("dir");
assertThat(this.root.resolve("dir")).isDirectory();
}
@Test
void whenAddDirectoryWithPathThenDirectoryIsCreated() {
Resources resources = new Resources(this.root);
resources.addDirectory("one/two/three/dir");
assertThat(this.root.resolve("one/two/three/dir")).isDirectory();
}
@Test
void whenAddDirectoryAndDirectoryAlreadyExistsThenDoesNotThrow() {
Resources resources = new Resources(this.root);
resources.addDirectory("one/two/three/dir");
resources.addDirectory("one/two/three/dir");
assertThat(this.root.resolve("one/two/three/dir")).isDirectory();
}
@Test
void whenAddDirectoryAndResourceAlreadyExistsThenIllegalStateExceptionIsThrown() {
Resources resources = new Resources(this.root);
resources.addResource("one/two/three/", "content");
assertThatIllegalStateException().isThrownBy(() -> resources.addDirectory("one/two/three"));
}
@Test
void whenAddResourceAndDirectoryAlreadyExistsThenIllegalStateExceptionIsThrown() {
Resources resources = new Resources(this.root);
resources.addDirectory("one/two/three");
assertThatIllegalStateException().isThrownBy(() -> resources.addResource("one/two/three", "content"));
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright 2012-2025 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.testsupport.classpath.resources;
/**
* Class to test the use of {@link WithPackageResources} on a super-class.
*
* @author Andy Wilkinson
*/
@WithPackageResources({ "resource-1.txt", "resource-2.txt", "sub/resource-3.txt" })
class WithPackageResourcesClass {
}

View File

@ -0,0 +1,53 @@
/*
* Copyright 2012-2025 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.testsupport.classpath.resources;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link WithPackageResources}.
*
* @author Andy Wilkinson
*/
class WithPackageResourcesTests {
@Test
@WithPackageResources({ "resource-1.txt", "resource-2.txt", "sub/resource-3.txt" })
void whenWithPackageResourcesIsUsedOnAMethodThenResourcesAreAvailable() throws IOException {
assertThat(new ClassPathResource("resource-1.txt").getContentAsString(StandardCharsets.UTF_8)).isEqualTo("one");
assertThat(new ClassPathResource("resource-2.txt").getContentAsString(StandardCharsets.UTF_8)).isEqualTo("two");
assertThat(new ClassPathResource("sub/resource-3.txt").getContentAsString(StandardCharsets.UTF_8))
.isEqualTo("three");
}
@Test
@WithPackageResources("sub/resource-3.txt")
void whenWithPackageResourcesOnlyIncludesSomeResourcesThenOnlyIncludedResourcesAreAvailable() throws IOException {
assertThat(new ClassPathResource("resource-1.txt").exists()).isFalse();
assertThat(new ClassPathResource("resource-2.txt").exists()).isFalse();
assertThat(new ClassPathResource("sub/resource-3.txt").getContentAsString(StandardCharsets.UTF_8))
.isEqualTo("three");
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright 2012-2025 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.testsupport.classpath.resources;
/**
* Class to test the use of {@link WithResource} on a super-class
*
* @author Andy Wilkinson
*/
@WithResource(name = "on-super-class", content = "super-class content")
class WithResourceClass {
}

View File

@ -0,0 +1,56 @@
/*
* Copyright 2012-2025 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.testsupport.classpath.resources;
import java.io.IOException;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link WithResourceDirectory}.
*
* @author Andy Wilkinson
*/
class WithResourceDirectoryTests {
@Test
@WithResourceDirectory("test")
void whenWithResourceDirectoryIsUsedOnAMethodThenDirectoryIsCreated() throws IOException {
assertThat(new ClassPathResource("test").getFile()).isDirectory();
}
@Test
@WithResourceDirectory("com/example/nested")
void whenWithResourceDirectoryNamesANestedDirectoryThenDirectoryIsCreated() throws IOException {
assertThat(new ClassPathResource("com/example/nested").getFile()).isDirectory();
}
@Test
@WithResourceDirectory("1")
@WithResourceDirectory("2")
@WithResourceDirectory("3")
void whenWithResourceDirectoryIsRepeatedOnAMethodThenAllResourceDirectoriesAreCreated() throws IOException {
assertThat(new ClassPathResource("1").getFile()).isDirectory();
assertThat(new ClassPathResource("2").getFile()).isDirectory();
assertThat(new ClassPathResource("3").getFile()).isDirectory();
}
}

View File

@ -0,0 +1,100 @@
/*
* Copyright 2012-2025 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.testsupport.classpath.resources;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link WithResource}.
*
* @author Andy Wilkinson
*/
class WithResourceTests {
@Test
@WithResource(name = "test", content = "content")
void whenWithResourceIsUsedOnAMethodThenResourceIsAvailable() throws IOException {
assertThat(new ClassPathResource("test").getContentAsString(StandardCharsets.UTF_8)).isEqualTo("content");
}
@Test
@WithResource(name = "test", content = "content")
void whenWithResourceIsUsedOnAMethodThenResourceIsAvailableFromFileResourcesRoot(@ResourcesRoot File root) {
assertThat(new File(root, "test")).hasContent("content");
}
@Test
@WithResource(name = "test", content = "content")
void whenWithResourceIsUsedOnAMethodThenResourceIsAvailableFromPathResourcesRoot(@ResourcesRoot Path root) {
assertThat(root.resolve("test")).hasContent("content");
}
@Test
@WithResource(name = "test", content = "content")
void whenWithResourceIsUsedOnAMethodThenResourceIsAvailableFromPathResourcePath(
@ResourcePath("test") Path resource) {
assertThat(resource).hasContent("content");
}
@Test
@WithResource(name = "test", content = "content")
void whenWithResourceIsUsedOnAMethodThenResourceIsAvailableFromFileResourcePath(
@ResourcePath("test") File resource) {
assertThat(resource).hasContent("content");
}
@Test
@WithResource(name = "test", content = "content")
void whenWithResourceIsUsedOnAMethodThenResourceIsAvailableFromStringResourcePath(
@ResourcePath("test") String resource) {
assertThat(new File(resource)).hasContent("content");
}
@Test
@WithResource(name = "test", content = "content")
void whenWithResourceIsUsedOnAMethodThenResourceContentIsAvailableAsAString(
@ResourceContent("test") String content) {
assertThat(content).isEqualTo("content");
}
@Test
@WithResource(name = "com/example/test-resource", content = "content")
void whenWithResourceNameIncludesADirectoryThenResourceIsAvailable() throws IOException {
assertThat(new ClassPathResource("com/example/test-resource").getContentAsString(StandardCharsets.UTF_8))
.isEqualTo("content");
}
@Test
@WithResource(name = "1", content = "one")
@WithResource(name = "2", content = "two")
@WithResource(name = "3", content = "three")
void whenWithResourceIsRepeatedOnAMethodThenAllResourcesAreAvailable() throws IOException {
assertThat(new ClassPathResource("1").getContentAsString(StandardCharsets.UTF_8)).isEqualTo("one");
assertThat(new ClassPathResource("2").getContentAsString(StandardCharsets.UTF_8)).isEqualTo("two");
assertThat(new ClassPathResource("3").getContentAsString(StandardCharsets.UTF_8)).isEqualTo("three");
}
}