Oleksii Fedorov

@oleksii_f

How Kotlin’s “@Deprecated” Relieves Pain of Colossal Refactoring?

I’m going to tell you a real story how we saved ourselves tons of time. The power of Kotlin’s @Deprecated refactoring is astounding.

Don’t think it is the same as Java’s @Deprecated. It is much more powerful. It allows for multi-step automated replacement.

So, let me tell you about it.

We were migrating our code base from Java to Kotlin. When somebody needed to touch an existing Java file, we would auto-convert it to Kotlin. And only then introduce change. Otherwise, all new code was written in Kotlin.

We were using mockito library in our unit tests. And as you might know, mockito heavily relies on Mockito.when method.

when is a reserved keyword in Kotlin. So we had to fence it with backticks.

To be honest, it looked quite ugly, and was awkward to type:

class AccountTest {
    private val statementReporter =
mock(AccountStatementReporter::class.java)

private val account = Account(statementReporter)
    @Test
fun `testing with Mockito when`() {
        `when`(statementReporter.canPrintItem())
.thenReturn(true)
        account.generateStatement()
        verify(statementReporter).printItem("-5,00\t73,91")
    }
}

At the time, we weren’t aware of the mockito-kotlin, so we have built our own whenever function to mitigate the keyword problem:

fun <T> whenever(x: T): OngoingStub<T> {
return `when`(x)
}

It is an alias for Mockito.when.

And it can be typed quickly, and IntelliJ helps with autocomplete. Which is nice:

    @Test
fun `testing with handcrafted whenever`() {
        whenever(statementReporter.canPrintItem())
.thenReturn(true)
        account.generateStatement()
        verify(statementReporter).printItem("-5,00\t73,91")
    }

Over the course of half a year, we had this whenever-based code everywhere.

Thousands of occurrences.

    @Test
fun `testing with handcrafted whenever – false case`() {
        whenever(statementReporter.canPrintItem())
.thenReturn(false)
        account.generateStatement()
        verify(statementReporter).canPrintItem()
verifyNoMoreInteractions(statementReporter)
    }

At that point, somebody has discovered BDDMockito API. It has allowed us to use slightly different words for when, thenReturn, thenThrow, and others.

Most importantly, it was part of the official mockito library!

By the way, mockito is usually used when testing in Java and Kotlin.

Curious about testing in Kotlin? Or Kotlin in general?

I suggest you download my free Ultimate Tutorial: Getting Started With Kotlin. You will be minutes away from your first full-fledged command-line application in Kotlin.

The tutorial is an 80-page book and is entirely hands-on. You can follow along with it while learning about various Kotlin and IntelliJ features. And lots of other things.

Anyways.

Here is an example of BDDMockito syntax for you:

    @Test
fun `testing with handcrafted whenever`() {
        given(statementReporter.canPrintItem())
.willReturn(true)
        account.generateStatement()
        verify(statementReporter).printItem("-5,00\t73,91")
    }

Of course, we wanted to switch from hand-crafted function to the official API!

By the way, BDDMockito is using slightly different naming. But the mocking structure is same:

// given vs when
given(statementReporter.canPrintItem())
// willReturn vs thenReturn
.willReturn(true)

As you can see, instead of when it allows us to use given, and all the then*methods become will*.

At that point, we decided to switch from whenever to given. We didn’t want to change thousands of occurrences in one go, as it will take a lot of time.

And we just might have changed some things that shouldn’t have been changed if we were to do the search & replace.

So, we decided to put a simple @Deprecated annotation on the wheneverfunction. That would make all the usages appear as deprecated (crossed out) in IntelliJ. New usages are unlikely to be introduced, as well.

As my pair was typing it out, I’ve noticed that IntelliJ was showing something interesting in the parameter list tooltip:

It got me curious about this replaceWith parameter. If I understood it correctly, this would allow some automated replacement.

We fiddled a bit with it. And it seemed like you can do such replacement.

Here is the code:

@Deprecated(
"use BDDMockito.given",
replaceWith = ReplaceWith(
"given(x)",
"org.mockito.BDDMockito.Companion.given"))
fun <T> whenever(x: T): OngoingStub<T> {
return `when`(x)
}

Now we tried to go to the usage of whenever, place our cursor on the deprecated whenever call, and press ALT + ENTER (the magic hotkey of IntelliJ).

What happened is amazing:

You can indeed do the automated replacement. And there is even an option to replace it in the WHOLE PROJECT!

Astounding!

But first, we decided to double-check how it will work just for that one occurrence.

That was the right decision:

It resulted in a compile error as BDDOngoingStub<T> doesn’t have thenReturnmethod defined on it. It should be willReturn.

And now it got us thinking.

What if we could define that method using “extension methods” feature of Kotlin? That would make things compile. Sure.

But then we still didn’t get rid of the custom code yet…

“So what if we deprecate that extension method too?!” — I asked.

“And then we can add replaceWith!” – my pair replied.

In rejoice, we started doing it:

fun <T> BDDOngoingStub<T>.thenReturn(x: T) {
willReturn(x)
}

That allowed us to fix the problem by auto-importing thenReturn extension:

And now, we were about to deprecate it with the same kind of annotation:

@Deprecated(
"use willReturn",
replaceWith = ReplaceWith(
"willReturn(x)"))
fun <T> BDDOngoingStub<T>.thenReturn(x: T) {
willReturn(x)
}

Now we were able to replace these thenReturn method calls with willReturn:

We could have run that replacement for the whole project, as well.

Neat.

We thought we were ready to do the “WHOLE PROJECT” refactoring:

That still forced us to do “ALT+ENTER” auto-importing in about 40 files.

But that, to be honest, was a quick one.

There is a shortcut to jump to the next compile error: CMD+ALT+DOWN. So the fix for that little problem was to press one shortcut and another right after.

Over and over and over again.

It took us a minute to do that.

We did the same “WHOLE PROJECT” replacement for thenReturn too:

That worked without any problems:

    @Test
fun `testing with handcrafted whenever`() {
        given(statementReporter.canPrintItem())
.willReturn(true)
        account.generateStatement()
        verify(statementReporter).printItem("-5,00\t73,91")
    }
    @Test
fun `testing with handcrafted whenever – false case`() {
        given(statementReporter.canPrintItem())
.willReturn(false)
        account.generateStatement()
        verify(statementReporter).canPrintItem()
verifyNoMoreInteractions(statementReporter)
    }

Finally, we double-checked that there are no occurrences of whenever, thenReturn, and thenThrow.

There wasn’t.

So we safely removed our testutils/whenever.kt file.

As you can guess, some wit goes into these @Deprecated definitions. But once in place – saves you a lot of time and relieves you of a lot of pain.

I genuinely thank you for reading this piece. It was quite a ride for me.

If you liked what you just read, I would appreciate some Medium claps! Social media shares are warmly welcome, as well. Same goes for the feedback!

If you are just starting in Kotlin, you will make me happy by downloading this free ultimate tutorial about getting started with Kotlin.

Thank you!

More exciting reading

More by Oleksii Fedorov

Topics of interest

More Related Stories