Creating a Todo Resource
We will focus on creating REST services for a basic todo management system. We will create services for the following:
- Retrieving a list of todos for a given user
- Retrieving details for a specific todo
- Creating a todo for a user
Request Methods, Operations, and Uris
One of the best practices of REST services is to use the appropriate HTTP request method based on the action we perform. In the services we exposed until now, we used the GET
method, as we focused on services that read data.
The following table shows the appropriate HTTP Request method based on the operation that we perform:
HTTP Request Method |
Operation |
---|---|
|
Read--Retrieve details for a resource |
|
Create--Create a new item or resource |
|
Update/replace |
|
Update/modify a part of the resource |
|
Delete |
Let's quickly map the services that we want to create to the appropriate request methods:
- Retrieving a list of todos for a given user: This is READ. We will use GET. We will use a URI:
/users/{name}/todos
. One more good practice is to use plurals for static things in the URI: users, todo, and so on. This results in more readable URIs. - Retrieving details for a specific todo: Again, we will use
GET
. We will use a URI/users/{name}/todos/{id}
. You can see that this is consistent with the earlier URI that we decided for the list of todos. - Creating a todo for a user: For the create operation, the suggested HTTP Request method is
POST
. To create a new todo, we will post toURI /users/{name}/todos
.
Beans and Services
To be able to retrieve and store details of a todo, we need a Todo bean and a service to retrieve and store the details.
Let's create a Todo Bean:
public class Todo { private int id; private String user; private String desc; private Date targetDate; private boolean isDone; public Todo() {} public Todo(int id, String user, String desc, Date targetDate, boolean isDone) { super(); this.id = id; this.user = user; this.desc = desc; this.targetDate = targetDate; this.isDone = isDone; } //ALL Getters }
We have a created a simple Todo bean with the ID, the name of user, the description of the todo, the todo target date, and an indicator for the completion status. We added a constructor and getters for all fields.
Let's add TodoService
now:
@Service public class TodoService { private static List<Todo> todos = new ArrayList<Todo>(); private static int todoCount = 3; static { todos.add(new Todo(1, "Jack", "Learn Spring MVC", new Date(), false)); todos.add(new Todo(2, "Jack", "Learn Struts", new Date(), false)); todos.add(new Todo(3, "Jill", "Learn Hibernate", new Date(), false)); } public List<Todo> retrieveTodos(String user) { List<Todo> filteredTodos = new ArrayList<Todo>(); for (Todo todo : todos) { if (todo.getUser().equals(user)) filteredTodos.add(todo); } return filteredTodos; } public Todo addTodo(String name, String desc, Date targetDate, boolean isDone) { Todo todo = new Todo(++todoCount, name, desc, targetDate, isDone); todos.add(todo); return todo; } public Todo retrieveTodo(int id) { for (Todo todo : todos) { if (todo.getId() == id) return todo; } return null; } }
Quick things to note are as follows:
- To keep things simple, this service does not talk to the database. It maintains an in-memory array list of todos. This list is initialized using a static initializer.
- We are exposing a couple of simple retrieve methods and a method to add a to-do.
Now that we have the service and bean ready, we can create our first service to retrieve a list of to-do's for a user.
Retrieving a Todo List
We will create a new RestController
annotation called TodoController
. The code for the retrieve todos method is shown as follows:
@RestController public class TodoController { @Autowired private TodoService todoService; @GetMapping("/users/{name}/todos") public List<Todo> retrieveTodos(@PathVariable String name) { return todoService.retrieveTodos(name); } }
A couple of things to note are as follows:
- We are autowiring the todo service using the
@Autowired
annotation - We use the
@GetMapping
annotation to map the Get request for the"/users/{name}/todos"
URI to theretrieveTodos
method
Executing the Service
Let's send a test request and see what response we get. The following screenshot shows the output:
The response for the http://localhost:8080/users/Jack/todos
URL is as follows:
[ {"id":1,"user":"Jack","desc":"Learn Spring MVC","targetDate":1481607268779,"done":false}, {"id":2,"user":"Jack","desc":"Learn Struts","targetDate":1481607268779, "done":false} ]
Unit Testing
The code to unit test the TodoController
class is shown in the following screenshot:
@RunWith(SpringRunner.class) @WebMvcTest(TodoController.class) public class TodoControllerTest { @Autowired private MockMvc mvc; @MockBean private TodoService service; @Test public void retrieveTodos() throws Exception { List<Todo> mockList = Arrays.asList(new Todo(1, "Jack", "Learn Spring MVC", new Date(), false), new Todo(2, "Jack", "Learn Struts", new Date(), false)); when(service.retrieveTodos(anyString())).thenReturn(mockList); MvcResult result = mvc .perform(MockMvcRequestBuilders.get("/users /Jack/todos").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()).andReturn(); String expected = "[" + "{id:1,user:Jack,desc:\"Learn Spring MVC\",done:false}" +"," + "{id:2,user:Jack,desc:\"Learn Struts\",done:false}" + "]"; JSONAssert.assertEquals(expected, result.getResponse() .getContentAsString(), false); } }
A few important things to note are as follows:
- We are writing a unit test. So, we want to test only the logic present in the
TodoController
class. So, we initialize a Mock MVC framework with only theTodoController
class using@WebMvcTest(TodoController.class)
. @MockBean private TodoService service
: We are mocking out theTodoService
using the@MockBean
annotation. In test classes that are run withSpringRunner
, the beans defined with@MockBean
will be replaced by a mock, created using the Mockito framework.when(service.retrieveTodos(anyString())).thenReturn(mockList)
: We are mocking theretrieveTodos
service method to return the mock list.MvcResult result = ..
: We are accepting the result of the request into an MvcResult variable to enable us to perform assertions on the response.JSONAssert.assertEquals(expected, result.getResponse().getContentAsString(), false)
: JSONAssert is a very useful framework to perform asserts on JSON. It compares the response text with the expected value.JSONAssert
is intelligent enough to ignore values that are not specified. Another advantage is a clear failure message in case of assertion failures. The last parameter, false, indicates using non-strict mode. If it is changed to true, then the expected should exactly match the result.
Integration Testing
The code to perform integration testing on the TodoController
class is shown in the following code snippet. It launches up the entire Spring context with all the controllers and beans defined:
@RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class TodoControllerIT { @LocalServerPort private int port; private TestRestTemplate template = new TestRestTemplate(); @Test public void retrieveTodos() throws Exception { String expected = "[" + "{id:1,user:Jack,desc:\"Learn Spring MVC\",done:false}" + "," + "{id:2,user:Jack,desc:\"Learn Struts\",done:false}" + "]"; String uri = "/users/Jack/todos"; ResponseEntity<String> response = template.getForEntity(createUrl(uri), String.class); JSONAssert.assertEquals(expected, response.getBody(), false); } private String createUrl(String uri) { return "http://localhost:" + port + uri; } }
This test is very similar to the integration test for BasicController
, except that we are using JSONAssert
to assert the response.
Retrieving Details for a Specific Todo
We will now add the method to retrieve details for a specific Todo:
@GetMapping(path = "/users/{name}/todos/{id}") public Todo retrieveTodo(@PathVariable String name, @PathVariable int id) { return todoService.retrieveTodo(id); }
A couple of things to note are as follows:
- The URI mapped is
/users/{name}/todos/{id}
- We have two path variables defined for
name
andid
Executing the Service
Let's send a test request and see what response we will get, as shown in the following screenshot:
The response for the http://localhost:8080/users/Jack/todos/1
URL is shown as follows:
{"id":1,"user":"Jack","desc":"Learn Spring MVC", "targetDate":1481607268779,"done":false}
Unit Testing
The code to unit test retrieveTodo
is as follows:
@Test public void retrieveTodo() throws Exception { Todo mockTodo = new Todo(1, "Jack", "Learn Spring MVC", new Date(), false); when(service.retrieveTodo(anyInt())).thenReturn(mockTodo); MvcResult result = mvc.perform( MockMvcRequestBuilders.get("/users/Jack/todos/1") .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()).andReturn(); String expected = "{id:1,user:Jack,desc:\"Learn Spring MVC\",done:false}"; JSONAssert.assertEquals(expected, result.getResponse().getContentAsString(), false); }
A few important things to note are as follows:
when(service.retrieveTodo(anyInt())).thenReturn(mockTodo)
: We are mocking theretrieveTodo
service method to return the mock todo.MvcResult result = ..
: We are accepting the result of the request into an MvcResult variable to enable us to perform assertions on the response.JSONAssert.assertEquals(expected, result.getResponse().getContentAsString(), false)
: Asserts whether the result is as expected.
Integration Testing
The code to perform integration testing on retrieveTodos
in TodoController
is shown in the following code snippet. This would be added to the TodoControllerIT
class:
@Test public void retrieveTodo() throws Exception { String expected = "{id:1,user:Jack,desc:\"Learn Spring MVC\",done:false}"; ResponseEntity<String> response = template.getForEntity( createUrl("/users/Jack/todos/1"), String.class); JSONAssert.assertEquals(expected, response.getBody(), false); }
Adding A Todo
We will now add the method to create a new Todo. The HTTP method to be used for creation is Post
. We will post to a "/users/{name}/todos"
URI:
@PostMapping("/users/{name}/todos") ResponseEntity<?> add(@PathVariable String name, @RequestBody Todo todo) { Todo createdTodo = todoService.addTodo(name, todo.getDesc(), todo.getTargetDate(), todo.isDone()); if (createdTodo == null) { return ResponseEntity.noContent().build(); } URI location = ServletUriComponentsBuilder.fromCurrentRequest() .path("/{id}").buildAndExpand(createdTodo.getId()).toUri(); return ResponseEntity.created(location).build(); }
A few things to note are as follows:
@PostMapping("/users/{name}/todos")
:@PostMapping
annotations map theadd()
method to the HTTP Request with aPOST
method.ResponseEntity<?> add(@PathVariable String name, @RequestBody Todo todo)
: An HTTP post request should ideally return the URI to the created resources. We useResourceEntity
to do this.@RequestBody
binds the body of the request directly to the bean.ResponseEntity.noContent().build()
: Used to return that the creation of the resource failed.ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(createdTodo.getId()).toUri()
: Forms the URI for the created resource that can be returned in the response.ResponseEntity.created(location).build()
: Returns a status of201(CREATED)
with a link to the resource created.
Postman
If you are on Mac, you might want to try the Paw application as well.
Let's send a test request and see what response we get. The following screenshot shows the response:
We will use Postman app to interact with the REST Services. You can install it from the website, https://www.getpostman.com/. It is available on Windows and Mac. A Google Chrome plugin is also available.
Executing the POST Service
To create a new Todo using POST
, we would need to include the JSON for the Todo in the body of the request. The following screenshot shows how we can use the Postman app to create the request and the response after executing the request:
A few important things to note are as follows:
- We are sending a POST request. So, we choose the
POST
from the top-left dropdown. - To send the Todo JSON as part of the body of the request, we select the
raw
option in theBody
tab (highlighted with a blue dot). We choose the content type as JSON (application/json
). - Once the request is successfully executed, you can see the status of the request in the bar in the middle of the screen:
Status: 201 Created
. - The location is
http://localhost:8080/users/Jack/todos/5
. This is the URI of the newly created todo that is received in the response.
Complete details of the request to http://localhost:8080/users/Jack/todos
are shown in the block, as follows:
Header Content-Type:application/json Body { "user": "Jack", "desc": "Learn Spring Boot", "done": false }
Unit Testing
The code to unit test the created Todo is shown as follows:
@Test public void createTodo() throws Exception { Todo mockTodo = new Todo(CREATED_TODO_ID, "Jack", "Learn Spring MVC", new Date(), false); String todo = "{"user":"Jack","desc":"Learn Spring MVC", "done":false}"; when(service.addTodo(anyString(), anyString(), isNull(),anyBoolean())) .thenReturn(mockTodo); mvc .perform(MockMvcRequestBuilders.post("/users/Jack/todos") .content(todo) .contentType(MediaType.APPLICATION_JSON) ) .andExpect(status().isCreated()) .andExpect( header().string("location",containsString("/users/Jack/todos/" + CREATED_TODO_ID))); }
A few important things to note are as follows:
String todo = "{"user":"Jack","desc":"Learn Spring MVC","done":false}"
: The Todo content to post to the create todo service.when(service.addTodo(anyString(), anyString(), isNull(), anyBoolean())).thenReturn(mockTodo)
: Mocks the service to return a dummy todo.MockMvcRequestBuilders.post("/users/Jack/todos").content(todo).contentType(MediaType.APPLICATION_JSON))
: Creates a POST to a given URI with the given content type.andExpect(status().isCreated())
: Expects that the status is created.andExpect(header().string("location",containsString("/users/Jack/todos/" + CREATED_TODO_ID)))
: Expects that the header containslocation
with the URI of created resource.
Integration Testing
The code to perform integration testing on the created todo in TodoController
is shown as follows. This would be added to the TodoControllerIT
class, as follows:
@Test public void addTodo() throws Exception { Todo todo = new Todo(-1, "Jill", "Learn Hibernate", new Date(), false); URI location = template .postForLocation(createUrl("/users/Jill/todos"),todo); assertThat(location.getPath(), containsString("/users/Jill/todos/4")); }
A few important things to note are as follows:
URI location = template.postForLocation(createUrl("/users/Jill/todos"), todo)
:postForLocation
is a utility method especially useful in tests to create new resources. We are posting the todo to the given URI and getting the location from the header.assertThat(location.getPath(), containsString("/users/Jill/todos/4"))
: Asserts that the location contains the path to the newly created resource.