Gradle, JUpiter, Kotlin and Mockito aboard the same boat
Introduction
When you create a new Android project, Android Studio generates the boilerplate you need to quickstart it. It generates the first screen files, the Gradle configuration and some tests based on JUnit4. Today we will migrate these tests to the new JUnit platform with JUpiter, the new JUnit API.
Because everyone in the Android sphere is talking about Kotlin, we can’t give examples in Java code anymore. So, in this post we will use this tools:
Of course, I based this post on an Android project, but almost all this tips and tricks can be applied to others frameworks.
Add JUnit5 platform to the project
First of all, add the JUnit5 plugin to your root build.gradle
.
classpath "de.mannodermaus.gradle.plugins:android-junit5:1.0.31"
Add dependencies to your module’s build.gradle
:
apply plugin: 'de.mannodermaus.android-junit5'
dependencies {
// Remove this line!
testImplementation 'junit:junit:4.12'
// Required
testImplementation junit5.unitTests()
// Optional
testImplementation junit5.parameterized()
// Android
androidTestImplementation junit5.instrumentationTests()
}
To start, write a test
Some annotations have been renamed by JUpiter, but @Test
stays @Test
within a new package:
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
class SimpleUnitTest {
@Test
fun testAddition() {
assertEquals(4, 2 + 2)
}
}
This test class will produce the following output, nothing new yet.
Parametrized tests
Parametrized tests are now JUnit natives, one way to use them is to provide inputs through a method:
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource
import java.util.stream.Stream
@DisplayName("Tests on calculator")
class SimpleUnitTest {
@DisplayName("Check if addition works")
@MethodSource("calculationSource")
@ParameterizedTest(name = "Adding {0} and {1} should give you {2}")
fun whateverTheName(a: Int, b: Int, result: Int) {
assertEquals(result, a + b)
}
companion object {
@JvmStatic
fun calculationSource(): Stream<Arguments> {
return Stream.of(
Arguments.of(2, 2, 4),
Arguments.of(1, 5, 6)
)
}
}
}
@DisplayName("Some human readable text")
can be used either on test classes or test methods to provide a more readable name than the technical name. @MethodSource("calculationSource")
sets the name of the method to call to provide test inputs. Take note of @JvmStatic
on calculationSource
, this method must be a static method of the class. You can also set the wording of each iteration of the test using @ParameterizedTest(name = "Adding {0} and {1} should give you {2}")
.
Maybe you’ll encouter an issue about Stream.of
or Arguments.of
that required Java 8 and still use Java 6, your may configure your build.gradle file:
android.kotlinOptions {
jvmTarget = "1.8"
}
Nested test classes
Nested test classes can be really useful to group test methods. For instance, say we have test methods that require the same kind of init before executing the code, on JUnit4 we used @Before
that apply on every methods, on Jupiter we can use @BeforeEach
on a Kotlin inner class
annotated @Nested
. The setup will be executed before each methods within the nested test class.
@DisplayName("Tests on calculator")
class NestedExample {
@Nested
@DisplayName("Adding to 0 return")
inner class AddingToZero {
@BeforeEach
fun setup() {
// Given
// ...
}
@ParameterizedTest(name = "Adding {0} to zero should give you {0}")
@DisplayName("Check if addition works")
@ValueSource(ints = [2, 4, 5])
fun whateverTheName(a: Int) {
// When
val addition = 0 + a
// Then
assertEquals(a, addition)
}
}
@Nested
@DisplayName("Another init")
inner class AnotherInitNeeded {
// ...
}
}
In Kotlin, inner
classes can’t have a companion object. When you need @MethodSource
within an inner class, you must change the lifecycle of the nested test class:
@DisplayName("Tests on calculator")
class NestedExample {
@Nested
@DisplayName("Adding two number")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class AddingTwoNumbers {
@ParameterizedTest(name = "Adding {0} to {1} should give you {2}")
@DisplayName("Check if addition works")
@MethodSource("calculationSource")
fun whateverTheName(a: Int, b: Int, result: Int) {
assertEquals(result, a + b)
}
fun calculationSource(): Stream<Arguments> {
return Stream.of(
Arguments.of(2, 2, 4),
Arguments.of(1, 5, 6)
)
}
}
}
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
tells JUnit5 to start an instance only once for all the methods of AddingTwoNumbers
class, instead of the default value PER_METHOD
. And now, the calculationSource()
method is not part of a companion anymore.
Mockito VS Kotlin
One of the biggest principles of Kotlin is having closed classes by default. It means that, by default, you cannot subclass any classes. You will have to declare the super-class as open
, to explicitly say that it is subclassable.
class A
class B: A() <= Compilation error
open class C
class D : C() <= OK
Why is this an issue? Because Mockito uses this mecanism to create mocks. I don’t know what’s your idea about it, but I won’t open all my classes just to make unit tests.
Hopefully, since version 2.1.0, Mockito offers a way to mock our Kotlin classes and methods without modifying our code (NB: It also applies for Java final classes and final methods). Please give a look to Mockito’s Javadoc about Mocking final.
Rules became Extensions
JUnit rules no longer exist in Jupiter, we now use the Jupiter Extensions. I didn’t find yet any official extensions for Mockito, but JUnit team proposed an implementation.
Here is my Kotlin version of the MockitoExtension:
import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.jupiter.api.extension.ExtensionContext.Namespace
import org.junit.jupiter.api.extension.ParameterContext
import org.junit.jupiter.api.extension.ParameterResolutionException
import org.junit.jupiter.api.extension.ParameterResolver
import org.junit.jupiter.api.extension.TestInstancePostProcessor
import org.mockito.Mock
import org.mockito.Mockito.mock
import org.mockito.MockitoAnnotations
import java.lang.reflect.Parameter
class MockitoExtension : TestInstancePostProcessor, ParameterResolver {
override fun postProcessTestInstance(testInstance: Any, context: ExtensionContext) {
MockitoAnnotations.initMocks(testInstance)
}
@Throws(ParameterResolutionException::class)
override fun supportsParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Boolean =
parameterContext.parameter.isAnnotationPresent(Mock::class.java)
@Throws(ParameterResolutionException::class)
override fun resolveParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Any =
getMock(parameterContext.parameter, extensionContext)
private fun getMock(parameter: Parameter, extensionContext: ExtensionContext): Any {
val mockType = parameter.type
val mocks = extensionContext.getStore(Namespace.create(MockitoExtension::class.java, mockType))
val mockName = getMockName(parameter)
return if (mockName != null) {
mocks.getOrComputeIfAbsent<String, Any>(mockName) { _ -> mock(mockType, mockName) }
} else {
mocks.getOrComputeIfAbsent<String, Any>(mockType.canonicalName) { _ -> mock(mockType) }
}
}
private fun getMockName(parameter: Parameter): String? {
val explicitMockName = parameter.getAnnotation(Mock::class.java).name.trim { it <= ' ' }
return when {
!explicitMockName.isEmpty() -> explicitMockName
parameter.isNamePresent -> parameter.name
else -> null
}
}
}
And use the @ExtendWith
to apply it on your tests:
class TheService {
fun getIt() : Int = 42
}
class TheSuv(val service: TheService) {
fun getItTwice() = service.getIt() * 2
}
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.BDDMockito.given
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.Mockito.verify
@ExtendWith(MockitoExtension::class)
class MockitoExample {
@Mock
lateinit var theService: TheService
@InjectMocks
lateinit var theSuv: TheSuv
@Test
fun testIt() {
// Given
given(theService.getIt()).willReturn(13)
// When
val twice = theSuv.getItTwice()
// Then
verify(theService).getIt()
assertEquals(26, twice)
}
}
Still any()
issue?
You may want to use the mocked method any()
from Mockito, but will face this error: java.lang.IllegalStateException: any() must not be null
. I invite you to use the nhaarman/mockito-kotlin library, it provides workarounds for this kind of call.
For instance (from Mockito-kotlin 2.x implementation):
inline fun <reified T : Any> any(): T {
return Mockito.any(T::class.java) ?: createInstance()
}
Conclusion
As you see it is not that easy to make all this tools to work together, but it is possbile. And I hope I conviced you to move from JUnit4 to JUnit5+Jupiter.
We haven’t talked about all the features available in the Jupiter’s API, there are a lot of nice posts about it.
About Mockito, you will maybe want to look at alternatives, you should start with Mockk. It is quite new, however it is a really good alternative to Mockito for Kotlin development.