I'm using Spring MVC Framework and I want to test my Controllers with JUnit. How should I mock POST or GET parameters of a Controller and how will I access the Model's attributes to check its content? My Controller's signature is the following:
#RequestMapping(value="/findings", method=RequestMethod.POST)
public String findUsers(#RequestParam("userInput") String userInput, Model m)
You can use spring-test and mockito alongside junit to accomplish the task.
spring-test enables you to test controllers and a whole bunch of other things in spring
mockito is a great library for creating mocked classes
This is a very high level overview of unit testing a controller. This may not be correct for your situation, but should give you a bit of a starting point.
public class SomeControllerTest {
private SomeController controller;
#Mock
private View view;
private MockMvc mockMvc;
#Before
public void setup(){
MockitoAnnotations.initMocks(this);
mockMvc = MockMvcBuilders
.standaloneSetup(controller)
.setSingleView(view)
.build();
}
#Test
public void test() throws Exception {
ObjectMapper mapper = new ObjectMapper();
Model m = new Model();
MvcResult mvcResult = mockMvc.perform(post("/findings")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(m)))
.andExpect(status().isOK())
.andReturn();
}
}
For doing that I would recomend you a simple test where you instanciate your Controller (you can mock all the dependencies) and after that you call your method pasing Model.
public class MyController (){
MyDependencyOne one;
MyDependencyTwo two ;
#Autowired
public MyController (MyDependencyOne one, MyDependencyTwo two){
this.one = one;
this.two = two;
}
public String findUsers(#RequestParam("userInput") String userInput, Model
m){
// do whatever
}
}
public class MyControllerTest (){
#Test
public void myTest(){
//MOCK your dependencies
MyController controller = new MyController(one, two);
Model model = new ExtendedModelMap()
controller.filter(model);
assertEquals("yourAtribute", model.asMap().get("yourAtribute");
}
}
Related
I would like to create Test for my rest controller:
#Controller
#RequestMapping("/v2/api/show/project")
public class ApiAccessController {
private final ApiAccessService apiAccessService;
#Autowired
ApiAccessController(ApiAccessService apiAccessService){
this.apiAccessService = apiAccessService;
}
#PutMapping(value = "/{id}/apikey")
public ResponseEntity<ApiKeyResponse> generateApiKey(#PathVariable("id")Long id, Principal principal) {
return apiAccessService.generateApiKey(id, principal.getName());
}
}
My test looks as follow:
#RunWith(SpringJUnit4ClassRunner.class)
public class ApiAccessControllerTest {
private MockMvc mockMvc;
Principal principal = new Principal() {
#Override
public String getName() {
return "TEST_PRINCIPAL";
}
};
#InjectMocks
ApiAccessController apiAccessController;
#Mock
ProjectRepository projectRepository;
#Before
public void setUp(){
mockMvc = MockMvcBuilders.standaloneSetup(apiAccessController).build();
}
#Test
public void testGenerateApiKey() throws Exception {
Project project = new Project();
project.setId((long) 1);
project.setName("test");
project.setDescription("testdesc");
project.setCiid("ciid");
when(projectRepository.save(any(Project.class))).thenReturn(project);
mockMvc.perform(MockMvcRequestBuilders.put("/v2/api/show/project/" + project.getId() +"/apikey").principal(principal))
.andExpect(MockMvcResultMatchers.status().isOk());
}
}
Which is ment to create project and then run generateApiKey on this project, however I get NullpointerException looking like mocked controller cannot find created entity
could anyone please point me in the right direction as I am just starting with testing?
You should mock ApiAccessService instead of ProjectRepository.
Have a look at the code:
#RunWith(SpringJUnit4ClassRunner.class)
public class ApiAccessControllerTest {
private MockMvc mockMvc;
private Principal principal = () -> "TEST_PRINCIPAL";
#InjectMocks
private ApiAccessController apiAccessController;
#Mock
private ApiAccessService apiAccessService;
#Before
public void setUp() {
mockMvc = MockMvcBuilders.standaloneSetup(apiAccessController).build();
}
#Test
public void testGenerateApiKey() throws Exception {
long id = 1L;
when(apiAccessService.generateApiKey(id, principal.getName())).thenReturn(new ApiKeyResponse(111L));
mockMvc.perform(MockMvcRequestBuilders.put("/v2/api/show/project/{id}/apikey", id).principal(principal))
.andExpect(MockMvcResultMatchers.status().isOk());
}
}
If you want to create integration test, that tests ApiAccessController -> ApiAccessService -> ProjectRepository integration you need to load your context (use for example #SpringBootTest).
Also you need to fix controller, use ResponseEntity.ok(...) :
#PutMapping(value = "/{id}/apikey")
public ResponseEntity<ApiKeyResponse> generateApiKey(#PathVariable("id") Long id, Principal principal) {
return ResponseEntity.ok(apiAccessService.generateApiKey(id, principal.getName()));
}
You can find really good examples of all test types in this repository MVC tests examples
The Mock you are creating is not referenced in the Controller. The Service you reference in the Controller is not part of your test setup. Therefore any access to the Service will cause a NullPointerException as the Service is not set.
I'm currently providing coverage - testing the validation of my DTO through MockMVC request call.
I recently introduced a new field in my Registration ConstraintValidator, supportedSpecializations, of which I inject the values from application.properties for easy maintenance and expandability. see code fragment below:
#Component
public class RegistrationValidator implements ConstraintValidator<Registration, String> {
//campus.students.supportedspecializations="J2E,.NET,OracleDB,MySQL,Angular"
#Value("${campus.students.supportedspecializations}")
private String supportedSpecializations;
private String specializationExceptionMessage;
//All ExceptionMessages are maintained in a separate class
#Override
public void initialize(Registration constraintAnnotation) {
exceptionMessage = constraintAnnotation.regionException().getMessage();
}
#Override
public boolean isValid(RegistrationData regData, ConstraintValidatorContext context) {
String[] specializations = supportedSpecializations.split(",");
boolean isValidSpecialization = Arrays.stream(specializations)
.anyMatch(spec -> spec.equalsIgnoreCase(regData.getSpec()));
if (!isValidSpecialization){
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(specializationExceptionMessage)
.addConstraintViolation();
return false;
}
//additional validation logic...
return true;
}
}
Unit tests now fail due to the field not being injected by the defined property of the #Value annotation.
I'm not sure if ReflectionTestUtils could help in my case, so any suggestions are greatly appreciated about how to inject the required values in UnitTests.
Spring version is 2.1.0
I'm currently testing with the following snippet:
#InjectMocks
private StudentController mockRestController;
#Mock
private StudentService mockStudentService;
#Mock
private ValidationExceptionTranslator mockExceptionTranslator;
#Value("${campus.students.supportedspecializations}")
private String supportedSpecializations;
private MockMvc mockMvc;
private static final String VALIDATION_SUCCESSFUL = "success";
private static final String VALIDATION_FAILED = "failed";
#Before
public void setup() {
MockitoAnnotations.initMocks(this);
this.mockMvc = MockMvcBuilders.standaloneSetup(mockRestController).build();
doReturn(
ResponseEntity.status(HttpStatus.OK)
.header("Content-Type", "text/html; charset=utf-8")
.body(VALIDATION_SUCCESSFUL))
.when(mockStudentService).insertStudent(Mockito.any());
doReturn(
ResponseEntity.status(HttpStatus.BAD_REQUEST)
.header("Content-Type", "application/json")
.body(VALIDATION_FAILED))
.when(mockExceptionTranslator).translate(Mockito.any());
}
#Test
public void testValidation_UnsupportedSpecialization() throws Exception {
MvcResult mvcResult = mockMvc.perform(
post("/Students").contentType(MediaType.APPLICATION_JSON_UTF8).content(
"{\"registrationData\":{\"spec\":\"unsupported\"}}"))
.andExpect(status().isBadRequest())
.andReturn();
assertEquals(VALIDATION_FAILED, mvcResult.getResponse().getContentAsString());
verify(mockExceptionTranslator, times(1)).translate(Mockito.any());
verify(mockStudentService, times(0)).insertStudent(Mockito.any());
}
I tried annotating my Test class with #RunWith(SpringRunner.class) and #SpringBootTest(classes = Application.class), but the validation test still fails due to #Value not being resolved. I might be wrong but I think the ConstraintValidator's instance is created before we reach the restController, so a MockMVC perform(...) call couldn't simply just make sure the appropriate #Value in the validator gets injected into supportedSpecializations.
Solved the issue the following way:
Added the following annotations to the Test Class
#RunWith(SpringRunner.class)
#SpringBootTest(classes = Application.class)
#AutoConfigureMockMvc
Then autowired the controller and mockMVC, finally annotated service and translator with Spring's #MockBean
So currently it looks something like this:
#RunWith(SpringRunner.class)
#SpringBootTest(classes = Application.class)
#AutoConfigureMockMvc
public class StudentValidatorTest {
#Autowired
private StudentController mockRestController;
#MockBean
private StudentService mockStudentService;
#MockBean
private ValidationExceptionTranslator mockExceptionTranslator;
#Autowired
private MockMvc mockMvc;
private static final String VALIDATION_SUCCESSFUL = "success";
private static final String VALIDATION_FAILED = "failed";
#Before
public void setup() {
MockitoAnnotations.initMocks(this);
this.mockMvc = MockMvcBuilders.standaloneSetup(mockRestController).build();
doReturn(
ResponseEntity.status(HttpStatus.OK)
.header("Content-Type", "text/html; charset=utf-8")
.body(VALIDATION_SUCCESSFUL))
.when(mockStudentService).insertStudent(Mockito.any());
doReturn(
ResponseEntity.status(HttpStatus.BAD_REQUEST)
.header("Content-Type", "application/json")
.body(VALIDATION_FAILED))
.when(mockExceptionTranslator).translate(Mockito.any());
}
//...and tests...
Yes,
use ReflectionTestUtil.
Use ReflectionTestUtil.setField to set the value of supportedSpecializationsin the
setup() method (junit < 1.4)
or in the #Before annotated method (junit > 1.4) in your unit test.
More Details
I recommend against using MockMVC for your unit tests;
it is fine for integration tests,
just not unit tests.
There is no need to start Spring for a Unit Test;
you never need Spring to perform injections for your unit tests.
Instead,
instantiate the class you are testing and call the methods directly.
Here is a simple example:
public class TestRegistrationValidator
{
private static final String VALUE_EXCEPTION_MESSAGE = "VALUE_EXCEPTION_MESSAGE";
private static final String VALUE_SUPPORTED_SPECIALIZATIONS = "BLAMMY,KAPOW";
private RegistrationValidator classToTest;
#Mock
private Registration mockRegistration;
#Mock
private RegionExceptionType mockRegionExceptionType; // use the actual type of regionExcpeption.
#Before
public void preTestSetup()
{
MockitoAnnotations.initMocks(this);
ReflectionTestUtils.setField(classToTest, "supportedSpecializations", VALUE_SUPPORTED_SPECIALIZATIONS);
doReturn(VALUE_EXCEPTION_MESSAGE).when(mockRegionExceptionType).getMessage();
doReturn(mockRegionExceptionType).when(mockRegion).regionException();
}
#Test
public void initialize_allGood_success()
{
classToTest.initialize(mockRegistration);
...assert some stuff.
...perhaps verify some stuff.
}
}
The best option i think for you is to use constructor injection in your RegistrationValidator.class , so that you can assign mock or test values directly for testing as well when required. Example :
#Component
class ExampleClass {
final String text
// Use #Autowired to get #Value to work.
#Autowired
ExampleClass(
// Refer to configuration property
// app.message.text to set value for
// constructor argument message.
#Value('${app.message.text}') final String text) {
this.text = text
}
}
This way you can set your mock values to the variables for unit testing.
Yes, you are right a custom constructor is not an option here, then you could introduce a configuration class where you have these values read from yml or property and autowire that in the validator .That should work for you.
Or
You can provide the #Value properties in a separate test.yml or test.properties and specify that to be taken up while running your integrated tests. In that case you should be able to resolve these values.
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = ExampleApplication.class)
#TestPropertySource(locations="classpath:test.properties")
public class ExampleApplicationTests {
}
The #TestPropertySource annotation has higher precedence order and it should resolve your values.
I have a controller which adds attributes from application.properties to a Model object:
#Value("${products}")
private String prod;
#GetMapping("/")
public String greetingForm(Model model) throws IOException {
List<String> products = Arrays.asList(prod.split("\\s*,\\s*"));
model.addAttribute("products",products);
return "form";
}
How can I test this method? I'm fairly new to unit testing so any advice would be appreciated. I know I have to mock Model somehow but every time I try to run my test, I get a NullPointerException.
My test:
private MockMvc mockMvc;
#MockBean
private Model model;
#Before
public void setUp() {
this.mockMvc = MockMvcBuilders.standaloneSetup(new WebController()).build();
}
#Test
public void testHomeRoute() {
try {
List<String> products = new ArrayList<String>();
products.add("Product1");
products.add("Product2");
mockMvc.perform(get("/"))
.andExpect(MockMvcResultMatchers.view().name("form"))
.andExpect(MockMvcResultMatchers.model().attribute("products",products));
;
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
Thanks in advance.
You need to make sure that a copy of application.properties is in your src/test/resources folder to make sure unit tests pick up these properties.
Also make sure your test class is annotated with #SpringBootTest so that the relevant configuration for loading properties is loaded into your tests.
EDIT: You also don't want to call new on your Controller here:
this.mockMvc = MockMvcBuilders.standaloneSetup(new WebController()).build();
By instatiating it yourself it is no longer a Spring bean and the #Value annotation will not be correctly injected into this class for you. Instead you should autowire your Controller in and use this to build your MockMvc.
#Autowired
private WebController webController;
#Before
public void setUp() {
this.mockMvc = MockMvcBuilders.standaloneSetup(webController).build();
}
I'm using SpringBoot 2 and Spring 5 (RC1) to expose reactive REST services. but I can't manage to write unit test for those controllers.
Here is my controller
#Api
#RestController
#RequestMapping("/")
public class MyController {
#Autowired
private MyService myService;
#RequestMapping(path = "/", method = RequestMethod.GET)
public Flux<MyModel> getPages(#RequestParam(value = "id", required = false) String id,
#RequestParam(value = "name", required = false) String name) throws Exception {
return myService.getMyModels(id, name);
}
}
myService is calling a database so I would like not to call the real one. (I don't wan't integration testing)
Edit :
I found a way that could match my need but I can't make it work :
#Before
public void setup() {
client = WebTestClient.bindToController(MyController.class).build();
}
#Test
public void getPages() throws Exception {
client.get().uri("/").exchange().expectStatus().isOk();
}
But I'm getting 404, seems it can't find my controller
You have to pass actual controller instance to bindToController method.
As you want to test mock environment, you'll need to mock your dependencies, for example using Mockito.
public class MyControllerReactiveTest {
private WebTestClient client;
#Before
public void setup() {
client = WebTestClient
.bindToController(new MyController(new MyService()))
.build();
}
#Test
public void getPages() throws Exception {
client.get()
.uri("/")
.exchange()
.expectStatus().isOk();
}
}
More test examples you can find here.
Also, I suggest switching to constructor-based DI.
When I run unit test on the controller by mocking the service bean, it looks like the service method is not called at all. Is this expected behvaior or am I missing something?
SearchController.java
#Controller
public class SearchController {
#Autowired
SearchService searchService;
#RequestMapping(value="/search", method=RequestMethod.GET)
public String showSearchPage(Model model){
model.addAttribute("list", searchService.findAll());
return "search";
}
}
SearchControllerTest.java
#RunWith(SpringJUnit4ClassRunner.class)
#WebAppConfiguration
#ContextConfiguration("file:src/main/webapp/WEB-INF/springapp-servlet.xml")
public class SearchControllerTest {
#Autowired
private WebApplicationContext webAppContext;
private MockMvc mockMvc;
private SearchService searchServiceMock;
#Before
public void setUp(){
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webAppContext).build();
this.searchServiceMock = Mockito.mock(SearchServiceImpl.class);
}
#Test
public void testShowSearchPage() throws Exception{
when(searchServiceMock.findAll())
.thenReturn(Arrays.asList("abc", "acb", "123"));
this.mockMvc.perform(get("/search.do"))
.andExpect(status().isOk())
.andExpect(view().name("search"))
.andExpect(forwardedUrl("/WEB-INF/jsp/search.jsp"))
.andExpect(model().attribute("list", hasSize(3)));
verify(searchServiceMock, times(1)).findAll(); //this test is failing
verifyNoMoreInteractions(searchServiceMock);
}
}
When I run the test, it seems like findAll() method isn't getting called and it is throwing exception. "Wanted but not invoked searchServiceImpl.findAll()"
What mistake am I making here?
-------------Update------------------
SearchControllerTestNew.java
public class SearchControllerTestNew {
#InjectMocks
SearchController searchController;
#Mock
SearchService searchServiceMock;
#Mock
View mockView;
MockMvc mockMvc;
#Before
public void setUp(){
MockitoAnnotations.initMocks(this);
mockMvc = MockMvcBuilders.standaloneSetup(searchController).setSingleView(mockView)
.build();
}
#Test
public void testShowSearchPage() throws Exception{
when(searchServiceMock.findAll())
.thenReturn(Arrays.asList("abc", "acb", "123"));
this.mockMvc.perform(get("/search.do"))
.andExpect(status().isOk())
.andExpect(view().name("search"))
.andExpect(model().attribute("list", hasSize(3)))
.andExpect(forwardedUrl("/WEB-INF/jsp/search.jsp"));//this fails now
verify(searchServiceMock, times(1)).findAll();
verifyNoMoreInteractions(searchServiceMock);
}
}
Your SearchController will use the autowired SearchService (part of your application context) not your mock, notice you create a mock but you are not using it anywhere, instead you create a MockMvc based on your application context.
A solution would be, using a standalone setup which gives you full control over the instantiation and initialization of controllers and their dependencies:
#Before
public void setUp(){
this.searchServiceMock = Mockito.mock(SearchServiceImpl.class);
mockMvc = MockMvcBuilders.standaloneSetup(new SearchController(seachServiceMock))
.setViewResolvers(viewResolver())
.build();
}
private ViewResolver viewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/jsp/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
I've omitted some MVC infrastructure configuration in this example check the MockMvcBuilder documentation for further configuration.
Although you have mocked the SearchService it isn't injected to the controller (at least not evident from code you shared).
Thus the verify(searchServiceMock, times(1)).findAll(); is bound to fail as it is indeed never invoked.
Take a look at #InjectMock how mocks are injected.
Also you need to add below LOCs for whole thing to work
First introduce a field
#InjectMocks
private SearchController controller;
Second in setUp() add
MockitoAnnotations.initMocks(this);
this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
You can use #MockBean for your mocks, then they will be injected to Context automatically.
Doc: https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-testing-spring-boot-applications-mocking-beans