I'm new to Spring MVC, and currently writing a @Controller class but none of the methods have business logic, let alone HTML files for the views under'/static/'. First, I wanted to see how I can Unit Test every method to make sure all end points responded 200/ok before I inserted the business logic, you know Test Driven Development. Then I had difficulty when unit testing @PostMapping annotation method that had a @ModelAttribute assigned to it. After my whole search yesterday, I put together code for someone to unit test such cases involving @PostMapping and @ModelAttribute where you need to update the parameter values of your model attribute on the 'post' method. I'm more than welcome positive feedback to make my tests better, just wanted to post this in cases someone else that's also new wanted to test and make sure that the new info will be saved after the post in the @ModelAttribute without needing a html/jsp file for views for standalone unit testing, look at @Controller2 and String updateQuoteRequest() method for reference, and the last test in class QuoteRequestManagementController_UnitTests for more details.
pom.xml file:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>spring-mvc-hotel-app</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>serving-web-content</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- Spring Boot Test Starter is Starter for testing Spring Boot applications
with libraries including JUnit, Hamcrest and Mockito. -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Model Attribute Class:
package com.corplithotel.eventsapp.domain;
//Create the Model Attribute class, and its class members
public class QuoteRequest {
String customer;
String age;
String budget;
String eventType;
String foodAllergies;
//getters and setters
public String getCustomer() {
return customer;
}
public void setCustomer(String customer) {
this.customer = customer;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
public String getBudget() {
return budget;
}
public void setBudget(String budget) {
this.budget = budget;
}
public String getEventType() {
return eventType;
}
public void setEventType(String eventType) {
this.eventType = eventType;
}
public String getFoodAllergies() {
return foodAllergies;
}
public void setFoodAllergies(String foodAllergies) {
this.foodAllergies = foodAllergies;
}
}
Main Class:
package com.corplithotel.eventsapp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CorpLitHotel {
public static void main(String[] args) {
SpringApplication.run(CorpLitHotel.class, args);
}
}
@Controller1
package com.corplithotel.eventsapp.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import com.corplithotel.eventsapp.domain.QuoteRequest;
//Step 1: * Create QuoteRequestController *
/*@Conroller annotation makes this class a controller, next we need to
* add 'handler mappings' to provide the controller some functionality.
* For Step 1, we won't add logic for @RequestMapping 'beginQuoteRequest()'
* & @Postrequest 'submitQuoteRequest()' methods, we will Mock the class
* and unit test in Step 2 for TDD examples:
*
*
*/
@Controller
public class QuoteRequestController {
/*@GetMapping annotation is a 'handler mapping' annotation.
* When a user comes to the page to fill out the Quote form, they
* first need to get the page. The return of the method will be a
* 'logical view name', which is just a string, and tends to correlate
* to some HTML, JSP or whatever file you're using for your View.
*
*/
@GetMapping("/newquote")
public String beginQuoteRequest(Model model) {
//Check Unit Test for logic
return "newQuote";
}//beginQuoteRequest()
/*@PosMapping annotation is another 'handler mapping' annotation.
* Once a user fills out the Quote form with their name and
* other event details, they may want to save or post that quote.
* We need to add a handler for the Post, and needs to be a separate
* method. Will be a separate page with a confirmation message to let
* the user know their Quote request has been received.
*/
@PostMapping("/newquote")
public String submitQuoteRequest(@ModelAttribute QuoteRequest formBean) {
//Check Unit Test for ideal logic
return "newQuoteConfirmation";
}//submitQuoteRequest()
}
Controller 1 Unit Tests:
package com.corplithotel.eventsapp.controller;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import com.corplithotel.eventsapp.domain.QuoteRequest;
/*Step 2 *Create Unit tests for QuoteRequestController*:
* this tests are assuming
*/
@ExtendWith(MockitoExtension.class)
@WebMvcTest( QuoteRequestController.class)
@TestInstance(Lifecycle.PER_CLASS)
public class QuoteRequestController_UnitTests {
@Mock
private WebApplicationContext wac;
@InjectMocks
private QuoteRequestController qrc;
private MockMvc qrcMockMvc;
@BeforeAll
public void setUp() {
qrcMockMvc = MockMvcBuilders.standaloneSetup(qrc).build();
}//BeforeAll
@Test
@DisplayName("testGetQuoteForm.. beginQuoteRequest().. Expected to pass..")
public void testGetQuoteForm() throws Exception {
//simulate getting a new form for the user to fill in (GET)
qrcMockMvc
.perform(get("/newquote"))
.andExpect(status().is(200))
.andReturn();
}//testGetQuoteForm()
@Test
@DisplayName("testPostQuoteForm().. submitQuoteRequest.. Expected to pass..")
public void testPostQuoteForm() throws Exception {
QuoteRequest aFormBean = new QuoteRequest();
qrcMockMvc
.perform(post("/newquote", aFormBean))
.andExpect(status().isOk())
.andReturn();
}//testGetQuoteForm()
}// QuoteRequestController_UnitTests
Result 1:
Junit Controller 1 Results
Controller 2:
package com.corplithotel.eventsapp.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.corplithotel.eventsapp.domain.QuoteRequest;
/*Step 3 *Creating QuoteRequestManagementController
* This is the controller that the sales team member
* uses to reply to a customer's request for an estimate.
* Sale's Team member can see all the incoming requests.
*
*Controller method's body for Step 3 will be empty, we will unit test
* every method of the Controller first in Step 4.
*/
@Controller
public class QuoteRequestManagementController {
/*
* We will be specifying, parameters, look for a parameter of
* a particular value; or looking for the absence of a parameter
*/
//Specifying: Sale's Team member can see all the incoming requests.
@GetMapping(path = "/quoteRequests")
public String listQuoteRequests() {
return "quoteRequestsList";
}//listRequests()
/*Parameter Of A Specific Value: Narrow down search for different
* types of sales reps. Look for 'eventType' = 'wedding' for sales reps that
* only deal with weddings and only see events associated
* with weddings.
*/
@GetMapping(path = "/quoteRequests", params="eventType=wedding")
public String listWeddingRequests() {
return "quoteWeddingRequestsList";
}//listWeddingRequests()
/*Parameter Of A Specific Value: Narrow down search for different types of sales
reps.
* Look for 'eventType' = 'birthday' for sales reps that
* only deal with weddings and only see events associated
* with weddings.
*/
@GetMapping(path = "/quoteRequests", params="eventType=birthday")
public String listBirthdayRequests() {
return "quoteBirthdayRequestsList";
}//listBirthdayRequests()
/*
* Look for 'eventType' parameter regardless of its value
*/
@GetMapping(path = "/quoteRequests", params="eventType")
public String listAllEventTypeRequests() {
return "quoteAllEventTypeRequestList";
}//listAllEventTypeRequests()
/*
* Absence of a parameter: Look for requests with no 'eventType' parameter
*/
@GetMapping(path = "/quoteRequests", params="!eventType")
public String listNoneEventTypeRequests() {
return "quoteNoneEventTypeRequestsList";
}//listNoneEventTypeRequests()
/*
* Specifying: Create another mapping for a sales rep to drill down
* from what I see in a list and pick one particular quote
* request. We will accomplish this by providing each
* quote request a unique quoteID using @PathVariable
*/
@GetMapping("/quoteRequests/{quoteID}")
public String viewQuoteRequest(@PathVariable int quoteID) {
//refer to quoteID in my implementation
return "quoteRequestsDetails";
}//viewQuoteRequest()
/*
*For this scenario lets say a sales rep is in a particular
* quote and maybe want to add a note, which will require them
* to save the content of the screen. This means we need a
* @PostMapping. The sales rep might want to update the customer
* name, event type, food allergy side note, etc.
*
*Once they hit 'save', all the data will come in and be accessible
* through @ModelAttribute and we can reference the Model Attribute in
* the method signature. So as we implement the logic in the controller
* we get to use a Model Bean and pull in all the updated data
* and ultimately save the data somewhere.
*/
@PostMapping ("/quoteUpdateDetails")
public String updateQuoteRequest(
@ModelAttribute("quoteRequest") QuoteRequest quoteRequest) {
//implement a save of all the form bean information
return "quoteUpdateDetails";
}//updateQuoteRequest()
}
Controller 2 Unit Test:
package com.corplithotel.eventsapp.controller;
import static
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static
org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;
import
org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import com.corplithotel.eventsapp.domain.QuoteRequest;
@ExtendWith(MockitoExtension.class)
@WebMvcTest( QuoteRequestManagementController.class)
@TestInstance(Lifecycle.PER_CLASS)
class QuoteRequestManagementController_UnitTests {
@Mock
private WebApplicationContext wac;
@InjectMocks
private QuoteRequestManagementController qrmc;
private MockMvc qrmcMockMvc;
@BeforeAll
public void setUp() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/");
viewResolver.setSuffix(".html");
qrmcMockMvc=
MockMvcBuilders.standaloneSetup(qrmc)
.setViewResolvers(viewResolver).build();
}//BeforeAll
@Test
@DisplayName("testListQuoteRequests().. Test should pass")
void testlistQuoteRequests() throws Exception {
qrmcMockMvc
.perform(get("/quoteRequests"))
.andExpect(status().is(200))
.andReturn();
}//testlistRequests()
@Test
@DisplayName("testListWeddingRequests() .. Parameter Of A Specific Value Test1.. Test should pass")
void testlistWeddingRequests() throws Exception {
qrmcMockMvc
.perform(get("/quoteRequests?eventType=wedding"))
.andExpect(status().is(200))
.andReturn();
}//testlistWeddingRequests()
@Test
@DisplayName("testListBirthdayRequests() .. Parameter Of A Specific Value Test2.. Test should pass")
void testlistBirthdayRequests() throws Exception {
qrmcMockMvc
.perform(get("/quoteRequests?eventType=birthday"))
.andExpect(status().is(200))
.andReturn();
}//testlistBirthdayRequests()
@Test
@DisplayName("testListAllEventsRequests() .. Parameter with no specified value.. Test should pass")
void testlistAllEventsRequests() throws Exception {
qrmcMockMvc
.perform(get("/quoteRequests?eventType"))
.andExpect(status().is(200))
.andReturn();
}//testlistBirthdayRequests()
@Test
@DisplayName("testNoneEventTypeRequests() .. no parameter .. Test should pass")
void testNoneEventTypeRequests() throws Exception {
qrmcMockMvc
.perform(get("/quoteRequests?!eventType"))
.andExpect(status().is(200))
.andReturn();
}//testlistBirthdayRequests()
@Test
@DisplayName("testViewQuoteRequest().. by 'quoteID'.. Test should pass")
void testViewQuoteRequest() throws Exception {
qrmcMockMvc
.perform(get("/quoteRequests/{quoteID}", 4))
.andExpect(status().is(200))
.andReturn();
}//testViewQuoteRequest()
@Test
@DisplayName("test2ViewQuoteRequest().. by 'quoteID'.. Test should pass")
void tes2tViewQuoteRequest() throws Exception {
qrmcMockMvc
.perform(get("/quoteRequests/{quoteID}", 415))
.andExpect(status().is(200))
.andReturn();
}//testViewQuoteRequest()
@Test
void testupdateQuoteRequest() throws Exception {
MockHttpServletRequestBuilder updateDetails = post("/quoteUpdateDetails")
.param("customer", "Joe")
.param("age", "12")
.param("budget", "$1209")
.param("eventType", "wedding")
.param("foodAllergies", "fish")
.flashAttr("quoteRequest", new QuoteRequest());
qrmcMockMvc
.perform( updateDetails)
.andExpect(status().is(200));
}
}