围绕 SharedPreference 对辅助类进行单元测试

rav*_*avi 4 android unit-testing mockito sharedpreferences kotlin

我有一个帮助程序类将用户对象保存到共享首选项。我在我的数据模型中使用了一个serialize(): String函数和一个create(serializedString: String)函数User。它们使用 GSon 串行器,并且按照单元测试的建议运行良好。

现在我的助手类被调用SharedPreferenceUserStore.kt,它接受一个Context对象。代码是:

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)
        }
    }
Run Code Online (Sandbox Code Playgroud)

我为这个辅助类编写了一些单元测试,如下所示:

@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, "")
    }
}
Run Code Online (Sandbox Code Playgroud)

因为我对单元测试和模拟库(如Mockito. 现在我的问题是我的测试有用吗?我想测试getLoggedInUser()我的帮助程序类的功能是否正在执行它应该执行的操作(即,如果共享首选项有它,则登录用户),我该怎么做?

此外,请建议我对我的测试或辅助类本身进行任何改进。谢谢。

Ens*_*lic 5

判断您的测试是什么 - 在主机上运行的单元测试,并使用 Mockito 模拟 Android 依赖项 - 它看起来很好,就像您所期望的那样。

不过,此类测试的效益与努力之比值得商榷。就我个人而言,我认为针对设备上的实际 SharedPreferences实现运行这样的测试并断言实际的副作用而不是在模拟上进行验证会更有价值。与模拟测试相比,这有几个好处:

  • 您不必重新实施SharedPreferences通过模拟重新实现
  • 你知道这SharedPreferenceUserStore将适用于真正的 SharedPreferences实施

但是,此类测试的效益与努力比也存在争议。对于单独的开发人员项目,请考虑哪种测试最重要。您的时间有限,因此您只能花时间编写最重要的测试。

最重要的测试类型是以用户使用应用程序的相同方式测试您的应用程序。换句话说,编写高级UI Automator测试。您可以根据需要编写多少个模拟或设备上单元测试。如果您不测试整个应用程序是否整体有效,您将不知道它是否有效。如果您不知道您的应用程序作为一个整体是否可以正常工作,那么您就无法发布它。因此,您必须以某种方式完整地测试您的应用程序。随着您添加越来越多的功能,手动操作很快就会变得非常耗费人力。持续测试应用程序的唯一方法是自动执行应用程序的高级 UI 测试。这样您还将获得重要的代码覆盖率代码覆盖率。

值得指出的是,高级 UI 测试的一大好处是,每当您更改应用程序中的某些实现细节时,您都不必更改它们。如果您有大量模拟单元测试,则在重构真正的应用程序代码时,您将不得不花费大量时间来重构单元测试,这可能非常耗时,因此如果您是独立开发人员,那么这是一个坏主意。您的UI Automator测试不依赖于低级实现细节,因此即使您更改实现细节也将保持不变。

例如,也许将来您想使用RoomAndroid Jetpack来存储您的用户数据,而不是SharedPreference. 您无需更改高级 UI 测试即可做到这一点。它们将是回归测试这种变化的好方法。如果您拥有的只是模拟的单元测试,那么重写所有相关的单元测试将需要大量工作Room