Basics Of Testing In Java
I often start teaching students coding using test driven development, or TDD for short. This is not because I'm a hard-core believer in TDD, but because I think it's a great tool to teach novice (even intermediate) programmers to build robust code. TDD, by design, helps you write a decoupled (read not a mess) and easy to maintain code without putting too much effort (apart from learning a test framework). Having tested code as a result is just the added benefit of TDD.
As one of the materials I teach is Java's Spring framework, I find that my students get often confused by the sheer number of annotations they need to learn. In this post, I want to briefly explain some of those annotations using the JUnit and Mockito libraries. Hopefully it helps someone to understand the concepts better. You can also always get in touch if you want to better understand this subject.
For our examples, let's use a simple Student
model such as:
@Entity
@Getter
@Setter
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "first_name", nullable = false, length = 64)
private String firstName;
@Column(name = "last_name", nullable = false, length = 128)
private String lastName;
@Column(name = "nickname", nullable = false)
private String nickname;
}
We'll use use StudentService which will be responsible for saving students to a database:
@Service
@RequiredArgsConstructor
public class StudentService {
private final StudentRepository repository;
private final NicknameService nicknameService;
public StudentService(StudentRepository repository, NicknameService service) {
this.repository = repository;
this.nicknameService = service;
}
public Student save(Student student) {
student.setNickname(nicknameService.retrieveNicknameFromApi(student.getFirstName()));
return repository.save(student);
}
}
As you can see in StudentService, there are two fields; StudentRepository
and NicknameService
.
StudentRepository
is just the underlying object which will handle the conversation with the database.
NicknameService
, on the other hand, is a class for which we'll pretend that calls an external API service to
retrieve a nickname from the student's first name:
@Service
public class NicknameService {
public String retrieveNicknameFromApi(String firstName) {
// let's imagine this is an API call
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return firstName + "y";
}
}
The classes above are the basis on top of which we'll develop unit and integration tests further down in the article. First, I'll focus on explaining what dependencies are, which will become relevant for understanding tests. Later, I'll explain some basics of testing, including various annotations used in testing that my students usually struggle with.
Dependencies
In simple terms, when you write some code which invokes another piece of code (such as another method or make a call to external service), you start depending on that other code/service. If your object, for example, invokes another method inside the same object, you'll typically have a full control of that dependency. Same is not true if your object invokes an external service. No matter which type of dependency it is, because your code (method) depends on it, changes to dependencies can break your method. Having said that, we have multiple levels of testing: unit testing, integration testing, acceptance testing etc.
In this article I'll focus on unit testing only. Unit testing tests one unit of code, which in object oriented programming would typically mean one method or one class. I will use class. That means, in unit testing I'll want to focus on testing one class, and I won't (and shouldn't) care about any code that my class in question depends on.
In order for StudentService
class above to provide any functionalities, it needs two extra objects: StudentRepository
and NicknameService
which we call dependencies of StudentService
that are forced by the StudentService
's constructor.
Therefore, to write a unit test for StudentService
, it turns out that we need to set up multiple objects on which StudentService
depends.
At times, creating these dependencies will be easy, but at other times such task may be complicated.
For example, NicknameService
itself could depend on some other objects, which would require setting those up before creating a functional NicknameService
object.
In addition, NicknameService
may want to call some external services (which may live on another server which you don't have under control) which complicates your job of testing StudentService
.
Moreover, when you write a unit test, all you want to do is to test StudentService
and not its dependencies.
You trust that those dependencies are tested by someone's else tests, in some other places, and that those dependencies are of good quality.
Unit testing
Here is our first unit test:
@RunWith(MockitoJUnitRunner.class)
public class StudentServiceTest {
@Mock
StudentRepository repository;
@Mock
NicknameService nicknameService;
@InjectMocks
StudentService service;
private Student student;
@Before
public void setup() {
setupStudent();
}
@Test
public void testSave() {
when(nicknameService.retrieveNicknameFromApi("George")).thenReturn("Georgey");
when(repository.save(any())).thenReturn(student);
Student result = service.save(student);
assertEquals(student.getFirstName(), result.getFirstName());
assertEquals(student.getLastName(), result.getLastName());
assertEquals("Georgey", result.getNickname());
}
private void setupStudent() {
student = new Student();
student.setFirstName("George");
student.setLastName("Osborne");
}
}
You can see the following annotations: @Mock
, @InjectMocks
, @Before
and @Test
.
Let's disect each one of them.
@Mock annotation
Mock is a special object that can replace another object which you don't have an intention to test.
Integration testing
code