Home Testing of web services Don’t try to simulate statics:test Timber Logger with trees

Don’t try to simulate statics:test Timber Logger with trees

by admin
Don't try to simulate statics:test Timber Logger with trees

Learn how to create a custom Timber Tree to test log output in unit tests. Mocking Timber, testing logs in unit tests.

What is Timber? GitHub – JakeWharton/timber:A logger with a small, extensible API which provides utility on top of Android’s normal Log class. github.com

Timber is the gold standard for logging in Android. It uses the concept of trees – you can treat them as different log message output channels.Normally in an Android app, you should write the following code to use Timber in debug mode Timber Tree placement for debugging logs :

class App:Application(){override fun onCreate(){super.onCreate()if(BuildConfig.DEBUG){Timber.plant(Timber.DebugTree())}}}

You can also use Timber to log messages to remote analytics services such as Sentry or Firebase:

class FirebaseLogging: Timber.Tree(){override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {FirebaseCrash.logcat(priority, tag, message);FirebaseCrash.report(t);}}

As I mentioned in previous article , sometimes testing logs can be very important for debugging. Therefore, in the next section, we will look at the technique of using Timber to test logs.

How do you model Timber?

First of all, forget about using the mock-static constructs from Mockito, MockK, or PowerMock for this purpose. While these tools are useful, they are not necessary in most cases.

So, how are we going to provide a test implementation for the registration framework? Let’s use indirect injection – let’s provide a custom Timber.Tree to the scope of the unit test.

Consider the system under test.

The system under test with Timber logging :

import timber.log.Timberimport java.lang.Exceptionclass 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)

Now let’s create a Timber tree in the same way we created TestAppender for SLF4J test:

  1. Extending Timber.Tree

  2. We get the incoming log (we also create an additional data class)

  3. Adding a log to the list

  4. Placing this Tree

Definition of the test tree :

import timber.log.Timberclass 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?)}

Now, using this TestTree, we can write a unit test for the happy and mistaken path :

import android.util.Logimport io.kotlintest.assertSoftlyimport io.kotlintest.matchers.collections.shouldBeEmptyimport io.kotlintest.matchers.string.shouldContainimport io.kotlintest.shouldBeimport io.kotlintest.specs.StringSpecimport io.mockk.everyimport io.mockk.mockkimport timber.log.Timberclass Test : StringSpec({"givenservice error whenget all called then log warn" {//prepare logging contextval testTree = TestTree()Timber.plant(testTree)//setup system under testval service = mockk<ItemsService> {every { getAll() }throws Exception("Something failed :(")}val systemUnderTest = SystemUnderTest(service)//execute system under testsystemUnderTest.fetchData()//capture last logged eventval 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 contextval testTree = TestTree()Timber.plant(testTree)//setup system under testval service = mockk<ItemsService> {every { getAll() }returns listOf(Entity(id = "1"))}val systemUnderTest = SystemUnderTest(service)//execute system under testsystemUnderTest.fetchData()testTree.logs.shouldBeEmpty()}})

The first test is the statement that the error has been logged. The second test is the statement that logs have not been logged.

In the first test example we have the following order of execution :

a) Prepare the logging context and create a test tree :

val testTree = TestTree()Timber.plant(testTree)

We can also quickly check if we have placed the tree correctly :

println(Timber.forest()) //[tech.michalik.project.TestTree@1e7a45e0]

b) Execute the operators given and when :

//setup system under testval service = mockk<ItemsService> {every { getAll() } throws Exception("Something failed :(")}val systemUnderTest = SystemUnderTest(service)//execute system under testsystemUnderTest.fetchData()

c) Take the last logged event from the test logger and make soft assertion :

val lastLoggedEvent = testTree.logs.last()assertSoftly {lastLoggedEvent.message shouldContain "fetchData returned exception instead of empty list"lastLoggedEvent.priority shouldBe Log.WARN}

I also created a helper function to provide a TestTree context anywhere in the test. Create and place a TestTree, execute a lambda-body, and delete a TestTree:

fun withTestTree(body: TestTree.() -> Unit) {val testTree = TestTree()Timber.plant(testTree)body(testTree)Timber.uproot(testTree)}

Reusing the test tree is much easier with this syntax. Testing with the withTestTree :

"given service error when get all called then log warn" {//setup system under testwithTestTree {val service = mockk<ItemsService> {every { getAll() } throws Exception("Something failed :(")}val systemUnderTest = SystemUnderTest(service)//execute system under testsystemUnderTest.fetchData()//capture last logged eventval lastLoggedEvent = logs.last()assertSoftly {lastLoggedEvent.message shouldContain "fetchData returned exception instead of empty list"lastLoggedEvent.priority shouldBe Log.WARN}}}

If you want to create and embed TestTree explicitly all the time, that’s fine. Reusing test configurations in this way is a matter of your preference and your team’s preference. Remember that readability comes first, and not everyone may be comfortable with this syntax.

Summary :

  1. If you need to check/validate loggers in tests, use indirect injection instead of static method mocking.

  2. Place Timber.Tree for tests the same way you place Timber trees in working code.

  3. Create helpers when there is a need to reuse easy configurations.


Material prepared as part of the course "Kotlin QA Engineer".

Everyone is welcome to a demo session "Testing native applications on Kotlin Native" This class will cover the basics of native development for Android/iOS, try to make and test a simple platform-side data application, and learn how to connect third-party libraries for Android/iOS (using OpenCV as an example).
> > REGISTRATION

You may also like