Unit-Tests in der Clean Swift-Architektur

Hallo Leser!


Es ist kein Geheimnis, dass das Testen ein wesentlicher Bestandteil jeder Entwicklung ist. In früheren Artikeln haben wir uns mit der Grundarchitektur der Clean Swift- Architektur befasst, und jetzt ist es an der Zeit, zu lernen, wie die Unit mit Tests abgedeckt wird. Wir werden das Projekt aus dem Artikel über Arbeiter als Grundlage nehmen und die Hauptpunkte analysieren.



Theorie


Dank Dependency Injection und Protokollorientierung sind alle Komponenten der Szene in Clean Swift unabhängig voneinander und können separat getestet werden. Interactor ist beispielsweise von Presenter und Worker abhängig, diese Abhängigkeiten sind jedoch optional und protokollbasiert. Auf diese Weise kann Interactor seine Arbeit (obwohl minderwertig) ohne Presenter und Worker ausführen, und wir können sie auch durch andere Objekte ersetzen, die unter ihren Protokollen signiert sind.


Da wir jede Komponente einzeln testen möchten, müssen wir die Abhängigkeiten durch Pseudokomponenten ersetzen. Dies wird uns helfen, Spione (Spy). Spy sind Testobjekte, die die Protokolle implementieren, die wir einschleusen möchten, und die darin enthaltenen Methodenaufrufe verfolgen. Mit anderen Worten, wir erstellen Spy for Presenter und Worker und fügen sie dann in Interactor ein , um Methodenaufrufe zu verfolgen.



Der Fairness halber möchte ich hinzufügen, dass es auch Testobjekte ( Test Doubles ) Dummy , Fake , Stub und Mock gibt . Im Rahmen dieses Artikels werden wir sie jedoch nicht beeinflussen. Lesen Sie hier mehr - TestDoubles


Übe


Kommen wir zum Geschäft, wenn wir mit den Worten fertig sind. Um Zeit zu sparen, wird abstrakter Code berücksichtigt, ohne auf die Implementierung der einzelnen Methoden einzugehen. Details zum Anwendungscode finden Sie hier: CleanSwiftTests


Für jede Komponente der Szene erstellen wir eine Datei mit Tests und Test-Doubles für Komponentenabhängigkeiten ( Test-Doubles ).


Ein Beispiel für eine solche Struktur:



Die Struktur jeder Testdatei (für Komponenten) sieht gleich aus und folgt in etwa der folgenden Reihenfolge:


  • Wir deklarieren eine Variable mit SUT (dem zu testenden Objekt) und Variablen mit ihren Hauptabhängigkeiten
  • Wir initialisieren das SUT und seine Abhängigkeiten in setUp () und löschen sie dann in tearDown ()
  • Testmethoden

Wie wir theoretisch besprochen haben, kann jede Komponente der Szene separat getestet werden. Wir können Testduplikate ( Spy ) in ihre Abhängigkeiten einschleusen und so die Funktionsweise der Methoden unseres SUT überwachen. Sehen wir uns den Prozess des Schreibens von Tests anhand des Interactor- Beispiels der Home- Szene genauer an.


HomeInteractor ist von zwei Objekten abhängig - Presenter und Worker . Beide Variablen in der Klasse haben einen Protokolltyp. Dies bedeutet, dass wir Testduplikate erstellen können, die unter den Protokollen HomePresentationLogic und HomeWorkingLogic signiert sind, und diese dann in HomeInteractor einfügen können .


final class HomeInteractor: HomeBusinessLogic, HomeDataStore { // MARK: - Public Properties var presenter: HomePresentationLogic? lazy var worker: HomeWorkingLogic = HomeWorker() var users: [User] = [] var selectedUser: User? // MARK: - HomeBusinessLogic func fetchUsers(_ request: HomeModels.FetchUsers.Request) { // ... } func selectUser(_ request: HomeModels.SelectUser.Request) { // ... } 

Wir werden zwei Methoden testen:


  • fetchUsers (:) . Verantwortlich für das Abrufen einer Benutzerliste per API . Eine API- Anfrage wird mit Worker gesendet.
  • selectUser (:) . Verantwortlich für die Auswahl des aktiven Benutzers ( selectedUser ) aus der Liste der geladenen Benutzer ( users ).

Um mit dem Schreiben der Interactor- Tests zu beginnen, müssen Sie Spione erstellen, die den Aufruf von Methoden in HomePresentationLogic und HomeWorkingLogic verfolgen . Erstellen Sie dazu die Klasse HomePresentationLogicSpy im Verzeichnis 'CleanSwiftTestsTests / Stores / Home / TestDoubles / Spies', signieren Sie das Protokoll HomePresentationLogic und implementieren Sie die Methode dieses Protokolls.


 final class HomePresentationLogicSpy: HomePresentationLogic { // MARK: - Public Properties private(set) var isCalledPresentFetchedUsers = false // MARK: - Public Methods func presentFetchedUsers(_ response: HomeModels.FetchUsers.Response) { isCalledPresentFetchedUsers = true } } 

Hier ist alles sehr transparent. Wenn die Methode presentFetchedUsers ( HomePresentationLogic ) aufgerufen wurde, setzen wir den Wert der Variablen isCalledPresentFetchedUsers auf true . Auf diese Weise können wir verfolgen, ob diese Methode beim Testen von Interactor aufgerufen wurde.


Erstellen Sie nach demselben Prinzip HomeWorkingLogicSpy . Einen Unterschied nennen wir Vervollständigung , weil Ein Teil des Codes in Interactor wird in den Abschluss dieser Methode eingeschlossen. HomeWorkingLogic- Methoden behandeln Netzwerkanforderungen. Wir müssen echte Netzwerkanforderungen während des Testens vermeiden. Zu diesem Zweck ersetzen wir es durch einen Test-Take, der Methodenaufrufe protokolliert und Vorlagendaten zurückgibt, jedoch keine Anforderungen an das Netzwerk stellt.


 final class HomeWorkingLogicSpy: HomeWorkingLogic { // MARK: - Public Properties private(set) var isCalledFetchUsers = false let users: [User] = [ User(id: 1, name: "Ivan", username: "ivan91"), User(id: 2, name: "Igor", username: "igor_test") ] // MARK: - Public Methods func fetchUsers(_ completion: @escaping ([User]?) -> Void) { isCalledFetchUsers = true completion(users) } } 

Als Nächstes erstellen wir die HomeInteractorTests- Klasse, mit der wir HomeInteractor testen.


 final class HomeInteractorTests: XCTestCase { // MARK: - Private Properties private var sut: HomeInteractor! private var worker: HomeWorkingLogicSpy! private var presenter: HomePresentationLogicSpy! // MARK: - Lifecycle override func setUp() { super.setUp() let homeInteractor = HomeInteractor() let homeWorker = HomeWorkingLogicSpy() let homePresenter = HomePresentationLogicSpy() homeInteractor.worker = homeWorker homeInteractor.presenter = homePresenter sut = homeInteractor worker = homeWorker presenter = homePresenter } override func tearDown() { sut = nil worker = nil presenter = nil super.tearDown() } // MARK: - Public Methods func testFetchUsers() { // ... } func testSelectUser() { // ... } } 

Wir geben drei Hauptvariablen an - Sut , Worker und Presenter .


Initialisieren Sie in setUp () die erforderlichen Objekte, fügen Sie Abhängigkeiten in Interactor ein und weisen Sie Klassenvariablen Objekte zu.
In tearDown () löschen wir Klassenvariablen für die Reinheit des Experiments.


Die Methode setUp () wird vor dem Start der Testmethode aufgerufen, z. B. testFetchUsers () und tearDown (), wenn diese Methode ihre Arbeit abgeschlossen hat. Daher erstellen wir das Testobjekt ( sut ) vor jedem Testmethodenlauf neu.


Als nächstes folgen die Testmethoden. Die Struktur ist in drei logische Hauptblöcke unterteilt: das Erstellen der erforderlichen Objekte, das Starten der getesteten Methode in SUT und die Überprüfung der Ergebnisse. Im folgenden Beispiel erstellen wir eine Anforderung (in unserem Fall hat sie keine Parameter), führen die Methode fetchUsers (:) Interactor'a aus und prüfen dann, ob die erforderlichen Methoden in HomeWorkingLogicSpy und HomePresentationLogicSpy aufgerufen wurden . Wir prüfen auch, ob Interactor die von Worker empfangenen Testdaten in seinem DataStore gespeichert hat .


 func testFetchUsers() { let request = HomeModels.FetchUsers.Request() sut.fetchUsers(request) XCTAssertTrue(worker.isCalledFetchUsers, "Not started worker.fetchUsers(:)") XCTAssertTrue(presenter.isCalledPresentFetchedUsers, "Not started presenter.presentFetchedUsers(:)") XCTAssertEqual(sut.users.count, worker.users.count) } 

Wir werden die Wahl des Benutzers anhand einer ähnlichen Struktur testen. Wir deklarieren die Variablen expectationId und expectationName , anhand derer wir das Ergebnis der Benutzerauswahl vergleichen. Die Variable users speichert eine Testliste der Benutzer, die wir Interactor zuweisen. Weil Testmethoden werden unabhängig voneinander aufgerufen und in tearDown () setzen wir die Daten auf Null, dann ist die Liste der Interactor- Benutzer leer und wir müssen sie mit etwas füllen. Und dann prüfen wir, ob der Benutzer im DataStore Interactor'a zugewiesen wurde, nachdem sut.selectUser (:) aufgerufen wurde , und ob der Benutzer der richtige ist.


 func testSelectUser() { let expectationId = 2 let expectationName = "Vasya" let users = [ User(id: 1, name: "Ivan", username: "ivan"), User(id: 2, name: "Vasya", username: "vasya91"), User(id: 3, name: "Maria", username: "maria_love") ] let request = HomeModels.SelectUser.Request(index: 1) sut.users = users sut.selectUser(request) XCTAssertNotNil(sut.selectedUser, "User not selected") XCTAssertEqual(sut.selectedUser?.id, expectationId) XCTAssertEqual(sut.selectedUser?.name, expectationName) } 

Das Testen von Presenter'a und ViewController'a erfolgt nach demselben Prinzip mit minimalen Unterschieden. Einer der Unterschiede besteht darin, dass Sie zum Testen von ViewController ein UIWindow erstellen und den Controller aus dem Storyboard in setUp () abrufen sowie Spy- Objekte für Tabellen und Sammlungen erstellen müssen . Diese Nuancen variieren jedoch je nach Bedarf.


Der Vollständigkeit halber empfehle ich Ihnen, sich über den Link am Ende des Artikels mit dem Projekt vertraut zu machen.


Fazit


Wir haben die Grundprinzipien des Testens von Anwendungen auf der Clean Swift- Architektur behandelt. Es gibt keine grundsätzlich starken Unterschiede zu Testprojekten auf anderen Architekturen, egal ob Testdoppel, Injektion oder Protokolle. Die Hauptsache ist nicht zu vergessen, dass jeder VIP- Zyklus eine (und nur eine!) Verantwortung haben sollte. Dadurch werden der Code sauberer und die Tests offensichtlicher.


Link zum Projekt: CleanSwiftTests
Hilfe beim Schreiben eines Artikels: Bastien


Artikelserie


  1. Übersicht über die saubere, schnelle Architektur
  2. Router und Datenübertragung in sauberer, schneller Architektur
  3. Arbeiter in sauberer, schneller Architektur
  4. Unit-Tests in der Clean Swift-Architektur (Sie sind hier)
  5. Ein Beispiel für eine einfache Online-Shop-Architektur Clean Swift

Source: https://habr.com/ru/post/de483882/


All Articles