Integrationstests zur Überprüfung auf Speicherlecks

Wir schreiben viele Unit-Tests und entwickeln die SoundCloud- Anwendung für iOS. Unit-Tests sehen sehr gut aus. Sie sind kurz, (hoffentlich) lesbar und geben uns das Vertrauen, dass der von uns geschriebene Code wie erwartet funktioniert. Unit-Tests decken jedoch, wie der Name schon sagt, nur einen Codeblock ab, meistens eine Funktion oder Klasse. Wie können Sie also Fehler abfangen, die bei Interaktionen zwischen Klassen auftreten - Fehler wie Speicherlecks ?


Speicherlecks


Manchmal ist es ziemlich schwierig, einen Speicherverlustfehler zu erkennen. Es besteht die Möglichkeit eines starken Verweises auf den Delegierten, aber es gibt auch Fehler, die viel schwieriger zu erkennen sind. Ist es beispielsweise offensichtlich, dass der folgende Code einen Speicherverlust enthalten kann?

final class UseCase { weak var delegate: UseCaseDelegate? private let service: Service init(service: Service) { self.service = service } func run() { service.makeRequest(handleResponse) } private func handleResponse(response: ServiceResponse) { // some business logic and then... delegate.operationDidComplete() } } 


Da der Service bereits implementiert ist, gibt es keine Garantie für sein Verhalten. Indem wir die Funktion handleResponse an eine private Funktion übergeben, die sich selbst erfasst, stellen wir dem Service einen starken Verweis auf UseCase zur Verfügung . Wenn der Service beschließt, diesen Link beizubehalten - und wir haben keine Garantie dafür, dass dies nicht geschieht -, tritt ein Speicherverlust auf. Bei einer flüchtigen Untersuchung des Codes ist es jedoch nicht offensichtlich, dass dies wirklich passieren kann.

Es gibt auch einen wunderbaren Beitrag von John Sandell über die Verwendung von Komponententests zum Erkennen von Speicherlecks für Klassen. Im obigen Beispiel, in dem es sehr einfach ist, einen Speicherverlust zu überspringen, ist jedoch nicht immer klar, wie ein solcher Komponententest geschrieben werden soll. (Natürlich sprechen wir hier nicht über Erfahrung.)

Wie Guilherme kürzlich in einem Beitrag schrieb , sind die neuen Funktionen in der SoundCloud-App für iOS nach „sauberen Architekturmustern“ geschrieben - meistens handelt es sich um eine Art VIPER . Die meisten dieser VIPER- Module werden mit der sogenannten ModuleFactory erstellt . Eine solche ModuleFactory benötigt einige Eingaben, Abhängigkeiten und Konfigurationen - und erstellt einen UIViewController , der bereits mit dem Rest des Moduls verbunden ist und auf den Navigationsstapel verschoben werden kann.

Dieses VIPER- Modul kann mehrere Delegaten, Beobachter und außer Kontrolle geratene Fehler aufweisen, von denen jeder dazu führen kann, dass der Controller nach dem Entfernen aus dem Navigationsstapel im Speicher verbleibt. In diesem Fall erhöht sich der Arbeitsspeicher, und das Betriebssystem entscheidet sich möglicherweise dafür, die Anwendung zu stoppen.

Ist es also möglich, so viele potenzielle Lecks abzudecken, indem Sie so wenige Unit-Tests wie möglich schreiben? Wenn nicht, dann war das alles eine enorme Zeitverschwendung.

Integrationstests


Die Antwort lautet, wie Sie vielleicht aus dem Titel dieses Beitrags erraten haben, ja. Und das tun wir durch Integrationstests. Mit dem Integrationstest soll getestet werden, wie Objekte miteinander interagieren. Natürlich sind VIPER- Module Gruppen von Objekten, Speicherlecks sind eine Form der Interaktion, die wir unbedingt vermeiden möchten.

Unser Plan ist einfach: Wir werden unsere ModuleFactory verwenden , um ein VIPER- Modul zu instanziieren. Dann entfernen wir den Link zum UIViewController und stellen sicher, dass alle wichtigen Teile des Moduls zusammen mit diesem zerstört werden.

Das erste Problem, dem wir gegenüberstehen, ist, dass wir von Natur aus nicht einfach auf einen anderen Teil des VIPER- Moduls als den UIViewController zugreifen können . Die einzige öffentliche Funktion in unserer ModuleFactory ist func make () -> UIViewController . Was aber, wenn wir nur für unsere Tests einen weiteren Einstiegspunkt hinzufügen? Diese neue Methode wird über intern deklariert, sodass wir nur über den @ testable-Import , das ModuleFactory- Framework, darauf zugreifen können . Es werden Links zu allen wichtigen Teilen des Moduls zurückgegeben, die wir dann halten könnten, damit schwache Links in unseren Test aufgenommen werden. Es sieht letztendlich so aus:

 public final class ModuleFactory { //     ,   ... public func make() -> UIViewController { makeAndExpose().view } typealias ModuleComponents = ( view: UIViewController, presenter: Presenter, Interactor: Interactor ) func makeAndExpose() -> ModuleComponents { // Set up code, and then... return ( view: viewController, presenter: presenter, interactor: interactor ) } } 


Dies löst das Problem des fehlenden direkten Zugriffs auf Objektdaten. Dies ist natürlich nicht ideal, aber es entspricht unseren Anforderungen. Schreiben wir also den Test. Es wird so aussehen:

 final class ModuleMemoryLeakTests: XCTestCase { //      .     //    . private var view: UIViewController? //        //   ,    // UIKit,   UIViewController  . private weak var presenter: Presenter? private weak var interactor: Interactor? //   setUp    ModuleFactory  //   makeAndExpose.     ,   //     ModuleComponents // ,          . //     . func setUp() { super.setUp() let moduleFactory = ModuleFactory(/* mocked dependencies & config */) let components = moduleFactory.makeAndExpose() view = components.view presenter = components.presenter interactor = components.interactor } //   ,   tearDown   , //        ,     ,   //     . func tearDown() { view = nil presenter = nil interactor = nil super.tearDown() } func test_module_doesNotLeakMemory() { //   ,      . //      ,  //          setUp. XCTAssertNotNil(presenter) XCTAssertNotNil(interactor) //        . //    ,   //     ,    //      . view = nil // ,  ,    //  Presenter  Interactor   . //  ,       //  ,    . XCTAssertNil(presenter) XCTAssertNil(interactor) } } 


So haben wir eine einfache Möglichkeit, Speicherlecks im VIPER- Modul zu erkennen. Es ist keineswegs ideal und erfordert eine bestimmte Benutzerarbeit für jedes neue Modul, das wir testen möchten, aber dies ist sicherlich viel weniger Arbeit als das Schreiben separater Komponententests für jeden möglichen Speicherverlust. Es hilft auch dabei, Speicherlecks zu identifizieren, die wir nicht einmal vermuten. Tatsächlich hat sich nach dem Schreiben mehrerer dieser Tests herausgestellt, dass wir einen Test haben, der nicht bestanden wird, und nach einigen Recherchen haben wir einen Speicherverlust im Modul festgestellt. Nach der Korrektur sollte der Test wiederholt werden.

Es gibt uns auch einen Ausgangspunkt für das Schreiben eines allgemeineren Satzes von Integrationstests für Module. Wenn wir nur eine starke Verbindung zu Presenter beibehalten und den UIViewController durch Mock ersetzen, können wir Benutzereingaben fälschen, dann die Presenter-Methoden aufrufen und die Dummy-Anzeige der Daten in der Ansicht überprüfen.

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


All Articles