Reputation: 477
My goal is to migrate a Spring Boot application previously developed with Spring Boot 1.3 to the newest Spring Boot version 1.4. The application consists of several maven modules and only one of them contains class annotated with @SpringBootApplication
.
One part of migration is to use @WebMvcTest
annotation to efficiently test controllers, and here I get an issue.
Consider an example application from Spring Boot github page. @WebMvcTest
annotation works perfectly, because, as far as I understand (after I did several tests), there is a class in the main package annotated with @SpringBootApplication
. Note that I follow the same concept as shown in the example above for my own @WebMvcTest
tests.
The only difference I see that in my application, controller classes are located in a separate maven module (without @SpringBootApplication
annotated class), but with @Configuration and SpringBootConfiguration
configurations. If I do not annotate any class with @SpringBootApplication
I always get an assertion while testing controller. My assertion is the same as when SampleTestApplication class in the example above modified to have only @EnableAutoConfiguration
and @SpringBootConfiguration
annotations (@SpringBootApplication
is not present):
getVehicleWhenRequestingTextShouldReturnMakeAndModel(sample.test.web.UserVehicleControllerTests) Time elapsed: 0.013 sec <<< FAILURE!
java.lang.AssertionError: Status expected:<200> but was:<404>
at org.springframework.test.util.AssertionErrors.fail(AssertionErrors.java:54)
at org.springframework.test.util.AssertionErrors.assertEquals(AssertionErrors.java:81)
at org.springframework.test.web.servlet.result.StatusResultMatchers$10.match(StatusResultMatchers.java:664)
at org.springframework.test.web.servlet.MockMvc$1.andExpect(MockMvc.java:171)
at sample.test.web.UserVehicleControllerTests.getVehicleWhenRequestingTextShouldReturnMakeAndModel(UserVehicleControllerTests.java:68)
How should I deal with that? Should I always have class annotated with @SpringBootApplication in order to run @WebMvcTest tests?
EDIT 1: I did a small maven project with 2 modules and a minimal configuration. It is here. Now, I get NoSuchBeanDefinitionException exception for repository defined in another module. If I configure "full" @SpringBootApplication - everything is fine.
EDIT 2: I modified small test project from EDIT 1 to give an original issue. I was playing with different annotations and added @ComponentScan on configuration class, because I suspected that beans are not registered properly. However, I expect that only @Controller bean (defined in @WebMvcTest(...class)) shall be registered based on magic behind @WebMvcTest behaviour.
EDIT 3: Spring Boot project issue.
Upvotes: 13
Views: 9051
Reputation: 543
There is one more solution. You can not use @WebMvcTest, but configure MockMvc yourself through the builder
class TestControllerTest {
private MockMvc mvc;
@BeforeEach
public void setup() {
mvc = MockMvcBuilders.standaloneSetup(new TestController())
.build();
}
@Test
void test() throws Exception {
// When
var res = mvc.perform(MockMvcRequestBuilders.get("/test/test"));
// Then
res.andExpect(status().isOk());
}
}
But this solution may entail a number of other problems, such as problems with configurations, environment property injections, etc.
Upvotes: 0
Reputation: 17045
Short answer: I believe so.
Long answer:
I believe @WebMvcTest
needs to find the SpringBootApplication configuration since WebMvcTest
's sole purpose is to help simplify tests (SpringBootApplication
would rather try to load the whole world).
In your specific case, since you don't have any in your non-test packages, I believe it also finds SampleTestConfiguration which is annotated with @ScanPackages
and somehow loads every beans.
Add the following in src/main/java/sample/test
@SpringBootApplication
public class SampleTestConfiguration {
}
And change your test to this:
@RunWith(SpringRunner.class)
@WebMvcTest(MyController.class)
public class MyControllerTest {
@Autowired
private MockMvc mvc;
@MockBean
private MyService ms;
@Autowired
private ApplicationContext context;
@Test
public void getDataAndExpectOkStatus() throws Exception {
given(ms.execute("1")).willReturn(false);
mvc.perform(get("/1/data").accept(MediaType.APPLICATION_JSON_VALUE)).andExpect(status().isOk()).andExpect(content().string("false"));
}
@Test
public void testMyControllerInAppCtx() {
assertThat(context.getBean(MyController.class), is(not(nullValue())));
}
@Test
public void testNoMyAnotherControllerInAppCtx() {
try {
context.getBean(MyAnotherController.class);
fail("Bean exists");
} catch (BeansException e) {
// ok
}
}
}
@WebMvcTest
finds the SpringBootApplication
, then load only a limited number of beans (see documentation):
@WebMvcTest will auto-configure the Spring MVC infrastructure and limit scanned beans to @Controller, @ControllerAdvice, @JsonComponent, Filter, WebMvcConfigurer and HandlerMethodArgumentResolver. Regular @Component beans will not be scanned when using this annotation.
WebMvcTest
requires SpringBootApplication
: WebMvcTest
inherits many AutoConfiguration, so it needs SpringBoot to load them. Then it disables many other AutoConfiguration and your Controllers become easily testable.
The whole point of using WebMvcTest
is when you have a SpringBootApplication
and you wish to make it simpler to test by disabling all beans except Controllers. If you don't have SpringBootApplication, then why use WebMvcTest at all?
Upvotes: 6
Reputation: 397
Yes,according to the spring boot docs
The search algorithm works up from the package that contains the test until it finds a @SpringBootApplication or @SpringBootConfiguration annotated class. As long as you’ve structure your code in a sensible way your main configuration is usually found.
But after I started using @WebMvcTest
,spring boot still try to load other beans, finally TypeExcludeFilter
did the trick.
@RunWith(SpringRunner.class)
@WebMvcTest(controllers = {JzYsController.class} )
public class JzYsControllerTest {
private static final String REST_V4_JZYS = "/rest/v4/JzYs";
@Autowired
private MockMvc mockMvc;
@MockBean
private JzYsService service;
@Test
public void deleteYsByMlbh() throws Exception {
Mockito.when(service.deleteYsByMlbh(Mockito.anyString())).thenReturn(Optional.of(1));
mockMvc.perform(delete(REST_V4_JZYS + "?mbbh=861FA4B0E40F5C7FECAF09C150BF3B01"))
.andExpect(status().isNoContent());
}
@SpringBootConfiguration
@ComponentScan(excludeFilters = @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class))
public static class config{
}
}
Upvotes: 2
Reputation: 779
It's an old topic, but there is a solution which wasn't mentioned here.
You can create a class annotated with SpringBootApplication
just in your test sources. Then, you still have a nice, multi-module structure of your project, with just one "real" SpringBootApplication
.
Upvotes: 2