├── .travis.yml ├── src ├── main │ └── java │ │ └── com │ │ └── blog │ │ └── samples │ │ ├── model │ │ ├── EnumAccountType.java │ │ └── Account.java │ │ ├── exception │ │ ├── AccountNotFoundException.java │ │ ├── InvalidAccountRequestException.java │ │ └── components │ │ │ └── ControllerExceptionHandler.java │ │ ├── service │ │ └── AccountService.java │ │ ├── Application.java │ │ └── controller │ │ └── AccountController.java ├── .project ├── .classpath └── test │ └── java │ └── com │ └── blog │ └── samples │ └── controller │ └── AccountControllerTest.java ├── README.md └── pom.xml /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - oraclejdk8 4 | script: mvn clean install 5 | -------------------------------------------------------------------------------- /src/main/java/com/blog/samples/model/EnumAccountType.java: -------------------------------------------------------------------------------- 1 | package com.blog.samples.model; 2 | 3 | public enum EnumAccountType { 4 | 5 | CURRENT, SAVINGS 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/blog/samples/exception/AccountNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.blog.samples.exception; 2 | 3 | public class AccountNotFoundException extends RuntimeException { 4 | 5 | private static final long serialVersionUID = 2468434988680850339L; 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/blog/samples/exception/InvalidAccountRequestException.java: -------------------------------------------------------------------------------- 1 | package com.blog.samples.exception; 2 | 3 | public class InvalidAccountRequestException extends RuntimeException { 4 | 5 | private static final long serialVersionUID = 2468434988680850339L; 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/blog/samples/service/AccountService.java: -------------------------------------------------------------------------------- 1 | package com.blog.samples.service; 2 | 3 | import com.blog.samples.model.Account; 4 | 5 | public interface AccountService { 6 | 7 | public Account loadAccount(Long accountId); 8 | 9 | public Long createAccount(Account account); 10 | } -------------------------------------------------------------------------------- /src/main/java/com/blog/samples/Application.java: -------------------------------------------------------------------------------- 1 | package com.blog.samples; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(Application.class, args); 11 | } 12 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/briansjavablog/rest-controller-testing-mock-mvc.svg?branch=master)](https://travis-ci.org/briansjavablog/rest-controller-testing-mock-mvc) 2 | 3 | # REST Controller Testing WIth MockMVC 4 | A simple REST controller and some unit tests using Springs MockMVC 5 | 6 | The blog post associated with this sample code can be found at https://www.briansdevblog.com/2017/05/rest-endpoint-testing-with-mockmvc 7 | -------------------------------------------------------------------------------- /src/main/java/com/blog/samples/model/Account.java: -------------------------------------------------------------------------------- 1 | package com.blog.samples.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | public class Account { 7 | 8 | @Getter 9 | @Setter 10 | private Long accountId; 11 | 12 | @Getter 13 | @Setter 14 | private EnumAccountType accountType; 15 | 16 | @Getter 17 | @Setter 18 | private Double balance; 19 | 20 | public Account(){} 21 | 22 | public Account(Long accountId, EnumAccountType accountType, Double balance){ 23 | this.accountId = accountId; 24 | this.accountType = accountType; 25 | this.balance = balance; 26 | } 27 | 28 | public Account(EnumAccountType accountType, Double balance){ 29 | this.accountType = accountType; 30 | this.balance = balance; 31 | } 32 | } -------------------------------------------------------------------------------- /src/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | rest-endpoint-testing 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.wst.common.project.facet.core.builder 10 | 11 | 12 | 13 | 14 | org.eclipse.jdt.core.javabuilder 15 | 16 | 17 | 18 | 19 | org.eclipse.m2e.core.maven2Builder 20 | 21 | 22 | 23 | 24 | org.springframework.ide.eclipse.core.springbuilder 25 | 26 | 27 | 28 | 29 | 30 | org.springframework.ide.eclipse.core.springnature 31 | org.eclipse.jdt.core.javanature 32 | org.eclipse.m2e.core.maven2Nature 33 | org.eclipse.wst.common.project.facet.core.nature 34 | 35 | 36 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | org.springframework 6 | rest-controller-testing-mock-mvc 7 | 0.1.1 8 | 9 | org.springframework.boot 10 | spring-boot-starter-parent 11 | 2.1.5.RELEASE 12 | 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-web 17 | 18 | 19 | org.projectlombok 20 | lombok 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-test 25 | test 26 | 27 | 28 | 29 | 1.8 30 | 31 | -------------------------------------------------------------------------------- /src/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/main/java/com/blog/samples/exception/components/ControllerExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.blog.samples.exception.components; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ControllerAdvice; 5 | import org.springframework.web.bind.annotation.ExceptionHandler; 6 | import org.springframework.web.bind.annotation.ResponseStatus; 7 | 8 | import com.blog.samples.exception.AccountNotFoundException; 9 | import com.blog.samples.exception.InvalidAccountRequestException; 10 | 11 | import lombok.extern.slf4j.Slf4j; 12 | 13 | @Slf4j 14 | @ControllerAdvice 15 | public class ControllerExceptionHandler { 16 | 17 | 18 | @ResponseStatus(HttpStatus.NOT_FOUND) // 404 19 | @ExceptionHandler(AccountNotFoundException.class) 20 | public void handleNotFound(AccountNotFoundException ex) { 21 | log.error("Requested account not found"); 22 | } 23 | 24 | @ResponseStatus(HttpStatus.BAD_REQUEST) // 400 25 | @ExceptionHandler(InvalidAccountRequestException.class) 26 | public void handleBadRequest(InvalidAccountRequestException ex) { 27 | log.error("Invalid account supplied in request"); 28 | } 29 | 30 | @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 500 31 | @ExceptionHandler(Exception.class) 32 | public void handleGeneralError(Exception ex) { 33 | log.error("An error occurred procesing request" + ex); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/blog/samples/controller/AccountController.java: -------------------------------------------------------------------------------- 1 | package com.blog.samples.controller; 2 | 3 | import javax.servlet.http.HttpServletResponse; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.web.bind.annotation.PathVariable; 8 | import org.springframework.web.bind.annotation.RequestBody; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RequestMethod; 11 | import org.springframework.web.bind.annotation.RestController; 12 | import org.springframework.web.context.request.WebRequest; 13 | 14 | import com.blog.samples.exception.AccountNotFoundException; 15 | import com.blog.samples.exception.InvalidAccountRequestException; 16 | import com.blog.samples.model.Account; 17 | import com.blog.samples.service.AccountService; 18 | 19 | @RestController 20 | public class AccountController { 21 | 22 | private AccountService accountService; 23 | 24 | @Autowired 25 | public AccountController(AccountService accountService) { 26 | this.accountService = accountService; 27 | } 28 | 29 | @RequestMapping(value = { "/api/account" }, method = { RequestMethod.POST }) 30 | public Account createAccount(@RequestBody Account account, 31 | HttpServletResponse httpResponse, 32 | WebRequest request) { 33 | 34 | Long accountId = accountService.createAccount(account); 35 | account.setAccountId(accountId); 36 | 37 | httpResponse.setStatus(HttpStatus.CREATED.value()); 38 | httpResponse.setHeader("Location", String.format("%s/api/account/%s", 39 | request.getContextPath(), accountId)); 40 | return account; 41 | } 42 | 43 | @RequestMapping(value = "/api/account/{accountId}", method = RequestMethod.GET) 44 | public Account getAccount(@PathVariable("accountId") Long accountId) { 45 | 46 | /* validate account Id parameter */ 47 | if (accountId < 9999) { 48 | throw new InvalidAccountRequestException(); 49 | } 50 | 51 | Account account = accountService.loadAccount(accountId); 52 | if(null==account){ 53 | throw new AccountNotFoundException(); 54 | } 55 | 56 | return account; 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /src/test/java/com/blog/samples/controller/AccountControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.blog.samples.controller; 2 | import static org.mockito.ArgumentMatchers.any; 3 | import static org.mockito.Mockito.when; 4 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 5 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 6 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 7 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; 8 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 9 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 10 | import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; 11 | 12 | import org.junit.Before; 13 | import org.junit.Test; 14 | import org.junit.runner.RunWith; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.boot.test.context.SpringBootTest; 17 | import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; 18 | import org.springframework.boot.test.mock.mockito.MockBean; 19 | import org.springframework.http.MediaType; 20 | import org.springframework.test.context.junit4.SpringRunner; 21 | import org.springframework.test.web.servlet.MockMvc; 22 | import org.springframework.web.context.WebApplicationContext; 23 | 24 | import com.blog.samples.Application; 25 | import com.blog.samples.model.Account; 26 | import com.blog.samples.model.EnumAccountType; 27 | import com.blog.samples.service.AccountService; 28 | 29 | @RunWith(SpringRunner.class) 30 | 31 | @SpringBootTest(webEnvironment=WebEnvironment.MOCK, classes={ Application.class }) 32 | public class AccountControllerTest { 33 | 34 | private MockMvc mockMvc; 35 | 36 | @Autowired 37 | private WebApplicationContext webApplicationContext; 38 | 39 | @MockBean 40 | private AccountService accountServiceMock; 41 | 42 | @Before 43 | public void setUp() { 44 | this.mockMvc = webAppContextSetup(webApplicationContext).build(); 45 | } 46 | 47 | @Test 48 | public void should_CreateAccount_When_ValidRequest() throws Exception { 49 | 50 | when(accountServiceMock.createAccount(any(Account.class))).thenReturn(12345L); 51 | 52 | mockMvc.perform(post("/api/account") 53 | .contentType(MediaType.APPLICATION_JSON) 54 | .content("{ \"accountType\": \"SAVINGS\", \"balance\": 5000.0 }") 55 | .accept(MediaType.APPLICATION_JSON)) 56 | .andExpect(status().isCreated()) 57 | .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) 58 | .andExpect(header().string("Location", "/api/account/12345")) 59 | .andExpect(jsonPath("$.accountId").value("12345")) 60 | .andExpect(jsonPath("$.accountType").value("SAVINGS")) 61 | .andExpect(jsonPath("$.balance").value(5000)); 62 | } 63 | 64 | @Test 65 | public void should_GetAccount_When_ValidRequest() throws Exception { 66 | 67 | /* setup mock */ 68 | Account account = new Account(12345L, EnumAccountType.SAVINGS, 5000.0); 69 | when(accountServiceMock.loadAccount(12345L)).thenReturn(account); 70 | 71 | mockMvc.perform(get("/api/account/12345") 72 | .accept(MediaType.APPLICATION_JSON)) 73 | .andExpect(status().isOk()) 74 | .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) 75 | .andExpect(jsonPath("$.accountId").value(12345)) 76 | .andExpect(jsonPath("$.accountType").value("SAVINGS")) 77 | .andExpect(jsonPath("$.balance").value(5000.0)); 78 | } 79 | 80 | @Test 81 | public void should_Return404_When_AccountNotFound() throws Exception { 82 | 83 | /* setup mock */ 84 | when(accountServiceMock.loadAccount(12345L)).thenReturn(null); 85 | 86 | mockMvc.perform(get("/api/account/12345") 87 | .accept(MediaType.APPLICATION_JSON)) 88 | .andExpect(status().isNotFound()); 89 | } 90 | 91 | } --------------------------------------------------------------------------------