4

I have a helper class to save user object to shared preferences. I have used a serialize(): String function and a create(serializedString: String) function in my User data model. They use GSon serializer and are working good as suggested by the unit tests on them.

Now my helper class is called SharedPreferenceUserStore.kt which takes a Context object. The code is:

class SharedPreferenceUserStore(context: Context) {
    companion object {
        val TAG = SharedPreferenceUserStore::class.java.simpleName
    }

    var userLocalSharedPref: SharedPreferences =
        context.getSharedPreferences(USER_LOCAL_STORE_SHARED_PREF_NAME, Context.MODE_PRIVATE)

    /*
    Store the required data to shared preference
     */
    @SuppressLint("ApplySharedPref")
    fun storeUserData(user: User) {
        val userLocalDatabaseEditor = userLocalSharedPref.edit()
        val serializedData = user.serialize()

        userLocalDatabaseEditor.putString(
            USER_LOCAL_STORE_SHARED_PREF_SERIALIZED_DATA_KEY,
            serializedData
        )
        if (userLocalDatabaseEditor.commit()) {
            Log.d(TAG, " Store Commit return true")
        }
    }


    /*
    Clear all the locally stored data from the shared pref
     */
    @SuppressLint("ApplySharedPref")
    fun clearUserData() {
        val userLocalDatabaseEditor = userLocalSharedPref.edit()
        userLocalDatabaseEditor.clear()
        userLocalDatabaseEditor.commit()
    }

    fun getLoggedInUser(): User? {
        val stringUser = userLocalSharedPref.getString(
            USER_LOCAL_STORE_SHARED_PREF_SERIALIZED_DATA_KEY, "")

        return if (stringUser==null || stringUser == ""){
            null
        } else{
            User.create(stringUser)
        }
    }

And I have written some unit tests for this helper class as follows:

@RunWith(JUnit4::class)
class SharedPreferenceUserStoreTest {

    lateinit var sharedPreferenceUserStore: SharedPreferenceUserStore
    lateinit var user: User

    //to be mocked
    lateinit var sharedPreferences: SharedPreferences
    lateinit var sharedPreferencesEditor: SharedPreferences.Editor
    lateinit var context: Context

    @Before
    fun setUp() {
        //mocking Context and SharedPreferences class
        context = mock(Context::class.java)
        sharedPreferences = mock(SharedPreferences::class.java)
        sharedPreferencesEditor = mock(SharedPreferences.Editor::class.java)

        //specifying that the context.getSharedPreferences() method call should return the mocked sharedpref
        `when`<SharedPreferences>(context.getSharedPreferences(anyString(), anyInt()))
            .thenReturn(sharedPreferences)
        //specifying that the sharedPreferences.edit() method call should return the mocked sharedpref editor
        `when`(sharedPreferences.edit()).thenReturn(sharedPreferencesEditor)
        //specifying that the sharedPreferencesEditor.putString() method call should return the mocked sharedpref Editor
        `when`(sharedPreferencesEditor.putString(anyString(), anyString())).thenReturn(
            sharedPreferencesEditor
        )
        `when`(sharedPreferences.getString(anyString(), anyString())).thenReturn("")

        //instantiating  SharedPreferenceUserStore from the mocked context
        sharedPreferenceUserStore = SharedPreferenceUserStore(context)


        user = User(
            35,
            "Prashanna Bhandary",
            "prashanna.bhandary@gmail.com",
            "dd58a617ea618010c2052cb54079ad67.jpeg",
            "98********",
            "test address 01",
            1,
            "yes",
            "2019-08-30 04:56:43",
            "2019-08-30 05:14:47",
            0
        )
    }

    @After
    fun tearDown() {
    }

    @Test
    fun passUser_storeUserData() {
        sharedPreferenceUserStore.storeUserData(user)

        verify(sharedPreferencesEditor).putString(
            Constants.USER_LOCAL_STORE_SHARED_PREF_SERIALIZED_DATA_KEY,
            user.serialize()
        )
        verify(sharedPreferencesEditor).commit()
    }

    @Test
    fun testClearUserData() {
        sharedPreferenceUserStore.clearUserData()

        verify(sharedPreferencesEditor).clear()
    }


    @Test
    fun testGetLoggedInUser_storeNotCalled() {
        //calling getLoggedInUser() without calling storeUserData() should give null
        assertEquals(null, sharedPreferenceUserStore.getLoggedInUser())
        //verify that getString() was called on the shared preferences
        verify(sharedPreferences).getString(Constants.USER_LOCAL_STORE_SHARED_PREF_SERIALIZED_DATA_KEY, "")
    }

    @Test
    fun testGetLoggedInUser_storeCalled(){

        //call getLoggedInUser(), we are expecting null
        assertNull(sharedPreferenceUserStore.getLoggedInUser())

        //verify that getString() was called on the shared preferences
        verify(sharedPreferences).getString(Constants.USER_LOCAL_STORE_SHARED_PREF_SERIALIZED_DATA_KEY, "")
    }
}

As I am really new to Unit Testing and Mocking libraries like Mockito. Now my question is are my tests any good? and I wanted to test if the getLoggedInUser() funciton of my helper class is doing what it is supposed to do (ie. get logged in user if shared pref has it), how do I do that?

In addition do suggest me any improvements I can make to my test or the helper class itself. Thank you.

ravi
  • 899
  • 8
  • 31
  • 1
    Agree with @Enselic answer in general. I would also recommend reading this [blog post](https://kentcdodds.com/blog/write-tests), which I think makes many good points, and links to lots of other good resources on testing. – kenny_k Oct 24 '19 at 18:47

3 Answers3

5

Judging your test for what it is - A unit test running on a host machine with Android dependencies mocked with Mockito - it looks fine and like what you would expect.

The benefit-to-effort ratio of such tests are debatable, though. Personally I think it would be more valuable to run such a test against the real SharedPreferences implementation on a device, and assert on actual side effects instead of verifying on mocks. This has a couple of benefits over mocked tests:

  • You don't have to re-implement SharedPreferences with mocking
  • You know that SharedPreferenceUserStore will work with the real SharedPreferences implementation

But, such tests also have a debatable benefit-to-effort ratio. For a solo developer project, think about what kind of testing that is most important. Your time is limited so you will only have time to spend on writing the most important kind of tests.

The most important kinds of tests are the ones that test your app in the same way your users will use it. In other words, write high-level UI Automator tests. You can write how many mocked or on-device unit tests as you want. If you don't test that your entire app as a whole works, you will not know that it works. And if you don't know that your app as a whole works, you can't ship it. So in some way you have to test your app in its entirety. Doing it manually quickly becomes very labour intensive as you add more and more functionality. The only way to continually test your app is to automate the high-level UI testing of your app. That way you will also get code coverage that matters.

One big benefit of high-level UI testing that is worth pointing out is that you don't have to change them whenever you change some implementation detail in your app. If you have lots of mocked unit tests, you will have to spend a lot of time to refactor your unit tests as you refactor the real app code, which can be very time consuming, and thus a bad idea if you are a solo developer. Your UI Automator tests do not depend on low-level implementation details and will thus remain the same even if you change implementation details.

For example, maybe in the future you want to use Room from Android Jetpack to store your user data instead of SharedPreference. You will be able to do that without changing your high level UI tests at all. And they will be a great way to regression test such a change. If all you have are mocked unit tests, it will be a lot of work to rewrite all relevant unit tests to work with Room instead.

Enselic
  • 4,434
  • 2
  • 30
  • 42
  • Hey. Thank you for your answer very much. I do have Espresso UI tests to test the UI, I wonder if that is similar to the UI automator tests that you are suggesting. I am writing unit tests only to learn more on testing, Another little question that I had was, is `verify`ing sort of enough? And am I stubbing it alright with `when` and `then`? – ravi Oct 25 '19 at 04:35
  • Also, how would I unit test something like `testGetLoggedInUser_storeCalled()` ? Or is that not possible in unit tests? – ravi Oct 25 '19 at 04:49
  • 1
    Yes Espress UI tests are similar to UI Automator tests. The latter allows you to test scenarios outside your app though, for example that your app is closed or handles being put into the background. Here is some more discussion around the differences: https://stackoverflow.com/questions/31076228/android-testing-uiautomator-vs-espresso . Like I said, your Mockito unit test looks fine. Well done :) But right now, stop writing more of them and prioritize writing the important kind of tests for a solo developer ;) – Enselic Oct 25 '19 at 06:07
  • Accepting your answer. Thank you. Could you also tell me how I could test some method like `testGetLoggedInUser_storeCalled()`? – ravi Oct 25 '19 at 06:15
  • You already have a `testGetLoggedInUser_storeCalled` so please clarify why you are unhappy with it. – Enselic Oct 25 '19 at 06:23
  • The test method definition has `assertNull()` which passes and is all good but I wanted to test if it actually returns `User` object when the shared pref has some value. I have not tested that. :D – ravi Oct 25 '19 at 06:26
  • I really don't think that is worth doing. Because, even if such a test would pass, **it could still fail against the real SharedPreference**. It will be a mostly pointless test since you would only verify with your **own mocked implementation** of SharedPreference that can and will in some way **differ from the real SharedPreference implementation**. – Enselic Oct 25 '19 at 06:27
  • Okay. Thank you very much :) – ravi Oct 25 '19 at 06:56
2

I agree with what @Enselic say about favoring Integration Test over Unit Tests. However I disagree with his statement that this mockito test looks fine.

The reason for that is that (almost) every line in your code under test involves a mock operation. Basically mocking the complete method would have the same result. What you are doing in your test is testing that mockito works as expected, which is something you should not need to test.

On the other hand your test is a complete mirror of the implementation itself. Which means everytime you refactor something, you have to touch the test. Preferably would be a black box test.

If you use Mockito you should try to restrict its use to methods that actually do something (that is not mocked).

Classes that generally should be mocked for testing purposes are dependencies that interact with external components (like a database or a webservice), however in these cases you are normally required to have Integration Tests as well.

And if your Integration-Tests already cover most part of the code, you can check whether you want to add a test using a mock for those parts that are not covered.


I have no official source for what I am trying to express, its just based on my experience (and therefore my own opinion). Treat it as such.

second
  • 4,069
  • 2
  • 9
  • 24
1

There is not much that can be said regarding the tests that guys before me haven't said.

However, one thing that you might want to consider is refactoring your SharedPreferenceUserStore to accept not Context(which is quite a huge thing, and if not handled properly could lead to unforeseen issues and/or memory leaks), but rather SharedPreferences themselves. This way, your class, that deals only with updating the prefs doesn't have access to more than it should.

r2rek
  • 2,083
  • 13
  • 16
  • So I would want to put `SharedPreference` as dependency for `UserLocalStore` in the constructor and put `Context` as a parameter for `storeUserData()` and `clearUserData()`? – ravi Oct 31 '19 at 04:38
  • There is no need for context as parameter for those methods. Once you have access to sharedPrefs(that would be injected via constructor), your methods will work. – r2rek Oct 31 '19 at 06:10
  • Where will this be done, then: `var userLocalSharedPref: SharedPreferences = context.getSharedPreferences(USER_LOCAL_STORE_SHARED_PREF_NAME, Context.MODE_PRIVATE)` – ravi Oct 31 '19 at 06:13
  • I'm assuming you're using some kind of DI. `` class SharedPreferenceUserStore(private val userLocalSharedPref: SharedPreferences){...} `` and those sharedPreferences would be injected into your UserStore class via constructor. – r2rek Oct 31 '19 at 07:07