
Узнайте, как создать пользовательское 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).
>> РЕГИСТРАЦИЯ
