firebase / firebase-android-sdk

Firebase Android SDK
https://firebase.google.com
Apache License 2.0
2.27k stars 575 forks source link

Unit testing Firestore with Robolectric #1352

Closed drizzd closed 3 years ago

drizzd commented 4 years ago

What feature would you like to see?

Demo: https://gitlab.com/drizzd/firebaserobodemo

I would like to test my app, including the Firestore code paths, with Robolectric. Unfortunately, Firestore and Robolectric depend on different versions of protobuf. I don't suppose it makes sense to just exclude one of the two?

debugUnitTestCompileClasspath - Compile classpath for compilation 'debugUnitTest' (target  (androidJvm)).                                                                                          
+--- com.google.firebase:firebase-firestore:21.4.1                                                                                                                                                 
    +--- io.grpc:grpc-protobuf-lite:1.21.0                                                                                                                                                        
    |    +--- io.grpc:grpc-api:1.21.0 (*)                                                                                                                                                         
    |    \--- com.google.protobuf:protobuf-lite:3.0.1                                                                                                                                             
[...]                                                                                                                                                                                              
+--- org.robolectric:robolectric:4.3.1                                                                                                                                                             
|    +--- org.robolectric:shadows-framework:4.3.1                                                                                                                                                  
|    |    +--- com.google.android.apps.common.testing.accessibility.framework:accessibility-test-framework:2.1                                                                                     
|    |    |    \--- com.google.protobuf:protobuf-java:2.6.1                                                                                                                                        
|    |    \--- com.android.support:support-annotations:28.0.0      

There is also another issue with the SharedPreferences filename on Windows, for which I have submitted to Robolectric: https://github.com/robolectric/robolectric/issues/5529.

How would you use it?

I would like to use AndroidX Test unit tests which are based on Robolectric in order to efficiently test Android apps which use Firestore. Combined with the Firebase emulator suite it would make a very powerful testing environment.

Dependency Error

For reference, this is the runtime error:

java.lang.NoSuchMethodError: com.google.firestore.v1.MapValue.makeImmutable()V

    at com.google.firestore.v1.MapValue.<clinit>(com.google.firebase:firebase-firestore@@21.4.1:490)
    at com.google.firebase.firestore.UserDataReader.parseMap(com.google.firebase:firebase-firestore@@21.4.1:290)
    at com.google.firebase.firestore.UserDataReader.parseData(com.google.firebase:firebase-firestore@@21.4.1:251)
    at com.google.firebase.firestore.UserDataReader.convertAndParseDocumentData(com.google.firebase:firebase-firestore@@21.4.1:232)
    at com.google.firebase.firestore.UserDataReader.parseSetData(com.google.firebase:firebase-firestore@@21.4.1:75)
    at com.google.firebase.firestore.DocumentReference.set(com.google.firebase:firebase-firestore@@21.4.1:166)
    at com.google.firebase.firestore.DocumentReference.set(com.google.firebase:firebase-firestore@@21.4.1:146)
google-oss-bot commented 4 years ago

I couldn't figure out how to label this issue, so I've labeled it for a human to triage. Hang tight.

var-const commented 4 years ago

Thanks for reporting this incompatibility, and sorry to hear about your troubles.

Excluding protobuf-lite from Firestore will definitely break the SDK. The only workaround would be to try excluding protobuf dependency from Robolectric -- this is what we do for our own tests. Note, however, that the two versions of the protobuf library are binary incompatible, and it's pretty much guaranteed that doing the exclusion will break something in Robolectric -- we've just been lucky that it didn't affect the way we use Robolectric. In other words, it might work, though it's certainly not a scenario supported by Robolectric and might end up broken for your use case. I'm sorry I don't have a better workaround for this.

drizzd commented 4 years ago

That fixes the version conflict. However, the callbacks are not invoked in the following test. It passes after the 10 second sleep:

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class FirebaseTest {
    private val context = ApplicationProvider.getApplicationContext<Context>()

    @Test
    fun write() {
        val settings = FirebaseFirestoreSettings.Builder()
            .setHost("127.0.0.1:8080")
            .setSslEnabled(false)
            .setPersistenceEnabled(false)
            .build()

        FirebaseApp.initializeApp(context)
        val firestore = FirebaseFirestore.getInstance()
        firestore.firestoreSettings = settings
        firestore.collection("ponys").document("foo").set(mapOf("key" to "bar"))
            .addOnSuccessListener {
                throw Exception("success")
            }
            .addOnFailureListener {
                throw Exception("failure")
            }
            .addOnCanceledListener {
                throw Exception("canceled")
            }
        Thread.sleep(10000)
    }

Looking at the network traffic, it seems that no GRPC/Protobuf traffic happens:

image

As opposed to the Node.js version which works as expected:

const firebase = require('firebase/app');
require('firebase/firestore');

let firebaseApp;

async function main() {
  firebaseInit();

  try {
    await run();
  } finally {
    firebaseDelete();
  }
}

function firebaseInit() {
  firebaseApp = firebase.initializeApp({
    apiKey: "<...>",
    authDomain: "<...>.firebaseapp.com",
    databaseURL: "https://<...>.firebaseio.com",
    projectId: "<...>",
    storageBucket: "<...>.appspot.com",
    messagingSenderId: "<...>",
    appId: "<...>"
  });
  firebaseApp.firestore().settings({
    host: 'localhost:8080',
    ssl: false
  });
}

function firebaseDelete() {
  firebaseApp.delete();
}

async function run() {
  let firestore = firebaseApp.firestore();
  await firestore.collection('ponys').doc('foo').set({key: 'bar'});
}
main();

image

drizzd commented 4 years ago

For reference, here is my build.gradle with protobuf-java excluded:

apply plugin: 'com.android.application'
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.getkeepsafe.dexcount'
apply plugin: 'com.google.firebase.firebase-perf'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'io.fabric'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "<...>"
        minSdkVersion 16
        targetSdkVersion 28
        multiDexEnabled true
        versionCode 2
        versionName "1.1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

    // Workaround for DuplicateRelativeFileException: More than one file was found with OS independent path 'META-INF/DEPENDENCIES'
    packagingOptions {
        exclude 'META-INF/DEPENDENCIES'
    }

    kotlinOptions {
        jvmTarget = '1.8'
    }

    testOptions.unitTests {
        includeAndroidResources = true
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'androidx.work:work-runtime-ktx:2.4.0-alpha01'
    testImplementation 'junit:junit:4.12'
    testImplementation 'androidx.test:core:1.2.0'
    testImplementation 'androidx.test.ext:junit:1.1.1'
    testImplementation 'androidx.test:runner:1.2.0'
    testImplementation('org.robolectric:robolectric:4.2') {
        exclude group: 'com.google.protobuf', module: 'protobuf-java'
    }
    testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.2'
    androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.2'
    androidTestImplementation 'androidx.test:core:1.2.0'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test:runner:1.2.0'

    implementation 'org.jetbrains.anko:anko-commons:0.10.4'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2'
    implementation 'com.google.firebase:firebase-core:17.2.3'
    implementation 'com.google.firebase:firebase-auth:19.2.0'
    implementation 'com.google.firebase:firebase-firestore:21.4.1'
    implementation 'com.google.firebase:firebase-ml-vision:24.0.1'
    implementation 'com.google.firebase:firebase-perf:19.0.5'
    implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
    implementation 'com.android.support:multidex:1.0.3'
    testImplementation 'io.mockk:mockk:1.8.13.kotlin13'
    testImplementation 'org.json:json:20171018'

    // https://developers.google.com/identity/sign-in/android/start-integrating
    implementation 'com.google.android.gms:play-services-auth:17.0.0'
    // https://github.com/gsuitedevs/android-samples/blob/master/drive/deprecation/app/build.gradle
    implementation('com.google.http-client:google-http-client-gson:1.26.0') {
        exclude group: 'org.apache.httpcomponents'
    }
    implementation('com.google.api-client:google-api-client-android:1.26.0') {
        exclude group: 'org.apache.httpcomponents'
    }
    implementation('com.google.apis:google-api-services-drive:v3-rev136-1.25.0') {
        exclude group: 'org.apache.httpcomponents'
    }
}
drizzd commented 4 years ago

Here's a complete demo: https://gitlab.com/drizzd/firebaserobodemo

var-const commented 4 years ago

I believe the issue is that future.get blocks the same thread that the callback is going to be invoked on; therefore, the callback never gets a chance to be invoked. Trying out your repro app (thanks for preparing it!) with logging enabled (FirebaseFirestore.setLoggingEnabled(true)), I see that Firestore receives the response from the server, but the callback is never invoked. I'm not very familiar with how waiting for asynchronous events works in Robolectric, sorry; I don't think this is something specific to Firestore. It would appear that you would need to call either flushForegroundThreadScheduler or flushBackgroundThreadScheduler before you can poll the future.

var-const commented 3 years ago

Closing the issue as it hasn't been updated in a while. Feel free to reopen to follow up.