Tomáš Repčík - 5. 11. 2023

How to think about fakes and mocks to make your testing easier

Avoid being blocked by unmatched input to the mock with fake and leverage power of mock when needed

While building your app, you implement the database, API client and other third-party solutions. If you follow the proper structure, you end up with abstractions for every data provider you use. A side-effect of abstractions is easier testability of all the components. Everything is done as should be and writing tests is one of the tasks of every proper developer.

Mocking framework is a toolbox part of every developer and should be used. I used to apply it almost everywhere and it was not the best solution. Here is why the overusing mocking framework can be a bad idea:

If you overuse the mocking framework, you can end up debugging the stubs and not making any progress on your real issue. I did it multiple times, when instead of solving the issue I ended up trying to tinker with the mocked stubs or hitting some new limitation of the mocking library. It took quite some time to find the right solution or it got embarrassing.

In realm of unit tests, we try to test the smallest units of logic and everything else should not interfere with the target of the test. Here, the mocks can do more harm than good. We do not need them in the most cases. A simple fake or dummy would resolve the whole situation.

Fake is simplified implementation of the original implementation. Dummy is just placeholder with no real implementation.

The solution is quite simple. Instead of using the mock, create some simple fake to use in the test. It has multiple advantages:

You might argue that writing all of the fakes will take a lot of time. Yes, that is true. In the beginning, it will take a lot of time, but testing and coding should share the same amount of time spent. A couple of fakes later, you will find yourself repurposing them in multiple test cases and time spent on them will come back eventually.

Mockups should be used only for 2 purposes:

Practical example

I will use Kotlin code for demonstration, but it is applicable in any language. Imagine, we want to store our settings options in the mobile app, from which we are get a simple boolean (It can be SharedPreferences in Android, UserDefaults in iOS or FlutterSecureStorage in Flutter).

For a simple demonstration, here is a sample code for abstraction and implementation.

/**
 * our abstraction to handle storage for premium user option
 */
abstract class SettingsStorageTemplate {

    abstract fun isPremium(): Boolean

    abstract fun setPremium(isPremium: Boolean)

}

/**
 * real implementation with Android preferences via [SharedPreferences]
 */
class SettingsStorage(private val preferences: SharedPreferences) : SettingsStorageTemplate() {

    override fun isPremium(): Boolean = preferences.getBoolean("isPremium", false)

    override fun setPremium(isPremium: Boolean) =
        preferences.edit().putBoolean("isPremium", isPremium).apply()

}

The implementation does not do anything extra. It stores and reads our premium option.

In testing, we could create a mock and stub it as follows with mockk library:

@Mock
lateinit var settingsStorage: SettingsStorageTemplate

settingsStorage.stub {
    on { this.isPremium() } doAnswer { true }
}

Writing a stub for every test case can be a bit tedious if the value changes frequently. We can do it a bit better with fake implementation. The fake implementation stores the options into the temporary memory and it is not written into any permanent database. Initialization of the settings resets the settings to default.

/**
 * fake implementation, which stores the value in memory
 * within unit test it will provide simplified, but full functionality
 */
class SettingsStorageFake : SettingsStorageTemplate() {

    private var isPremium: Boolean = false

    override fun isPremium(): Boolean = isPremium

    override fun setPremium(isPremium: Boolean) {
        this.isPremium = isPremium
    }

    /**
     * additional static constructors for quick startup
     */
    companion object {
        fun premiumUser(): SettingsStorageTemplate = SettingsStorageFake().apply {
            setPremium(true)
        }

        fun regularUser(): SettingsStorageTemplate = SettingsStorageFake().apply {
            setPremium(false)
        }
    }

}

After setting up our fake, the test case arrangement would look like this:

  val settings = SettingsStorageFake().apply {
    setPremium(true)
  }

… and that’s it! It is much more readable and clear what the test case is intended to set up. If you need quick initialization of settings, you can use methods from companion objects like:

  SettingsStorageFake.premiumUser()
  SettingsStorageFake.regularUser()

To read more about dependency injection and abstraction of components go here.

Thanks for reading!

Subscribe for more
LinkedIn GitHub Mail Medium