Модульный тест вспомогательного класса вокруг SharedPreference - PullRequest
2 голосов
/ 21 октября 2019

У меня есть вспомогательный класс для сохранения объекта пользователя в общих настройках. Я использовал функцию 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)
        }
    }

И я написал несколько модульных тестов для этого вспомогательного класса следующим образом:

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

Поскольку я действительно новичок в библиотеках модульного тестирования и Mocking, таких как Mockito. Теперь мой вопрос: мои тесты хороши? , и я хотел проверить, выполняет ли функция getLoggedInUser() моего вспомогательного класса то, что он должен делать (т. Е. Войти в систему пользователя, если общий преф имеетэто), как мне это сделать?

Кроме того, предложите мне любые улучшения, которые я могу внести в свой тест или в сам вспомогательный класс. Спасибо.

Ответы [ 3 ]

4 голосов
/ 24 октября 2019

Оценка вашего теста на предмет того, что это такое - модульный тест, запущенный на хост-компьютере с зависимостями Android, позаимствованный с Mockito, - он выглядит хорошо и соответствует ожиданиям.

Соотношение выгоды и усилийтакие тесты спорны, хотя. Лично я думаю, что было бы более полезно выполнить такой тест против реализации real SharedPreferences на устройстве и утверждать о фактических побочных эффектах вместо проверки на ложные показания. Это имеет несколько преимуществ по сравнению с фиктивными тестами:

  • Вам не нужно повторно внедрять SharedPreferences с насмешкой
  • Вы знаете, что SharedPreferenceUserStore будет работать с реальная 1017 * реализация

Но , такие тесты также имеют дискуссионное соотношение пользы-усилия. Для индивидуального девелоперского проекта подумайте, какое именно тестирование наиболее важно . Ваше время ограничено, поэтому у вас будет время только на написание самых важных видов тестов.

Самыми важными видами тестов являются те, которые тестируют ваше приложение так же, как ваши пользователииспользуйте это . Другими словами, пишите высокоуровневые тесты UI Automator . Вы можете написать сколько проверенных или встроенных тестов на устройстве вы хотите. Если вы не проверите, что все ваше приложение в целом работает, , вы не узнаете, что оно работает . И если вы не знаете, что ваше приложение в целом работает, вы не сможете его отправить. Таким образом, некоторым способом вы должны протестировать свое приложение в полном объеме. Выполнение этого вручную быстро становится очень трудоемким, так как вы добавляете все больше и больше функций. Единственный способ непрерывного тестирования вашего приложения - автоматизировать высокоуровневое тестирование пользовательского интерфейса вашего приложения. Таким образом, вы также получите покрытие кода, которое имеет значение .

Одно большое преимущество высокоуровневого тестирования пользовательского интерфейса, на которое стоит обратить внимание, заключается в том, что вам не нужно менять его при каждом изменении. некоторые детали реализации в вашем приложении. Если у вас много поддельных модульных тестов, вам придется потратить много времени на рефакторинг своих модульных тестов, поскольку вы реорганизуете реальный код приложения, который может занять очень много времени, и, следовательно, плохая идея, если вы являетесь индивидуальным разработчиком. Ваши тесты UI Automator не зависят от подробностей реализации низкого уровня и, таким образом, останутся прежними, даже если вы измените детали реализации.

Например, возможно, в будущем вы захотите использовать Room от Android Jetpack для хранения пользовательских данных вместо SharedPreference. Вы сможете сделать это, не изменяя свои тесты пользовательского интерфейса высокого уровня вообще. И они будут отличным способом регрессионного теста такого изменения. Если все, что у вас есть, это проверенные юнит-тесты, будет много работы, чтобы переписать все соответствующие юнит-тесты для работы с Room.

2 голосов
/ 26 октября 2019

Я согласен с тем, что @Enselic говорит о предпочтении Интеграционного теста по сравнению с юнит-тестами. Однако я не согласен с его утверждением, что это mockito test looks fine.

Причина этого заключается в том, что (почти) каждая строка в тестируемом коде включает в себя операцию mock. По сути, насмешка над полным методом будет иметь тот же результат. В своем тесте вы проверяете, что mockito работает должным образом, и вам не нужно тестировать его.

С другой стороны, ваш тест является полным отражением самой реализации. Это означает, что каждый раз, когда вы что-то рефакторите, вы должны коснуться теста. Предпочтительно это будет тест черного ящика.

Если вы используете Mockito, вы должны попытаться ограничить его использование методами, которые на самом деле что-то делают (которые не являются поддельными).

Классы, которые обычно должны быть смоделированы для целей тестирования, являются зависимостями, которые взаимодействуют с внешними компонентами (такими как база данных или веб-служба), однако в этих случаях обычно требуется также иметь Integration Тесты.

И если ваш Integration-Tests уже покрывает большую часть кода, вы можете проверить, хотите ли вы добавить тест, используя макет для тех частей, которые не охвачены.


У меня нет официального источника того, что я пытаюсь выразить, это просто основано на моем опыте (и, следовательно, на моем собственном мнении). Относитесь к нему как к такому.

1 голос
/ 29 октября 2019

Не так много можно сказать о тестах, которые ребята до меня не сказали.

Однако одна вещь, которую вы, возможно, захотите рассмотреть, - это рефакторинг вашего SharedPreferenceUserStore, чтобы он не принимал Context (что довольно много, и, если его неправильно обработать, может привести к непредвиденным проблемам и / или утечкам памяти), ноСкорее SharedPreferences сами. Таким образом, ваш класс, который занимается только обновлением префов, не имеет доступа к большему, чем должен.

...