Validating beans on service method during tests with Spring MockMvc - java

I have a Spring 5 web application which does validations only on service level DTOs. That is to say, the incoming request is not validated in controller, but the DTO going into a service layer method is validated on that level. This is a requirement because this is how this is done on other applications and other people want to keep it like that for consistency.
I have tests that use Spring MockMvc to test the controller methods. I'd like to test the error response on validation errors but when I run my tests, bean validation is not done. It works fine when I build and deploy the app, though. Validation also works for tests if I put the validation annotations on the class representing the incoming request and add #Valid to the controller method argument corresponding to the request. It just doesn't work when its done on the service layer.
So my question is is this even supposed to work with MockMvc, that is to say, should validation occur normally when the controller calls the service method? Or is there some configuration I need to have in place for this to work during tests? I doubt it since validation works if I try adding it to controller level...
Here is my test class setup:
#ContextConfiguration(classes = {MyTestConfig.class})
#WebAppConfiguration
#RunWith(SpringJUnit4ClassRunner.class)
public class MyTest {
private MockMvc mvc;
#Autowired
private WebApplicationContext wac;
#Before
public void setup() throws Exception {
this.mvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
}
The MyTestConfig class just has #EnableWebMvc annotation, base packages for scanning and some unrelated bean definitions.
My DTO has #NotNull annotation on a property, the service method has a #Validated annotation on the class and a #Valid annotation on the method parameter which is the DTO.
With these settings, when I make a request in a test to the controller endpoint using the MockMvc instance mvc above, validation does not occur on the service method.
So is this even supposed to work?

Related

WebApplicationContext is required - MockMVC

I'm trying to unit test a controller class, but have been stuck with the following error. I tried changing notations and following some online tutorials but it has not been working, I always get this same error.
Here's the stackTrace:
java.lang.IllegalArgumentException: WebApplicationContext is required
at org.springframework.util.Assert.notNull(Assert.java:201)
at org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder.<init>(DefaultMockMvcBuilder.java:52)
at org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup(MockMvcBuilders.java:51)
at br.com.gwcloud.smartplace.catalog.controller.test.ItemControllerTest.setUp(ItemControllerTest.java:66)
...
And this is my controller test class:
#SpringBootTest
#WebMvcTest(controllers = ItemController.class)
#ActiveProfiles("test")
#WebAppConfiguration
public class ItemControllerTest {
#MockBean
private ItemRepository ir;
#Autowired
private MockMvc mockMvc;
#Autowired
private ModelMapper modelMapper;
#Autowired
private WebApplicationContext webApplicationContext;
#Autowired
private ObjectMapper objectMapper;
#Before
public void setUp() {
this.mockMvc = webAppContextSetup(webApplicationContext).build();
DefaultMockMvcBuilder builder = MockMvcBuilders.webAppContextSetup(this.webApplicationContext);
this.mockMvc = builder.build();
}
#Test
public void shouldCreateNewItem() throws Exception {
ItemDTO itemDTO = new ItemDTO();
itemDTO.setName("Leo");
itemDTO.setDescription("abc description");
itemDTO.setEnabled(true);
itemDTO.setPartNumber("leo123");
Item item = itemDTO.convertToEntity(modelMapper);
mockMvc.perform(
post("/api/item/").contentType(
MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(item))).andExpect(
status().isOk());
}
}
The error you are encountering might be solved by adding:
#RunWith(SpringRunner.class)
Just below #SpringBootTest. Or you could instead extend AbstractJUnit4SpringContextTests.
Another problem is that the WebApplicationContext might not be available in an #Before annotated method. Try moving it into the test method itself.
That said, I usually avoid unit testing controllers, since I don't put any business logic in them. The stuff controllers do is specifying paths, mapping request arguments, error handling (although that is better handled in a separate ControllerAdvice class), setting up the view model and view target etc. These are all what I call 'plumbing' and tie in heavily with the framework you are using. I don't unit test that.
Instead, this plumbing can be validated by having a couple of high level integration tests that actually do a remote call to the controller and execute a complete flow, including all the plumbing.
Any business logic should be taken outside of the controller (typically in services) and unit tested there, in isolation.
Have you tried removing #SpringBootTest and #WebAppConfiguration. If you are only interested in testing controller you don't need a full-blown application through these annotations.

Rollback Transactions when testing with Spring MockMvc

I have seen multiple threads about it. However, no one really solves my problem.
I have a Spring Boot 2.3 application with the three traditional layers: Controller, Service and DAO. The transactions are declared in my Service layer.
I would like to test my Controller layer using MockMvc, and I want the transactions to rollback at the end of the tests so that they all remain independent. However, I don't want the tests to give the Controller classes an access to the transactional context in order to have the same configuration as in runtime.
I created the following class:
#SpringBootTest
#AutoConfigureMockMvc
public class ApiIT {
#Autowired
private MockMvc mvc;
#Test
void restEndpointTest() {
...
This configuration doesn't rollback the transactions at the end of the tests.
When I annotate the class with #Transactional, they rollback, but the Controller classes can access the transactional context.

How to manage spring hierarchy during unit/integration tests?

I am working on a Spring-enabled embedded Tomcat application using annotation-based configuration. The application uses Spring MVC Controllers for its REST endpoints. As a separation of concerns, and to avoid having duplicate beans in separate contexts, the parent context contains all beans that are not REST endpoints, and the Spring Web MVC context contains all beans that are REST endpoints.
I want to write new and refactor old integration tests for these endpoints that are representative of the structure of the app. There are existing test classes like so:
import com.stuff.web.MyEndpoint;
#Configuration
#ComponentScan(basePackages = {"com.stuff"})
public class SpringConfig { ... }
#WebAppConfiguration
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(classes = {SpringConfig.class})
public class TestMyEndpoint {
#Autowired
private MyEndpoint myEndpoint;
private MockMvc mockMvc;
#Before
public void setUp() {
mockMvc = MockMvcBuilders
.standaloneSetup(myEndpoint)
.build();
}
#Test
public void testMyEndpoint() throws Exception {
mockMvc.perform(get("/myendpoint")
.accept(MediaType.APPLICATION_JSON_VALUE))
.andExpect(status().isOk())
.andReturn();
}
}
The problem is that the context that I am using for this test now has every bean loaded, whereas I would like to ensure that there are not non-REST beans loaded that call into RestController beans during the execution of the tests.
Adding something like
#Configuration
#ComponentScan(basePackages = {"com.stuff"},
excludeFilters = {
#Filter(type = FilterType.REGEX, pattern = "com.stuff.web.*")})
public class SpringConfig { ... }
Would ensure the kind of separation I'm going for, but then I don't have access to the com.stuff.web.MyEndpoint class that I'm trying to test.
Am I missing something easy? Let me know if I'm explaining the situation clearly.
The kind of separation you're describing (mvc vs non-mvc) made sense 10 years ago, not anymore. Separate your code by functionality/design patterns (web/service/repository etc), and have #Configuration classes specific to that layer. The Spring stereotype annotations are good enough hint how your app should be broken up. Then, put your tests in the same package as your target code, and mock/override any dependencies.
It doesn't appear you're using Spring Boot (you really should) but they have a great section in the docs for testing "slices" of your application.
https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-testing.html#boot-features-testing-spring-boot-applications-testing-autoconfigured-tests

Is it possible to test Spring REST Controllers without #DirtiesContext?

I'm working on a Spring-Boot web application. The usual way of writing integration tests is:
#Test
#Transactional
#Rollback(true)
public void myTest() {
// ...
}
This works nicely as long as only one thread does the work. #Rollback cannot work if there are multiple threads.
However, when testing a #RestController class using the Spring REST templates, there are always multiple threads (by design):
the test thread which acts as the client and runs the REST template
the server thread which receives and processes the request
So you can't use #Rollback in a REST test. The question is: what do you use instead to make tests repeatable and have them play nicely in a test suite?
#DirtiesContext works, but is a bad option because restarting the Spring application context after each REST test method makes the suite really slow to execute; each test takes a couple of milliseconds to run, but restarting the context takes several seconds.
First of all, testing a controller using a Spring context is no unit test. You should consider writing a unit test for the controller by using mocks for the dependencies and creating a standalone mock MVC:
public class MyControllerTest {
#InjectMocks
private MyController tested;
// add #Mock annotated members for all dependencies used by the controller here
private MockMvc mvc;
// add your tests here using mvc.perform()
#Test
public void getHealthReturnsStatusAsJson() throws Exception {
mvc.perform(get("/health"))
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.status", is("OK")));
}
#Before
public void createControllerWithMocks() {
MockitoAnnotations.initMocks(this);
MockMvcBuilders.standaloneSetup(controller).build()
}
}
This even works if you use an external #ControllerAdvice for error handling etc by simply calling setControllerAdvice() on the MVC builder.
Such a test has no problems running in parallel and is much faster by no need to setup a Spring context at all.
The partial integration test you described is also useful to make sure the right wiring is used and all tested units work together as expected. But I would more go for a more general integration test including multiple/all endpoints checking if they work in general (not checking the edge cases) and mocking only services reaching out to external (like internal REST clients, replacing the database by one in memory, ...). With this setup you start with a fresh database and maybe will not even need to rollback any transaction. This is of course most comfortable using a database migration framework like Liquibase which would setup your in memory db on the fly.
Below is my implement followed by #4
private MockMvc mockMvc;
#Mock
private LoginService loginService;
#Mock
private PersonalService personalService;
#InjectMocks
private LoginController loginController;
#BeforeEach
public void setUp() {
MockitoAnnotations.openMocks(this);
mockMvc = MockMvcBuilders.standaloneSetup(loginController).build();
}
#Test
void simple_login() throws Exception {
Mockito.when(loginService.login(Mockito.anyString(), Mockito.anyString()))
.thenReturn(UserAccessData.builder()
.accessToken("access_token_content")
.refreshToken("refresh_token_count")
.build());
mockMvc.perform(
MockMvcRequestBuilders.post("/login/simple")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.param("accountName", "1234561")
.param("password", "e10adc3949ba59abbe56e057f20f883e")
)
.andDo(print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(jsonPath("$.tokens.accessToken", is("access_token_content")))
.andExpect(jsonPath("$.tokens.refreshToken", is("refresh_token_count")))
;
}

Unit testing controllers with annotations

I'm trying to write some unit tests for my controllers in a Spring MVC web app. I have already got a fairly comprehensive set of unit tests for the domain model, but for completeness I want to test the controllers too.
The issue I'm facing is trying to test them without loading a Spring context. I thought I could get around this with mocking, but the method in the controller has a #Transactional annotation which attempts to open a transaction (and fails with a NullPointerException because no Spring context is loaded).
Code example:
public class UsersController {
#Autowired private UserManager userManager;
#Transactional
#RequestMapping(method = RequestMethod.POST)
public ModelAndView create(User user) {
userManager.save(user);
ModalAndView mav = new ModelAndView();
mav.addObject("user", user);
mav.setViewName("users/view");
return mav;
}
}
So essentially I want to test the behaviour without loading a context and actually persisting the user.
Does anyone have any ideas on how I can achieve this?
Cheers,
Caps
I'd say mocking is the way to go here. The #Transactional annotation will have no effect unless there is a Spring context loaded and instructed to configure annotation-based transactions.
Make sure that you aren't instructing JUnit to run your test within a spring context by specifying something like:
#ContextConfiguration(locations = "classpath:spring/ITestAssembly.xml")
#RunWith(SpringJUnit4ClassRunner.class)
To prevent confusion, I keep my unit tests (not running in a spring context) in separate files than my integration tests. Typically all mocking occurs in the unit tests and not in integration tests.
The NullPointerException occurs not because of the Transactional, but because nothing gets injectedas UserManager. You have two options:
run with the spring test runner
mock the userManager and set it.

Categories