Sharing Our Passion for Technology
& Continuous Learning
Test Driven Groovy: StubFor
After years of being immersed in Java development, I must admit that I got spoiled by its strong and mature ecosystem. Hence, whenever I want to pick up a new technology or programming language the following must be there:
- Support by my favorite IDE (Eclipse or IntelliJ IDEA)
- Mature building framework. It does not have to be Maven or Gradle but it needs to be at least better than Ant.
- Easy TDD. This could be the trickiest one to achieve because not only do I need a testing framework, but it must also be supported by my IDE and build tool. Moreover, it must have an adequate mocking framework.
What to test?
To demonstrate the ease of testing let's say we want to unit testStudentScoreService
before implementing StudentRepository
:
class StudentScoreService {
private StudentRepository studentRepository
String getStudentStatus(studentId) {
studentRepository = new StudentRepository()
def student = studentRepository.findStudent(studentId)
if (student == null) {
return "StudentId#$studentId was not found"
}
def score = student.score
def status = score > 200 ? "PASS" : "FAIL";
return "$student.fullName: $status"
}
}
class StudentRepository {
Student findStudent(studentId) {
throw new UnsupportedOperationException()
}
}
class Student {
int studentId
String fullName
double score
List<String> courses
}
StubFor: demand
and use
Now, the first challenge with testing the service is the fact that is constructs its own instance of the repository. In Java land, I'd remedy this by using the Factory Method pattern or dependency injection. But guess what? Groovy has a third option called StubFor
.
import groovy.mock.interceptor.StubFor
class StudentScoreServiceTest1 extends GroovyTestCase {
private StudentScoreService service
StubFor repoStub
@Override
void setUp() {
//First, let's create a StubFor object to use in place of StudentRepository
repoStub = new StubFor(StudentRepository)
//Now, whenever findStudent is called, it will return null
repoStub.demand.findStudent { studentId ->
return null
}
service = new StudentScoreService()
}
void testNonExistingStudentStatus() {
//To use the stub, let's wrap our test with repoStub.use
repoStub.use {
assertEquals("StudentId#1 was not found", service.getStudentStatus(1))
}
}
}
As you can see, before the unit test method is called we are creating a StubFor
object (in the setup method) to use in place of the real StudentRepository
. By wrapping the test with repoStub.use
, all calls to new StudentRepository()
will be replaced with our stub. And that's it!
StubFor: Cardinality and verify
When we stub a method, the stubbed method is assigned a cardinality as a Groovy Range
. Cardinality is how many times the stubbed method should be called.
When we call repoStub.demand.findStudent
, it is assigned the default cardinality of (1..1) which means findStudent
should be called exactly once. It's equivalent to repoStub.demand.findStudent(1..1)
. But To enforce such expectation, we must add a call to verify
at the end of the test method.
Before we start using verify
, let's see what happens if we do NOT use verify
:
- If
findStudent
is never called, then the test passes - If
findStudent
is called once, then the test passes - If
findStudent
is called more than once, then the test fails
....
//PASS
void test_noVerify_FindStudentNeverCalled() {
repoStub.use {
}
}
//PASS
void test_noVerify_FindStudentCalledOnce() {
repoStub.use {
assertEquals("StudentId#1 was not found", service.getStudentStatus(1))
}
}
//FAIL with "No more calls to 'findStudent' expected at this point. End of demands."
void test_noVerify_FindStudentCalledMoreThanOnce() {
repoStub.use {
assertEquals("StudentId#1 was not found", service.getStudentStatus(1))
assertEquals("StudentId#2 was not found", service.getStudentStatus(2))
}
}
If we do use verify
, then:
- If
findStudent
is never called, then the test fails - If
findStudent
is called once, then the test passes - If
findStudent
is called more than once, then the test fails
....
//FAIL with "expected 1..1 call(s) to 'findStudent' but was called 0 time(s)"
void test_withVerify_FindStudentNeverCalled() {
repoStub.use {
}
repoStub.verify()
}
//PASS
void test_withVerify_FindStudentCalledOnce() {
repoStub.use {
assertEquals("StudentId#1 was not found", service.getStudentStatus(1))
}
repoStub.verify()
}
//FAIL with "No more calls to 'findStudent' expected at this point. End of demands."
void test_withVerify_FindStudentCalledMoreThanOnce() {
repoStub.use {
assertEquals("StudentId#1 was not found", service.getStudentStatus(1))
assertEquals("StudentId#2 was not found", service.getStudentStatus(2))
}
repoStub.verify()
}
In other words, verify will enforce the minimum and maximum number of calls to the stubbed method. While not using verify, will only enforce the maximum number of calls to the stubbed method.