Writing tests isn’t the most interesting part of developing a new feature, yet it’s an important one. Well written tests will help you make sure that your newly added feature won’t break any other functionality. However while testing DAOs and Services you may notice some repeatability in your test. Fortunately you can create reusable tests.
Some abstraction
In order to create tests for basic CRUD DAO and Service we need to create a few traits that will help us.
First trait that will be needed is BaseDAO
. It should look similar to the trait in the snippet below.
trait BaseDAO[T] { def count: Future[Long] def delete(id: UUID): Future[Option[String]] def find(id: UUID): Future[Option[T]] def save(data: T): Future[Option[T]] def update(data: T): Future[Option[T]] }
As you can see by extending this trait, we will have to implement basic CRUD methods and the count
method. It should return the number of elements stored in the database.
Next step is creating a similar trait for BaseService
. It should look like one presented below.
trait BaseService[T] { def count: Future[Long] def delete(id: UUID): Future[Option[String]] def retrieve(id: UUID): Future[Option[T]] def save(data: T): Future[Option[T]] def update(data: T): Future[Option[T]] }
As you can see, this trait also implements basic CRUD methods and a count
.
Last trait that we will need to create is BaseModel
which will represent a basic data model that will be used by DAOs and Services. It should look like this:
trait BaseModel { def id: UUID }
It will allow us to access id
of the model in tests.
Since we have created all basic traits, we need to implement them in all DAOs, Services and models that use the basic CRUD.
Creating reusable DAO tests
In order to create reusable tests for DAO, we will need to create a helper trait BaseDAOSpec
. This trait will contain all tests for basic DAO. It should look like this:
trait BaseDAOSpec[T]{ this: PlaySpec => def baseDAO(dao: BaseDAO[T], newModel: T, updatedModel: T) = { "baseDAO" should { "return 0 for empty table" in { waitFor(dao.count) mustBe 0 } "save model" in { waitFor(dao.save(newModel)) mustBe Some(newModel) } // ... more tests here } } }
Method baseDAO
should contain all tests for DAO. As you can see it takes a few parameters. First one is a instance of DAO that will be tested, newModel
will be an instance of data model that will be saved to database and updatedModel
will be an updated version of this model – with few fields changed.
Now let’s use these tests on UserDAO
. The implementation will look like this:
class UserDAOSpec extends PlaySpec with GuiceOneAppPerSuite with play.api.test.Injecting with BaseDAOSpec[User] with WithInMemoryDatabase { private implicit val executionContext: ExecutionContext = inject[ExecutionContext] val userDAO = new UserDAOImpl(databaseConnector) val newUser: User = User(UUID.randomUUID(), "Test User", ...) val updatedUser: User = newUser.copy(fullname = "Updated User") "UserDAO" should { behave like baseDAO(userDAO, newUser, updatedUser) } }
As you can see, this class extends a few traits. The first is PlaySpec
– a test style for Play apps, the next one is GuiceOneAppPerSuite
which will provide a new Application instance for every test. Next it extends play.api.test.Injecting
which provides a way to inject instances. Finally it extends our BaseDAOSpec[User]
with type User
. The last trait that we will need is WithInMemoryDatabase
which provides a memory database for testing (you will need to implement it by yourself). Then since my DAO uses Future
for asynchronous access to data, we will need to inject ExecutionContext
. In next three lines we will create the DAO that we will test – UserDAOImpl
and two User
– new user which will be created from scratch and an updated one which will be a copy of previous one, with a different name. We will need it to test the update method of DAO. The next two lines actually run all tests implemented in BaseDAOSpec
.
Creating reusable Service tests
In a similar way you can write reusable tests for services. Since services mostly depend on DAOs or other services for data access, we will need to create mocks, so we will test the service layer independently.
The BaseServiceSpec
trait will look a bit different then DAO’s. Since it’s more probable that your service will need to do more things while executing CRUD methods, we will create multiple reusable test sets. Firstly, our trait should look like this:
trait BaseServiceSpec[T] extends WithInMemoryDatabase with MockitoSugar { this: PlaySpec => def getMockDAO: BaseDAO[T] def createServiceWithMockedDAO(mockDAO: BaseDAO[T]): BaseService[T] def serviceWithBasicSave(newModel: T) = {...} def serviceWithBasicUpdate(updatedModel: T) = {...} def serviceWithBasicFind(newModel: T) = {...} def serviceWithBasicDelete = {...} def baseService(newModel: T, updatedModel: T) = { behave like serviceWithBasicCount behave like serviceWithBasicSave(newModel) behave like serviceWithBasicUpdate(updatedModel) behave like serviceWithBasicFind(newModel) behave like serviceWithBasicDelete } }
First two methods will stay abstract – we will implement them later in every ServiceSpec. Next four methods should be implemented in this trait. They will contain sets of tests for each feature of our CRUD.
Mocking DAO methods
Since we want to test only the Service layer, we will need to mock DAO functions, so they will return a specific value regardless of DAO’s implementation. To illustrate it, we will implement the serviceWithBasicDelete
test set.
First we need to create a test:
def serviceWithBasicDelete = { "behave like Service with basic delete" should { "return None on delete existing model" in { val id = UUID.randomUUID() val mockDAO = getMockDAO when(mockDAO.delete(id)).thenReturn(Future.successful(None)) val service = createServiceWithMockedDAO(mockDAO) waitFor(service.delete(id)) mustBe None } } }
As you can see, we used both getMockDAO
and createServiceWithMockedDAO
methods. We’ll get to their implementation soon. First lets explain how this code works. First we create a UUID
. Next we call method that will return us a mocked DAO instance. Then we will mock the return of method delete
, so every time it receives a previously created UUID, it will return None. Then we create a instance of service that uses this mocked DAO. Last step is calling service.delete(id)
which will internally call delete(id)
on mocked DAO. This will allow us to check if the service correctly passes a result of our DAO method.
Using reusable service test
Now let’s implement this BaseServiceSpec
and test UserService
with it. This should look similar to this:
class UserServiceSpec extends PlaySpec with GuiceOneAppPerSuite with play.api.test.Injecting with BaseServiceSpec[User] { private implicit val executionContext: ExecutionContext = inject[ExecutionContext] val newUser: User = User(UUID.randomUUID(), None, RoleType.ADMIN, CredentialsProvider.ID, "test@goodsoft.pl", "test1", "Test User") val updatedUser: User = newUser.copy(fullname = "Updated User") override def getMockDAO: UserDAOImpl = mock[UserDAOImpl] override def createServiceWithMockedDAO(mockDAO: BaseDAO[User]): UserServiceImpl = new UserServiceImpl(mockDAO.asInstanceOf[UserDAOImpl]) "UserService" should { behave like baseService(newUser, updatedUser) } }
Analogically to DAO we extend similar traits, then create some explicit values and some helper values (newUser and updatedUser). Since BaseServiceSpec
declares getMockDAO
and createServiceWithMockedDAO
we need to implement them here. The first method will return a basic mock of UserDAOImpl
. The second function takes mocked DAO as an argument and creates an instance of UserServiceImpl
. We used both these methods in BaseServiceSpec
. Lastly we need to run a whole set of CRUD tests.
Using this approach of creating tests, you can easily add tests that will check some basic functionality of your code. This will allow you to focus on creating tests only for more complicated functions.