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() {
}
}
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.