【问题标题】:Unit test a helper class around SharedPreference围绕 SharedPreference 对辅助类进行单元测试
【发布时间】:2019-10-21 12:00:23
【问题描述】:

我有一个帮助类来将用户对象保存到共享首选项。我在User 数据模型中使用了serialize(): String 函数和create(serializedString: String) 函数。他们使用 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)
        }
    }

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

@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, "")
    }
}

因为我对单元测试和模拟库(如Mockito)真的很陌生。现在我的问题是我的测试有什么好处吗?我想测试我的助手类的getLoggedInUser() 功能是否正在做它应该做的事情(即,如果共享偏好,请登录用户有),我该怎么做?

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

【问题讨论】:

  • 一般同意@Enselic 的回答。我还建议阅读此blog post,我认为它有很多优点,并链接到许多其他良好的测试资源。

标签: android unit-testing kotlin mockito sharedpreferences


【解决方案1】:

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

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

  • 您不必使用 mocking 重新实现 SharedPreferences
  • 您知道SharedPreferenceUserStore 将与real SharedPreferences 实现一起使用

但是,这样的测试也有一个值得商榷的效益与努力比。对于独立开发者项目,请考虑最重要的测试类型。你的时间是有限的,所以你只有时间花在编写最重要的测试上。

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

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

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

【讨论】:

  • 嘿。非常感谢您的回答。我确实有 Espresso UI 测试来测试 UI,我想知道这是否类似于您建议的 UI 自动化测试。我正在编写单元测试只是为了了解更多关于测试的信息,我遇到的另一个小问题是,verifying 是否足够?我可以使用whenthen 来处理它吗?
  • 另外,我将如何对 testGetLoggedInUser_storeCalled() 之类的东西进行单元测试?或者这在单元测试中是不可能的?
  • 是的 Espress UI 测试类似于 UI Automator 测试。后者允许您在应用程序之外测试场景,例如,您的应用程序已关闭或句柄被置于后台。以下是关于差异的更多讨论:stackoverflow.com/questions/31076228/…。就像我说的,你的 Mockito 单元测试看起来不错。干得好 :) 但是现在,不要再写更多了,优先考虑为独立开发者编写重要的测试;)
  • 接受您的回答。谢谢你。你能告诉我如何测试像testGetLoggedInUser_storeCalled()这样的方法吗?
  • 您已经有一个testGetLoggedInUser_storeCalled,所以请说明您对它不满意的原因。
【解决方案2】:

我同意 @Enselic 关于支持集成测试而不是单元测试的说法。 但是我不同意他的说法,即mockito test looks fine

这样做的原因是(几乎)您的测试代码中的每一行都涉及mock 操作。基本上模拟完整的方法会得到相同的结果。 您在测试中所做的是测试 mockito 是否按预期工作,这是您不需要测试的。

另一方面,您的测试是实现本身的完整镜像。这意味着每次重构某些东西时,都必须接触测试。最好是黑盒测试。

如果您使用Mockito,您应该尝试将其使用限制在实际执行某些操作(未模拟)的方法中。

通常应该为测试目的模拟的类是与外部组件(如数据库或 Web 服务)交互的依赖项,但在这些情况下,您通常还需要有 Integration 测试。

如果您的Integration-Tests 已经涵盖了大部分代码,您可以检查是否要使用模拟为未涵盖的部分添加测试。


对于我想要表达的内容,我没有官方来源,这只是基于我的经验(因此也是我自己的观点)。就这样对待它。

【讨论】:

    【解决方案3】:

    对于我之前的人没有说过的测试,没有什么可以说的。

    但是,您可能需要考虑的一件事是重构您的 SharedPreferenceUserStore 以不接受 Context(这是一件相当大的事情,如果处理不当可能导致无法预料的问题和/或内存泄漏),而是接受 SharedPreferences 本身.这样一来,您的类只处理更新首选项就无法访问超出其应有的权限。

    【讨论】:

    • 所以我想将SharedPreference 作为UserLocalStore 的依赖项放在构造函数中,并将Context 作为storeUserData()clearUserData() 的参数?
    • 这些方法不需要上下文作为参数。一旦您可以访问 sharedPrefs(将通过构造函数注入),您的方法就会起作用。
    • 这将在哪里完成,然后:var userLocalSharedPref: SharedPreferences = context.getSharedPreferences(USER_LOCAL_STORE_SHARED_PREF_NAME, Context.MODE_PRIVATE)
    • 我假设您正在使用某种 DI。 `` class SharedPreferenceUserStore(private val userLocalSharedPref: SharedPreferences){...} `` 并且这些 sharedPreferences 将通过构造函数注入到您的 UserStore 类中。
    猜你喜欢
    • 1970-01-01
    • 2010-12-24
    • 1970-01-01
    • 2017-01-06
    • 2013-03-06
    • 2011-06-20
    • 1970-01-01
    • 1970-01-01
    • 2012-04-22
    相关资源
    最近更新 更多