Skip to content

Testing library ‐ mockk

Devrath edited this page Oct 24, 2023 · 14 revisions

How it is useful

  • So we know when we have a class used in another class, We can make a fake version of that class and pass the fake implementation where it is being used to avoid errors in the actual implementation.
  • There is another way where we can mock it.
  • Basically mocking involves using a mirror version of the actual class implementation and once it is mirrored we can make the methods of that particular class behave as we want so that when the implementation of the class is used in another class they return or respond in a particular way.

When we use relaxed param in mockk

  • Consider the code mockk() and compared to another mockk(relaxed = true)
  • When relaxed=true is used the framework will give random values like empty, false, etc .. for the functions that contain return types otherwise the return types are not provided, and the mocked instance bay gives an error while running the test(No answer found).
  • Also we use this when we need the instance and do not bother with specific return types so random return types are fine.

When to use every{ ... } and when to use coEvery{ ... }

  • When normal functions are to be invoked we used every { ... }
  • When sus[end functions are involved , We use coEvery{ ... }

Mocking a class that is not accessible directly via the constructor of an implementation

  • Sometimes we mock an implementation and via the mock, we access a method of the instance.
  • But sometimes there might be an implementation mentioned inside the function and it can't be accessed via the constructor.
  • In such a scenario, We need to use mockkConstructor

Code

ProductApi.kt

interface ProductApi {
    @POST("/purchase")
    suspend fun purchaseProducts(@Body products: ProductsDto)

    @POST("/cancelPurchase/{id}")
    suspend fun cancelPurchase(@Path("id") purchaseId: String)
}

ProductRepositoryImpl.kt

class ProductRepositoryImpl(
    private val productApi: ProductApi,
    private val analyticsLogger: AnalyticsLogger
): ProductRepository {

    override suspend fun purchaseProducts(products: List<Product>): Result<Unit> {
        return try {
            // Observe that we cannot pass the custom Product in constructor since the functionality does not provide it
            val custProduct = CustomProduct(id = 1, name = "Power Ranger", price = 10.9)
            println(custProduct.name)

            productApi.purchaseProducts(
                products = ProductsDto(products)
            )
            Result.success(Unit)
        } catch (e: HttpException) {
            analyticsLogger.logEvent(
                "http_error",
                LogParam("code", e.code()),
                LogParam("message", e.message()),
            )
            Result.failure(e)
        } catch(e: IOException) {
            analyticsLogger.logEvent(
                "io_error",
                LogParam("message", e.message.toString())
            )
            Result.failure(e)
        } catch (e: Exception) {
            if(e is CancellationException) throw e
            Result.failure(e)
        }
    }

    override suspend fun cancelPurchase(purchaseId: String): Result<Unit> {
        TODO("Not yet implemented")
    }
}

TestCases

class ProductRepositoryImplTest {

    // Define the SUT: System under test :-> In our case it is repository
    private lateinit var sut : ProductRepositoryImpl

    private lateinit var api: ProductApi
    private lateinit var logger: AnalyticsLogger


    @BeforeEach
    fun setup(){
        api = mockk()
        logger = mockk(relaxed = true)
        sut = ProductRepositoryImpl(productApi = api, analyticsLogger = logger)
    }

    @Test
    fun `When there is Response Error for any input , Exception is logged`() = runBlocking {
        // Call the API and make it to generate exception mentioned whenever the api is used with any input
        coEvery {
            api.purchaseProducts(any())
        } throws mockk<HttpException>(){
            // <---- This describes how this Http Mock should behave ---->
            // On the exception, The code() block should return 404 error
            every { code() } returns 404
            // On the exception, THe message() block should return
            every { message() } returns "Test Message"
        }

        // Now the result must return specific error based on what we assert
        val result = sut.purchaseProducts(listOf())

        // Check if the failure is true
        assertThat(result.isFailure).isTrue()

        // Check if the logger event was triggered when error had occured
        verify {

            logger.logEvent(
                "http_error",
                LogParam("code", 404),
                LogParam("message", "Test Message")
            )
        }

    }


    @Test
    fun `When there is Response Error for particular input , Exception is logged`() = runBlocking{
        // THe api will throw the exception mentioned  when its being called with specific input
        coEvery {
            api.purchaseProducts(
                ProductsDto(
                    listOf(
                        Product(id = 1, name = "Hello", 5.0)
                    )
                )
            )
        } throws mockk<HttpException>(){
            // <---- This describes how this Http Mock should behave ---->
            // On the exception, The code() block should return 404 error
            every { code() } returns 404
            // On the exception, THe message() block should return
            every { message() } returns "Test Message"
        }

        // Now the result must return specific error based on what we assert
        val result = sut.purchaseProducts(listOf(
            Product(id = 1, name = "Hello", 5.0)
        ))

        // Check if the failure is true
        assertThat(result.isFailure).isTrue()

        // Check if the logger event was triggered when error had occured
        verify {

            logger.logEvent(
                "http_error",
                LogParam("code", 404),
                LogParam("message", "Test Message")
            )
        }

    }



    @Test
    fun `Mock object that you don't have access to , Exception is logged`() = runBlocking {
        // Call the API and make it to generate exception mentioned whenever the api is used with any input
        coEvery {
            api.purchaseProducts(any())
        } throws mockk<HttpException>(){
            // <---- This describes how this Http Mock should behave ---->
            // On the exception, The code() block should return 404 error
            every { code() } returns 404
            // On the exception, THe message() block should return
            every { message() } returns "Test Message"
        }

        // Observe the output instead of `power rangers` the output `thunder cats` is printed
        mockkConstructor(CustomProduct::class)
        every { anyConstructed<CustomProduct>().name  } returns "Thunder cats"

        // Now the result must return specific error based on what we assert
        val result = sut.purchaseProducts(listOf())

        // Check if the failure is true
        assertThat(result.isFailure).isTrue()

        // Check if the logger event was triggered when error had occured
        verify {

            logger.logEvent(
                "http_error",
                LogParam("code", 404),
                LogParam("message", "Test Message")
            )
        }

    }
}