Currently, our team has been handling 3 Grails applications – two of which were already in production. Using Grails as a full stack web framework enabled us to rapidly develop these apps. In this blog, I’ll share how you can start a new Grails project using Test-Driven Development and Spock.
SETUP
The versions that I’ll be using are the following:
- Grails : 3.0.8
- Groovy : 2.4.4
- Gradle : 2.3
- Java : 1.7.0_79
To start, let’s create our Grails application:
$ grails create-app grailstdd $ cd grailstdd
Typing grails
will activate interactive mode which keeps the JVM running and allows for quicker execution of commands.
$ grails BUILD SUCCESSFUL | Enter a command name to run. Use TAB for completion: grails>
DOMAIN
Suppose we want to create a domain class Customer, we could do this by:
grails> create-domain-class com.grailstdd.Customer | Created grails-app/domain/com/grailstdd/Customer.groovy | Created src/test/groovy/com/grailstdd/CustomerSpec.groovy
Notice the generated domain class Customer.groovy and its corresponding unit test class CustomerSpec.groovy.
package com.grailstdd class Customer { static constraints = { } }
package com.grailstdd import grails.test.mixin.TestFor import spock.lang.Specification /** * See the API for {@link grails.test.mixin.domain.DomainClassUnitTestMixin} for usage instructions */ @TestFor(Customer) class CustomerSpec extends Specification { def setup() { } def cleanup() { } void "test something"() { expect:"fix me" true == false } }
The @TestFor
annotation indicates the class under test and triggers TestMixin AST transform. Specification is the base class for Spock unit tests. Setup()
and cleanup()
methods are equivalent to JUnit’s @Before
and @After
, respectively.
Let’s write our first test, say we want our customers to have properties customerName and email. Replace the “test something” method with
void "Test that Customer has properties customerName and email"() { when: "A customer is created with the given customerName and email" String customerName = 'Test Customer' String email = 'test@customer.com' Customer customer = new Customer(customerName:customerName, email:email) then: "customerName and email are set to the customer" customer.customerName == customerName customer.email == email }
When
and then
blocks describe a stimulus and the expected response. All top level expressions under then
are implicitly treated as conditions. Conditions define an expected state, similar to JUnit’s assertions. However, conditions are written as plain boolean expressions, eliminating the need for an assertion API.
To run our test:
grails> test-app com.grailstdd.CustomerSpec com.grailstdd.CustomerSpec > test that Customer has properties customerName and email FAILED groovy.lang.MissingPropertyException at CustomerSpec.groovy:25 1 test completed, 1 failed :test FAILED
It fails. Good! Now let’s add the necessary code to pass our test.
class Customer { String customerName String email static constraints = { } }
Then re-run it again.
BUILD SUCCESSFUL Total time: 1.842 secs | Tests PASSED
Easy, right? Next, suppose we want to limit our customer names to contain letters and spaces only, and their email addresses to have valid format. Also, we want each property to be unique for every customer. For these, we’ll have to write some constraint tests.
void "Test that customerName should allow letters and spaces only"() { expect: "Customer instance is valid/invalid" new Customer(customerName:validCustomerName, email:"valid@mail.com").validate() !new Customer(customerName:invalidCustomerName, email:"valid@mail.com").validate() where: "Given valid/invalid customerNames" validCustomerName << ["customer", " customer ", "customer name"] invalidCustomerName << ["123customer456", "customer-test", "customer@email.com"] } void "Test that customerName should be unique"() { when: "The first customer is saved" String firstName = 'Unique Name' String firstEmail = 'test@customer.com' Customer firstCustomer = new Customer(customerName: firstName, email: firstEmail) firstCustomer.save(flush:true) String secondEmail = 'second@customer.com' Customer customerWithSameCustomerName = new Customer(customerName: firstName, email: secondEmail) then: "Another customer with same customerName is invalid" Customer.count() == 1 !customerWithSameCustomerName.validate() } void "Test that email should have proper format"() { expect: "Customer instance is valid/invalid" new Customer(customerName:"customer", email:validEmail).validate() !new Customer(customerName:"customer", email:invalidEmail).validate() where: "Given valid/invalid emails" validEmail << ["customer@email.com", "1234567890@example.com", "email@test.museum"] invalidEmail << ["plainaddress", "@example.com", "#@%^%x!.com"] } void "Test that email should be unique"() { when: "The first customer is saved" String firstName = 'First Name' String firstEmail = 'first@email.com' Customer firstCustomer = new Customer(customerName: firstName, email: firstEmail) firstCustomer.save(flush:true) String secondName = 'Second Name' Customer customerWithSameEmail = new Customer(customerName: secondName, email: firstEmail) then: "Another customer with same customerName is invalid" Customer.count() == 1 !customerWithSameEmail.save(flush:true) }
An expect
block is similar to a then
block but more useful when describing both stimulus and expected response in a single expression. A where
block is used to write data-driven feature methods. In the above tests, the where
blocks effectively create three “versions” of valid-invalid pairs.
Try to run the tests and you’ll get the following errors:
com.grailstdd.CustomerSpec > Test that customerName should allow letters and spaces only FAILED org.spockframework.runtime.ConditionNotSatisfiedError at CustomerSpec.groovy:32 org.spockframework.runtime.ConditionNotSatisfiedError at CustomerSpec.groovy:32 org.spockframework.runtime.ConditionNotSatisfiedError at CustomerSpec.groovy:32 com.grailstdd.CustomerSpec > Test that customerName should be unique FAILED org.spockframework.runtime.ConditionNotSatisfiedError at CustomerSpec.groovy:51 com.grailstdd.CustomerSpec > Test that email should have proper format FAILED org.spockframework.runtime.ConditionNotSatisfiedError at CustomerSpec.groovy:57 org.spockframework.runtime.ConditionNotSatisfiedError at CustomerSpec.groovy:57 org.spockframework.runtime.ConditionNotSatisfiedError at CustomerSpec.groovy:57 com.grailstdd.CustomerSpec > Test that email should be unique FAILED org.spockframework.runtime.ConditionNotSatisfiedError at CustomerSpec.groovy:76 5 tests completed, 4 failed :test FAILED
These lines should make our tests pass:
static constraints = { customerName matches: "^[a-zA-Z ]+\$", unique: true email email: true, unique: true }
BUILD SUCCESSFUL Total time: 1.787 secs | Tests PASSED
CONTROLLERS and VIEWS
We’re done for our Customer domain. Let’s generate our controller and views.
grails> generate-all com.grailstdd.Customer | Rendered template Controller.groovy to destination grails-app/controllers/com/grailstdd/CustomerController.groovy | Rendered template Spec.groovy to destination src/test/groovy/com/grailstdd/CustomerControllerSpec.groovy | Rendered template edit.gsp to destination grails-app/views/customer/edit.gsp | Rendered template create.gsp to destination grails-app/views/customer/create.gsp | Rendered template index.gsp to destination grails-app/views/customer/index.gsp | Rendered template show.gsp to destination grails-app/views/customer/show.gsp | Scaffolding completed for grails-app/domain/com/grailstdd/Customer.groovy
As you can see, there are 2 generated groovy files: CustomerController and CustomerControllerSpec, and 4 generated gsp files: edit, create, index and show.
If you view CustomerControllerSpec.groovy, you’ll notice that Grails already created 7 unit test cases: one each for index, create, save, show, edit, update, and delete controller actions. Let’s run them!
grails> test-app com.grailstdd.CustomerControllerSpec com.grailstdd.CustomerControllerSpec > Test the save action correctly persists an instance FAILED org.spockframework.runtime.ConditionNotSatisfiedError at CustomerControllerSpec.groovy:15 com.grailstdd.CustomerControllerSpec > Test that the show action returns the correct model FAILED org.spockframework.runtime.ConditionNotSatisfiedError at CustomerControllerSpec.groovy:15 com.grailstdd.CustomerControllerSpec > Test that the edit action returns the correct model FAILED org.spockframework.runtime.ConditionNotSatisfiedError at CustomerControllerSpec.groovy:15 com.grailstdd.CustomerControllerSpec > Test the update action performs an update on a valid domain instance FAILED org.spockframework.runtime.ConditionNotSatisfiedError at CustomerControllerSpec.groovy:15 com.grailstdd.CustomerControllerSpec > Test that the delete action deletes an instance if it exists FAILED org.spockframework.runtime.ConditionNotSatisfiedError at CustomerControllerSpec.groovy:15 7 tests completed, 5 failed :test FAILED
Only 2 tests passed while 5 failed. To fix this, notice the first method in CustomerControllerSpec,
def populateValidParams(params) { assert params != null // TODO: Populate valid properties like... //params["name"] = 'someValidName' assert false, "TODO: Provide a populateValidParams() implementation for this generated test suite" }
We have to implement this method correctly for our Customer domain.
def populateValidParams(params) { assert params != null params.customerName = 'valid customerName' params.email = 'valid@email.com' }
And re-run.
BUILD SUCCESSFUL Total time: 11.796 secs | Tests PASSED
That’s all for our controller. Try to start the application:
grails> run-app | Running application... Grails application running at http://localhost:8080 in environment: development
TEST REPORTS
For the Test Reports, re-run again all unit tests,
grails> test-app
then view your test report:
$ open build/reports/tests/index.html
SOURCE CODE
The source code is available at https://github.com/renzrollon/grailstdd.git
REFERENCES
http://grails.github.io/grails-doc/3.0.x/guide/single.html
https://code.google.com/p/spock/wiki/SpockBasics
4 thoughts on “Starting Grails with Test-Driven Development”
This tutorial is great and helpful. However, the auto generated code for the Controller Spec didn’t explain further. Specifically, the “when” and “then” code on it.
Regards,
Thanks 🙂
Hi Yana,
Thanks for the feedback! I didn’t explain CustomerControllerSpec.groovy further because the auto-generated methods were already named with descriptive String literals, similar to the when and then blocks.
Hi Carl,
Nice and clear tutorial. Your effort is appreciated.
Thanks for the guide, but is there a need for testing the domain attributes?
isn’t framework level testing at that point?
thanks