Search code examples
javaspring-bootunit-testingjunitmockito

JUnit Tests Mocked Objects are returning null values


I am trying to run JUnit tests on a Service layer that uses a Configuration Class and a RestTemplate Class. However, whenever I try to run a test, I am getting null values for my configuration and for my Service methods' return values.

Here is my Service class:

package com.blogposts.assessment.restapi;

import com.blogposts.assessment.classes.Post;
import com.blogposts.assessment.classes.Posts;
import com.blogposts.assessment.exceptions.BadInputException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

/*
This is a Service component in Spring Boot that implements the business logic for the api. It de-duplicates posts in
the list and implements sort based on the parameters received from the rest controller.
 */

@Service
public class APIService {

    private final RestTemplate restTemplate;
    private final APIConfiguration apiConfig;

    //This looks for the beans that were created for the APIConfiguration and the RestTemplate so it utilizes
    //the same instance of those beans.
    @Autowired
    public APIService(RestTemplate restTemplate, APIConfiguration apiConfig) {
        this.restTemplate = restTemplate;
        this.apiConfig = apiConfig;
    }

    //Implements business logic on Get Request
    public Posts getBlogPosts(String tags, String sortBy, String direction) throws ExecutionException, InterruptedException {

        Posts consolidatedPosts = getBlogPostsWithMultipleTags(tags);
        List<Post> listOfPosts = consolidatedPosts.getPosts();

        //If direction is "asc" or blank, it sorts in an ascending manner
        if (direction.equals("asc") || direction.equals("")) {

            //Switch used to select how to sort
            switch (sortBy) {
                case "id":
                    Collections.sort(listOfPosts, Post.compareById);
                    break;
                case "":
                    Collections.sort(listOfPosts, Post.compareById);
                    break;
                case "likes":
                    Collections.sort(listOfPosts, Post.compareByLikes);
                    break;
                case "reads":
                    Collections.sort(listOfPosts, Post.compareByReads);
                    break;
                case "popularity":
                    Collections.sort(listOfPosts, Post.compareByPopularity);
                    break;
                default:
                    throw new BadInputException("sortBy parameter is invalid");
            }
        }

        //If direction is "desc", posts are sorted in reverse order.
        else if (direction.equals("desc")) {
            switch (sortBy) {
                case "id":
                    Collections.sort(listOfPosts, Post.compareById.reversed());
                    break;
                case "":
                    Collections.sort(listOfPosts, Post.compareById.reversed());
                    break;
                case "likes":
                    Collections.sort(listOfPosts, Post.compareByLikes.reversed());
                    break;
                case "reads":
                    Collections.sort(listOfPosts, Post.compareByReads.reversed());
                    break;
                case "popularity":
                    Collections.sort(listOfPosts, Post.compareByPopularity.reversed());
                    break;
                default:
                    throw new BadInputException("sortBy parameter is invalid");
            }
        }

        //Throws exception if direction parameter is not desc, asc, or blank.
        else {
            throw new BadInputException("direction parameter is invalid");
        }
        return consolidatedPosts;
    }

    //This method conducts multiple get requests to the Hatchways API based on the number of tags included and
    //de-duplicates the posts so that no post is repeated.
    public Posts getBlogPostsWithMultipleTags(String tags) throws ExecutionException, InterruptedException {

        //Checks to see if the tags parameter is left blank or is null
        if(tags.equals("")) {
            throw new BadInputException("The tags parameter is missing and cannot be null.");
        }

        //Splits the comma separated tags into an array of Strings
        String[] tagsArray = tags.split(",");

        //Utilizing a hashset to store each posts' id if it's added to the Posts object's List.
        //Hashset was used because lookup time is O(1) with the blog id value.
        Posts combinedPosts = new Posts();
        Posts[] separatePosts = new Posts[tagsArray.length];
        HashSet<Long> consolidatedPostIDs = new HashSet<Long>();

        //Iterate through each tag and run a get request to the Hatchways API
        for (int i = 0; i < tagsArray.length; i++) {
            String url = apiConfig.getApiUrl() + "/?tag=" + tagsArray[i];
            CompletableFuture<Posts> postsFuture = getBlogPostWithOneTag(tagsArray[i]);
            separatePosts[i] = postsFuture.get();
        }

        // Loop through the Posts[] array to review each Posts object. Look at an individual post within each Posts object
        // For each blog individual post, check to see if it's already in our combinedPosts object. If it is, ignore it, otherwise add
        // it to the hashset and the combinedPosts object.
        for (Posts posts : separatePosts) {
            for(int i = 0; i < posts.getPosts().size(); i++) {
                Post currentPost = posts.getPosts().get(i);
                if(consolidatedPostIDs.contains(currentPost.getId())) {
                    continue;
                } else {
                    combinedPosts.addPost(currentPost);
                    consolidatedPostIDs.add(currentPost.getId());
                }
            }
        }

        return combinedPosts;
    }

    @Async
    public CompletableFuture<Posts> getBlogPostWithOneTag(String tag){
        String url = apiConfig.getApiUrl() + "/?tag=" + tag;
        Posts posts = restTemplate.getForObject(url,Posts.class);
        return CompletableFuture.completedFuture(posts);
    }

}

I am trying to test this with a Junit test and Mockito but when the canGetBlogPosts() test is run, it prints out null values for both of the System.out.println() calls.

package com.blogposts.assessment.restapi;

import com.blogposts.assessment.classes.Posts;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.MockitoJUnitRunner;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.client.RestTemplate;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

import static org.junit.jupiter.api.Assertions.assertNotNull;

@ExtendWith(MockitoExtension.class)
public class APIServiceTest {

    @Mock
    private RestTemplate restTemplate;

    @Mock
    private APIConfiguration apiConfiguration;
    private APIService underTest;

    @BeforeEach
    void setUp() {
        underTest = new APIService(restTemplate,apiConfiguration);
    }

    @Test
    void canGetBlogPosts() throws ExecutionException, InterruptedException {
        //when
        System.out.println(apiConfiguration.getApiUrl());
        CompletableFuture<Posts> posts = underTest.getBlogPostWithOneTag("science");
        System.out.println(posts.get());

    }

    @Test
    @Disabled
    void getBlogPostsWithMultipleTags() {
    }

    @Test
    @Disabled
    void getBlogPostWithOneTag() {
    }
}

Solution

  • It's very simple. You are creating your mocks, but you are not configuring them using the special mockito when-then syntax.

    Here's how you can do it.

    You can do it in the @BeforeEach method, if you'd like the mocks to behave in the same way for each test. Or you can configure them individually for each test.

    Here's the basic syntax. You can get the details here.

    List mockList = Mockito.mock(ArrayList.class);
    Mockito.when(mockList.size()).thenReturn(100);
    
    

    Also you might need to add this to get the mocks injected by MockitoExtension

    @Before
    public void init() {
        MockitoAnnotations.initMocks(this);
    }
    

    Does this solve your problem ? Let me know in the comments.