Spock is a testing framework based on the Groovy language but can also be used for regular Java projects. It is based on JUnit so running the test cases is as easy as running any regular JUnit tests, be it from an IDE or continuous integration servers. The framework aims to allow developers and testers to write specifications that are easily read and, ideally, may serve as documentation for actual code.
Spock is specially interesting in testing regular Java applications as it uses well many dynamic functionalities provided by Groovy, offering a nice set of options to write meaningful specifications. The specifications output is also nicely formatted to show what are the causes of the failure, as in:
Condition not satisfied:
max(a, b) == c
| | | | |
3 1 3 | 2
false
As in JUnit a Test Case is composed by a set of tests, in Spock a Specification is composed by a set of feature methods.
Feature methods are sub-divided in logical block, each of them containing a label, meaning and a set of statements.
The available blocks are:
given (or setup);
when
then
expect
where
Additionally, the block and can be used to connect statements inside a block without changing the test behaviour but improving the meaning and readability.
The basic block usage are:
- [given: <initialization>] when: <stimulus> then: <condition>
- [given: <initialization>] expect: <condition>
- [given: <initialization>] expect: <condition> where: <parameterization>
Spock Specifications use assertions implicitly, so the logical conditions must be placed in the logical blocks where they belong to.
For comparison, here are some test cases written in Java with JUnit and one equivalent Spock feature method for each of them:
@Test
public void testGetPriceProjectUsed() {
project.setTariff(10.0);
assertEquals(25f, activity.getActivityPrice(), 0f);
project.setTariff(20.0);
assertEquals(50f, activity.getActivityPrice(), 0f);
}
def "project's price is used to calculate activity's price"() {
when:
project.tariff = 10.0
then:
activity.activityPrice == 25.0
when:
project.tariff = 20.0
then:
activity.activityPrice == 50.0
}
where is a special block to parameterize a feature method.
In this block local variables used in the feature method have their values re-assigned so that the same method can be used to test the same feature in different situations. One single feature method runs as many times as the variable is re-assigned.
The assignments are done using lists literals or external/helper methods that return collections.
The following example tests the same thing than the previous test, but it is written using expect/where.
In this case, the feature method runs twice, each time with different values.
Check how the variables are actually defined inside the where block and used before it.
def "project's price is used to calculate activity's price - another way to test"() {
given:
project.tariff = newTariff
expect:
activity.activityPrice == expectedPrice
where:
newTariff << [10.0, 20.0]
expectedPrice << [25.0, 50.0]
}
Another simple example:
@Test
public void activityPriceDoesntChangeAfterClosing() {
project.setTariff(10.0);
assertEquals(25f, activity.getActivityPrice(), 0f);
activity.setStatus(ActivityStatus.CLOSED);
project.setTariff(20.0);
assertEquals(25f, activity.getActivityPrice(), 0f);
}
def "activity's price doesn't change after it is closed"() {
given:
project.tariff = 10.0
expect:
activity.activityPrice == 25.0
when:
activity.status = CLOSED
project.tariff = 20.0
then:
activity.activityPrice == 25f
}
Spock is also very useful when there's the need to use mocks, or interactions:
@Test public void projectOwnerCanNotDeleteEmptyProjectWithExpenses() throws Exception { expect(userService.getCurrentUser()).andReturn(user); expect(permissionDao.getProjectUserRole(user.getId(), project.getId())).andReturn( Role.PROJECT_OWNER); expect(projectDao.hasActivities(project)).andReturn(false); expect(expenseReportDao.hasExpenseItems(project)).andReturn(true); replay(userService, permissionDao, projectDao, expenseReportDao); try { projectService.deleteProject(project); fail(); } catch (BindException e) { verify(userService, permissionDao, projectDao, expenseReportDao); } }
def "project can't be deleted if it has expenses even if it has no activities"() { given: 1 * userService.getCurrentUser() >> user 1 * permissionDao.getProjectUserRole(user.id, project.id) >> Role.PROJECT_OWNER 1 * projectDao.hasActivities( { it.id == project.id } ) >> false 1 * expenseReportDao.hasExpenseItems(project) >> true when: service.deleteProject(project) then: 0 * projectDao.delete(project) thrown(BindException) }
The method can even be rewritten using helper methods (that can be re-used by other feature methods) so that it is even more readable:
def "project can't be deleted if it has expenses"() { given: currentUserIsProjectOwner() projectHasExpenses() projectHasNoActivities() when: service.deleteProject(project) then: projectIsNotDeleted() thrown(BindException) } private void currentUserIsProjectOwner() { 1 * userService.getCurrentUser() >> user 1 * permissionDao.getProjectUserRole(user.id, project.id) >> Role.PROJECT_OWNER } private void projectHasExpenses() { 1 * expenseReportDao.hasExpenseItems(project) >> true } private void projectHasNoActivities() { 1 * projectDao.hasActivities( { it.id == project.id } ) >> false } private void projectIsNotDeleted() { 0 * projectDao.delete(project) }
To use Spock in a Java project with Eclipse one has to install Groovy Eclipse Plug-in (available at http://groovy.codehaus.org/Eclipse+Plugin), add the Groovy nature to the project (which will import Groovy libs), and finally add Spock and JUnit 4.7 jar files to the build path.
That done, new Spock Specifications can already be added to the project and run from inside Eclipse as JUnit tests.
Conclusions
Spock seems to be a very interesting option for writing meaningful tests. Even though at first the division of feature methods in logical blocks might be a bit tricky for programmers who are used to just enumerate a sorted set of statements and assertions to write regular unit tests. But it shouldn't be a problem as it's very easy to understand the blocks concept and what each one should contain.
The resulting specifications look definitely more interesting than regular unit tests. This added to the ease to start using the framework make Spock a tempting alternative for writing tests if compared to the old boring styles.
But, in the other hand, bad unreadable tests can be written even using Spock Specifications. The same way, a good developer can write meaningful and readable tests even using plain JUnit.
In the end it all comes down to the ones operating the keyboard
which doesn't diminish Spock's very good job at what it intends.