瀏覽代碼

Add testing utilities to override injections in classes injected with AndroidInjection

This includes Activities, Fragments, Services, etc. Other classes should use constructor injection,
and as such shouldn't need this.

This works by overriding the androidInjector method in the Application class (new TestMainApp added)
and allowing to use custom injectors there.

A InjectionOverrideRule is included for ease of use, but for more complicated cases, injection can be
controlled by accessing TestMainApp directly.

A sample is included in InjectionTestActivityTest.

Signed-off-by: Álvaro Brey <alvaro.brey@nextcloud.com>
Álvaro Brey 2 年之前
父節點
當前提交
7ea6283af4

+ 44 - 0
app/src/androidTest/java/com/nextcloud/test/InjectionOverrideRule.kt

@@ -0,0 +1,44 @@
+/*
+ * Nextcloud Android client application
+ *
+ *  @author Álvaro Brey
+ *  Copyright (C) 2023 Álvaro Brey
+ *  Copyright (C) 2023 Nextcloud GmbH
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or 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.test
+
+import android.app.Instrumentation
+import androidx.test.platform.app.InstrumentationRegistry
+import dagger.android.AndroidInjector
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+class InjectionOverrideRule(private val overrideInjectors: Map<Class<*>, AndroidInjector<*>>) : TestRule {
+    override fun apply(base: Statement, description: Description): Statement = object : Statement() {
+        override fun evaluate() {
+            val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+            val testApp = instrumentation.targetContext.applicationContext as TestMainApp
+            overrideInjectors.entries.forEach {
+                testApp.addTestInjector(it.key, it.value)
+            }
+            base.evaluate()
+            testApp.clearTestInjectors()
+        }
+    }
+}

+ 62 - 0
app/src/androidTest/java/com/nextcloud/test/InjectionTestActivityTest.kt

@@ -0,0 +1,62 @@
+/*
+ * Nextcloud Android client application
+ *
+ *  @author Álvaro Brey
+ *  Copyright (C) 2023 Álvaro Brey
+ *  Copyright (C) 2023 Nextcloud GmbH
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or 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.test
+
+import androidx.test.core.app.launchActivity
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import com.nextcloud.client.preferences.AppPreferences
+import com.owncloud.android.R
+import dagger.android.AndroidInjector
+import io.mockk.every
+import io.mockk.mockk
+import org.junit.Rule
+import org.junit.Test
+
+class InjectionTestActivityTest {
+
+    @get:Rule
+    val injectionOverrideRule =
+        InjectionOverrideRule(
+            mapOf(
+                InjectionTestActivity::class.java to AndroidInjector<InjectionTestActivity> { activity ->
+                    val appPreferencesMock = mockk<AppPreferences>()
+                    every { appPreferencesMock.lastUploadPath } returns INJECTED_STRING
+                    activity.appPreferences = appPreferencesMock
+                }
+            )
+        )
+
+    @Test
+    fun testInjectionOverride() {
+        launchActivity<InjectionTestActivity>().use { _ ->
+            onView(withId(R.id.text)).check(matches(withText(INJECTED_STRING)))
+        }
+    }
+
+    companion object {
+        private const val INJECTED_STRING = "injected string"
+    }
+}

+ 17 - 23
app/src/androidTest/java/com/nextcloud/test/ScreenshotTestRunner.java → app/src/androidTest/java/com/nextcloud/test/ScreenshotTestRunner.kt

@@ -3,8 +3,10 @@
  * Nextcloud Android client application
  *
  * @author Tobias Kaminsky
+ * @author Álvaro Brey
  * Copyright (C) 2019 Tobias Kaminsky
- * Copyright (C) 2019 Nextcloud GmbH
+ * Copyright (C) 2023 Álvaro Brey
+ * Copyright (C) 2023 Nextcloud GmbH
  *
  * 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
@@ -19,33 +21,25 @@
  * You should have received a copy of the GNU Affero General Public License
  * along with this program. If not, see <https://www.gnu.org/licenses/>.
  */
-
-package com.nextcloud.test;
-
-import android.app.Application;
-import android.content.Context;
-import android.os.Build;
-import android.os.Bundle;
-
-import com.facebook.testing.screenshot.ScreenshotRunner;
-import com.github.tmurakami.dexopener.DexOpener;
-
-import com.karumi.shot.ShotTestRunner;
-
-public class ScreenshotTestRunner extends ShotTestRunner {
-
-    @Override
-    public Application newApplication(ClassLoader cl, String className, Context context)
-        throws ClassNotFoundException, IllegalAccessException, InstantiationException {
-
+package com.nextcloud.test
+
+import android.app.Application
+import android.app.Instrumentation
+import android.content.Context
+import android.os.Build
+import com.github.tmurakami.dexopener.DexOpener
+import com.karumi.shot.ShotTestRunner
+
+class ScreenshotTestRunner : ShotTestRunner() {
+    @Throws(ClassNotFoundException::class, IllegalAccessException::class, InstantiationException::class)
+    override fun newApplication(cl: ClassLoader, className: String, context: Context): Application {
         /*
          * Initialize DexOpener only on API below 28 to enable mocking of Kotlin classes.
          * On API 28+ the platform supports mocking natively.
          */
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
-            DexOpener.install(this);
+            DexOpener.install(this)
         }
-
-        return super.newApplication(cl, className, context);
+        return Instrumentation.newApplication(TestMainApp::class.java, context)
     }
 }

+ 74 - 0
app/src/androidTest/java/com/nextcloud/test/TestMainApp.kt

@@ -0,0 +1,74 @@
+/*
+ * Nextcloud Android client application
+ *
+ *  @author Álvaro Brey
+ *  Copyright (C) 2023 Álvaro Brey
+ *  Copyright (C) 2023 Nextcloud GmbH
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or 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.test
+
+import com.owncloud.android.MainApp
+import com.owncloud.android.lib.common.utils.Log_OC
+import dagger.android.AndroidInjector
+import dagger.android.DispatchingAndroidInjector
+
+/**
+ * The purpose of this class is to allow overriding injections in Android classes (which use parameter injection instead
+ * of constructor injection).
+ *
+ * To automate its usage, pair with [InjectionOverrideRule]; or call [addTestInjector] manually for more control.
+ */
+class TestMainApp : MainApp() {
+
+    val foo = "BAR"
+    private var overrideInjectors: MutableMap<Class<*>, AndroidInjector<*>> = mutableMapOf()
+
+    /**
+     * If you call this before a test please remember to call [clearTestInjectors] afterwards
+     */
+    fun addTestInjector(clazz: Class<*>, injector: AndroidInjector<*>) {
+        Log_OC.d(TAG, "addTestInjector: added injector for $clazz")
+        overrideInjectors[clazz] = injector
+    }
+
+    fun clearTestInjectors() {
+        overrideInjectors.clear()
+    }
+
+    override fun androidInjector(): AndroidInjector<Any> {
+        @Suppress("UNCHECKED_CAST")
+        return InjectorWrapper(dispatchingAndroidInjector, overrideInjectors as Map<Class<*>, AndroidInjector<Any>>)
+    }
+
+    class InjectorWrapper(
+        private val baseInjector: DispatchingAndroidInjector<Any>,
+        private val overrideInjectors: Map<Class<*>, AndroidInjector<Any>>
+    ) : AndroidInjector<Any> {
+        override fun inject(instance: Any) {
+            baseInjector.inject(instance)
+            overrideInjectors[instance.javaClass]?.let { customInjector ->
+                Log_OC.d(TAG, "Injecting ${instance.javaClass} with ${customInjector.javaClass}")
+                customInjector.inject(instance)
+            }
+        }
+    }
+
+    companion object {
+        private const val TAG = "TestMainApp"
+    }
+}

+ 3 - 1
app/src/debug/AndroidManifest.xml

@@ -11,7 +11,9 @@
     <application
         android:testOnly="false"
         tools:ignore="GoogleAppIndexingWarning">
-
+        <activity
+            android:name="com.nextcloud.test.InjectionTestActivity"
+            android:exported="false" />
         <activity android:name="com.nextcloud.test.TestActivity" />
     </application>
 </manifest>

+ 10 - 8
app/src/debug/java/com/nextcloud/client/di/BuildTypeComponentsModule.java → app/src/debug/java/com/nextcloud/client/di/BuildTypeComponentsModule.kt

@@ -19,19 +19,21 @@
  * You should have received a copy of the GNU Affero General Public License
  * along with this program. If not, see <https://www.gnu.org/licenses/>.
  */
+package com.nextcloud.client.di
 
-package com.nextcloud.client.di;
-
-import com.nextcloud.test.TestActivity;
-
-import dagger.Module;
-import dagger.android.ContributesAndroidInjector;
+import com.nextcloud.test.InjectionTestActivity
+import com.nextcloud.test.TestActivity
+import dagger.Module
+import dagger.android.ContributesAndroidInjector
 
 /**
  * Register classes that require dependency injection. This class is used by Dagger compiler only.
  */
 @Module
-abstract class BuildTypeComponentsModule {
+interface BuildTypeComponentsModule {
+    @ContributesAndroidInjector
+    fun testActivity(): TestActivity?
+
     @ContributesAndroidInjector
-    abstract TestActivity testActivity();
+    fun injectionTestActivity(): InjectionTestActivity?
 }

+ 46 - 0
app/src/debug/java/com/nextcloud/test/InjectionTestActivity.kt

@@ -0,0 +1,46 @@
+/*
+ * Nextcloud Android client application
+ *
+ *  @author Álvaro Brey
+ *  Copyright (C) 2023 Álvaro Brey
+ *  Copyright (C) 2023 Nextcloud GmbH
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or 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.test
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import com.nextcloud.client.di.Injectable
+import com.nextcloud.client.preferences.AppPreferences
+import com.owncloud.android.databinding.ActivityInjectionTestBinding
+import javax.inject.Inject
+
+/**
+ * Sample activity to check test overriding injections
+ */
+class InjectionTestActivity : AppCompatActivity(), Injectable {
+    @Inject
+    lateinit var appPreferences: AppPreferences
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        val binding = ActivityInjectionTestBinding.inflate(layoutInflater)
+        // random pref, just needs to match the one in the test
+        binding.text.text = appPreferences.lastUploadPath
+        setContentView(binding.root)
+    }
+}

+ 42 - 0
app/src/debug/res/layout/activity_injection_test.xml

@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud Android client application
+  ~
+  ~  @author Álvaro Brey
+  ~  Copyright (C) 2023 Álvaro Brey
+  ~  Copyright (C) 2023 Nextcloud GmbH
+  ~
+  ~ This program is free software; you can redistribute it and/or
+  ~ modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+  ~ License as published by the Free Software Foundation; either
+  ~ version 3 of the License, or 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/>.
+  ~
+  -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context="com.nextcloud.test.InjectionTestActivity">
+
+    <TextView
+        android:id="@+id/text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="Default text"
+        android:textSize="50sp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        tools:ignore="HardcodedText" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>