Removed MigrationManager Robolectric tests

Robolectric tests are removed as we don't want to emulate
platform.

Renamed instrumentation tests to match expected naming.

Updated detekt rule to allow human readable test function
names in kt files. This naming convention was adopted in
test code to enable human-readable test reports, but
DEX does not support spaces in names. Underscore _ is
a good replacement providnig similar level of readability.

Signed-off-by: Chris Narkiewicz <hello@ezaquarii.com>
This commit is contained in:
Chris Narkiewicz 2020-03-03 23:13:21 +00:00 committed by tobiasKaminsky
parent 1ab8d98d85
commit 439dd4ff78
No known key found for this signature in database
GPG Key ID: 0E00D4D47D0C5AF7
8 changed files with 23 additions and 457 deletions

View File

@ -387,7 +387,6 @@ dependencies {
// androidJacocoAnt "org.jacoco:org.jacoco.agent:${jacocoVersion}"
implementation "com.github.stateless4j:stateless4j:2.6.0"
testImplementation "org.robolectric:robolectric:4.3.1"
androidTestImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
androidTestImplementation "org.mockito:mockito-android:3.3.0"
}

View File

@ -265,6 +265,7 @@ naming:
functionPattern: '^([a-z$][a-zA-Z$0-9]*)|(`.*`)$'
excludeClassPattern: '$^'
ignoreOverridden: true
excludes: "**/*Test.kt"
FunctionParameterNaming:
active: true
parameterPattern: '[a-z][A-Za-z0-9]*'

View File

@ -19,6 +19,7 @@
*/
package com.nextcloud.client.migrations
import androidx.test.annotation.UiThreadTest
import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.appinfo.AppInfo
import com.nextcloud.client.core.ManualAsyncRunner
@ -31,15 +32,12 @@ import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.robolectric.RobolectricTestRunner
import java.lang.RuntimeException
import java.util.LinkedHashSet
@RunWith(RobolectricTestRunner::class)
class TestMigrationsManager {
class MigrationsManagerTest {
companion object {
const val OLD_APP_VERSION = 41
@ -83,7 +81,7 @@ class TestMigrationsManager {
}
@Test
fun `inital status is unknown`() {
fun inital_status_is_unknown() {
// GIVEN
// migration manager has not been used yets
@ -93,7 +91,7 @@ class TestMigrationsManager {
}
@Test
fun `applied migrations are returned in order`() {
fun applied_migrations_are_returned_in_order() {
// GIVEN
// some migrations are marked as applied
// migration ids are stored in random order
@ -117,7 +115,7 @@ class TestMigrationsManager {
@Test
@Suppress("MagicNumber")
fun `registering new applied migration preserves old ids`() {
fun registering_new_applied_migration_preserves_old_ids() {
// WHEN
// some applied migrations are registered
val appliedMigrationIds = setOf("0", "1", "2")
@ -135,7 +133,8 @@ class TestMigrationsManager {
}
@Test
fun `migrations are scheduled on background thread`() {
@UiThreadTest
fun migrations_are_scheduled_on_background_thread() {
// GIVEN
// migrations can be applied
assertEquals(0, migrationsManager.getAppliedMigrations().size)
@ -153,7 +152,8 @@ class TestMigrationsManager {
}
@Test
fun `applied migrations are recorded`() {
@UiThreadTest
fun applied_migrations_are_recorded() {
// GIVEN
// no migrations are applied yet
// current app version is newer then last recorded migrated version
@ -178,7 +178,8 @@ class TestMigrationsManager {
}
@Test
fun `migration error is recorded`() {
@UiThreadTest
fun migration_error_is_recorded() {
// GIVEN
// no migrations applied yet
@ -208,7 +209,8 @@ class TestMigrationsManager {
}
@Test
fun `migrations are not run if already run for an app version`() {
@UiThreadTest
fun migrations_are_not_run_if_already_run_for_an_app_version() {
// GIVEN
// migrations were already run for the current app version
whenever(appInfo.versionCode).thenReturn(NEW_APP_VERSION)
@ -229,7 +231,8 @@ class TestMigrationsManager {
}
@Test
fun `new app version is marked as migrated if no new migrations are available`() {
@UiThreadTest
fun new_app_version_is_marked_as_migrated_if_no_new_migrations_are_available() {
// GIVEN
// migrations were applied in previous version
// new version has no new migrations
@ -253,7 +256,8 @@ class TestMigrationsManager {
}
@Test
fun `optional migration failure does not trigger a migration failure`() {
@UiThreadTest
fun optional_migration_failure_does_not_trigger_a_migration_failure() {
// GIVEN
// pending migrations
// mandatory migrations are passing

View File

@ -27,7 +27,7 @@ import org.junit.Assert.assertNotSame
import org.junit.Assert.assertTrue
@Suppress("MagicNumber")
class TestMockSharedPreferences {
class MockSharedPreferencesTest {
private lateinit var mock: MockSharedPreferences
@ -37,7 +37,7 @@ class TestMockSharedPreferences {
}
@Test
fun `get set string set`() {
fun getSetStringSet() {
val value = setOf("alpha", "bravo", "charlie")
mock.edit().putStringSet("key", value).apply()
val copy = mock.getStringSet("key", mutableSetOf())
@ -46,7 +46,7 @@ class TestMockSharedPreferences {
}
@Test
fun `get set int`() {
fun getSetInt() {
val value = 42
val editor = mock.edit()
editor.putInt("key", value)
@ -56,7 +56,7 @@ class TestMockSharedPreferences {
}
@Test
fun `get set boolean`() {
fun getSetBoolean() {
val value = true
val editor = mock.edit()
editor.putBoolean("key", value)
@ -66,7 +66,7 @@ class TestMockSharedPreferences {
}
@Test
fun `get set string`() {
fun getSetString() {
val value = "a value"
val editor = mock.edit()
editor.putString("key", value)

View File

@ -1,104 +0,0 @@
/*
* Nextcloud Android client application
*
* @author Chris Narkiewicz
* Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.client.migrations.android
import android.content.SharedPreferences
import java.lang.UnsupportedOperationException
import java.util.TreeMap
@Suppress("TooManyFunctions")
class MockSharedPreferences : SharedPreferences {
class MockEditor(val store: MutableMap<String?, Any?>) : SharedPreferences.Editor {
val editorStore: MutableMap<String?, Any?> = TreeMap()
override fun clear(): SharedPreferences.Editor = throw UnsupportedOperationException()
override fun putLong(key: String?, value: Long): SharedPreferences.Editor =
throw UnsupportedOperationException()
override fun putInt(key: String?, value: Int): SharedPreferences.Editor {
editorStore.put(key, value)
return this
}
override fun remove(key: String?): SharedPreferences.Editor = throw UnsupportedOperationException()
override fun putBoolean(key: String?, value: Boolean): SharedPreferences.Editor {
editorStore.put(key, value)
return this
}
override fun putStringSet(key: String?, values: MutableSet<String>?): SharedPreferences.Editor {
editorStore.put(key, values?.toMutableSet())
return this
}
override fun commit(): Boolean = true
override fun putFloat(key: String?, value: Float): SharedPreferences.Editor =
throw UnsupportedOperationException()
override fun apply() = store.putAll(editorStore)
override fun putString(key: String?, value: String?): SharedPreferences.Editor {
editorStore.put(key, value)
return this
}
}
val store: MutableMap<String?, Any?> = TreeMap()
override fun contains(key: String?): Boolean = store.containsKey(key)
override fun getBoolean(key: String?, defValue: Boolean): Boolean = store.getOrDefault(key, defValue) as Boolean
override fun unregisterOnSharedPreferenceChangeListener(
listener: SharedPreferences.OnSharedPreferenceChangeListener?
) = throw UnsupportedOperationException()
override fun getInt(key: String?, defValue: Int): Int = store.getOrDefault(key, defValue) as Int
override fun getAll(): MutableMap<String, *> {
throw UnsupportedOperationException()
}
override fun edit(): SharedPreferences.Editor {
return MockEditor(store)
}
override fun getLong(key: String?, defValue: Long): Long {
throw UnsupportedOperationException()
}
override fun getFloat(key: String?, defValue: Float): Float {
throw UnsupportedOperationException()
}
override fun getStringSet(key: String?, defValues: MutableSet<String>?): MutableSet<String>? {
return store.getOrDefault(key, defValues) as MutableSet<String>?
}
override fun registerOnSharedPreferenceChangeListener(
listener: SharedPreferences.OnSharedPreferenceChangeListener?
) = throw UnsupportedOperationException()
override fun getString(key: String?, defValue: String?): String? = store.getOrDefault(key, defValue) as String?
}

View File

@ -1,257 +0,0 @@
/*
* Nextcloud Android client application
*
* @author Chris Narkiewicz
* Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.client.migrations.android
import androidx.test.annotation.UiThreadTest
import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.appinfo.AppInfo
import com.nextcloud.client.core.ManualAsyncRunner
import com.nextcloud.client.migrations.Migrations
import com.nextcloud.client.migrations.MigrationsManager
import com.nextcloud.client.migrations.MigrationsManagerImpl
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.never
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import java.lang.RuntimeException
import java.util.LinkedHashSet
@Suppress("FunctionNaming")
class TestMigrationsManager {
companion object {
const val OLD_APP_VERSION = 41
const val NEW_APP_VERSION = 42
}
lateinit var migrations: List<Migrations.Step>
@Mock
lateinit var appInfo: AppInfo
lateinit var migrationsDb: MockSharedPreferences
@Mock
lateinit var userAccountManager: UserAccountManager
lateinit var asyncRunner: ManualAsyncRunner
internal lateinit var migrationsManager: MigrationsManagerImpl
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
val migrationStep1: Runnable = mock()
val migrationStep2: Runnable = mock()
migrations = listOf(
Migrations.Step(0, "first migration", migrationStep1),
Migrations.Step(1, "second migration", migrationStep2)
)
asyncRunner = ManualAsyncRunner()
migrationsDb = MockSharedPreferences()
whenever(appInfo.versionCode).thenReturn(NEW_APP_VERSION)
migrationsManager = MigrationsManagerImpl(
appInfo = appInfo,
migrationsDb = migrationsDb,
asyncRunner = asyncRunner,
migrations = migrations
)
}
@Test
fun inital_status_is_unknown() {
// GIVEN
// migration manager has not been used yets
// THEN
// status is not set
assertEquals(MigrationsManager.Status.UNKNOWN, migrationsManager.status.value)
}
@Test
fun applied_migrations_are_returned_in_order() {
// GIVEN
// some migrations are marked as applied
// migration ids are stored in random order
val storedMigrationIds = LinkedHashSet<String>()
storedMigrationIds.apply {
add("3")
add("0")
add("2")
add("1")
}
migrationsDb.store.put(MigrationsManagerImpl.DB_KEY_APPLIED_MIGRATIONS, storedMigrationIds)
// WHEN
// applied migration ids are retrieved
val ids = migrationsManager.getAppliedMigrations()
// THEN
// returned list is sorted
assertEquals(ids, ids.sorted())
}
@Test
@Suppress("MagicNumber")
fun registering_new_applied_migration_preserves_old_ids() {
// WHEN
// some applied migrations are registered
val appliedMigrationIds = setOf("0", "1", "2")
migrationsDb.store.put(MigrationsManagerImpl.DB_KEY_APPLIED_MIGRATIONS, appliedMigrationIds)
// WHEN
// new set of migration ids are registered
// some ids are added again
migrationsManager.addAppliedMigration(2, 3, 4)
// THEN
// new ids are appended to set of existing ids
val expectedIds = setOf("0", "1", "2", "3", "4")
assertEquals(expectedIds, migrationsDb.store.get(MigrationsManagerImpl.DB_KEY_APPLIED_MIGRATIONS))
}
@Test
@UiThreadTest
fun migrations_are_scheduled_on_background_thread() {
// GIVEN
// migrations can be applied
assertEquals(0, migrationsManager.getAppliedMigrations().size)
// WHEN
// migration is started
val count = migrationsManager.startMigration()
// THEN
// all migrations are scheduled on background thread
// single task is scheduled
assertEquals(migrations.size, count)
assertEquals(1, asyncRunner.size)
assertEquals(MigrationsManager.Status.RUNNING, migrationsManager.status.value)
}
@Test
@UiThreadTest
fun applied_migrations_are_recorded() {
// GIVEN
// no migrations are applied yet
// current app version is newer then last recorded migrated version
whenever(appInfo.versionCode).thenReturn(NEW_APP_VERSION)
migrationsDb.store.put(MigrationsManagerImpl.DB_KEY_LAST_MIGRATED_VERSION, OLD_APP_VERSION)
// WHEN
// migration is run
whenever(userAccountManager.migrateUserId()).thenReturn(true)
val count = migrationsManager.startMigration()
assertTrue(asyncRunner.runOne())
// THEN
// total migrations count is returned
// migration functions are called
// applied migrations are recorded
// new app version code is recorded
assertEquals(migrations.size, count)
assertEquals(setOf("0", "1"), migrationsDb.store.get(MigrationsManagerImpl.DB_KEY_APPLIED_MIGRATIONS))
assertEquals(NEW_APP_VERSION, migrationsDb.store.get(MigrationsManagerImpl.DB_KEY_LAST_MIGRATED_VERSION))
}
@Test
@UiThreadTest
fun migration_error_is_recorded() {
// GIVEN
// no migrations applied yet
// WHEN
// migrations are applied
// one migration throws
val lastMigration = migrations.last()
val errorMessage = "error message"
whenever(lastMigration.function.run()).thenThrow(RuntimeException(errorMessage))
migrationsManager.startMigration()
assertTrue(asyncRunner.runOne())
// THEN
// failure is marked in the migration db
// failure message is recorded
// failed migration id is recorded
assertEquals(MigrationsManager.Status.FAILED, migrationsManager.status.value)
assertTrue(migrationsDb.getBoolean(MigrationsManagerImpl.DB_KEY_FAILED, false))
assertEquals(
errorMessage,
migrationsDb.getString(MigrationsManagerImpl.DB_KEY_FAILED_MIGRATION_ERROR_MESSAGE, "")
)
assertEquals(
lastMigration.id,
migrationsDb.getInt(MigrationsManagerImpl.DB_KEY_FAILED_MIGRATION_ID, -1)
)
}
@Test
@UiThreadTest
fun migrations_are_not_run_if_already_run_for_an_app_version() {
// GIVEN
// migrations were already run for the current app version
whenever(appInfo.versionCode).thenReturn(NEW_APP_VERSION)
migrationsDb.store.put(MigrationsManagerImpl.DB_KEY_LAST_MIGRATED_VERSION, NEW_APP_VERSION)
// WHEN
// app is migrated again
val migrationCount = migrationsManager.startMigration()
// THEN
// migration processing is skipped entirely
// status is set to applied
assertEquals(0, migrationCount)
migrations.forEach {
verify(it.function, never()).run()
}
assertEquals(MigrationsManager.Status.APPLIED, migrationsManager.status.value)
}
@Test
@UiThreadTest
fun new_app_version_is_marked_as_migrated_if_no_new_migrations_are_available() {
// GIVEN
// migrations were applied in previous version
// new version has no new migrations
whenever(appInfo.versionCode).thenReturn(NEW_APP_VERSION)
migrationsDb.store.put(MigrationsManagerImpl.DB_KEY_LAST_MIGRATED_VERSION, OLD_APP_VERSION)
val applied = migrations.map { it.id.toString() }.toSet()
migrationsDb.store.put(MigrationsManagerImpl.DB_KEY_APPLIED_MIGRATIONS, applied)
// WHEN
// migration is started
val startedCount = migrationsManager.startMigration()
// THEN
// no new migrations are run
// new version is marked as migrated
assertEquals(0, startedCount)
assertEquals(
NEW_APP_VERSION,
migrationsDb.getInt(MigrationsManagerImpl.DB_KEY_LAST_MIGRATED_VERSION, -1)
)
}
}

View File

@ -1,77 +0,0 @@
/*
* Nextcloud Android client application
*
* @author Chris Narkiewicz
* Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.client.migrations.android
import org.junit.Before
import org.junit.Test
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotSame
import org.junit.Assert.assertTrue
@Suppress("MagicNumber", "FunctionNaming")
class TestMockSharedPreferences {
private lateinit var mock: MockSharedPreferences
@Before
fun setUp() {
mock = MockSharedPreferences()
}
@Test
fun get_set_string_set() {
val value = setOf("alpha", "bravo", "charlie")
mock.edit().putStringSet("key", value).apply()
val copy = mock.getStringSet("key", mutableSetOf())
assertNotSame(value, copy)
assertEquals(value, copy)
}
@Test
fun get_set_int() {
val value = 42
val editor = mock.edit()
editor.putInt("key", value)
assertEquals(100, mock.getInt("key", 100))
editor.apply()
assertEquals(42, mock.getInt("key", 100))
}
@Test
fun get_set_boolean() {
val value = true
val editor = mock.edit()
editor.putBoolean("key", value)
assertFalse(mock.getBoolean("key", false))
editor.apply()
assertTrue(mock.getBoolean("key", false))
}
@Test
fun get_set_string() {
val value = "a value"
val editor = mock.edit()
editor.putString("key", value)
assertEquals("default", mock.getString("key", "default"))
editor.apply()
assertEquals("a value", mock.getString("key", "default"))
}
}