Salut, Habr. Je m'appelle Ilya Smirnov, je suis développeur Android chez FINCH. Je veux vous montrer quelques exemples de travail avec les tests unitaires que nous avons développés dans notre équipe.
Deux types de tests unitaires sont utilisĂ©s dans nos projets: la vĂ©rification de la conformitĂ© et la vĂ©rification des appels. ArrĂȘtons-nous sur chacun d'eux plus en dĂ©tail.
Test de conformité
Les tests de conformité vérifient si le résultat réel de l'exécution de certaines fonctions correspond ou non au résultat attendu. Permettez-moi de vous montrer un exemple - imaginez qu'il existe une application qui affiche une liste de nouvelles pour la journée:

Les données d'actualités proviennent de différentes sources et, à la sortie de la couche métier, se transforment en le modÚle suivant:
data class News( val text: String, val date: Long )
Selon la logique d'application, un modÚle de la forme suivante est requis pour chaque élément de la liste:
data class NewsViewData( val id: String, val title: String, val description: String, val date: String )
La classe suivante sera chargée de convertir un modÚle de
domaine en modĂšle de
vue :
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) ) ) } } } }
Ainsi, nous savons que certains objets
News( "Super News. Some description and bla bla bla", 1551637424401 )
Sera converti en un objet
NewsViewData( "1551637424401", "Super News", "Some description and bla bla bla", "2019-03-03 21:23" )
Les données d'entrée et de sortie sont connues, ce qui signifie que vous pouvez écrire un test pour la méthode
mapToNewsViewData , qui vérifiera la conformité des données de sortie en fonction de l'entrée.
Pour ce faire, dans le dossier app / src / test / ..., créez la classe
NewsMapperTest avec le contenu suivant:
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") } } }
Le résultat obtenu est comparé aux attentes à l'aide des méthodes du package
org.junit.Assert . Si une valeur ne correspond pas à l'attente, le test échoue.
Il y a des moments oĂč le constructeur de la classe testĂ©e prend certaines dĂ©pendances. Il peut s'agir d'un simple ResourceManager pour accĂ©der aux ressources ou d'un
Interactor complet pour exécuter la logique métier. Vous pouvez créer une instance d'une telle dépendance, mais il est préférable de créer un objet simulé similaire. Un objet factice fournit une implémentation fictive d'une classe, avec laquelle vous pouvez suivre l'appel des méthodes internes et remplacer les valeurs de retour.
Il existe un framework
Mockito populaire pour créer des simulations.
Dans Kotlin, toutes les classes sont finales par défaut, vous ne pouvez donc pas créer de faux objets sur Mockito à partir de zéro. Pour contourner cette limitation, il est recommandé d'ajouter la dépendance
mockito-inline .
Si vous utilisez kotlin dsl lors de l'écriture de tests, vous pouvez utiliser différentes bibliothÚques, telles que
Mockito-Kotlin .
Supposons que
NewsMapper prenne la forme d'une dépendance d'un certain
NewsRepo , qui enregistre des informations sur l'utilisateur qui consulte un
élément particulier. Ensuite, il est logique de se moquer de
NewsRepo et de vérifier les valeurs de retour de la méthode
mapToNewsViewData en fonction du résultat de
isNewsRead .
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) ... } ⊠}
Ainsi, l'objet maquette vous permet de simuler diverses options de valeurs de retour pour tester différents cas de test.
En plus des exemples ci-dessus, les tests de conformité incluent divers validateurs de données. Par exemple, une méthode qui vérifie la présence de caractÚres spéciaux et la longueur minimale du mot de passe entré.
Test d'appel
Tester un appel vĂ©rifie si la mĂ©thode d'une classe appelle ou non les mĂ©thodes nĂ©cessaires d'une autre classe. Le plus souvent, ces tests sont appliquĂ©s Ă
Presenter , qui envoie des commandes spécifiques à View pour changer d'état. Retour à l'exemple de liste de news:
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) } } ⊠}
La chose la plus importante ici est le fait mĂȘme d'appeler des mĂ©thodes d'
Interactor et de
View . Le test ressemblera Ă ceci:
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()) } }
DiffĂ©rentes solutions peuvent ĂȘtre nĂ©cessaires pour exclure les dĂ©pendances de plate-forme des tests, comme tout dĂ©pend de la technologie pour travailler avec le multithreading. L'exemple ci-dessus utilise Kotlin Coroutines avec une portĂ©e remplacĂ©e pour exĂ©cuter des tests, comme utilisĂ© dans le code du programme Dispatchers.Main fait rĂ©fĂ©rence au thread d'interface utilisateur Android, ce qui est inacceptable dans ce type de test. L'utilisation de RxJava nĂ©cessitera d'autres solutions, par exemple, la crĂ©ation d'une TestRule qui permute le flux d'exĂ©cution de code.
Pour vérifier qu'une méthode a été appelée, la méthode verify est utilisée, qui peut prendre des méthodes qui indiquent le nombre d'appels à la méthode testée comme arguments supplémentaires.
*****
Les options de test considérées peuvent couvrir un pourcentage assez important du code, ce qui rend l'application plus stable et prévisible. Le code couvert par le test est plus facile à gérer, plus facile à mettre à l'échelle, car Il y a une certaine confiance que lors de l'ajout de nouvelles fonctionnalités, rien ne se cassera. Et bien sûr, un tel code est plus facile à refactoriser.
La classe la plus facile à tester ne contient pas de dépendances de plate-forme, car lorsque vous l'utilisez, vous n'avez pas besoin de solutions tierces pour créer des objets fantÎmes de plate-forme. Par conséquent, nos projets utilisent une architecture qui minimise l'utilisation des dépendances de plateforme dans la couche testée.
Un bon code doit pouvoir ĂȘtre testĂ©. La complexitĂ© ou l'impossibilitĂ© d'Ă©crire des tests unitaires montre gĂ©nĂ©ralement que quelque chose ne va pas avec le code testĂ©, et il est temps de penser Ă une refactorisation.
Le code source de l'exemple est disponible sur
GitHub .