Hallo Habr. Mein Name ist Ilya Smirnov, ich bin ein Android-Entwickler bei FINCH. Ich möchte Ihnen einige Beispiele für die Arbeit mit Unit-Tests zeigen, die wir in unserem Team entwickelt haben.
In unseren Projekten werden zwei Arten von Unit-Tests verwendet: Konformitätsprüfung und Anrufprüfung. Lassen Sie uns näher auf jeden einzelnen eingehen.
Konformitätstests
Die Konformitätsprüfung prüft, ob das tatsächliche Ergebnis der Ausführung einer Funktion mit dem erwarteten Ergebnis übereinstimmt oder nicht. Lassen Sie mich Ihnen ein Beispiel zeigen - stellen Sie sich vor, es gibt eine Anwendung, die eine Liste mit Nachrichten für diesen Tag anzeigt:

Daten zu den Nachrichten stammen aus verschiedenen Quellen und werden beim Verlassen der Geschäftsschicht in das folgende Modell umgewandelt:
data class News( val text: String, val date: Long )
Gemäß der Anwendungslogik ist für jedes Element der Liste ein Modell der folgenden Form erforderlich:
data class NewsViewData( val id: String, val title: String, val description: String, val date: String )
Die folgende Klasse ist für die Konvertierung eines
Domänenmodells in ein
Ansichtsmodell verantwortlich :
class NewsMapper { fun mapToNewsViewData(news: List<News>): List<NewsViewData> { return mutableListOf<NewsViewData>().apply{ news.forEach { val textSplits = it.text.split("\\.".toRegex()) val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale("ru")) add( NewsViewData( id = it.date.toString(), title = textSplits[0], description = textSplits[1].trim(), date = dateFormat.format(it.date) ) ) } } } }
Somit wissen wir, dass ein Objekt
News( "Super News. Some description and bla bla bla", 1551637424401 )
Wird in ein Objekt konvertiert
NewsViewData( "1551637424401", "Super News", "Some description and bla bla bla", "2019-03-03 21:23" )
Die Eingabe- und Ausgabedaten sind bekannt.
Dies bedeutet, dass Sie einen Test für die
mapToNewsViewData- Methode schreiben können, mit dem die Übereinstimmung der Ausgabedaten abhängig von der Eingabe überprüft wird.
Erstellen Sie dazu im Ordner app / src / test / ... die
NewsMapperTest- Klasse
mit den folgenden Inhalten:
class NewsMapperTest { private val mapper = NewsMapper() @Test fun mapToNewsViewData() { val inputData = listOf( News("Super News. Some description and bla bla bla", 1551637424401) ) val outputData = mapper.mapToNewsViewData(inputData) Assert.assertEquals(outputData.size, inputData.size) outputData.forEach { Assert.assertEquals(it.id, "1551637424401") Assert.assertEquals(it.title, "Super News") Assert.assertEquals(it.description, "Some description and bla bla bla") Assert.assertEquals(it.date, "2019-03-03 21:23") } } }
Das erhaltene Ergebnis wird mit Methoden aus dem Paket
org.junit.Assert mit den Erwartungen verglichen. Wenn ein Wert nicht den Erwartungen entspricht, schlägt der Test fehl.
Es gibt Zeiten, in denen der Konstruktor der getesteten Klasse einige Abhängigkeiten akzeptiert. Dies kann entweder ein einfacher ResourceManager für den Zugriff auf Ressourcen oder ein vollständiger
Interactor für die Ausführung der Geschäftslogik sein. Sie können eine Instanz einer solchen Abhängigkeit erstellen, es ist jedoch besser, ein ähnliches Scheinobjekt zu erstellen. Ein Scheinobjekt bietet eine fiktive Implementierung einer Klasse, mit der Sie den Aufruf interner Methoden verfolgen und Rückgabewerte überschreiben können.
Es gibt ein beliebtes
Mockito- Framework zum Erstellen von Mock.
In Kotlin sind standardmäßig alle Klassen endgültig, sodass Sie auf Mockito keine Scheinobjekte von Grund auf neu erstellen können. Um diese Einschränkung zu
umgehen , wird empfohlen, die
Mockito-Inline- Abhängigkeit hinzuzufügen.
Wenn Sie beim Schreiben von Tests kotlin dsl verwenden, können Sie verschiedene Bibliotheken verwenden, z. B.
Mockito-Kotlin .
Angenommen,
NewsMapper nimmt in Form einer Abhängigkeit einen bestimmten
NewsRepo an , der Informationen über den Benutzer
aufzeichnet , der eine bestimmte Nachricht
anzeigt . Dann ist es sinnvoll,
NewsRepo zu verspotten und die Rückgabewerte der
mapToNewsViewData- Methode abhängig vom Ergebnis von
isNewsRead zu überprüfen .
class NewsMapperTest { private val newsRepo: NewsRepo = mock() private val mapper = NewsMapper(newsRepo) … @Test fun mapToNewsViewData_Read() { whenever(newsRepo.isNewsRead(anyLong())).doReturn(true) ... } @Test fun mapToNewsViewData_UnRead() { whenever(newsRepo.isNewsRead(anyLong())).doReturn(false) ... } … }
Auf diese Weise können Sie mit dem Mock-Objekt verschiedene Optionen für Rückgabewerte simulieren, um verschiedene Testfälle zu testen.
Zusätzlich zu den obigen Beispielen umfasst die Konformitätsprüfung verschiedene Datenvalidatoren. Zum Beispiel eine Methode, die das eingegebene Passwort auf das Vorhandensein von Sonderzeichen und die Mindestlänge überprüft.
Test testen
Beim Testen auf einen Aufruf wird geprüft, ob die Methode einer Klasse die erforderlichen Methoden einer anderen Klasse aufruft oder nicht. Am häufigsten werden solche Tests auf
Presenter angewendet, der View-spezifische Befehle zum Ändern des Status sendet. Zurück zum Beispiel der Nachrichtenliste:
class MainPresenter( private val view: MainView, private val interactor: NewsInteractor, private val mapper: NewsMapper ) { var scope = CoroutineScope(Dispatchers.Main) fun onCreated() { view.setLoading(true) scope.launch { val news = interactor.getNews() val newsData = mapper.mapToNewsViewData(news) view.setLoading(false) view.setNewsItems(newsData) } } … }
Das Wichtigste dabei ist die Tatsache, dass Methoden aus
Interactor und
View aufgerufen werden . Der Test sieht folgendermaßen aus:
class MainPresenterTest { private val view: MainView = mock() private val mapper: NewsMapper = mock() private val interactor: NewsInteractor = mock() private val presenter = MainPresenter(view, interactor, mapper).apply { scope = CoroutineScope(Dispatchers.Unconfined) } @Test fun onCreated() = runBlocking { whenever(interactor.getNews()).doReturn(emptyList()) whenever(mapper.mapToNewsViewData(emptyList())).doReturn(emptyList()) presenter.onCreated() verify(view, times(1)).setLoading(true) verify(interactor).getNews() verify(mapper).mapToNewsViewData(emptyList()) verify(view).setLoading(false) verify(view).setNewsItems(emptyList()) } }
Möglicherweise sind unterschiedliche Lösungen erforderlich, um Plattformabhängigkeiten von Tests auszuschließen Alles hängt von der Technologie für die Arbeit mit Multithreading ab. Im obigen Beispiel werden Kotlin Coroutines mit einem überschriebenen Bereich zum Ausführen von Tests verwendet Der im Programmcode Dispatchers.Main verwendete Hauptcode bezieht sich auf den Android-UI-Thread, der bei dieser Art von Tests nicht akzeptabel ist. Für die Verwendung von RxJava sind andere Lösungen erforderlich, z. B. das Erstellen einer TestRule, die den Code-Ausführungsfluss umschaltet.
Um zu überprüfen, ob eine Methode aufgerufen wurde, wird die Überprüfungsmethode verwendet, die Methoden, die die Anzahl der Aufrufe der zu testenden Methode angeben, als zusätzliche Argumente verwenden kann.
*****
Die berücksichtigten Testoptionen können einen relativ großen Prozentsatz des Codes abdecken, wodurch die Anwendung stabiler und vorhersehbarer wird. Testbezogener Code ist einfacher zu warten und einfacher zu skalieren, weil Es besteht ein gewisses Maß an Vertrauen, dass beim Hinzufügen neuer Funktionen nichts kaputt geht. Und natürlich ist ein solcher Code einfacher zu überarbeiten.
Die am einfachsten zu testende Klasse enthält keine Plattformabhängigkeiten, weil Wenn Sie damit arbeiten, benötigen Sie keine Lösungen von Drittanbietern zum Erstellen von Plattform-Mock-Objekten. Daher verwenden unsere Projekte eine Architektur, die die Verwendung von Plattformabhängigkeiten in der zu testenden Schicht minimiert.
Guter Code sollte testbar sein. Die Komplexität oder Unfähigkeit, Komponententests zu schreiben, zeigt normalerweise, dass etwas mit dem getesteten Code nicht stimmt, und es ist Zeit, über Refactoring nachzudenken.
Der Quellcode für das Beispiel ist auf
GitHub verfügbar.