Skip to content

Commit

Permalink
Merge pull request #124 from cucumber/allow_field_injection_of_hooks_…
Browse files Browse the repository at this point in the history
…in_hilt

Allow field injection of hooks in HiltObjectFactory
  • Loading branch information
lsuski authored Dec 14, 2023
2 parents f0da1b6 + 059c015 commit 5bfed16
Show file tree
Hide file tree
Showing 15 changed files with 116 additions and 32 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## In Git

* handle multiple classes (features) and methods (scenarios) specified in `class` argument to better align with tools which requests specific scenarios to be executed
* support fields injection in steps classes with Hilt

## [7.14.0] - 2023-09-25

Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,13 @@ class KotlinSteps(

Such class:
- must have `@HiltAndroidTest` annotation to let Hilt generate injecting code
- can have Cucumber managed objects in constructor
- can have Cucumber managed objects like hooks injected in constructor
- can have Cucumber managed objects injected in fields but such objects have to be annotated with `@Singleton` annotation and constructor has to be annotated with `@Inject` annotation
- can have Hilt managed objects injected only using field injection - cannot have them injected in constructor
- can have objects injected in base class

Also:
after each scenario Hilt will clear all objects and create new ones (even these marked as @Singleton) (like it does for each test class in Junit)

##### 2. @WithJunitRule

Expand Down
8 changes: 4 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import java.time.Duration

buildscript {
ext.kotlin_version = '1.9.10'
ext.hilt_version = '2.48'
ext.kotlin_version = '1.9.21'
ext.hilt_version = '2.48.1'
repositories {
google()
mavenCentral()
Expand All @@ -11,7 +11,7 @@ buildscript {
maven { url 'https://jitpack.io' }
}
dependencies {
classpath 'com.android.tools.build:gradle:8.1.1'
classpath 'com.android.tools.build:gradle:8.1.4'
classpath "io.github.gradle-nexus:publish-plugin:1.3.0"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
Expand All @@ -26,7 +26,7 @@ ext {
buildToolsVersion = '34.0.0'
minSdkVersion = '14'

robolectricVersion = '4.10.3'
robolectricVersion = '4.11.1'

snapshotRepository = "https://oss.sonatype.org/content/repositories/snapshots"
releaseRepository = "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
package io.cucumber.android.hilt

import android.app.Application
import androidx.test.core.app.ApplicationProvider
import dagger.hilt.android.internal.testing.HiltExposer
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.internal.GeneratedComponentManager
import io.cucumber.core.backend.ObjectFactory
import io.cucumber.junit.TestRuleAccessor
import io.cucumber.junit.TestRulesData
import io.cucumber.junit.TestRulesExecutor
import org.junit.rules.TestRule
import org.junit.runner.Description
import java.util.Locale
import java.util.concurrent.Executors
import javax.inject.Provider

@HiltAndroidTest
class HiltObjectFactory:ObjectFactory {
class HiltObjectFactory : ObjectFactory {

private val executor = Executors.newSingleThreadExecutor()
private lateinit var rulesExecutor: TestRulesExecutor
private val testDescription = Description.createTestDescription(javaClass, "start")

private val objects = hashMapOf<Class<*>,Any?>()
private val objects = hashMapOf<Class<*>, Any?>()

override fun start() {

Expand All @@ -41,11 +46,11 @@ class HiltObjectFactory:ObjectFactory {
objects.clear()
}

override fun addClass(glueClass: Class<*>?): Boolean = true
override fun addClass(glueClass: Class<*>?): Boolean = true

override fun <T : Any?> getInstance(glueClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return objects.getOrPut(glueClass){
return objects.getOrPut(glueClass) {
val instance = createInstance(glueClass)
injectWithHilt(glueClass, instance)
instance
Expand All @@ -56,10 +61,22 @@ class HiltObjectFactory:ObjectFactory {
HiltExposer.getTestComponentData(glueClass)?.testInjector()?.injectTest(instance)
}

private fun <T : Any?> createInstance(glueClass: Class<T>):T {
return glueClass.declaredConstructors.single().let { constructor ->
@Suppress("UNCHECKED_CAST")
constructor.newInstance(*constructor.parameterTypes.map { getInstance(it) }.toTypedArray()) as T
}
private fun <T : Any?> createInstance(glueClass: Class<T>): T {
return tryFindProviderInHiltComponent(glueClass) ?: createInstanceUsingConstructor(glueClass)
}

private fun <T : Any?> createInstanceUsingConstructor(glueClass: Class<T>) = glueClass.declaredConstructors.single().let { constructor ->
@Suppress("UNCHECKED_CAST")
constructor.newInstance(*constructor.parameterTypes.map { getInstance(it) }.toTypedArray()) as T
}


@Suppress("UNCHECKED_CAST")
private fun <T : Any?> tryFindProviderInHiltComponent(glueClass: Class<T>): T? {
val component = (ApplicationProvider.getApplicationContext<Application>() as GeneratedComponentManager<Any>).generatedComponent()
return component.javaClass.declaredFields.find { it.name == "${glueClass.simpleName.replaceFirstChar { it.lowercase(Locale.ROOT) }}Provider" }?.let {
it.isAccessible = true
it.get(component) as Provider<T>
}?.get()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,18 @@ class HiltObjectFactoryTest {
hiltObjectFactory.stop()

}

@Test
fun `inject cucumber and hilt dependencies using field injection into base class`() {
hiltObjectFactory.start()

val someSteps1 = hiltObjectFactory.getInstance(StepsWithBaseClass::class.java)
val hook = hiltObjectFactory.getInstance(SomeCucumberHook::class.java)


assertNotNull(someSteps1.someDependencies)
assertNotNull(someSteps1.someSingletonDependency)
assertSame(someSteps1.someCucumberHook, hook)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.cucumber.android.hilt

import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class SomeCucumberHook @Inject constructor() {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.cucumber.android.hilt

import dagger.hilt.android.testing.HiltAndroidTest
import javax.inject.Inject

abstract class BaseSteps {

@Inject
lateinit var someCucumberHook: SomeCucumberHook

@Inject
lateinit var someDependencies: SomeDependencies
}

@HiltAndroidTest
class StepsWithBaseClass:BaseSteps() {
@Inject
lateinit var someSingletonDependency: SomeSingletonDependency
}
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
sdk=33
1 change: 0 additions & 1 deletion cucumber-android/src/test/resources/robolectric.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
sdk=33
shadows=io.cucumber.android.shadows.ExtendedShadowPackageManager,io.cucumber.android.shadows.ShadowDexFile
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
sdk=33
10 changes: 5 additions & 5 deletions cukeulator/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

ext {
composeCompilerVersion = '1.5.3'
composeVersion = '1.5.1'
composeCompilerVersion = '1.5.6'
composeVersion = '1.5.4'
composeMaterialVersion = composeVersion
}

Expand Down Expand Up @@ -71,8 +71,8 @@ android {
dependencies {

implementation 'androidx.appcompat:appcompat:1.6.1'
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.activity:activity-compose:1.7.2'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'androidx.activity:activity-compose:1.8.2'
implementation "com.google.accompanist:accompanist-themeadapter-appcompat:0.28.0"
implementation "androidx.compose.ui:ui-tooling:$composeVersion"
implementation "androidx.compose.runtime:runtime:$composeVersion"
Expand All @@ -93,6 +93,6 @@ dependencies {
androidTestImplementation project(":cucumber-android-hilt")

//desugaring
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.3"
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.4"

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package cucumber.cukeulator.test

import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.SemanticsNodeInteractionCollection
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import cucumber.cukeulator.GreetingService
import javax.inject.Inject

abstract class BaseKotlinSteps: SemanticsNodeInteractionsProvider {

@Inject
lateinit var composeRuleHolder: ComposeRuleHolder

@Inject
lateinit var greetingService: GreetingService

override fun onAllNodes(matcher: SemanticsMatcher, useUnmergedTree: Boolean): SemanticsNodeInteractionCollection {
return composeRuleHolder.composeRule.onAllNodes(matcher, useUnmergedTree)
}

override fun onNode(matcher: SemanticsMatcher, useUnmergedTree: Boolean): SemanticsNodeInteraction {
return composeRuleHolder.composeRule.onNode(matcher, useUnmergedTree)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@ import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.junit4.createEmptyComposeRule
import io.cucumber.junit.WithJunitRule
import org.junit.Rule
import javax.inject.Inject
import javax.inject.Singleton

@WithJunitRule("not @CustomComposable")
class ComposeRuleHolder {
@Singleton
class ComposeRuleHolder @Inject constructor() {

@get:Rule(order = 1)
val composeRule = createEmptyComposeRule()
}

@WithJunitRule("@CustomComposable")
class CustomComposableRuleHolder {
@Singleton
class CustomComposableRuleHolder @Inject constructor(){

@get:Rule(order = 1)
val composeRule = createComposeRule()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package cucumber.cukeulator.test

import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithText
import androidx.test.core.app.ApplicationProvider
Expand All @@ -10,22 +9,17 @@ import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.platform.app.InstrumentationRegistry
import cucumber.cukeulator.ComposeTestActivity
import cucumber.cukeulator.GreetingService
import cucumber.cukeulator.R
import dagger.hilt.android.testing.HiltAndroidTest
import io.cucumber.java.en.Then
import io.cucumber.java.en.When
import org.junit.Assert
import javax.inject.Inject

@HiltAndroidTest
class KotlinSteps(
val composeRuleHolder: ComposeRuleHolder,
val scenarioHolder: ActivityScenarioHolder
):SemanticsNodeInteractionsProvider by composeRuleHolder.composeRule {
): BaseKotlinSteps() {

@Inject
lateinit var greetingService:GreetingService


@Then("I should see {string} on the display")
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip

0 comments on commit 5bfed16

Please sign in to comment.