Java: Multithread testing using JUnit
We all know that Java offers a mechanism to avoid race conditions by synchronizing thread access to shared data: the synchronized keyword. But what if you want to test a synchronized method using JUnit? Let’s do this.
Introduction
Testing multithreaded code is something tricky to do. Since I’m a great fan of TDD, I’m always looking to reach the maximum test coverage in every project that I take part. And testing a synchronized method is important not only to guarantee that the system behaves the way you expect, but it helps you to have a better comprehension of what’s going on.
The test project
I’ve wrote a small project using Spring Boot to illustrate the following situation: how can a system gracefully handle concurrent threads trying to persist the same object to the database?
This is a situation that you could face when writing a REST API to manage reservations for a hotel, for example. You don’t want the system to crack when two different people try to book the same dates, right?
The classes
Suppose we have the following table. Note that we have a UNIQUE constraint to prevent duplicate combinations of user and email:
CREATE TABLE IF NOT EXISTS `user` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(200) NOT NULL,
`email` varchar(200) NOT NULL,
PRIMARY KEY(`id`),
UNIQUE(`name`, `email`)
)
This is the entity class for it:
package com.tiago.entity;
import java.util.Objects;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
/**
* Entity for table "User".
*
*
* @author Tiago Melo (tiagoharris@gmail.com)
*
*/
@Entity(name = "user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String email;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof User)) {
return false;
}
User user = (User) o;
if (!Objects.equals(getId(), user.getId())) {
return false;
} else if (!Objects.equals(getName(), user.getName())) {
return false;
} else if (!Objects.equals(getEmail(), user.getEmail())) {
return false;
}
return true;
}
@Override
public int hashCode() {
return Objects.hash(getId());
}
}
We are using this Spring Data JPA repository:
package com.tiago.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.tiago.entity.User;
/**
* Repository for {@link User} entity.
*
* @author Tiago Melo (tiagoharris@gmail.com)
*
*/
@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
}
This is the service class:
package com.tiago.service;
import com.tiago.entity.User;
/**
* Service to manage users.
*
* @author Tiago Melo (tiagoharris@gmail.com)
*
*/
public interface UserService {
/**
* Saves a user
*
* @param user to be saved
* @return the saved user
*/
User save(User user);
}
… and its implementation, which we want to test:
package com.tiago.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.tiago.entity.User;
import com.tiago.repository.UserRepository;
import com.tiago.service.UserService;
/**
* Implements {@link UserService} interface.
*
* @author Tiago Melo (tiagoharris@gmail.com)
*
*/
@Service
public class UserServiceImpl implements UserService {
@Autowired
UserRepository userRepository;
/* (non-Javadoc)
* @see com.tiago.service.UserService#save(com.tiago.entity.User)
*/
@Override
public User save(User user) {
userRepository.save(user);
return user;
}
}
Sure, the UserServiceImpl#save() method should be marked as synchronized in order to prevent errors by concurrent threads trying to persist the same User object. But let’s write a multithreaded test to reproduce this scenario.
The test class
This is how you test the service class mentioned above.
package com.tiago.service.impl;
import static org.junit.Assert.assertEquals;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.junit4.SpringRunner;
import com.tiago.entity.User;
import com.tiago.repository.UserRepository;
import com.tiago.service.UserService;
/**
* This class represents a test case of multiple threads attempting to
* save the same User.
*
* @author Tiago Melo (tiagoharris@gmail.com)
*
*/
@RunWith(SpringRunner.class)
@DataJpaTest
public class UserServiceImplMultithreadedTest {
@Autowired
UserService service;
@Autowired
UserRepository userRepository;
AtomicInteger threadExecutionCount = new AtomicInteger();
@Test
public void testSaveMethod() throws InterruptedException, ExecutionException {
// Number of threads that will try to persist the same User object
int threadCount = 10;
// The User object
User newUser = buildUser();
// A task that persists the User object
Callable<User> task = getCallable(newUser);
// A list of the task mentioned above
List<Callable<User>> tasks = Collections.nCopies(threadCount, task);
// The thread pool that will execute all tasks from the list
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
// Here we are asking to execute all the tasks
List<Future<User>> futures = executorService.invokeAll(tasks);
// This set will hold the persisted User object
HashSet<User> userSet = new HashSet<User>();
// Here we will check for exceptions
for (Future<User> future : futures) {
// Counts the number of threads that were executed
threadExecutionCount.incrementAndGet();
// future.get() will throw java.util.concurrent.ExecutionException if an exception was
// thrown by any task.
//
// If UserServiceImpl.save(User user) method were not
// synchronized, a task would throw a org.springframework.dao.DataIntegrityViolationException.
//
// The User is stored only once at the database; future.get() returns the
// same object.
userSet.add(future.get());
}
// Since a Set does not stores duplicated objects, this set will contain only
// one User object.
assertEquals(1, userSet.size());
// Here we assure that all threads were executed
assertEquals(threadExecutionCount.get(), threadCount);
// There will be only one User object in the database
assertEquals(newUser, userSet.iterator().next());
}
private Callable<User> getCallable(User newUser) {
Callable<User> task = new Callable<User>() {
@Override
public User call() {
return service.save(newUser);
}
};
return task;
}
private User buildUser() {
User newUser = new User();
newUser.setName("Steve Harris");
newUser.setEmail("steve@ironmaiden.com");
return newUser;
}
}
If we run it, the test will fail as expected:
This is the detailed stack trace:
java.util.concurrent.ExecutionException:
org.springframework.dao.DataIntegrityViolationException:
could not execute statement; SQL [n/a];
constraint ["CONSTRAINT_INDEX_2 ON PUBLIC.USER(NAME, EMAIL)
VALUES ('Steve Harris', 'steve@ironmaiden.com', 1)";
SQL statement: insert into user (id, email, name) values (null, ?, ?)
[23505-197]]; nested exception is
org.hibernate.exception.ConstraintViolationException:
could not execute statement
This happens because UserServiceImpl#save() method is not marked as synchronized.
Let’s make this test pass: we will expect an ExecutionException to be thrown.
@Test(expected = ExecutionException.class)
public void testSaveMethod() throws InterruptedException, ExecutionException {
// Number of threads that will try to persist the same User object
int threadCount = 10;
// The User object
User newUser = buildUser();
// A task that persists the User object
Callable < User > task = getCallable(newUser);
// A list of the task mentioned above
List < Callable < User >> tasks = Collections.nCopies(threadCount, task);
// The thread pool that will execute all tasks from the list
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
// Here we are asking to execute all the tasks
List < Future < User >> futures = executorService.invokeAll(tasks);
// This set will hold the persisted User object
HashSet < User > userSet = new HashSet < User > ();
// Here we will check for exceptions
for (Future < User > future: futures) {
// Counts the number of threads that were executed
threadExecutionCount.incrementAndGet();
// future.get() will throw java.util.concurrent.ExecutionException if an exception was
// thrown by any task.
//
// If UserServiceImpl.save(User user) method were not
// synchronized, a task would throw a org.springframework.dao.DataIntegrityViolationException.
//
// The User is stored only once at the database; future.get() returns the
// same object.
userSet.add(future.get());
}
// Since a Set does not stores duplicated objects, this set will contain only
// one User object.
assertEquals(1, userSet.size());
// Here we assure that all threads were executed
assertEquals(threadExecutionCount.get(), threadCount);
// There will be only one User object in the database
assertEquals(newUser, userSet.iterator().next());
}
It will pass as expected:
Alright. Time to make it right. Let’s mark UserServiceImpl#save() method as synchronized.
package com.tiago.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.tiago.entity.User;
import com.tiago.repository.UserRepository;
import com.tiago.service.UserService;
/**
* Implements {@link UserService} interface.
*
* @author Tiago Melo (tiagoharris@gmail.com)
*
*/
@Service
public class UserServiceImpl implements UserService {
@Autowired
UserRepository userRepository;
/* (non-Javadoc)
* @see com.tiago.service.UserService#save(com.tiago.entity.User)
*/
@Override
public synchronized User save(User user) {
userRepository.save(user);
return user;
}
}
Now let’s change our test: we do not expect any exception to occur, and only one User object should be persisted even if 10 threads attempts to save the same object.
package com.tiago.service.impl;
import static org.junit.Assert.assertEquals;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.junit4.SpringRunner;
import com.tiago.entity.User;
import com.tiago.repository.UserRepository;
import com.tiago.service.UserService;
/**
* This class represents a test case of multiple threads attempting to
* save the same User.
*
* @author Tiago Melo (tiagoharris@gmail.com)
*
*/
@RunWith(SpringRunner.class)
@DataJpaTest
public class UserServiceImplMultithreadedTest {
@Autowired
UserService service;
@Autowired
UserRepository userRepository;
AtomicInteger threadExecutionCount = new AtomicInteger();
@Test
public void testSaveMethod() throws InterruptedException, ExecutionException {
// Number of threads that will try to persist the same User object
int threadCount = 10;
// The User object
User newUser = buildUser();
// A task that persists the User object
Callable<User> task = getCallable(newUser);
// A list of the task mentioned above
List<Callable<User>> tasks = Collections.nCopies(threadCount, task);
// The thread pool that will execute all tasks from the list
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
// Here we are asking to execute all the tasks
List<Future<User>> futures = executorService.invokeAll(tasks);
// This set will hold the persisted User object
HashSet<User> userSet = new HashSet<User>();
// Here we will check for exceptions
for (Future<User> future : futures) {
// Counts the number of threads that were executed
threadExecutionCount.incrementAndGet();
// future.get() will throw java.util.concurrent.ExecutionException if an exception was
// thrown by any task.
//
// If UserServiceImpl.save(User user) method were not
// synchronized, a task would throw a org.springframework.dao.DataIntegrityViolationException.
//
// The User is stored only once at the database; future.get() returns the
// same object.
userSet.add(future.get());
}
// Since a Set does not stores duplicated objects, this set will contain only
// one User object.
assertEquals(1, userSet.size());
// Here we assure that all threads were executed
assertEquals(threadExecutionCount.get(), threadCount);
// There will be only one User object in the database
assertEquals(newUser, userSet.iterator().next());
}
private Callable<User> getCallable(User newUser) {
Callable<User> task = new Callable<User>() {
@Override
public User call() {
return service.save(newUser);
}
};
return task;
}
private User buildUser() {
User newUser = new User();
newUser.setName("Steve Harris");
newUser.setEmail("steve@ironmaiden.com");
return newUser;
}
}
It passes as expected:
Conclusion
Through this simple example we learnt to test the multithreaded code using JUnit. It’s a little bit tricky but it pays the price: now we can assure that the system will behave as expected.
Download the source code
Here: https://bitbucket.org/tiagoharris/multithread-test/src/master/