Starting Grails with Test-Driven Development

Posted on Posted in Java

Starting Grails with Test-Driven Development
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  = '[email protected]'
        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:"[email protected]").validate()
        !new Customer(customerName:invalidCustomerName, email:"[email protected]").validate()

    where: "Given valid/invalid customerNames"
        validCustomerName << ["customer", "    customer   ", "customer name"]
        invalidCustomerName << ["123customer456", "customer-test", "[email protected]"]
}

void "Test that customerName should be unique"() {
    when: "The first customer is saved"
        String firstName = 'Unique Name'
        String firstEmail  = '[email protected]'
        Customer firstCustomer = new Customer(customerName: firstName, email: firstEmail)
        firstCustomer.save(flush:true)

        String secondEmail = '[email protected]'
        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 << ["[email protected]", "[email protected]", "[email protected]"]
        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  = '[email protected]'
        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 = '[email protected]'
}

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

  1. 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 🙂

  2. 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.

  3. Thanks for the guide, but is there a need for testing the domain attributes?
    isn’t framework level testing at that point?
    thanks

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.