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) {
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 {
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 {
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.