Level 7 - Milestone 10

Milestone 10 - Unit Testing the Controller and Service Classes

In this section we will complete the unit testing of the controller and service-layer classes

Before completion of this milestone, students will:

  • Add a class/unit tests for their controller class
  • Add a class/unit tests for their service class

Create a Class for the Controller Test

This will be accomplished the exact same way you created a test class for the HomeController.

A Slight Difference This Time

After creating the test class, there are a couple additional considerations we have for this controller class. Our HomeController was very simple: when the endpoint received a request, it simply returned a redirect as a String, requiring no help from additional classes. Our "real" controller class, on the other hand, requires an instance of the service class in the constructor, and calls a method in the service class when an endpoint receives a request. This introduces multiple levels of complexity, as the service class itself requires an instance of the repository, with the repository relying on a request to an external service in order to fetch data for our users. However we can simplify this problem using mocking.

By mocking the instance of our service class, we are given an instance without having to consider the service class' own dependencies, with the added benefit that we no longer rely on the service class' code to work properly in order for our controller unit tests to pass. That being said, the optimal situation is that our unit tests require no mocking. So under what circumstances should we consider mocking the dependency of a class?

  • When the dependency is difficult to construct
    • e.g. its constructor takes a lot of parameters which themselves are difficult to construct
  • When the dependency uses an unreliable resource
    • e.g. http request or reading from a file
  • When we want to verify specific actions occurred when the dependency's method was invoked (white-box testing)


In our case, point number 2 is the real reason why we are mocking, although perhaps a case could be made for the first point as well. Since our repository class relies on an HTTP request to an external service, we do not want to rely on that service to work in order for our unit tests to pass.

Mocking the Service Class

To mock the service class, we will first declare it as a member variable. However, we will annotate this field with @Mock, and instead of instantiating it in our setUp() method like a normal object, we will let Mockito create the mock for us:

    private LocController locController;

    @Mock
    private LocService locService;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);

        locController = new LocController(locService);
    }
                

Testing getResults()

Testing this method will follow the same general steps as when we tested the home() method in the HomeController, with one notable difference: we want to stub the method that is called in the service class. When we mock an object, what happens behind the scenes is that Mockito creates a subclass of that object where all of the methods simply return default values. This may be undesirable, especially since the "actual" value which we will make an assertion on will likely be a result of the class which we have mocked. To get around this, we can using stubbing to specify the exact value that we want to be returned when we invoke a method in a mocked class. We will use some special syntax to specify the method we wish to stub, as well as specify the value we want returned when the method is invoked. You will notice that the stub specifically starts with the when() method, but the test in its entirety should look like this:

@Test
void givenGoodQuery_whenGetResults_thenReturnListOfResults() {
    //given
    String query = "Java";
    Result result = new Result();
    result.setTitle("TITLE");
    result.setLink("LINK");
    result.setAuthors(Collections.singletonList("AUTHORS"));
    List<Result> expectedResults = Collections.singletonList(result);

    when(locService.getResults(query))
            .thenReturn(expectedResults);

    //when
    List<Result> actualResults = locController.getResults(query);

    //then
    assertEquals(expectedResults, actualResults);
}
                

The Unhappy Path

When our controller method is invoked, there are two possible outcomes: either the method returns a list of results, or the list of results that the controller method receives is empty and the application throws an exception results in a 404 status code being returned to the user. We have tested the "happy path", in which the user receives a list of results related to their query, but we still need to write a unit test that covers the case where they supplied a search term for which there are no results. Fortunately, this test is actually a lot more simple than the happy path. When a method that returns a list is called on a mocked object, the default behavior is an empty list being returned. Therefore we don't need to instantiate any objects or do any stubbing to receive the desired empty list which would simulate no results being returned from the searching Library of Congress. We will need to use some new syntax to catch the exception that is being thrown and check that the message is what we expected, but the unit test for the unhappy path should look like this:

@Test
    void givenBadQuery_whenGetResults_thenThrowsException() {
    //given
    String query = "Java";

    //when
    //then
    Throwable exceptionThrown = assertThrows(ResponseStatusException.class, () -> locController.getResults(query));
    assertEquals(exceptionThrown.getMessage(), "404 NOT_FOUND \"Result(s) not found.\"");
}
                    

Testing getResults() in the Service Class

Assuming you were consistent with your method names throughout the controller, service, and repository classes, the service class test will be nearly identical to controller class test. Remember that in a more complex application, there may be a lot of logic contained within the service layer, often times making it the most complex layer to test. However since our service class is merely calling a method in the repository class, the only real difference from our controller test is the classes on which we are calling the methods. Using all of the topics we have discussed so far, go ahead and complete the tests for the service class.

Notice that there is only one unit test to be written in the service layer, instead of the two that we saw in the controller class. This is because at the point the the application there is only a "happy path", since the list of Results is returned to the controller class regardless of whether or not it is empty.

Summary of Code Changes for this Milestone

    Cheetah-Search
    • src
      • main
        • java
          • org.jointheleague.level7.cheetah
            • config
              • ApiDocConfig.java
            • presentation
              • HomeController.java
              • LocController.java
            • service
              • LocService.java
            • repository
              • dto
                • Result.java
                • LocResponse.java
              • LocRepository.java
          • resources
            • application.yml
      • test
    • build.gradle