Testing Room as JUnit Test Not AndroidTest - android

Trying to test room migration using the MigrationTestHelper class and Robolectric. We want it as a JUnit test because our CI environment cannot fire up an emulator. (Please no answers with CI fixes for emulators, CI is not in my control) Only issue I have is that the test fails because it can't find the schemas. My build.gradle has this in it already
android {
sourceSets {
test.assets.srcDirs += files("$projectDir/schemas".toString())
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
test {
java.srcDirs += "$projectDir/src/sharedTest/java"
}
testOptions {
unitTests {
includeAndroidResources = true
}
unitTests.all {
systemProperty 'robolectric.enabledSdks', '21'
}
}
}
dependencies {
// has all the proper dependencies from mockito adn robolectric to kotlin and junit.
}
Here is the test code but again its mostly just the schema can't be found when the database creation is called. Also the json files are there in the schema directory
#RunWith(RobolectricTestRunner::class)
class Migration19To20Test {
private val migration = MyDatabase.MIGRATION_19_20
private val fromVersion = 19
private val toVersion = 20
#get:Rule
val helper: MigrationTestHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
MyDatabase::class.java.canonicalName,
FrameworkSQLiteOpenHelperFactory())
private val testDatabaseName = "migration-test"
#Test
fun insertsFirmwareVersionFullColumn() {
givenADatabase()
val validateDroppedTables = true
val db = helper.runMigrationsAndValidate(
testDatabaseName,
toVersion,
validateDroppedTables,
migration)
db.query("select * from ${DatabaseConstants.Table.People}").use { cursor ->
cursor.moveToFirst()
assertTrue("table should contain the ${DatabaseConstants.Column.People.NAME_FULL} column as it should have been added",
cursor.columnNames.contains(DatabaseConstants.Column.People.NAME_FULL))
}
}
private fun givenADatabase() {
// Test fails here
helper.createDatabase(testDatabaseName, fromVersion)
}
}

I solved it by copying MigrationTestHelper to my test source code and modified loadSchema method to look like this
private SchemaBundle loadSchema(Context context, int version) throws IOException {
// InputStream input = context.getAssets().open(mAssetsFolder + "/" + version + ".json");
InputStream input = new FileInputStream("./schemas/" + mAssetsFolder + "/" + version + ".json");
return SchemaBundle.deserialize(input);
}
The schemas directory is configured in build.gradle
android {
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
arguments = [
"room.schemaLocation" : "$projectDir/schemas".toString(),
"room.incremental" : "true",
"room.expandProjection": "true"]
}
}
}
}
As you can see the cause is that this class looks for migration in assets directory because Google assumes that you will run those migration on device. In such case you must include them as part of android assets.
With Robolectric it's just reading from a directory and putting that into a stream.

My scenario was not exactly the same - I had an instrumented migration test case - but perhaps the cause is the same. If you have something like the rule below, aapt will strip the schema files and it will do so in debug builds as well as release ones.
buildTypes {
debug {...}
release {
aaptOptions {
ignoreAssetsPattern '!*.json'
}
}
}

Related

Actual/Expect classes doesn't work in Kotlin Mutliplatform proyect

I was develoing an KMM App, where I try to implement a regular localDatasource and remoteDatasource, using SQLDelight and Ktor respectively.
My problems comes when I try to shared the native code from AndroidApp and iosMain, into commonModule. I start to get the following error into my commonModule expect class:
Expected function 'cache' has no actual declaration in module KMM-APP.shared for JVM
Expected function 'cache' has no actual declaration in module KMM-APP.shared.iosArm64Main for Native
Expected function 'cache' has no actual declaration in module KMM-APP.shared.iosX64Main for Native
It's a bit confuse, in order I don't make use of jvm module in my proyect, although I do for IOS module.
Here it's my cacheAndroid.kt of AndroidApp module:
import android.content.Context
import com.example.kmp_app.db.PetsDatabase
import com.squareup.sqldelight.android.AndroidSqliteDriver
lateinit var appContext: Context
internal actual fun cache(): PetsDatabase {
val driver = AndroidSqliteDriver(PetsDatabase.Schema, appContext, "petsDB.db")
return PetsDatabase(driver)
}
Here is the classes of my IOS module:
import com.example.kmp_app.db.PetsDatabase
import com.squareup.sqldelight.drivers.native.NativeSqliteDriver
internal actual fun cache(): PetsDatabase {
val driver = NativeSqliteDriver(PetsDatabase.Schema, "petsDB.db")
return PetsDatabase(driver)
}
And the use into commonModule:
internal expect fun cache(): PetsDatabase
I in this last line of code where I reciving the error above, but I also get the error into the actual classes of Android and IOS modules, into their expect class variant.
Finally regarding my build.gradle(common)
plugins {
kotlin("multiplatform")
kotlin("native.cocoapods")
id("com.android.library")
id("kotlinx-serialization")
id("com.squareup.sqldelight")
}
version = "1.0"
kotlin {
targets{
ios {
binaries {
framework {
baseName = "shared"
}
}
}
// Block from https://github.com/cashapp/sqldelight/issues/2044#issuecomment-721299517.
val onPhone = System.getenv("SDK_NAME")?.startsWith("iphoneos") ?: false
if (onPhone) {
iosArm64("ios")
} else {
iosX64("ios")
}
android()
//iosSimulatorArm64() sure all ios dependencies support this target
}
cocoapods {
summary = "Some description for the Shared Module"
homepage = "Link to the Shared Module homepage"
ios.deploymentTarget = "14.1"
podfile = project.file("../iosApp/Podfile")
}
sourceSets {
all {
languageSettings.apply {
useExperimentalAnnotation("kotlinx.coroutines.ExperimentalCoroutinesApi")
}
}
val commonMain by getting{
dependencies {
implementation(kotlin("stdlib-common"))
implementation(Coroutines.Core.core)
implementation(Ktor.Core.common)
implementation(Ktor.Json.common)
implementation(Ktor.Logging.common)
implementation(Ktor.Serialization.common)
implementation(SqlDelight.runtime)
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common"))
implementation(Ktor.Mock.common)
}
}
val androidMain by getting{
dependencies {
implementation(kotlin("stdlib"))
implementation(Coroutines.Core.core)
implementation(Ktor.android)
implementation(Ktor.Core.jvm)
implementation(Ktor.Json.jvm)
implementation(Ktor.Logging.jvm)
implementation(Ktor.Logging.slf4j)
implementation(Ktor.Mock.jvm)
implementation(Ktor.Serialization.jvm)
implementation(Serialization.core)
implementation(SqlDelight.android)
}
}
val androidAndroidTestRelease by getting
val androidTest by getting {
dependsOn(androidAndroidTestRelease)
dependencies {
implementation(kotlin("test-junit"))
implementation("junit:junit:4.13.2")
}
}
val iosX64Main by getting
val iosArm64Main by getting
//val iosSimulatorArm64Main by getting
val ios by creating {
dependsOn(commonMain)
iosX64Main.dependsOn(this)
iosArm64Main.dependsOn(this)
//iosSimulatorArm64Main.dependsOn(this)
dependencies {
implementation(SqlDelight.native)
}
}
}
}
android {
compileSdk = 31
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
defaultConfig {
minSdk = 21
targetSdk = 31
versionCode = 1
versionName = "1.0"
}
}
sqldelight {
database("PetsDatabase") {
packageName = "com.example.kmp_app.db"
sourceFolders = listOf("sqldelight")
}
}
And my proyect build.gradle:
buildscript {
repositories {
google()
mavenCentral()
jcenter()
}
dependencies {
classpath("com.android.tools.build:gradle:4.2.0")
classpath(kotlin("gradle-plugin", version = Versions.kotlin))
classpath(kotlin("serialization", version = Versions.kotlin))
classpath("com.squareup.sqldelight:gradle-plugin:${Versions.sqldelight}")
}
}
allprojects {
repositories {
google()
mavenCentral()
jcenter()
}
}
plugins{
//kotlin("android") version "${Versions.kotlin}" apply false
}
I hope you can help and if like this, take thanks in advance !
I think it related with packageName in your Gradle:
packageName = "com.example.kmp_app.db"
Try to pass route of your cache function instead of "com.example.kmp_app.db"
like if my cache function exists on dataSource.cacheSource, we will pass "com.example.kmp_app.db.dataSource.cacheSource"
Be sure your Cache actual / expect function have the same package name like this "com.example.kmp_app.db.dataSource.cacheSource"
Shared gradle
sqldelight {
database("RecipeDatabase") {
packageName = "com.example.food1fork.Food1ForkKmm.DataSource.cacheSource"
sourceFolders = listOf("SqlDelight")
}
}
iOS module
package com.example.food1fork.Food1ForkKmm.DataSource.cacheSource
actual class DriverFactory {
actual fun createDriver(): SqlDriver {
return NativeSqliteDriver(RecipeDatabase.Schema, "recipes.db")
}
}
Android module
package com.example.food1fork.Food1ForkKmm.DataSource.cacheSource
actual class DriverFactory(private val context: Context) {
actual fun createDriver(): SqlDriver {
return AndroidSqliteDriver(RecipeDatabase.Schema, context, "recipes.db")
}
}
Shared module
package com.example.food1fork.Food1ForkKmm.DataSource.cacheSource
expect class DriverFactory {
fun createDriver(): SqlDriver
}

Android and Kotlin DeteKt: Custom rules not running

Trying to create some playground over Detekt custom rules and it just doesn't work. Doesn't even try to find the rule-set file.
The gradle goes
plugins {
id 'com.android.application'
id 'kotlin-android'
id("io.gitlab.arturbosch.detekt").version("1.17.1")
}
...
detekt {
toolVersion = "$detekt_version" // 1.17.1
input = files("src/main/java")
config = files("../detekt/detekt-config.yml")
autoCorrect = true
reports { ... }
}
dependencies {
detektPlugins "io.gitlab.arturbosch.detekt:detekt-formatting:$detekt_version"
compileOnly "io.gitlab.arturbosch.detekt:detekt-api:$detekt_version"
detekt "io.gitlab.arturbosch.detekt:detekt-cli:$detekt_version"
...
}
The rule goes
class SomeCustomRule(config: Config) : Rule(config) {
override val issue: Issue
get() = Issue("Import thingy", Severity.Minor, "I don't like this string", Debt(10))
override fun visitImportDirective(importDirective: KtImportDirective) {
val import: String = importDirective.importPath?.pathStr ?: ""
if ("appcompat" in import) {
report(CodeSmell(
issue,
Entity.from(importDirective),
"Importing $import which is an internal import"))
}
}
}
The provider is
class CustomRuleSetProvider : RuleSetProvider {
override val ruleSetId: String = "detekt-custom-rules"
override fun instance(config: Config) = RuleSet(ruleSetId, listOf(SomeCustomRule(config)))
}
And last but not least
At
src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider
is
com.playgrounds.detekt.customrule.CustomRuleSetProvider
Still - nothing. I tried to add gibberish at the provider - nothing, no error. I even tried to spoil the declaration at META-INF. Nothing.
Of course I tried to add the provider into my config file, or add a project line in the gradle. Error, not recognised.
What have I missed?
Thanks

Android's R file in androidTest is not generating when applicationIdSuffix is used

I've created an Android project from scratch, with package name com.example.myapplication
I've a strings.xml file in the main package (app module), with a String called yyy.
I've another strings.xml file in the androidTest package, with a String called xxx.
I can use both strings without problem in a instrumented test, simply using the correct R file:
class ExampleInstrumentedTest {
#Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
val stringFromApp = appContext.getString(com.example.myapplication.R.string.yyy)
val stringFromTest = appContext.getString(com.example.myapplication.test.R.string.xxx)
assertEquals("com.example.myapplication", appContext.packageName)
}
}
But, if I add an applicationIdSuffix in debug variant, then androidTest's R file is not generated correctly. It seems a bug on the system:
debug {
applicationIdSuffix '.imasuffix'
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
class ExampleInstrumentedTest {
#Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
val stringFromApp = appContext.getString(com.example.myapplication.R.string.yyy)
val stringFromTest = appContext.getString(com.example.myapplication.test.R.string.xxx)
val stringFromNewSuffix = appContext.getString(com.example.myapplication.imasuffix.test.R.string.xxx)
assertEquals("com.example.myapplication", appContext.packageName)
}
}
As you can see, Android Studio suggests me to use the com.example.myapplication.imasuffix.test.R file but is not working, even Rebuilding the project or invalidating caches.
I've seen a similar issue related with Roboelectric, but I'm using the standard AndroidJUnit4 tests.
Thanks.

How can we write custom lint rule for our app?

I used this link to create our own custom lint rules
link
then create WrongLayoutDetector.class to detect wrong layout name
private val allowedPrefixes = listOf("activity_", "view_", "fragment_", "dialog_", "bottom_sheet_", "adapter_item_", "divider_", "space_", "popup_window_")
val ISSUE_WRONG_LAYOUT_NAME = Issue.create("WrongLayoutName",
"Layout names should be prefixed accordingly.",
"The layout file name should be prefixed with one of the following: ${allowedPrefixes.joinToString()}. This will improve consistency in your code base as well as enforce a certain structure.",
CORRECTNESS, 7, WARNING,
Implementation(WrongLayoutNameDetector::class.java, RESOURCE_FILE_SCOPE))
class WrongLayoutNameDetector : LayoutDetector() {
override fun visitDocument(context: XmlContext, document: Document) {
val modified = allowedPrefixes.map {
val resourcePrefix = context.project.resourcePrefix()
.forceUnderscoreIfNeeded()
if (resourcePrefix != it) resourcePrefix + it else it
}
val doesNotStartWithPrefix = modified.none { context.file.name.startsWith(it) }
val notEquals = modified.map {
it.dropLast(1) // Drop the trailing underscore.
}.none { context.file.name == "$it.xml" }
if (doesNotStartWithPrefix && notEquals) {
context.report(ISSUE_WRONG_LAYOUT_NAME, document, context.getLocation(document), "Layout does not start with one of the following prefixes: ${modified.joinToString()}")
}
}
}
private fun String.forceUnderscoreIfNeeded() = if (isNotEmpty() && !endsWith("_")) plus("_") else this
fun Project.resourcePrefix() = if (isGradleProject) computeResourcePrefix(gradleProjectModel).orEmpty() else ""
IssueRegistry class
class IssueRegistry : IssueRegistry() {
override val issues: List<Issue>
get() = listOf( ISSUE_WRONG_LAYOUT_NAME,ISSUE_ALERT_DIALOG_USAGE)
override val api: Int = com.android.tools.lint.detector.api.CURRENT_API
}
build gradle file
apply plugin: 'java-library'
dependencies {
// For a description of the below dependencies, see the main project README
compileOnly "com.android.tools.lint:lint-api:26.2.0-rc02"
compileOnly "com.android.tools.lint:lint-checks:26.2.0-rc02"
testCompile "junit:junit:4.12"
testCompile "com.android.tools.lint:lint:26.2.0-rc02"
testCompile "com.android.tools.lint:lint-tests:26.2.0-rc02"
testCompile "com.android.tools:testutils:26.2.0-rc02"
}
sourceCompatibility = "1.8"
targetCompatibility = "1.8"
jar {
manifest {
// Only use the "-v2" key here if your checks have been updated to the
// new 3.0 APIs (including UAST)
attributes("Lint-Registry-v2": "com.example.lint.checks.IssueRegistry")
}
}
now in our code
lint.xml file
<lint>
<!-- Change the severity of hardcoded strings to "error" -->
<issue id="WrongLayoutName"
severity="warning" />
</lint>
and in app gradle file
lintOptions {
tasks.lint.enabled = true
abortOnError true
lintConfig file('./code_quality_tools/lint.xml')
}
now i am analyze code but not getting any warning like WrongLayoutName.
How could i implement custom lint rules with our app?
if i add <issue id="AllCaps" /> in lint file its working fine and getting warning
You have to add the link-check module in your dependency:
If you have this structure:
lint-check
-- build.gradle
app
-- build.gradle
settings.gradle
In settings.gradle:
include ':app'
include ':lint-checks'
In app/build.gradle:
dependencies {
lintChecks project(':lint-checks')
}

Android Room can not create JSON schema for testing migrations

I have created migration from 1 to 2 version of my database.
I have the app in a few modules like:
app
data
domain
I have tried adding this into build.gradle of app and data modules:
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
Here is my MigrationTest class:
#RunWith(AndroidJUnit4.class)
public class MigrationTest {
private static final String TEST_DB = "migration-test";
#Rule public MigrationTestHelper helper;
private Profile profile;
public MigrationTest() {
helper = new MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase.class.getCanonicalName(),
new FrameworkSQLiteOpenHelperFactory());
}
#Before
public void setUp(){
profile = createProfile();
}
#Test public void migrate1To2() throws IOException {
SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1);
insertProfile(db);
db.close();
AppDatabase database = (AppDatabase) helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2);
Single<ProfileData> profileDataSingle = database.profileDao().getById("userId");
ProfileData profileData = profileDataSingle.blockingGet();
Profile currentProfile = ProfileMapper.transform(profileData);
assertEquals(currentProfile.getUserId(), profile.getUserId());
}
Here is failing test:
java.io.FileNotFoundException: Cannot find the schema file in the
assets folder. Make sure to include the exported json schemas in your
test assert inputs. See
https://developer.android.com/topic/libraries/architecture/room.html#db-migration-testing
for details. Missing file: org.app.app.data.sql.AppDatabase/1.json
For Kotliners:
android{
defaultConfig {
// ...
kapt {
arguments {
arg("room.schemaLocation", "$projectDir/schemas")
}
}
}
sourceSets {
getByName("androidTest"){
assets.srcDirs(File(projectDir, "schemas"))
}
}
}
For me it helps, when I add this (I simply forgot it). Maybe it helps someone
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
This solution in Kotlin is:
Add the following to build.gradle(app):
android {
defaultConfig {
kapt {
arguments {
arg("room.schemaLocation", "$projectDir/schemas")
}
}
}
sourceSets {
getByName("androidTest") {
assets.srcDirs(files(projectDir, "schemas"))
}
}
dependencies {
implementation 'androidx.room:room-runtime:2.2.3'
kapt 'androidx.room:room-compiler:2.2.3'
annotationProcessor 'androidx.room:room-compiler:2.2.3'
implementation 'androidx.room:room-rxjava2:2.2.3'
androidTestImplementation 'androidx.room:room-testing:2.2.3'
}
P.S: Dont Forget to set exportSchema = true in the Database file.
I had a similar problem to Zookey. All I was doing wrong was looking for a schema file that didn't exist. I was testing migration 8 to 9, but version 8.json wasn't generated. I had to created my database from version 9.json instead, and test from there.
Happens because the json schema for the migration version is not found in schemas folder.
For instance, if you are testing migration from version 1 to 2, the file /schemas/*/1.json must exist.

Categories

Resources