Das Schreiben von Tests ist nicht der interessanteste Teil der Entwicklung einer neuen Funktion, aber ein sehr wichtiger. Mit gut geschriebenen Tests können Sie sicherstellen, dass Ihre neu hinzugefügte Funktion keine anderen Funktionen beeinträchtigt. Beim Testen von DAOs und Diensten kann es jedoch vorkommen, dass Sie Wiederholungen in Ihrem Test feststellen. Zum Glück können Sie wiederverwendbare Tests erstellen.

Some abstraction

Um Tests für grundlegende CRUD DAO und Service zu erstellen, müssen wir ein paar Traits erstellen, die uns dabei helfen werden.

Der erste Trait, der benötigt wird, ist BaseDAO. Er sollte ähnlich wie der Trait im folgenden Ausschnitt aussehen.

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]]
}

Wie Sie sehen können, müssen wir durch die Erweiterung dieses Traits grundlegende CRUD-Methoden und diecount Methode. Sie sollte die Anzahl der in der Datenbank gespeicherten Elemente zurückgeben.

Der nächste Schritt ist die Erstellung eines ähnlichen Traits für BaseService. Sie sollte wie unten dargestellt aussehen.

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]]
}

Wie Sie sehen können, implementiert dieser Trait auch grundlegende CRUD-Methoden und ein count.

Der letzte Trait, den wir erstellen müssen, ist BaseModel der ein grundlegendes Datenmodell darstellt, das von DAOs und Services verwendet wird. Es sollte wie folgt aussehen:

trait BaseModel {
   def id: UUID
}

Sie ermöglicht uns den Zugriff auf die id des Modells in Tests.

Da wir alle grundlegenden Traits erstellt haben, müssen wir sie in allen DAOs, Services und Modellen implementieren, die die grundlegende CRUD verwenden.

Wiederverwendbare DAO-Tests erstellen

Um wiederverwendbare Tests für DAO zu erstellen, müssen wir einen Helper Trait BaseDAOSpec. erstellen. Dieser Trait wird alle Tests für DAO enthalten. Sie sollte wie folgt aussehen:

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
       }
   }
} 

Die Methode baseDAO sollte alle Tests für DAO enthalten. Wie Sie sehen können, benötigt sie einige Parameter. Der erste ist eine Instanz der DAO, die getestet werden soll, newModel ist eine Instanz des Datenmodells, das in der Datenbank gespeichert wird, und updatedModel ist eine aktualisierte Version dieses Modells – mit einigen geänderten Feldern.

Nun wollen wir diese Tests auf UserDAO. Die Implementierung wird wie folgt aussehen:

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)
   }
}

Wie Sie sehen können, erweitert diese Klasse einige Traits. Die erste ist PlaySpec – ein Teststil für Play-Apps, die nächste ist GuiceOneAppPerSuite, die eine neue Anwendungsinstanz für jeden Test bereitstellt. Als nächstes erweitert es play.api.test.Injecting, das eine Möglichkeit bietet, Instanzen zu injizieren. Schließlich erweitert es unsere BaseDAOSpec[User] um den Typ User. Der letzte Trait, den wir benötigen, ist WithInMemoryDatabase, der eine Speicherdatenbank für Tests bereitstellt (Sie müssen ihn selbst implementieren). Da mein DAO Future für den asynchronen Zugriff auf Daten verwendet, müssen wir ExecutionContext einfügen. In den nächsten drei Zeilen erstellen wir die DAO, die wir testen werden – UserDAOImpl und zwei User – einen neuen User, der von Grund auf neu erstellt wird, und einen aktualisierten, der eine Kopie des vorherigen ist, mit einem anderen Namen. Wir brauchen sie, um die Aktualisierungsmethode von DAO zu testen. Die nächsten beiden Zeilen führen alle in BaseDAOSpec implementierten Tests aus.

Wiederverwendbare Service-Tests erstellen

Auf ähnliche Weise können Sie wiederverwendbare Tests für Dienste schreiben. Da Dienste meist von DAOs oder anderen Diensten für den Datenzugriff abhängen, müssen wir Mocks erstellen, damit wir die Dienstebene unabhängig testen können.

Die BaseServiceSpec Eigenschaft sieht ein wenig anders aus als DAOs. Da es wahrscheinlicher ist, dass Ihr Dienst mehr Dinge tun muss, während er CRUD-Methoden ausführt, werden wir mehrere wiederverwendbare Testsätze erstellen. Erstens sollte unser Trait so aussehen:

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
    }
} 

Die ersten beiden Methoden werden abstrakt bleiben – wir werden sie später in jeder ServiceSpec implementieren. Die nächsten vier Methoden sollten in diesem Trait implementiert werden. Sie werden Sätze von Tests für jede Funktion unserer CRUD enthalten.

Mocking von DAO-Methoden

Da wir nur die Dienstschicht testen wollen, müssen wir DAO-Funktionen nachbilden, damit sie unabhängig von der DAO-Implementierung einen bestimmten Wert zurückgeben. Um dies zu veranschaulichen, implementieren wir den Testsatz serviceWithBasicDelete.

Zunächst müssen wir einen Test erstellen:

 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
       }
   }
}

Wie Sie sehen können, haben wir die Methoden getMockDAO und createServiceWithMockedDAO verwendet. Wir werden bald zu ihrer Implementierung kommen. Zunächst wollen wir erklären, wie dieser Code funktioniert. Zuerst erstellen wir eine UUID. Dann rufen wir eine Methode auf, die uns eine gespottete DAO-Instanz zurückgibt. Dann spiegeln wir die Rückgabe der Methode delete, so dass sie jedes Mal, wenn sie eine zuvor erstellte UUID erhält, None zurückgibt. Dann erstellen wir eine Instanz des Dienstes, die diese gespottete DAO verwendet. Der letzte Schritt ist der Aufruf von service.delete(id), der intern delete(id) auf der gespiegelten DAO aufruft. So können wir überprüfen, ob der Dienst ein Ergebnis unserer DAO-Methode korrekt weitergibt.

Using reusable service test

Nun wollen wir diese BaseServiceSpec implementieren und UserService damit testen. Dies sollte in etwa so aussehen:

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)
   }
}

Analog zu DAO erweitern wir ähnliche Traits, erstellen dann einige explizite Werte und einige Hilfswerte (newUser und updatedUser). Da BaseServiceSpec getMockDAO und createServiceWithMockedDAO deklariert, müssen wir sie hier implementieren. Die erste Methode gibt eine einfache Attrappe von UserDAOImpl zurück. Die zweite Funktion nimmt die nachgebildete DAO als Argument und erstellt eine Instanz von UserServiceImpl. Wir haben diese beiden Methoden in BaseServiceSpec verwendet. Schließlich müssen wir noch eine ganze Reihe von CRUD-Tests durchführen.

Mit diesem Ansatz der Testerstellung können Sie leicht Tests hinzufügen, die einige grundlegende Funktionen Ihres Codes überprüfen. So können Sie sich darauf konzentrieren, nur Tests für kompliziertere Funktionen zu erstellen.