Testing HTTP Methods using Mockito framework

In this post, you will learn how to create HTTP methods in Spring Boot and test them using the Mockito framework. If you are not familiar with unit testing, see my post on Medium. For those who are impatient, you can find the source code on GitHub.

HTTP Verbs Overview

The most-commonly-used HTTP verbs are POST, GET, PUT, PATCH, and DELETE. These are create, read, update, and delete operations, respectively. There are a number of other verbs but are utilized less frequently [1]. Here is a summary of the methods.

HTTP VerbCRUDEntire Collection (e.g. /customers)Specific Item (e.g. /customers/{id})
POSTCreate201 (Created), ‘Location’ header with link to /customers/{id} containing new ID.404 (Not Found), 409 (Conflict) if resource already exists..
GETRead200 (OK), list of customers. Use pagination, sorting and filtering to navigate big lists.200 (OK), list of customers. Use pagination, sorting and filtering to navigate big lists.
PUTUpdate405 (Method Not Allowed), unless you want to update/replace every resource in the entire collection.200 (OK) or 204 (No Content). 404 (Not Found), if ID not found or invalid.
PATCHUpdate/Modify405 (Method Not Allowed), unless you want to modify the collection itself.200 (OK) or 204 (No Content). 404 (Not Found), if ID not found or invalid.

GET

The HTTP GET method retrieves the representation of a resource. In the successful scenario, GET returns an HTTP response code of 200 (OK). If there is an error, it returns a 404 (NOT FOUND) or 400 (BAD REQUEST).

POST

The POST verb is most-often utilized to create new resources. It is also possible to create a new resource for a specific parent. In this case, POST takes care of associating the new resource with the parent, assigning an ID (new resource URI).

PUT

PUT is most-often utilized for update capabilities, PUT-ing to a known resource URI with the request body containing the newly-updated representation of the original resource.

POST vs PUT

POST and PUT differs in idempotence and resource id allocation. Remember, this is the default interview question for http verbs.

Idempotent

In the context of REST APIs, when making multiple identical requests has the same effect as making a single request – then that REST API is called idempotent [2]. Making N POST requests will result in N new resources on the server and contain the same information. However, making N PUT requests will result in only 1 resource on the server. The first request will update the resource; then rest N-1 requests will just overwrite the same resource.

Resource id

PUT can be used to create a resource in the case where the resource ID is chosen by the client instead of by the server. When we create a new resource with POST, the server determines the resource ID.

PATCH

PATCH is used for modifying capabilities. The PATCH request only needs to contain the changes to the resource, not the complete resource. PUT and PATCH looks very similar but there is an important difference. PUT updates the entire object, whereas PATCH modifies only the requested attributes of the object.

Setting Spring Boot Project

Let’s create a project step by step. First, we define a Food resource to be consumed by HTTP methods.

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Food {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    public long id;
    private String name;
    private Nutrition nutrition;
    private Optional<String> description;
}

Second, we create a repository for CRUD operations

@Repository
public interface NutritionRepository extends CrudRepository<Food, Long> { }

Third, we create a NutritionService to perform CRUD operations.

@Service
@AllArgsConstructor
public class NutritionService implements INutritionService {

    @Autowired
    private final NutritionRepository nutritionRepository;

    @Override
    public Optional<Food> getFood(long id) {
       // implementation here...
    }

    @Override
    public Optional<Food> createFood(Food food) {
       // implementation here...
    }

    @Override
    public Optional<Food> updateFood(Food food) {
     // implementation here...
    }

    @Override
    public Optional<Food> updatePartialFood(Food food){
     // implementation here...
    }
}

Last, we create a NutritionController where we define the HTTP methods.

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/nutrition")
public class NutritionController {

    @Autowired
    private final NutritionService nutritionService;
    
    @GetMapping(value = "/get-food/{id}")
    public ResponseEntity<Food> getFood(@PathVariable long id) {
		// implementation here...
    }

    @PostMapping(value = "/create-new-food")
    public ResponseEntity<Food> createFood(@RequestBody Food food) throws URISyntaxException {
		// implementation here...
    }

    @PutMapping(value = "/update-existing-food/{id}")
    public ResponseEntity<Food> updateFood(@PathVariable long id, @RequestBody Food food) {
		// implementation here...
    }

    @PatchMapping(value = "/update-partial-food/{id}")
    public ResponseEntity<Object> updatePartialFood(@PathVariable long id, Food food) throws Exception {
		// implementation here...
    }
}

Testing with Mockito

The NutritionControllerTest class is the skeleton for testing the web layer. We will define the test methods one by one.

@RunWith(SpringRunner.class)
@WebMvcTest(value = NutritionController.class)
public class NutritionControllerTest {

    @MockBean
    private NutritionRepository nutritionRepository;

    @MockBean
    private NutritionService nutritionService;

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private WebApplicationContext webApplicationContext;

    @Before
    public void setUp() throws Exception {
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
    }

    // test methods are here...

}

GET

Create a method /get-food/{id}, which retrieves a food with the given id, in NutritionController.

    @GetMapping(value = "/get-food/{id}")
    public ResponseEntity<Food> getFood(@PathVariable long id) {
        Optional<Food> optionalFood = nutritionService.getFood(id);
        return ResponseEntity.ok(optionalFood.get());
    }

Create getFood() test method in NutritionControllerTest. After we create a food object, we mimic the behavior of nutritionService.getFood(1L) method using When/ThenReturn at Line 6. This is called stubbing. This is required as the NutritionController is dependent on the NutritionService object. The last step is to test the HTTP GET method using mockMvc.perform() on Line 8.

@Test
public void getFood() throws Exception {

Food food = ...create new food object..

when(nutritionService.getFood(1L)).thenReturn(Optional.ofNullable(food));

mockMvc.perform(get("/api/nutrition/get-food/1")
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(1L))
                .andExpect(jsonPath("$.name").value("egg"))            .andExpect(jsonPath("$.nutrition.servingSize").value("GRAM"))
.andExpect(jsonPath("$.nutrition.calories").value(200))
.andExpect(jsonPath("$.nutrition.fat").value((15)))
.andExpect(jsonPath("$.nutrition.carbohydrate").value((55)))
.andExpect(jsonPath("$.nutrition.protein").value((25)))
.andExpect(jsonPath("$.description").value("my favourite"))
.andReturn();
}

POST

Create a method /create-new-food/, which creates a new food resource.

@PostMapping(value = "/create-new-food")
public ResponseEntity<Food> createFood(@RequestBody Food food) throws URISyntaxException {
        Optional<Food> optionalFood = nutritionService.createFood(food);
        Food createdFood = optionalFood.get();
        return ResponseEntity.created(new URI(String.valueOf(createdFood.getId()))).body(createdFood);
    }

Create createFood() test method in NutritionControllerTest. Testing the POST method is similar to the GET. The difference is the way we stub the createFood function and passing the food object to the POST method.

Line 6 stubs the createFood function with any object of Food using any() argument matcher. We assume that we will get the food object after we perform a POST request.

When we perform a POST request (/api/nutrition/create-new-food), we need to pass the food object as a content using content(asJsonString(food)) at Line 9.

@Test
public void createFood() throws Exception {

Food food = ...create new food object..

when(nutritionService.createFood(any(Food.class))).thenReturn(Optional.of(food));

mockMvc.perform(post("/api/nutrition/create-new-food")
                .content(asJsonString(food))
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.id").value(3L))
                .andExpect(jsonPath("$.name").value("rice"))
.andExpect(jsonPath("$.nutrition.calories").value(123))
.andExpect(jsonPath("$.nutrition.carbohydrate").value((45)))
.andExpect(jsonPath("$.nutrition.fat").value((5)))
.andExpect(jsonPath("$.nutrition.protein").value((15)))
.andExpect(jsonPath("$.nutrition.servingSize").value("GRAM"))
.andReturn();
}

public static String asJsonString(final Object obj) {
   // here we serialize food object as a String
}

PUT

Create a method /update-existing-food/{id}, which updates an existing food resource.

@PutMapping(value = "/update-existing-food/{id}")
public ResponseEntity<Food> updateFood(@PathVariable long id, @RequestBody Food food) {
        Optional<Food> optionalFood = nutritionService.updateFood(food);
        return ResponseEntity.ok(optionalFood.get());
}

Create updateFood() test method in NutritionControllerTest. Since PUT is an update operation, we create a new food object by stubbing the createFood method. Then, we apply the same procedure by stubbing the updateFood method that returns an updated food object.

Now, we are ready to execute the PUT operation. We pass the id and the content of the food object to be updated to the PUT request.

@Test
public void updateFood() throws Exception {

    Food food = Food.builder()
        .id(2L)
        .name("beef")
         ...
        .build();

    when(nutritionService.createFood(any(Food.class))).thenReturn(Optional.of(food));

    Food foodUpdated = Food.builder()
        .id(2L)
        .name("beef-light")
         ...
        .build();

   	when(nutritionService.updateFood(any(Food.class))).thenReturn(Optional.of(foodUpdated));

    mockMvc.perform(put("/api/nutrition/update-existing-food/{id}", 2)
        .content(asJsonString(foodUpdated))
        .contentType(MediaType.APPLICATION_JSON)
        .accept(MediaType.APPLICATION_JSON))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.id").value(2L))
        .andExpect(jsonPath("$.name").value("beef-light"))
        .andExpect(jsonPath("$.nutrition.calories").value(260))
        .andExpect(jsonPath("$.nutrition.carbohydrate").value((10)))
        .andExpect(jsonPath("$.nutrition.fat").value((25)))
        .andExpect(jsonPath("$.nutrition.protein").value((50)))
        .andExpect(jsonPath("$.nutrition.servingSize").value("GRAM"))
        .andReturn();
}

PATCH

PATCH is the most interesting HTTP verb as the service implementation is different than other methods. You can look at the service implementation on GitHub.

Create a method /update-partial-food/{id}, which modifies only the specific attributes of the food item.

    @PatchMapping(value = "/update-partial-food/{id}")
    public ResponseEntity<Object> updatePartialFood(@PathVariable long id, Food food) throws Exception {
        Optional<Food> optionalFood = nutritionService.updatePartialFood(food);
        return ResponseEntity.ok(optionalFood);
    }

The test method is almost the same as the PUT. The only difference is that we stub the nutritionService.updatePartialFood(any(Food.class)) where we modify the name, calories, and the carbohydrate attributes.

    @Test
    public void updatePartialFood() throws Exception {
        Food food = Food.builder()
                .id(1L)
                .name("egg")
                .nutrition(Nutrition.builder()
                        .calories(200)
                        .carbohydrate(55)
                        .fat(15)
                        .protein(25)
                        .servingSize(ServingSize.GRAM)
                        .build())
                .description(java.util.Optional.of("my favourite"))
                .build();


        Food foodPartialUpdate = Food.builder()
                .id(1L)
                .name("organic-egg")
                .nutrition(Nutrition.builder()
                        .calories(220)
                        .carbohydrate(50)
                        .fat(15)
                        .protein(25)
                        .servingSize(ServingSize.GRAM)
                        .build())
                .description(java.util.Optional.of("my favourite"))
                .build();

when(nutritionService.createFood(any(Food.class))).thenReturn(Optional.of(food));
       
when(nutritionService.updatePartialFood(any(Food.class))).thenReturn(Optional.of(foodPartialUpdate));

        mockMvc.perform(patch("/api/nutrition/update-partial-food/1")
                .param("name", "organic-egg")
                .param("nutrition.calories", "220")
                .param("nutrition.carbohydrate", "50")
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(1L))
                .andExpect(jsonPath("$.name").value("organic-egg"))
                .andExpect(jsonPath("$.nutrition.servingSize").value("GRAM"))
                .andExpect(jsonPath("$.nutrition.calories").value(220))
                .andExpect(jsonPath("$.nutrition.fat").value((15)))
                .andExpect(jsonPath("$.nutrition.carbohydrate").value((50)))
                .andExpect(jsonPath("$.nutrition.protein").value((25)))
                .andExpect(jsonPath("$.description").value("my favourite"))
                .andReturn();
    }

References

  1. https://www.restapitutorial.com/lessons/httpmethods.html
  2. https://restfulapi.net/idempotent-rest-apis/
  3. https://nullbeans.com/using-put-vs-patch-when-building-a-rest-api-in-spring/
  4. https://www.javadevjournal.com/spring/rest/http-put-vs-http-patch-in-a-rest-api/
  5. https://www.logicbig.com/tutorials/spring-framework/spring-web-mvc/http-patch-test.html

Leave a Reply

Your email address will not be published. Required fields are marked *