Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Узнайте, как создать пользовательское Timber Tree для проверки вывода журналов в модульных тестах. Мокинг Timber, тестирование журналов в модульных тестах.
Что такое Timber?
Timber — это золотой стандарт ведения журнала в Android. Он использует концепцию деревьев — вы можете рассматривать их как различные каналы вывода сообщений журнала.Обычно в приложении для Android вы должны написать следующий код, чтобы использовать Timber в режиме отладки. Размещение Timber Tree для журналов отладки:
class App: Application(){
override fun onCreate(){
super.onCreate()
if(BuildConfig.DEBUG){
Timber.plant(Timber.DebugTree())
}
}
}
Вы также можете использовать Timber для регистрации сообщений в удаленных аналитических службах, таких как Sentry или Firebase:
class FirebaseLogging: Timber.Tree(){
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
FirebaseCrash.logcat(priority, tag, message);
FirebaseCrash.report(t);
}
}
Как я уже упоминал в предыдущей статье, иногда тестирование журналов может быть очень важным для отладки. Поэтому в следующем разделе мы рассмотрим технику использования Timber для тестирования журналов.
Как моделировать Timber?
Прежде всего, забудьте об использовании для этой цели mock-статических конструкций из Mockito, MockK или PowerMock. Хотя эти инструменты и полезны, в большинстве случаев они не нужны.
Итак, как мы собираемся обеспечить тестовую реализацию для фреймворка регистрации? Воспользуемся косвенной инъекцией — предоставим пользовательский Timber.Tree в область действия модульного теста.
Рассмотрим тестируемую систему. Тестируемая система с Timber протоколированием:
import timber.log.Timber
import java.lang.Exception
class SystemUnderTest(private val service: ItemsService) {
fun fetchData(): List<Entity> {
return try {
service.getAll()
} catch (exception: Exception) {
Timber.w(exception, "Service.getAll returned exception instead of empty list")
emptyList<Entity>()
}
}
}
interface ItemsService {
fun getAll(): List<Entity>
}
data class Entity(val id: String)
Теперь давайте создадим дерево Timber таким же образом, как мы создали TestAppender для теста SLF4J:
Расширяем Timber.Tree
Получаем входящий журнал (мы также создаем дополнительный класс данных)
Добавляем журнал в список
Размещаем это Tree (дерево)
Определение дерева испытаний:
import timber.log.Timber
class TestTree : Timber.Tree() {
val logs = mutableListOf<Log>()
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
logs.add(Log(priority, tag, message, t))
}
data class Log(val priority: Int, val tag: String?, val message: String, val t: Throwable?)
}
Теперь, используя это TestTree, мы можем написать модульный тест для счастливого и ошибочного пути:
import android.util.Log
import io.kotlintest.assertSoftly
import io.kotlintest.matchers.collections.shouldBeEmpty
import io.kotlintest.matchers.string.shouldContain
import io.kotlintest.shouldBe
import io.kotlintest.specs.StringSpec
import io.mockk.every
import io.mockk.mockk
import timber.log.Timber
class Test : StringSpec({
"given service error when get all called then log warn" {
//prepare logging context
val testTree = TestTree()
Timber.plant(testTree)
//setup system under test
val service = mockk<ItemsService> {
every { getAll() } throws Exception("Something failed :(")
}
val systemUnderTest = SystemUnderTest(service)
//execute system under test
systemUnderTest.fetchData()
//capture last logged event
val lastLoggedEvent = testTree.logs.last()
assertSoftly {
lastLoggedEvent.message shouldContain "Service.getAll returned exception instead of empty list"
lastLoggedEvent.priority shouldBe Log.WARN
}
}
"given service return values when get all called then do not log anything" {
//prepare logging context
val testTree = TestTree()
Timber.plant(testTree)
//setup system under test
val service = mockk<ItemsService> {
every { getAll() } returns listOf(Entity(id = "1"))
}
val systemUnderTest = SystemUnderTest(service)
//execute system under test
systemUnderTest.fetchData()
testTree.logs.shouldBeEmpty()
}
})
Первый тест — утверждение, что ошибка была зарегистрирована. Второй тест — утверждение, что журналы не были записаны.
В первом тестовом примере у нас следующий порядок выполнения:
a) Подготовьте контекст протоколирования и создайте тестовое дерево:
val testTree = TestTree()
Timber.plant(testTree)
Мы также можем быстро проверить, правильно ли мы разместили дерево:
println(Timber.forest()) //[tech.michalik.project.TestTree@1e7a45e0]
b) Выполните операторы given
и when
:
//setup system under test
val service = mockk<ItemsService> {
every { getAll() } throws Exception("Something failed :(")
}
val systemUnderTest = SystemUnderTest(service)
//execute system under test
systemUnderTest.fetchData()
c) Возьмите последнее зарегистрированное событие из тестового логгера и сделайте мягкое утверждение (soft assertion):
val lastLoggedEvent = testTree.logs.last()
assertSoftly {
lastLoggedEvent.message shouldContain "fetchData returned exception instead of empty list"
lastLoggedEvent.priority shouldBe Log.WARN
}
Я также создал хелпер-функцию для предоставления контекста TestTree в любом месте теста. Создать и разместить TestTree, выполнить лямбда-тело и удалить TestTree:
fun withTestTree(body: TestTree.() -> Unit) {
val testTree = TestTree()
Timber.plant(testTree)
body(testTree)
Timber.uproot(testTree)
}
С помощью этого синтаксиса использовать тестовое дерево повторно можно гораздо проще. Тестирование с помощью метода withTestTree
:
"given service error when get all called then log warn" {
//setup system under test
withTestTree {
val service = mockk<ItemsService> {
every { getAll() } throws Exception("Something failed :(")
}
val systemUnderTest = SystemUnderTest(service)
//execute system under test
systemUnderTest.fetchData()
//capture last logged event
val lastLoggedEvent = logs.last()
assertSoftly {
lastLoggedEvent.message shouldContain "fetchData returned exception instead of empty list"
lastLoggedEvent.priority shouldBe Log.WARN
}
}
}
Если вы хотите все время создавать и внедрять TestTree явно, это нормально. Повторное использование тестовых конфигураций таким образом — является делом ваших предпочтений и предпочтений вашей команды. Помните, что читабельность стоит на первом месте, и не всем может быть удобен такой синтаксис.
Резюме:
Если вам нужно проверять/утверждать логгеры в тестах, используйте косвенную инъекцию вместо мокинга статического метода.
Размещайте Timber.Tree для тестов так же, как вы размещаете деревья Timber в рабочем коде.
Создавайте хелперы, когда возникает необходимость в повторном легком использовании конфигураций.
Материал подготовлен в рамках курса «Kotlin QA Engineer».
Всех желающих приглашаем на demo-занятие «Тестирование нативных приложений на Kotlin Native». На занятии рассмотрим основы нативной разработки для Android/iOS, попробуем сделать и протестировать простое приложение по работе с данными на стороне платформы, а также научимся подключать сторонние библиотеки для Android/iOS (на примере OpenCV).
>> РЕГИСТРАЦИЯ