问题描述
Java 8、JUnit 4 和 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% 的时间对我有用。但是,我现在正在尝试编写一些更像 integration 测试的单元测试,其中不是注入的模拟,我希望 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,etc.
}
}
我怎样才能做到这一点?
解决方法
您可以使用 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;
}
,
您不需要模拟任何东西,只需在您的测试中注入您的类,您的配置就会向您的 ThingDoerinator
类提供 bean,您的测试用例将像调用 {{1} } 来自某个控制器或其他服务的方法。
TL;DR
我认为,您对运行整个 spring 上下文的测试和单元测试感到困惑,它们只测试我们编写的函数/逻辑而不运行整个上下文。编写单元测试是为了测试函数片段,如果该函数在给定的输入集下按预期运行。
理想的单元测试是不需要模拟的,我们只传递输入并产生输出(纯函数)但在实际应用中通常情况并非如此,我们可能会在交互时发现自己处于这种情况与我们应用程序中的其他一些对象。由于我们不打算测试该对象交互,而是关注我们的功能,因此我们模拟了该对象行为。
看来,您对单元测试没有问题,因此您可以在测试类 ThingDoerinator. doStuff()
中模拟 bean 的 List
并将它们注入您的测试用例中,并且您的测试用例运行良好。
现在,你想用 ThingDoerinator
做同样的事情,所以我举一个假设的例子来证明你现在不需要模拟对象,因为 spring 上下文会有它们,当 {{1 }} 将加载整个上下文来运行您的单个测试用例。
假设我有一个服务 @SpringBootTest
并且它有一个方法 @springBootTest
FizzService
此处使用构造函数注入1
现在,我的 getFizzes()
将通过配置(类似于您的情况)创建,并被告知 spring 上下文根据需要注入它们。
@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);
}
}
暂时忽略@Profile 注解2
现在我将进行 spring boot 测试,以检查此服务是否会提供与我提供给上下文的 List<Fizzes
相同的列表,以及它何时将在生产环境中运行
@Profile("dev")
@Configuration
public class FizzConfig {
@Bean
public List<Fizz> allFizzes() {
return asList(Fizz.of("Batman"),Fizz.of("Aquaman"),Fizz.of("Wonder Woman"));
}
}
现在,当我运行测试时,我看到它有效,那么它是如何工作的,让我们了解一下。因此,当我使用 fizzes
注释运行测试时,它的运行方式与在生产环境中使用嵌入式服务器运行时类似。
所以,我之前创建的配置类将被扫描,然后在那里创建的 bean 将被加载到上下文中,我的服务类也用 @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");
}
}
注释进行了注释,这样它也将被加载和 spring将识别它需要 @SpringBootTest
然后它会查看它的上下文,如果它有那个 bean,那么它就会找到并注入它,因为我们是从我们的配置类提供它。
在我的测试中,我自动连接服务类,所以 spring 将注入它拥有的 @Service
对象,它也会有我的 List<Fizz>
(在前面的段落中解释)。
所以,你不必做任何事情,如果你定义了你的配置并且那些正在加载并在生产中工作,那些应该在你的 spring 引导测试中以相同的方式工作,除非你有一些自定义设置而不是默认 spring启动应用程序。
现在,让我们看一个案例,当您可能希望在测试中使用不同的 FizzService
时,在这种情况下,您将不得不排除生产配置并包含一些测试配置。
在我的示例中,我将在我的测试文件夹中创建另一个 List<Fizz>
并使用 List<Fizz>
注释它
FizzConfig
我会稍微改变我的测试
@TestConfiguration
使用不同的配置文件进行测试,因此使用@ActiveProfile 注释,否则默认配置文件集是 @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");
}
}
类,因为它会扫描所有包( 2记住上面的 dev
注释,这将确保之前的配置只在 production/dev env 中运行)。这是我的 application.yml 以使其正常工作。
FizzConfig
另外,请注意,我在此处导入带有 @Profile
注释的 spring:
profiles:
active: dev
---
spring:
profiles: test
,您也可以使用 FizzConfiguration
执行相同的操作。
因此,您可以看到测试用例的值与生产代码中的值不同。
编辑
如注释所述,由于您不希望在此测试中测试连接到数据库,因此您必须禁用 spring 数据 JPA 的自动配置,默认情况下,spring boot 会这样做。
3您可以创建另一个配置类,如
@Import
然后将其包含在您的 @SpringBootTest(classes = FizzConfig.class)
中,这将禁用默认数据库设置,然后您将面临最后一个难题,即您可能在课堂上进行的任何存储库交互。
因为现在,您的测试用例中没有自动配置,这些存储库只是接口,您可以在测试类中轻松模拟
@Configuration
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class,DataSourceTransactionManagerAutoConfiguration.class,HibernateJpaAutoConfiguration.class})
public TestDataSourceConfiguration {}
1在生产代码中建议使用构造函数注入而不是`@Autowired`,如果你的依赖项确实需要该类工作,所以请尽可能避免它。
3请注意,您只能在测试包中创建此类配置或使用一些配置文件进行标记,请勿在您的生产包中创建它,否则它将禁用您运行代码的数据库。