代码之家  ›  专栏  ›  技术社区  ›  hotmeatballsoup

将@Configuration类定义的非模拟注入到单元测试中

  •  0
  • hotmeatballsoup  · 技术社区  · 4 年前

    Java 8、JUnit4和Spring Boot 2.3。我有一种情况,我有一个 @Component -带注释的Spring Boot类 @Autowired 及其所有依赖项(bean在 @Configuration -带注释的配置类):

    @Configuration
    public class SomeConfig {
    
        @Bean
        public List<Fizz> fizzes() {
            Fizz fizz = new Fizz(/*complex logic here*/);
            return new Collections.singletonList(fizz);
        }
    
        @Bean
        public Buzz buzz() {
            return new Buzz(/*complex logic here*/);
        }
        
    }
    
    @Component
    public class ThingDoerinator {
    
        @Autowired
        private Lizz<Fizz> fizzes;
    
        @Autowired
        private Buzz buzz;
    
        public String doStuff() {
            // uses fizz and buzz extensively inside here...
        }
        
    }
    

    我可以很容易地编写一个单元测试,将所有这些依赖项作为模拟注入:

    public class ThingDoerinatorTest extends AbstractBaseTest {
    
        @Mock
        private List<Fizz> fizzes;
    
        @Mock
        private Buzz buzz;
    
        @InjectMocks
        private ThingDoerinator thingDoerinator;
    
        @Test
        public void should_do_all_the_stuff() {
    
            // given
            // TODO: specify fizzes/buzz mock behavior here
    
            // when
            String theThing = thingDoerinator.doStuff();
    
            // then
            // TODO: make some assertions, verify mock behavior, etc.
    
        }
    }
    

    80%的时间对我来说是有效的。然而,我现在正试图编写一些更像 整合 测试中,我希望bean像正常情况一样被实例化,并连接到 ThingDoerinator 类,就像他们在生产中一样:

    public class ThingDoerinatorTest extends AbstractBaseTest {
    
        @Mock
        private List<Fizz> fizzes;
    
        @Mock
        private Buzz buzz;
    
        @InjectMocks
        private ThingDoerinator thingDoerinator;
    
        @Test
        public void should_do_all_the_stuff() {
    
            // given
            // how do I grab the same Fizz and Buzz beans
            // that are defined in SomeConfig?
    
            // when -- now instead of mocks, fizzes and buzz are actually being called
            String theThing = thingDoerinator.doStuff();
    
            // then
            // TODO: make some assertions, verify mock behavior, etc.
    
        }
    }
    

    我怎样才能做到这一点?

    0 回复  |  直到 4 年前
        1
  •  2
  •   Akif Hadziabdic    4 年前

    您可以使用SpringBootTest。

    @SpringBootTest(classes = {Fizz.class, Buzz.class, ThingDoerinator.class})
    @RunWith(SpringRunner.class)
    public class ThingDoerinatorTest {
        @Autowired
        private Fizz fizz;
    
        @Autowired
        private Buzz buzz;
    
        @Autowired
        private ThingDoerinator thingDoerinator;
    }
    
        2
  •  2
  •   code_mechanic    4 年前

    你不需要模拟任何东西,只需在测试中注入你的类,你的配置就会为你提供bean ThingDoerinator 类和测试用例将像调用 ThingDoerinator. doStuff() 方法来自某个控制器或其他服务。

    TL;博士

    我认为,您会混淆运行整个spring上下文的测试和仅测试我们编写的函数/逻辑而不运行整个上下文的单元测试。编写单元测试是为了测试函数,如果该函数在给定的输入集下按预期运行。

    理想的单元测试是不需要模拟的,我们只需传递输入,它就会产生输出(纯函数),但在实际应用中,情况往往并非如此,当我们与应用程序中的其他对象交互时,我们可能会发现自己处于这种情况。由于我们不打算测试对象交互,而是关心我们的功能,因此我们模拟了对象行为。

    看来,你对单元测试没有问题,所以你可以嘲笑你的 List 你的测试课上有豆子 ThingDoerinator 并将它们注入到测试用例中,测试用例就工作得很好。

    现在,你想对…做同样的事情 @SpringBootTest ,所以我举了一个假设的例子来证明,当 @springBootTest 将加载整个上下文以运行单个测试用例。

    假设我有一项服务 FizzService 它有一种方法 getFizzes()

    @Service
    public class FizzService {
        private final List<Fizz> fizzes;
    
        public FizzService(List<Fizz> fizzes) {
            this.fizzes = fizzes;
        }
    
        public List<Fizz> getFizzes() {
            return Collections.unmodifiableList(fizzes);
        }
    }
    

    在这里使用构造函数注入 1.

    现在,我的 List<Fizzes 将通过配置(类似于您的情况)创建,并被告知根据需要向spring上下文注入它们。

    @Profile("dev")
    @Configuration
    public class FizzConfig {
    
        @Bean
        public List<Fizz> allFizzes() {
            return asList(Fizz.of("Batman"), Fizz.of("Aquaman"), Fizz.of("Wonder Woman"));
        }
    }
    

    暂时忽略@Profile注释 2.

    现在,我将进行春季启动测试,以检查此服务是否会给出相同的列表 fizzes 我向上下文提供了它何时投入生产

    @ActiveProfiles("test")
    @SpringBootTest
    class FizzServiceTest {
        @Autowired
        private FizzService service;
    
        @Test
        void shouldGiveAllTheFizzesInContext() {
            List<Fizz> fizzes = service.getFizzes();
    
            assertThat(fizzes).isNotNull().isNotEmpty().hasSize(3);
            assertThat(fizzes).extracting("name")
                              .contains("Wonder Woman", "Aquaman");
        }
    
    }
    

    现在,当我运行测试时,我看到它是有效的,那么它是如何工作的,让我们来了解一下。所以当我用 @SpringBootTest 注释,它将像在嵌入式服务器的生产环境中一样运行。

    因此,我之前创建的配置类将被扫描,然后在那里创建的bean将被加载到上下文中,我的服务类也将被注释为 @Service 注释,这样它也会被加载,spring会识别它需要什么 List<Fizz> 然后它会查看它的上下文,如果它有那个bean,它就会找到并注入它,因为我们是从配置类提供它的。

    在我的测试中,我正在自动布线服务类,所以spring会注入 Fizz服务 对象它有,它会有我的 列表<Fizz> 也可以(在前面的段落中解释)。

    因此,您不必做任何事情,如果您定义了配置,并且这些配置正在加载并在生产环境中工作,那么这些配置应该在您的春季引导测试中以相同的方式工作,除非您有一些比默认春季引导应用程序更自定义的设置。

    现在,让我们来看一个案例,当你可能想要不同的 列表<Fizz> 在测试中,在这种情况下,您必须排除生产配置,并包含一些测试配置。

    在我的例子中,我会创建另一个 FizzConfig 在我的测试文件夹中,并为其添加注释 @TestConfiguration

    @TestConfiguration
    public class FizzConfig {
        @Bean
        public List<Fizz> allFizzes() {
            return asList(Fizz.of("Flash"), Fizz.of("Mera"), Fizz.of("Superman"));
        }
    }
    

    我会稍微改变一下我的测试

    @ActiveProfiles("test")
    @SpringBootTest
    @Import(FizzConfig.class)
    class FizzServiceTest {
        @Autowired
        private FizzService service;
    
        @Test
        void shouldGiveAllTheFizzesInContext() {
            List<Fizz> fizzes = service.getFizzes();
            assertThat(fizzes).extracting("name")
                              .contains("Flash", "Superman");
        }
    
    }
    

    使用不同的配置文件进行测试,因此使用@ActiveProfile注释,否则默认配置文件集为 dev 这将给生产带来压力 FizzConfig 类,因为它会扫描所有包( 2. 记住 @Profile 上面的注释,这将确保早期的配置只在product/dev-env中运行)。这是我的应用程序。yml可以让它工作。

    spring:
      profiles:
        active: dev
    ---
    spring:
      profiles: test
    

    另外,请注意,我正在导入我的 FizzConfiguration 具有 @Import 在这里添加注释,您也可以使用 @SpringBootTest(classes = FizzConfig.class) .

    所以,您可以看到测试用例的值与生产代码中的值不同。

    编辑

    如前所述,由于您不希望测试在此测试中连接到数据库,因此您必须禁用spring数据JPA的自动配置,默认情况下spring boot会这样做。

    3. 您可以创建另一个配置类,如下所示

    @Configuration
    @EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class, HibernateJpaAutoConfiguration.class})
    public TestDataSourceConfiguration {}
    

    然后把它和你的 @SpringBootTest ,这将禁用默认数据库设置,然后您将剩下最后一块拼图,即您在类中可能进行的任何存储库交互。

    因为现在,您的测试用例中没有自动配置,这些存储库只是接口,您可以在测试类中轻松模拟它们

    @ActiveProfiles("test")
    @SpringBootTest
    @Import(FizzConfig.class, TestDataSourceConfiguration.class)
    class FizzServiceTest {
        @MockBean
        SomeRepository repository;
    
        @Autowired
        private FizzService service;
    
        @Test
        void shouldGiveAllTheFizzesInContext() {
            // Use Mockito to mock behaviour of repository
            Mockito.when(repository.findById(any(), any()).thenReturn(Optional.of(new SomeObject));
     
            List<Fizz> fizzes = service.getFizzes();
            assertThat(fizzes).extracting("name")
                              .contains("Flash", "Superman");
        }
    
    }
    

    1. 建议在生产代码中使用构造函数注入,而不是“@Autowired”,如果该类真的需要依赖关系才能工作,所以如果可能的话,请避免使用它。
    3. 请注意,您只能在测试包中创建此类配置或用一些配置文件标记,不要在生产包中创建,否则它将为您正在运行的代码禁用数据库。