Spring MVC Based Application Test Driven Development Part 2 – Product Search Code Re-factoring and New Test Cases

In the part 1 of this post series, we learned how to set up our first Spring MVC project also wrote the failing test and the production code to support the Product Search functionality.

In this post, we will re-factor our code and continue from what we have left in the part 1, to add a new test case for the Product Search in the case of a given keyword does not match any product in our application. The expectation in this case is the result returned must be an information message, “Could not find any product matches ‘<keyword>’“, where <keyword> is a keyword a user provided.

Code Re-factoring

Typically, we re-factor codes to make it better; run faster or increase its readability etc. The code written in the part 1 can be improved by removing what we does not use from the productSearch() method; the local and model arguments.

If you notice the method, you will see that we have no code for getting the q parameter used as keyword to search a product yet; we will add the code for this. Also, we will add a new code to write a log to a console or file to trace the method call. The code after re-factoring is as follow:

package com.mycompany.ppms;

import java.io.IOException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Controller
public class ProductSearch {

  final Logger logger = LoggerFactory.getLogger(ProductSearch.class);

  @RequestMapping(value = "/product/search", method = RequestMethod.GET)
  public void productSearch(HttpServletRequest request, HttpServletResponse response) throws IOException {
    String keyword = request.getParameter("q");
    logger.info("productSearch called with '" + StringUtils.stripToEmpty(keyword) + "'");

    response.setContentType(MediaType.APPLICATION_JSON_VALUE);
    response.getWriter().write("{\"name\":\"Very Nice Shoes\"}");
  }
}

The above code has the new argument called, request, this object store HTTP request parameters and other data which can be retrieved via their corresponding methods. We get the q parameter by using the getParameter() method; request.getParameter(“q”). The logger object used to write the method call trace to the log.

Save the code change and re-run the ProductSearchTest again to see if it works properly. If you look into the Console panel, you will see the ProductSearch’s log as the example shown below.

INFO : com.mycompany.ppms.ProductSearch - productSearch called with 'Very Nice Shoes'

New Test Case for Product Search

Next we will add the new test case, let’s open the ProductSearchTest we created for the part 1 and add the following code to it.

@Test
public void testSearchProductByNameNotFound() throws Exception {
  String keyword = "Soft Shoes";
  String infoText = String.format("Could not find any product matches '%s'", keyword);

  this.mockMvc.perform(get("/product/search")
    .param("q", keyword)
    .accept(MediaType.APPLICATION_JSON))
    .andExpect(status().isOk())
    .andExpect(content().contentType(MediaType.APPLICATION_JSON))
    .andExpect(jsonPath("$.infoText").value(infoText));
  }

What this test do is similar to the first test, it requests the “/product/search” page with a given keyword, check if the HTTP status is OK and the content type is JSON format however it expects the information message in the JSON object, the infoText field, returned to it. Save the change we have done in the ProductSearchTest file and re-run the tests again, now only the old test is passed but the new one is failed.

Product Search Enhancement

We have to enhance our code to support what the new test case expects. Open the ProductSearch file and go to the productSearch() method, it must return not only a product name when it found the product but also the information message when it cannot find any product matches the keyword.

The simplest way to do this to check whether the keyword is a “Very Nice Shoes” or not. If it is, return the JSON object with the name field otherwise the JSON object with the infoText field as the code shown below.

@RequestMapping(value = "/product/search", method = RequestMethod.GET)
  public void productSearch(HttpServletRequest request, HttpServletResponse response) throws IOException {
  String keyword = StringUtils.stripToEmpty(request.getParameter("q"));
  String infoText = String.format("Could not find any product matches '%s'", keyword);

  logger.info("productSearch called with '" + StringUtils.stripToEmpty(keyword) + "'");

  response.setContentType(MediaType.APPLICATION_JSON_VALUE);

  if("Very Nice Shoes".equals(keyword)) {
    response.getWriter().write("{\"name\":\"Very Nice Shoes\"}");
  } else {
    response.getWriter().write("{\"infoText\":\"" + infoText + "\"}");
  }
}

After we finished to modify the code, try to run all the tests again; this time we should see the green bar in the JUnit panel. As you see now we hard code the product names rather than store it in a file, database or other kinds of a persistence storage.

I would say our application at this stage is considered as prototype because it cannot be used in the real situation that users search for a product for its detail however our application provide only a product name.  Also, it must support more than one product and we must be able to add more products to the application dynamically, no hard code for the product list.

Evolving Code by Test Cases

In the rest of this post, we will add more new test cases and you will see how these tests drive our application development. Before we start to write more new tests, I will define the requirements for our application features in each section. We will add new tests corresponding to the requirements defined. Also, we might change the tests we have written so far to match the requirements.

Product Search Feature
Requirement No. 1

Product Search must return one or more product details if a given keyword matches product names; these product names contain the keyword. The JSON format is as follow:

{
"status": "found", 
"products":
[
  {
    "name": "name1",
    "description": "description1"
  },
  {
    "name": "name2",
    "description": "description2",
  }
]
}

Test Case No. 1

  • Given the product list has ‘Very Nice Shoes’ and ‘Cool Shoes’ products
  • When search for products by the q parameter as ‘Shoes’
  • Then the two product details returned with the status as ‘found’

The test for this case is as follow:

@Test
public void testSearchProductByNameShoesFoundTwo() throws Exception {
  String keyword = "Shoes";

  this.mockMvc.perform(get("/product/search")
    .param("q", keyword)
    .accept(MediaType.APPLICATION_JSON))
    .andExpect(status().isOk())
    .andExpect(content().contentType(MediaType.APPLICATION_JSON))
    .andExpect(jsonPath("$.status").value("found"))
    .andExpect(jsonPath("$.products[0].name").value("Very Nice Shoes"))
    .andExpect(jsonPath("$.products[1].name").value("Cool Shoes"));
}

Also, we will change the two previous tests to match with the new JSON format as follows:

@Test
public void testSearchProductByNameFound() throws Exception {
  String keyword = "Very Nice Shoes";

  this.mockMvc.perform(get("/product/search")
    .param("q", keyword)
    .accept(MediaType.APPLICATION_JSON))
    .andExpect(status().isOk())
    .andExpect(content().contentType(MediaType.APPLICATION_JSON))
    .andExpect(jsonPath("$.status").value("found"))
    .andExpect(jsonPath("$.products[0].name").value("Very Nice Shoes"));
}

@Test
public void testSearchProductByNameNotFound() throws Exception {
  String keyword = "Soft Shoes";
  String text = String.format("Could not find any product matches '%s'", keyword);

  this.mockMvc.perform(get("/product/search")
    .param("q", keyword)
    .accept(MediaType.APPLICATION_JSON))
    .andExpect(status().isOk())
    .andExpect(content().contentType(MediaType.APPLICATION_JSON))
    .andExpect(jsonPath("$.status").value("not found"))
    .andExpect(jsonPath("$.text").value(text));
}

The code change for the product search will be:

@Controller
public class ProductSearch {
  final Logger logger = LoggerFactory.getLogger(ProductSearch.class);

  final static List<String> products = new ArrayList<String>();
  static {
    products.add("{\"name\": \"Very Nice Shoes\", \"description\":\"Very nice shoes made in Thailand.\"}");
    products.add("{\"name\": \"Cool Shoes\", \"description\":\"Cool shoes made in Japan.\"}");
  }

  @RequestMapping(value = "/product/search", method = RequestMethod.GET)
  public void productSearch(HttpServletRequest request, HttpServletResponse response) throws IOException {
    String keyword = StringUtils.stripToEmpty(request.getParameter("q"));
    String text = String.format("Could not find any product matches '%s'", keyword);

    logger.info("productSearch called with '" + StringUtils.stripToEmpty(keyword) + "'");

    response.setContentType(MediaType.APPLICATION_JSON_VALUE);

    /* search the products */
    List<String> matchedProducts = new ArrayList<String>();
    for(String product: products) {
      if(product.contains(keyword)) {
        matchedProducts.add(product);
      }
    }

    /* generate the response */
    StringBuffer jsonBuffer = new StringBuffer();
    if(!matchedProducts.isEmpty()) {
      jsonBuffer.append("{\"status\":\"found\", \"products\": [");

      for(String product: matchedProducts) {
        jsonBuffer.append(product + ",");
      }
      jsonBuffer.append("]}");
    } else {
      jsonBuffer.append("{\"status\":\"not found\", \"text\":\"" + text + "\"}");
    }

    response.getWriter().write(jsonBuffer.toString());
  }
}

Test Case No. 2

  • Given the product list has ‘Very Nice Shoes’ and ‘Cool Shoes’ products
  • When search for products by the q parameter as ‘Cool Shoes’
  • Then the ‘Cool Shoes’ product detail returned with the status as ‘found’

The test for this case is as follow:

@Test
public void testSearchProductByNameCoolShoesFoundOne() throws Exception {
  String keyword = "Cool Shoes";

  this.mockMvc.perform(get("/product/search")
    .param("q", keyword)
    .accept(MediaType.APPLICATION_JSON))
    .andExpect(status().isOk())
    .andExpect(content().contentType(MediaType.APPLICATION_JSON))
    .andExpect(jsonPath("$.status").value("found"))
    .andExpect(jsonPath("$.products[1]").doesNotExist())
    .andExpect(jsonPath("$.products[0].name").value("Cool Shoes"));
}

Since the code change we done previously also cover this test case, no additional code change for it. You might notice that we still use the List object to store all the product details.

In the next post, we will change change this to be a persistence storage instead; with the existing tests, we can re-run them to check if any further changes will cause our application not meet the requirements or not. The full source can be cloned from the part2 branch of the Git repository https://github.com/kkasemos/spring-mvc-tdd

Tagged with: , , , , ,
Posted in Spring MVC Framework, Spring Test Framework, Test Driven Development
4 comments on “Spring MVC Based Application Test Driven Development Part 2 – Product Search Code Re-factoring and New Test Cases
  1. […] the previous post, Spring MVC Based Application Test Driven Development Part 2 – Product Search Code Re-factoring and…, we wrote the tests for the Product Search Requirement No. 1 and re-factored the code. However, […]

    • Mikhail says:

      Hello, thanks for this tutorial! Awesome content and highlighting just in place!
      Just some remark about last ProductSearch implementation, it’s good on github, but wrong in this article, static block should be:

      static
      {
      products.add(“{\”name\”:\”Very Nice Shoes\”, \”description\”:\”Very nice shoes made in Thailand.\”}”);
      products.add(“{\”name\”:\”Cool Shoes\”, \”description\”:\”Cool shoes made in Japan.\”}”);
      }

      Keep it cool!

  2. Bruno says:

    Hello,

    Thanks for your tutorial. I did the two first sessions but I wonder why we add a green test at the end. Normally shoud we not add only red test ?

    Thanks a lot. I continue 🙂

    • Hi,

      Thank you for the question.

      Regarding the green test at the end, its purpose is to test if the application returns only one result for a very specific keyword. My assumption while I was writing this test is that the application might not cover this scenario regardless I know it is a green test or not; sometimes it turned out to be a green test and sometimes not.

      In my opinion, we should add tests are worth for our application (cover possible critical paths) regardless they are a green or red test.

      Our code might pass those tests today without having to change anything but we need to take into account that everyone can change the code and the tests might fail if someone changes the code incorrectly in the future.

      Let’s say we change how to compare a keyword against a product detail and there is a bug that cause the application return two product details rather than only one. Without the green test added at the end, the bug will not be caught and might pass all the tests and released to customers. That’s why the green test is worth to add here.

      Regards,
      Krit K.

Leave a reply to Spring MVC Based Application Test Driven Development Part 3 – Product Service | Code With Zen Mind Cancel reply