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:
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:
setup()
prints the number of User objects (0) each timeassert
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):
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:
nullable: true
in the constraintsSo 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.
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.