在 Spring Boot Test 中,如何将临时文件夹映射到配置属性?

usr*_*ΛΩΝ 7 junit spring-test temporary-files spring-boot

我想做自洁测试

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

public class FileRepositoryManagerImpl implements ....

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

}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

尝试了内部类失败:

@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");
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我在想修补Environment,但什么应该是在春季启动测试程序注入性质的正确方法是什么?

Dmi*_*tov 13

如果您使用 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);
        }
    }
}

Run Code Online (Sandbox Code Playgroud)


Kam*_*kol 5

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

方法一: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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

改变basePath每个测试之后ReflectionTestUtils.setField。因为我们使用的是 Spring 的TestExecutionListener,它在 Junit 规则初始化之前被初始化,我们被迫管理beforeTestExecutionand 中的临时文件夹afterTestMethod

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();
        }
    }
}



Run Code Online (Sandbox Code Playgroud)

方法 2:配置属性

为您的应用程序配置引入配置属性类。它免费为您提供类型安全,我们不再依赖反射。

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;
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

因为我们使用的是 Spring 的TestExecutionListener,它在 Junit 规则初始化之前被初始化,我们被迫管理beforeTestExecutionand 中的临时文件夹afterTestMethod

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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

方法 3:重构你的代码(我最喜欢的)

提取basePath到它自己的类并将其隐藏在 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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我们不再需要任何额外的测试设施,我们可以使用@RuleonTemporaryFolder代替。

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());
    }

}

Run Code Online (Sandbox Code Playgroud)

方法四: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());
    }
}
Run Code Online (Sandbox Code Playgroud)