Узнайте, как создать пользовательское 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:

  1. Расширяем Timber.Tree

  2. Получаем входящий журнал (мы также создаем дополнительный класс данных)

  3. Добавляем журнал в список

  4. Размещаем это 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 явно, это нормально. Повторное использование тестовых конфигураций таким образом — является делом ваших предпочтений и предпочтений вашей команды. Помните, что читабельность стоит на первом месте, и не всем может быть удобен такой синтаксис.

Резюме:

  1. Если вам нужно проверять/утверждать логгеры в тестах, используйте косвенную инъекцию вместо мокинга статического метода.

  2. Размещайте Timber.Tree для тестов так же, как вы размещаете деревья Timber в рабочем коде.

  3. Создавайте хелперы, когда возникает необходимость в повторном легком использовании конфигураций.


Материал подготовлен в рамках курса «Kotlin QA Engineer».

Всех желающих приглашаем на demo-занятие «Тестирование нативных приложений на Kotlin Native». На занятии рассмотрим основы нативной разработки для Android/iOS, попробуем сделать и протестировать простое приложение по работе с данными на стороне платформы, а также научимся подключать сторонние библиотеки для Android/iOS (на примере OpenCV).
>> РЕГИСТРАЦИЯ

Комментарии (0)