Introduction to Kotlin Unit Testing with MockK library

MockK

  • First-class support for Kotlin features
  • Offer a pure Kotlin mocking DSL for writing clean and idiomatic code 
  • Mock support for final classes and functions 

Why do we mock?

  • Your source code is complicated 
  • Your code has external inter-actors 
  • Mocking is a process used in unit testing when the unit being tested has external dependencies. The purpose of mocking is to isolate and focus on the code being tested and not on the behavior or state of external dependencies.

Available Testing Frameworks 

What is mockk? Mocking library for Kotlin. It is an open-source library focused on making mocking in Kotlin great. It is a library with the possibility of mocking default arguments, final classes, varargs, coroutines and extension methods.

What is Mockito? Tasty mocking framework for unit tests in Java. It is a mocking framework that tastes really good. It lets you write beautiful tests with a clean & simple API. It doesn’t give you hangover because the tests are very readable and they produce clean verification errors.

MockK Implementation : 


testImplementation "io.mockk:mockk:1.10.5"
class CarTest{
@Test
fun `given direction and speed when driven a car then test it's speed`(){
//given
//create a mock/spy
val car = mockk<Car>()

// stub calls
every { car.drives(Direction.NORTH) } returns "Driving North!"

//execute code to test
car.drives(Direction.NORTH)

//verify
verify { car.drives(Direction.NORTH) }
}
}

Here we have two conditions: 


  • Strict mock : By default mocks are strict, so we need to strictly mock before executing it otherwise we will get error. 
  • Relaxed mock : will provide a dummy stub 
val car = mockk<Car>(relaxed = true)

Tip : Use Strict mock for nice and clear tests 



Mock an Object 

// mock the object
val mockObj = mockkObject(Math)

Hierarchical mocking 
Mixing mocks and real objects 


@Test
fun `given address book then test it`(){
val addressBookMock = mockk<AddressBook> {
every { contacts } returns listOf(
mockk{
every { name } returns "Sam"
every { telephone } returns "3334567889"
every { address.city } returns "Milan"
every { address.zip } returns "20513"
},
mockk{
every { name } returns "Sunny"
every { telephone } returns "333456222"
every { address } returns mockk {
every { city } returns "Rome"
every { zip } returns "12304"
}
}
)
}
}


Spy : also mix mocks and real objects 



@Test
fun `given direction test drive method using spy`(){
//create a spy
val car = spyk(Car())

// when
car.drives(Direction.NORTH)

// then
verify { car.drives(Direction.NORTH) }
}

Annotations 



// Annotations
@MockK
lateinit var car1: Car

@RelaxedMockK
lateinit var car2: Car

@MockK(relaxUnitFun = true)
lateinit var car3: Car

@SpyK
var car4 = Car()

@InjectMockKs
var trafficSystem = TrafficSystem()

@Before
fun setUp() = MockKAnnotations.init(this)

every{} - mock the data with this method 

verify{} - method to verify if the specific method is called (and how many times)

  1. verifyAll
  2. verifySequence
  3. verifyOrder 
  4. confirmVerified
verify(atLeast = 1) { car.drives(any()) }
verify(atMost = 2) { car.drives(any()) }
verify(exactly = 3) { car.drives(any()) }
verify(timeout = 1000L) { car.drives(any()) }

Group Verifications 

verify { 
car.drives(Direction.NORTH)
car.drives(Direction.EAST)
car.drives(Direction.SOUTH,speed = 40)
}

Verify in order 


//verify in sequence order 
car.drives(Direction.NORTH)
car.drives(Direction.EAST)
verifySequence {
car.drives(Direction.NORTH)
car.drives(Direction.EAST)
}

Confirming verifications 


Check whether all calls were covered by verify statements 

@Test
fun `given Dog when barks then test`(){
// mock
val dog = mockk<Dog>()

every { dog.barks() } returns "Bow! Bow!"

dog.barks()

verify { dog.barks() } // if you comment this line the test will fail
confirmVerified(dog)
}

Option to exclude the less significant calls 

// exclude calls 
excludeRecords { car.drives(Direction.SOUTH) }

Matcher expressions 

// combine matcher expressions
every {
car.drives(
direction = or(Direction.SOUTH, Direction.EAST),
speed = range(10L, 150L, fromInclusive = true, toInclusive = true)
)
} returns "Driving!!"

Capturing Arguments 


@Test
fun `capturing Arguments Test`(){
val car = mockk<Car>()
val list = mutableListOf<Long>()

every { car.drives(any(), capture(list)) } returns "Driving.."

car.drives(Direction.EAST, 55)
car.drives(Direction.SOUTH, 80)
Assert.assertTrue(list.containsAll(listOf(55L,60L)))
}


Mocking Constructor 


mock the newly created objects 

@Test
fun `mock Constructor`(){
// mock
mockkConstructor(Car::class)

every { anyConstructed<Car>().drives(any()) } returns "Driving!"

Car().drives(Direction.EAST)

verify { anyConstructed<Car>().drives(Direction.EAST) }

}


Private Function Mocking 


@Test
fun `private function test`(){
// mock
val car = spyk<Car>(recordPrivateCalls = true)
// between square bracket specify the method of private function to mock it
every { car["accelerate"]() } returns "going not so fast"

car.drives(Direction.EAST)

verifySequence {
car.drives(Direction.EAST)
car["accelerate"]()
}
}

Property mocking 


every { car getProperty "speed" } returns 22
every { car setProperty "acceleration" value less(5) } just Runs
every { car invokeNoArgs "accelerate" } returns "going slowly"
every { car invoke "accelerate" withArguments listOf("foo", "bar") } returns "going slowly"


Coroutine Mocking 

testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.5"
@Test
fun `coroutine mock test`(){
val car = mockk<Car>()
coEvery { car.drives(Direction.EAST) } returns "Driving East"
car.drives(Direction.EAST)
coVerify { car.drives(Direction.EAST) }
}

Clear and informative error messages 


Verification failed: call 1 of 1: Car(#5).drives(any(), eq(30))). 5 matching calls found, but needs at least 4 and at most 4 calls
Calls:
Error Code: 
verify(exactly = 4) { car.drives(any()) }





Thank you. 
N.K.















Comments

Popular Posts