Search code examples
unit-testinggrailsnullgrails-ormspock

Grails Unit Test Null Strangeness


I have a set of basic unit tests in grails and I am getting some strange, inconsistent behaviour. So basically the unit tests are for a Service for interacting with my User Domain.

The issue is at a high level - If I run the full Spec of tests - 4 tests fail each time. If I run them individually - they pass. The obvious thing here is that there is state being held between tests - but given this is a unit test - there shouldn't be (and I have verified that with logging).

The components in play here are:

  • UserService
  • User Domain
  • Address Domain
  • UserCommand (Verifiable)

So looking at the Spec I have the following in setup:

User user

def setup() {

    println "================ Setting Up User (${User.count()}) ================"

    // Set the roles
    new Role(authority: "ROLE_ADMIN").save()
    new Role(authority: "ROLE_OPERATOR").save()
    def role = new Role(authority: "ROLE_USER").save()

    def userAddress = new Address()
    userAddress.with {
        name = "Name"
        address1 = "Address 1"
        address2 = "Address 2"
        townCity = "Town"
        postcode = "BT19"
        county = "Down"
    }

    // Create an admin user
    user = new User()
    user.with {
        christianName = "Phil"
        surname = "Preston"
        email = "[email protected]"
        username = "admin"
        password = "password123"
        phone = ""
        skype = ""
        address = userAddress
    }

    println(user.properties)

    // Save the test user
    if (user.validate()) {
        println "Saving"
        user.save(flush: true, failOnErrors: true)
        UserRole.create(user, role)
    }
    else {
        user.errors.allErrors.eachWithIndex { i, x ->
            println "${i} - ${x}"
        }
    }


    assert user
}

And the two simple tests are as follows:

void "Test updateUser - changing a user password"() {

    given: "An update to the user"
    UserCommand cmd = new UserCommand()
    cmd.with {
        id = user.id
        christianName = user.christianName
        surname = user.surname
        username = user.username
        email = user.email
        skype = user.skype
        phone = user.phone
        password = "newPassword"
        passwordCheck = "newPassword"
        isAdmin = user.isAdmin()
        isOperator = user.isOperator()
    }

    when: "attempt to update"
    User updated = service.updateUser(cmd)

    then: "the password should be update - but nothing else"
    updated.password == cmd.password
}

void "Test updateUser - changing a user detail"() {

    given: "An update to the user"
    UserCommand cmd = new UserCommand()
    cmd.with {
        id = user.id
        christianName = user.christianName
        surname = user.surname
        username = user.username
        email = "[email protected]"
        skype = user.skype
        phone = user.phone
        password = user.password
        isAdmin = user.isAdmin()
        isOperator = user.isOperator()
    }

    when: "attempt to update"
    User updated = service.updateUser(cmd)

    then: "the email should be update - but nothing else"
    updated.email == cmd.email
}

(There are others - but this is all to demonstrate the problem)

So the strangeness is as follows:

  • The first test passes, the second one fails.
  • If I swap the order - the first one still passes, second fails
  • If I run both individually - they both will pass
  • The code in setup() prints the number of User objects (0) each time
  • It verifies the object is created each time (assert and logging)

The unit test fails because I throw an exception when validation fails forthe User domain object I am udating. So the following:

def updateUser(UserCommand userCommand) {
    assert userCommand

    // Get the current
    User foundUser = User.findById(userCommand.id)
    def wasAdmin = foundUser.admin
    def wasOperator = foundUser.operator

    // Bind Data
    bindData(foundUser, userCommand, ['class', 'operator', 'admin', 'address'])
    foundUser?.address?.with {
        name = userCommand.name
        address1 = userCommand.address1
        address2 = userCommand.address2
        townCity = userCommand.townCity
        county = userCommand.county
        postcode = userCommand.postcode
    }

    // THIS VALIDATION FAILS ON SECOND TEST
    if (foundUser.validate()) {
        foundUser.save() 
        // ... removed 
        return foundUser.refresh()
    }
    else {
        Helper.copyErrorToCmd(foundUser, userCommand)
        log.error("Errors: ${foundUser.errors.allErrors}")
        throw new UserServiceException(cmd: userCommand, message: "There were errors updating user")
    }

The errors on the User domain object are basically (shortened):

  • 'skype': rejected value [null]
  • 'phone': rejected value [null]

So you can see that I have empty strings for these fields - and with conversion to null (as Grails will do), this is the reason. However the issue with that is:

  • It should fail for both, and certainly fail when run individually
  • Both fields are marked as nullable: true in the constraints
  • In UserService I have used the debugger to check the objects that are being saved when running both tests - the phone and skype are null for both tests, yet one fails and one doesn't

So the constraints in the User domain object are as follows:

static constraints = {
    christianName blank: false
    surname blank: false
    username blank: false, unique: true, size: 5..20
    password blank: false, password: true, minSize: 8
    email email: true, blank: false
    phone nullable: true
    skype nullable: true
    address nullable: true
}

And I am using a Command object which has the following constraints:

static constraints = {
    importFrom User
    importFrom Address
    password blank: false, password: true, minSize: 8
    passwordCheck(blank: false, password: true, validator: { pwd, uco -> return pwd == uco.password })
}

Note I have even tried adding the Skype and phone fields in the UserCommand constraints closure as well.

This issue goes away if I add text to the fields in setup, but thats not going to possible in the actual app as these fields can be left blank.

Any help on how this inconsistency happens would be greatly appreciated.

RECREATION

I have added a minimal GitHub project which recreates the issue: Github Project Recreating Issue. To see the problem:

./grailsw test-app

Will run the two test, first passes second fails. To run the failed test individually run:

./gradlew test --tests "org.arkdev.bwmc.accountmission.UserServiceSpec.*Test updateUser - changing a user detail"

And you can see the test pass.

The issue seems to be that the skype and phone fields are nullable:true for the first test, and nullable:false on the second test (I step into user.validate(), and step into the GrailsDomainClassValidator.java, in the validate(Object,Errors,boolean) call I can see the constrainedProperties Map (which is basically field -> constraints). This shows that skype is NullableConstraint.nullable == true, on the first call of setup, but is showing that NullableConstraint.nullable == false on the setup call for the second test.). This makes no sense.


Solution

  • After the first call

    bindData(foundUser, userCommand, ['class', 'operator', 'admin', 'address'])
    

    in UserService.updateUser(UserCommand) (called by feature method Test updateUser - changing a user password) the static member User.constraints becomes null. Go figure. I have no idea whatsoever how Grails works, so maybe someone else can make sense of it. But to me it seems that this should not happen. Maybe you are somehow making a mistake in that call.