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 (
assertand 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: truein 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.
Users.count()line that precedes the test user creation to verify its starting from 0. - Phil Preston