When it comes to testing, you must follow Test Driven Development principles which have great practices to keep your code and logic clean. One important thing to remember about testing, You must not include logic in your tests.
Rules of Test Driven Development
Integration testing allows you to spin up the server, hook up all components of your application, and you make a call to the endpoint. When this happens, you able to hit the flow of logic the endpoint is meant to execute. You can mock 3rd party api calls if you want.
For example. Let’s say you have an Activity entity. You have an endpoint that exposes a search for activities. This is how it is set up.
@RunWith(SpringRunner.class)
@SpringBootTest
@DirtiesContext
public class ControllerTest {
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Resource
private WebApplicationContext webApplicationContext;
@Autowired
private Filter springSecurityFilterChain;
@MockBean
private UserCacheService userCacheService;
@Before
public void setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).addFilters(springSecurityFilterChain).build();
ApiTokenSession apiTokenSession = new ApiTokenSession();
apiTokenSession.setPrimary(true);
apiTokenSession.setUserUuid("adminUuid");
apiTokenSession.setUserAuthorities(Arrays.asList("USER"));
apiTokenSession.setExpiredAt(DateUtils.addDays(new Date(), 1));
apiTokenSession.setDeviceId("test_agent");
when(userCacheService.findApiSessionToken("admin_token")).thenReturn(Optional.of(sideCarApiTokenSessionAdmin));
}
@Transactional
@Test
public void searchActivity_with_includes() throws Exception {
String memberUuid = "test-member-uuid-0e045fb7-038a-49e1-b49f-b0b1cec70939";
Activity activity = new Activity();
// account,member
activity.setEntityName("member");
activity.setDescription("A profile for {{entity}} was created by {{actor}}");
activity.setEntityUuid(memberUuid);
activity.setEntityLabel("Test");
activity.setType("CREATE");
activityRepository.saveAndFlush(activity);
activity = new Activity();
// account,member,expense
activity.setEntityName("member");
activity.setDescription("{{actor}} edited member {{entity}}");
activity.setEntityUuid(memberUuid);
activity.setEntityLabel("Zen Smith ASO-T");
activity.setType("EDIT");
activityRepository.saveAndFlush(activity);
activity = new Activity();
// account,member,expense
activity.setEntityName("member");
activity.setDescription("{{actor}} deleted member {{entity}}");
activity.setEntityUuid(memberUuid);
activity.setEntityLabel("Zen Smith ASO-T");
activity.setType("DELETE");
activityRepository.saveAndFlush(activity);
// @formatter:off
RequestBuilder requestBuilder = MockMvcRequestBuilders.get("/activity/search")
.header("token", "admin_token")
.param("entityName", "member")
.param("entityUuid", memberUuid)
.param("includeTypes", "CREATE","DELETE","EDIT")
.accept(MediaType.APPLICATION_JSON_UTF8);
MvcResult result = this.mockMvc.perform(requestBuilder)
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk()).andReturn();
String contentAsString = result.getResponse().getContentAsString();
CustomPage<Activity> activityResult = objectMapper.readValue(contentAsString, new TypeReference<CustomPage<Activity>>() {});
assertThat(activityResult).isNotNull();
List<Activity> activities = activityResult.getContent();
log.info("activities={}",activities);
assertThat(activities).isNotNull();
assertThat(activities.size()).isGreaterThan(0);
Optional<Activity> optActivity= activities.stream().filter(actvty -> actvty.getType().equals("CREATE") && actvty.getEntityUuid().equals(memberUuid)).findFirst();
assertThat(optActivity.isPresent()).isTrue();
optActivity= activities.stream().filter(actvty -> actvty.getType().equals("EDIT") && actvty.getEntityUuid().equals(memberUuid)).findFirst();
assertThat(optActivity.isPresent()).isTrue();
optActivity= activities.stream().filter(actvty -> actvty.getType().equals("DELETE") && actvty.getEntityUuid().equals(memberUuid)).findFirst();
assertThat(optActivity.isPresent()).isTrue();
// @formatter:on
}
}
Testing endpoints(just the controller web layer) with MockMvc and Mockito
@RunWith(SpringRunner.class)
@WebMvcTest(UserController.class)
public class UserRestControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
public void testUserSave() throws Exception {
User user = new User("kinga", "kaveinga", 21, "kinga@gmail.com");
User savedUser = new User(1, "kinga", "kaveinga", 21, "kinga@gmail.com");
/**
* thenReturn or doReturn() are used to specify a value to be returned <br/>
* upon method invocation.
*/
when(userService.save(user)).thenReturn(savedUser);
this.mockMvc.perform(post("/users").contentType(MediaType.APPLICATION_JSON).content(user.toJson()))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.firstName", is("kinga")))
.andExpect(jsonPath("$.id", is(1)));
verify(userService, times(1)).save(user);
verifyNoMoreInteractions(userService);
}
@Test
public void testGetUserById() throws Exception {
User mockUser = new User("folau", 21, "fkaveinga@gmail.com");
when(userService.getById(1)).thenReturn(mockUser);
this.mockMvc.perform(get("/users/1").contentType(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(jsonPath("$.firstName", is("folau")))
.andExpect(jsonPath("$.age", is(21)));
}
@Test
public void testGetAllUsers() throws Exception {
List<User> users = Arrays.asList(new User("folaulau", 21, "folaulau@gmail.com"), new User("kinga", 21, "kinga@gmail.com"));
when(userService.getAll()).thenReturn(users);
this.mockMvc.perform(get("/users").contentType(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].firstName", is("folaulau")))
.andExpect(jsonPath("$[0].age", is(21)));
}
@Test
public void testUpdateUser() throws Exception {
User user = new User("kinga", "kaveinga", 21, "kinga@gmail.com");
User savedUser = new User(1, "kinga", "kaveinga", 21, "kinga@gmail.com");
when(userService.update(user)).thenReturn(savedUser);
this.mockMvc.perform(patch("/users/update").contentType(MediaType.APPLICATION_JSON).content(user.toJson()))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.firstName", is("kinga")))
.andExpect(jsonPath("$.id", is(1)));
}
@Test
public void testRemoveUser() throws Exception {
long id = 1;
when(userService.remove(id)).thenReturn(true);
this.mockMvc.perform(delete("/users/" + id)).andDo(print()).andExpect(status().isOk()).andExpect(MockMvcResultMatchers.content().string("true"));
}
}
Unit Testing business logic with Mockito
@Test
public void testSignUp() throws Exception {
log.info("testSignUp()");
User user = ConstantUtils.generateUser();
when(userDAO.save(any(User.class))).thenReturn(user);
when(userNtcService.sendWelcomeEmail(any(User.class))).thenReturn(true);
User signedUpUser = userService.signUp(user);
InOrder inOrder = Mockito.inOrder(userDAO, userNtcService);
inOrder.verify(userDAO, times(1)).save(userCaptor.capture());
assertEquals(user, signedUpUser);
log.info("user captor email={}", userCaptor.getValue().getEmail());
assertThat(userCaptor.getValue()).isSameAs(signedUpUser);
log.info("testSignUp() - passed\n\n");
}
Testing DAO(Database Access Object)
@RunWith(SpringRunner.class)
@DataJpaTest
public class UserRepositoryTest {
private final Logger log = LoggerFactory.getLogger(this.getClass());
@Autowired
private UserRepository userRepository;
List<User> testUsers = new ArrayList<User>();
@Before
public void setup() {
for (int i = 0; i < 7; i++) {
User user = ConstantUtils.generateUser();
user = userRepository.saveAndFlush(user);
testUsers.add(user);
// log.info("user={}",ObjectUtils.toJson(user));
}
System.out.println("\n");
}
@Transactional
@Test
public void testSaveUser() {
User user = ConstantUtils.generateUser();
log.info("user={}", ObjectUtils.toJson(user));
User savedUser = userRepository.saveAndFlush(user);
log.info("savedUser={}", ObjectUtils.toJson(savedUser));
assertNotNull(savedUser);
log.info("savedUser==user -> {}", savedUser == user);
assertEquals(user, savedUser);
long count = userRepository.count();
log.info("count={}", count);
assertEquals(8, count);
log.info("\n\ntestSave passed\n");
}
@Transactional
@Test
public void testFindByName() throws InterruptedException, ExecutionException {
log.info("testFindByName({})", testUsers.get(0).getFirstName());
String lastName = testUsers.get(0).getLastName();
List<User> savedUsers = userRepository.findByLastName(lastName);
log.info(savedUsers.toString());
assertNotNull(savedUsers);
assertNotEquals(savedUsers.size(), 0);
log.info("\n\ntestFindByName passed\n");
}
@Transactional
@Test
public void testFindByEmail() throws InterruptedException, ExecutionException {
log.info("testFindByEmail({})", testUsers.get(0).getEmail());
User savedUser = userRepository.findByEmail(testUsers.get(0).getEmail());
assertNotNull(savedUser);
assertNotEquals(savedUser.getId().longValue(), 0);
System.out.println(savedUser.toString());
log.info("\n\ntestFindByEmail passed\n");
}
}
Spy
Spy is a wrapper which is used to override method(s) and return value(s) and ignore logic with those methods.
@Spy
private UserService spyUserService = new UserServiceImp();
@Test
public void test_SignUpWithPlanAndSpy() throws Exception {
log.info("test_SignUpWithPlanAndSpy()");
User user = null;
double planAmount = 200.0;
Plan plan = new Plan(1L, planAmount, user);
planAmount = 500.0;
Plan savedPlan = new Plan(1L, planAmount, user);
// This call is not actually made. Execution flow does not get within the signUpForPlan(check the log within the
// method)
lenient().doReturn(savedPlan).when(spyUserService).signUpForPlan(plan);
assertThat(savedPlan.getAmount()).isEqualTo(planAmount);
log.info("test_SignUpWithPlanAndSpy() - passed!");
}