I’ve been writing Unit Tests with Spock and Groovy on and off for a couple of years now. I’ve often had to check the properties of object argument parameters that are passed to methods on mocked objects. I’ve used a number of approaches to this, but realised I didn’t fully understand the main differences between them (they all worked fine and did what they were supposed to). So I decided to take a few minutes to mock up a simple test system and try each one in isolation to improve my understanding.
My simple test system consisted of 4 very simple Groovy classes.
- User.groovy – User POJO (or rather POGO)
- Controller.groovy – this is the class we will test
- Service.groovy – service interface we will mock
- ControllerTest.groovy – our Spock Unit Test class
Source code of these sample classes is shown at the very end of the article for reference.
Unit Test Overview
These classes are deliberately simple. We call the controller to register a new user – supplying username and email. The controller will create a User object with these values and pass it to the Service to do the actual business logic.
All we are concerned about here is testing the Controller. We can mock in the Service (which is why its only defined as an interface here – we don’t care about the implementation for now). In terms of the unit testing – we just need to ensure that the controller correctly creates the User object and passes it on to the service.
(Note: in a real world scenario we would also build in tests for error scenarios and the like – but that’s not important for the purposes of this article).
Spock Argument Constraints
To test that the user object the controller passes to the service is what we expect – we can use ‘argument constraints’. These can be very simple constraints – examples shown below:
1 * mockService.register(_) >> 'id1' // checks that we call the register method on the service called exactly once (strict mocking) and passed any parameter 1 * mockService.register(_ as User) >> 'id1' // verifies that the parameter is of the User class 1 * mockService.register(!null) >> 'id1' // verifies that the parameter is not null
However, we really want to be more precise – and verify the actual properties of the User object passed to the Service match what we originally passed into the Controller. In this instance we can use:
1 * mockService.register({User u -> u.name == 'Jimbo' && u.email == 'jb@test.com'}) >> 'id1'
This same code can be shortened to:
1 * mockService.register({it.name == 'Jimbo' && it.email == 'jb@test.com'}) >> 'id1'
You can mix and match these closure style constraints with others. For example, the subscribe method contains 2 parameters. You could combine constraints like this:
1 * mockService.subscribe({it.name == 'Jimbo'}, "computer studies") 1 * mockService.subscribe({it.name == 'Jimbo' >> it.email == 'jb@test.com'}, !null) 1 * mockService.subscribe(_, !null)
Spock Argument Capture
Similar to argument constraints is argument capture. The syntax for this is slightly different. In the past I’ve used a combination of both to verify argument parameters match what I expect. However, after digging into it a bit more – if that’s all you want you want to do – then I think argument constraints (as outlined above) are a better and cleaner solution than the solution below.
Argument capture is still valid – and gives you more power (if you need it). With the argument capture approach you have programmatic access to the parameter values – and can change them and use them in other ways if you need to (e.g. setting other variables, use to call other methods). A couple of examples are included in the Unit Test below.
Full ControllerTest Example
import spock.lang.Specification /** * Unit tests for {@link Controller} */ class ControllerTest extends Specification { private static final String USERNAME = 'Duggie' private static final String EMAIL = 'Duggie@test.com' /** * Long hand example of using an Object Argument Constraint in Spock. */ def 'test_register_using_explicit_argument_constraint'(){ given: 'a mocked service' def mockService = Mock(Service) def controller = new Controller() controller.service = mockService when: 'controller registers a new user' controller.register(USERNAME, EMAIL) then: 'service is called with expected parameters' 1 * mockService.register({User u -> u.name == USERNAME && u.email == EMAIL}) } /** * Short hand version of code above, using implicit 'it'. */ def 'test_register_using_implicit_argument_constraint'(){ given: 'a mocked service' def mockService = Mock(Service) def controller = new Controller() controller.service = mockService when: 'controller registers a new user' controller.register(USERNAME, EMAIL) then: 'service is called with expected parameters' 1 * mockService.register({it.name == USERNAME && it.email == EMAIL}) } /** * Using Argument Capture approach - this is not as neat for verifying contents of arguments * (which is what argument constraints are for), but it does also allow you to manipulate those * arguments if you need to */ def 'test_register_using_argument_capture'(){ given: 'a mocked service' def mockService = Mock(Service) def test def controller = new Controller() controller.service = mockService when: 'controller registers a new user' controller.register(USERNAME, EMAIL) then: 'service is called with expected parameters' 1 * mockService.register(_ as User) >> {arguments -> assert arguments[0].name == USERNAME && arguments[0].email == EMAIL // Note with an argument capture - as well as verifying input // you can also do other things - (if you need to), e.g.: arguments[0].name = 'I changed the name in the test!' test = arguments[0].name } assert test == 'I changed the name in the test!' } }
Appendix: Source Code
User.groovy
class User { String name String email 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 } }
Controller.groovy
class Controller { Service service String register(String name, String email) { User u = new User() u.setName(name) u.setEmail(email) return service.register(u) } void subscribe(User user, String course) { service.subscribe(user, course) } }
Service.groovy
interface Service { String register(User user) void subscribe(User user, String course) }