mercredi 27 mai 2015

How to unit-test Slick 3.x dbio combinators?

I have a class with a single method that I want to unit-test:

@Singleton
class RegistrationWorkflow @Inject()(userService: UserService,
                                 addUserValidator: RegisterUserValidator,
                                 db: Database) {

  def registerUser(registerForm: RegisterUserForm): Future[Vector[FormError]] = {
    val dbActions = addUserValidator.validate(registerForm).flatMap({ validation =>
      if (validation.isEmpty) {
        userService.add(User(GUID.shortGuid(),
          registerForm.username,
          registerForm.email,
          BCrypt.hashpw(registerForm.password, BCrypt.gensalt())))
          .andThen(DBIO.successful(validation))
      } else {
        DBIO.successful(validation)
      }
    }).transactionally
    db.run(dbActions)
  }
}

addUserValidator validates the form and returns a Vector of form errors. If there were no errors, the user is inserted into database. I am returning the form errors because in the controller I'm either returning a 201 or a 400 with a list of errors.

I have written a specs2 test for this:

class RegistrationWorkflowTest extends Specification with Mockito with TestUtils {
  "RegistrationControllerWorkflow.registerUser" should {
    "insert a user to database if validation succeeds" in new Fixture {
      registerUserValidatorMock.validate(testUserFormData) returns DBIO.successful(Vector())
      userServiceMock.add(any) returns DBIO.successful(1)
      val result = await(target.registerUser(testUserFormData))

      result.isEmpty must beTrue

      there was one(registerUserValidatorMock).validate(testUserFormData)
      there was one(userServiceMock).add(beLike[User] { case User(_, testUser.username, testUser.email, _) => ok })
    }

    "return error collection if validation failed" in new Fixture {
      registerUserValidatorMock.validate(testUserFormData) returns DBIO.successful(Vector(FormError("field", Vector("error"))))
      val result = await(target.registerUser(testUserFormData))

      result.size must beEqualTo(1)
      result.contains(FormError("field", Vector("error"))) must beTrue

      there was one(registerUserValidatorMock).validate(testUserFormData)
      there was no(userServiceMock).add(any)
    }
  }

  trait Fixture extends Scope with MockDatabase {
    val userServiceMock = mock[UserService]
    val registerUserValidatorMock = mock[RegisterUserValidator]
    val target = new RegistrationWorkflow(userServiceMock, registerUserValidatorMock, db)
    val testUser = UserFactory.baseUser()
    val testUserFormData = RegisterUserFactory.baseRegisterUserForm()
  }
}

The issue with this test is that it just asserts that userService.add was called. This means that I can change my implementation to the following:

val dbActions = addUserValidator.validate(registerForm).flatMap({   validation =>
  if (validation.isEmpty) {
    userService.add(User(GUID.shortGuid(),
      registerForm.username,
      registerForm.email,
      BCrypt.hashpw(registerForm.password, BCrypt.gensalt())))
    DBIO.successful(validation)
  } else {
    DBIO.successful(validation)
  }
}).transactionally
db.run(dbActions)

The test still passes, but the user will not be inserted, because I am not ussing andThen combinator on the DBIO that was returned by userService.add method.

I know that I could use an in memory database and then assert that the user was actually inserted, but I am wondering if there is a better way. Thanks.

Aucun commentaire:

Enregistrer un commentaire