在 Spring 启动测试中,如何将临时文件夹映射到配置 属性?

In Spring Boot Test, how do I map a temporary folder to a configuration property?

我想做一个自清洁测试

在我的情况下,我有一个组件依赖于目录

public class FileRepositoryManagerImpl implements ....

    @Value("${acme.fileRepository.basePath}")
    private File basePath; 
}

该值在application.yml文件中定义,在DEV中指向build.

下的一个目录

这不是最坏的主意,因为 gradle clean 最终会清理测试造成的混乱。

但是,实际上,我想在这里实现的是确保每个测试都在执行后清理的隔离临时目录中运行。

我知道 JUnit 有一个用于临时目录的工具。但是一旦我在 JUnit 4 的范围内定义了该目录,我该如何告诉 Spring 使用该临时目录?

试过内部class不成功:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = { SecurityBeanOverrideConfiguration.class, App.class })
@EnableConfigurationProperties
public abstract class AbstractFileRepositoryManagerIntTests {

    private final static TemporaryFolder temporaryFolder = new TemporaryFolder();

    @ClassRule
    public static TemporaryFolder getTemporaryFolder()
    {
        return temporaryFolder;
    }

    @ConfigurationProperties(prefix = "acme")
    static class Configuration
    {

        public FileRepository getFileRepository()
        {
            return new FileRepository();
        }

        static class FileRepository
        {

            public File basePath() throws Exception
            {
                return temporaryFolder.newFolder("fileRepositoryBaseDir");
            }
        }
    }
}

我正在考虑修补 Environment,但是在 Spring 启动测试中以编程方式注入属性的正确方法应该是什么?

我至少可以想到四种不同的方法来解决您的问题。各有优缺点。

方法一:ReflectionTestUtils

您正在私有实例 属性 上使用 @Value 注释(请不要再这样做了!)。因此,您不能在没有反射的情况下即时更改 acme.fileRepository.basePath

package demo;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Component;

import java.io.File;

@SpringBootApplication
public class FileRepositoryApp {

    public static void main(String[] args) {
        SpringApplication.run(FileRepositoryApp.class, args);
    }

    @Component
    public class FileRepository {

        @Value("${acme.fileRepository.basePath}")
        private File basePath;

        public File getBasePath() {
            return basePath;
        }
    }
}

每次测试 ReflectionTestUtils.setField 后更改 basePath。因为我们正在使用 Spring 的 TestExecutionListener,它在 Junit 规则初始化之前被初始化,我们被迫管理 beforeTestExecutionafterTestMethod.[=32 中的临时文件夹=]

package demo;

import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestExecutionListener;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.util.ReflectionTestUtils;

import java.io.IOException;

import static junit.framework.TestCase.assertEquals;
import static org.springframework.test.context.TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = FileRepositoryApp.class)
@TestExecutionListeners(listeners = FileRepositoryAppTest.SetBasePath.class, mergeMode = MERGE_WITH_DEFAULTS)
public class FileRepositoryAppTest {

    private static TemporaryFolder temporaryFolder = new TemporaryFolder();

    @Autowired
    private FileRepositoryApp.FileRepository fileRepository;

    @Test
    public void method() {
        System.out.println(temporaryFolder.getRoot().getAbsolutePath());
        System.out.println(fileRepository.getBasePath());
        assertEquals(temporaryFolder.getRoot(), fileRepository.getBasePath());
    }

    @Test
    public void method1() {
        System.out.println(temporaryFolder.getRoot().getAbsolutePath());
        System.out.println(fileRepository.getBasePath());
        assertEquals(temporaryFolder.getRoot(), fileRepository.getBasePath());
    }

    static class SetBasePath implements TestExecutionListener {

        @Override
        public void beforeTestExecution(TestContext testContext) throws IOException {
            temporaryFolder.create();
            if (testContext.hasApplicationContext()) {
                FileRepositoryApp.FileRepository bean = testContext.getApplicationContext().getBean(FileRepositoryApp.FileRepository.class);
                ReflectionTestUtils.setField(bean, "basePath", temporaryFolder.getRoot());
            }
        }

        @Override
        public void afterTestMethod(TestContext testContext) {
            temporaryFolder.delete();
        }
    }
}

方法 2:配置属性

为您的应用程序配置引入一个配置属性class。它免费为您提供 type safety,我们不再依赖反射。

package demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.io.File;

@SpringBootApplication
public class FileRepositoryWithPropertiesApp {

    public static void main(String[] args) {
        SpringApplication.run(FileRepositoryWithPropertiesApp.class, args);
    }

    @Component
    public class FileRepository {

        private final FileRepositoryProperties fileRepositoryProperties;

        public FileRepository(FileRepositoryProperties fileRepositoryProperties) {
            this.fileRepositoryProperties = fileRepositoryProperties;
        }

        public File getBasePath() {
            return fileRepositoryProperties.getBasePath();
        }
    }

    @Component
    @ConfigurationProperties(prefix = "acme.file-repository")
    public class FileRepositoryProperties {

        private File basePath;

        public File getBasePath() {
            return basePath;
        }

        public void setBasePath(File basePath) {
            this.basePath = basePath;
        }
    }

}

因为我们正在使用 Spring 的 TestExecutionListener,它在 Junit 规则初始化之前被初始化,我们被迫管理 beforeTestExecution 和 [=22= 中的临时文件夹].

package demo;

import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestExecutionListener;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringRunner;

import java.io.IOException;

import static junit.framework.TestCase.assertEquals;
import static org.springframework.test.context.TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = FileRepositoryWithPropertiesApp.class)
@TestExecutionListeners(listeners = FileRepositoryWithPropertiesTest.SetBasePath.class, mergeMode = MERGE_WITH_DEFAULTS)
public class FileRepositoryWithPropertiesTest {

    private static TemporaryFolder temporaryFolder = new TemporaryFolder();

    @Autowired
    private FileRepositoryWithPropertiesApp.FileRepository bean;

    @Test
    public void method() {
        System.out.println(temporaryFolder.getRoot().getAbsolutePath());
        System.out.println(bean.getBasePath());
        assertEquals(temporaryFolder.getRoot(), bean.getBasePath());
    }

    @Test
    public void method1() {
        System.out.println(temporaryFolder.getRoot().getAbsolutePath());
        System.out.println(bean.getBasePath());
        assertEquals(temporaryFolder.getRoot(), bean.getBasePath());
    }

    static class SetBasePath implements TestExecutionListener {

        @Override
        public void beforeTestExecution(TestContext testContext) throws IOException {
            temporaryFolder.create();
            if (testContext.hasApplicationContext()) {
                FileRepositoryWithPropertiesApp.FileRepositoryProperties bean = testContext.getApplicationContext().getBean(FileRepositoryWithPropertiesApp.FileRepositoryProperties.class);
                bean.setBasePath(temporaryFolder.getRoot());
            }
        }

        @Override
        public void afterTestMethod(TestContext testContext) {
            temporaryFolder.delete();
        }
    }
}

方法 3:重构代码(我的最爱)

basePath 提取到它自己的 class 中并将其隐藏在 api 后面。现在您不再需要查看您的应用程序属性和临时文件夹。

package demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.io.File;

@SpringBootApplication
public class FileRepositoryWithAbstractionApp {

    public static void main(String[] args) {
        SpringApplication.run(FileRepositoryWithAbstractionApp.class, args);
    }

    @Component
    public class FileRepository {

        private final FileRepositorySource fileRepositorySource;

        public FileRepository(FileRepositorySource fileRepositorySource) {
            this.fileRepositorySource = fileRepositorySource;
        }

        public File getBasePath() {
            return fileRepositorySource.getBasePath();
        }
    }

    @Component
    public class FileRepositorySource {

        private final FileRepositoryProperties fileRepositoryProperties;

        public FileRepositorySource(FileRepositoryProperties fileRepositoryProperties) {
            this.fileRepositoryProperties = fileRepositoryProperties;
        }

        // TODO for the sake of brevity no real api here
        public File getBasePath() {
            return fileRepositoryProperties.getBasePath();
        }
    }

    @Component
    @ConfigurationProperties(prefix = "acme.file-repository")
    public class FileRepositoryProperties {

        private File basePath;

        public File getBasePath() {
            return basePath;
        }

        public void setBasePath(File basePath) {
            this.basePath = basePath;
        }
    }
}

我们不再需要任何额外的测试设施,我们可以在 TemporaryFolder 上使用 @Rule

package demo;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit4.SpringRunner;

import static junit.framework.TestCase.assertEquals;
import static org.mockito.Mockito.when;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = FileRepositoryWithAbstractionApp.class)
public class FileRepositoryWithAbstractionTest {

    @Rule
    public TemporaryFolder temporaryFolder = new TemporaryFolder();

    @MockBean
    private FileRepositoryWithAbstractionApp.FileRepositorySource fileRepositorySource;

    @Autowired
    private FileRepositoryWithAbstractionApp.FileRepository bean;

    @Before
    public void setUp() {
        when(fileRepositorySource.getBasePath()).thenReturn(temporaryFolder.getRoot());
    }

    @Test
    public void method() {
        System.out.println(temporaryFolder.getRoot().getAbsolutePath());
        System.out.println(bean.getBasePath());
        assertEquals(temporaryFolder.getRoot(), bean.getBasePath());
    }

    @Test
    public void method1() {
        System.out.println(temporaryFolder.getRoot().getAbsolutePath());
        System.out.println(bean.getBasePath());
        assertEquals(temporaryFolder.getRoot(), bean.getBasePath());
    }

}

方法 4:TestPropertySource

使用 Spring 的 TestPropertySource 注释选择性地覆盖测试中的属性。因为 Java 注释不能有动态值,所以您需要事先决定要在何处创建目录,并记住由于使用了 os 路径,您的测试绑定到特定操作系统分隔符。

package demo;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

import static demo.FileRepositoryTestPropertySourceTest.BASE_PATH;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = FileRepositoryApp.class)
@TestPropertySource(properties = "acme.fileRepository.basePath=" + BASE_PATH)
public class FileRepositoryTestPropertySourceTest {

    static final String BASE_PATH = "/tmp/junit-base-path";

    private Path basePath = Paths.get(BASE_PATH);;

    @Autowired
    private FileRepositoryApp.FileRepository fileRepository;

    @Before
    public void setUp() throws IOException {
        Files.deleteIfExists(basePath);
        Files.createDirectories(basePath);
    }

    @After
    public void after() throws IOException {
        Files.deleteIfExists(basePath);
    }

    @Test
    public void method() {
        System.out.println(fileRepository.getBasePath());
    }
}

如果您使用 JUnit 5.4+,那么您可以利用它们的 @TempDir,无需手动对目录进行生命周期管理即可正常工作。也就是说,您不需要手动创建和删除它,这与 JUnit 4 中的 @TemporaryFolder 形成对比。

以下是您如何实现目标的工作示例:

//Your bean into which you want to inject the property
@Component
public class FileRepositoryManager {
    @Value("${acme.fileRepository.basePath}")
    private File basePath;

    public File getBasePath() {
        return basePath;
    }
}

//Test that uses ApplicationContextInitializer machinery to set the desired properties
@SpringBootTest
@ContextConfiguration(initializers = Initializer.class)
class FileRepositoryManagerTest {
    @TempDir
    static File tempDir;

    @Autowired
    FileRepositoryManager fileRepositoryManager;

    @Test
    void basePathIsSet() {
        assertNotNull(fileRepositoryManager.getBasePath());
    }

    static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
        @Override
        public void initialize(ConfigurableApplicationContext context) {
            TestPropertyValues.of(
                    "acme.fileRepository.basePath=" + tempDir
            ).applyTo(context);
        }
    }
}