Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add HiltObjectFactory #112

Merged
merged 2 commits into from
Feb 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
*.iml
local.properties
.gradle
/build/
build
/cucumber-android/build/
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,48 @@ class KotlinSteps(val composeRuleHolder: ComposeRuleHolder, val scenarioHolder:

### Hilt

There are 2 solutions for using Hilt with Cucumber:

##### 1. HiltObjectFactory

Add dependency:
```groovy
androidTestImplementation "io.cucumber:cucumber-android-hilt:$cucumberVersion"
```

Don't use any other dependency with `ObjectFactory` like `cucumber-picocontainer`

`HiltObjectFactory` will be automatically used as `ObjectFactory`.

To inject object managed by Hilt into steps or hook or any other class managed by Cucumber:

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

@Inject
lateinit var greetingService:GreetingService

@Then("I should see {string} on the display")
fun I_should_see_s_on_the_display(s: String?) {
Espresso.onView(withId(R.id.txt_calc_display)).check(ViewAssertions.matches(ViewMatchers.withText(s)))
}

}
```

Such class:
- must have `@HiltAndroidTest` annotation to let Hilt generate injecting code
- can have Cucumber managed objects in constructor
- can have Hilt managed objects injected only using field injection - cannot have them injected in constructor


##### 2. @WithJunitRule


Hilt requires to have rule in actual test class (which for cucumber is impossible
because there is no such class). To workaround that:

Expand Down Expand Up @@ -142,3 +184,5 @@ class HiltRuleHolder {

}
```

then you can inject such class to steps class using Cucumber dependency injector (like picocontainer)
73 changes: 69 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.7.20'
ext.hilt_version = '2.44'
ext.kotlin_version = '1.8.10'
ext.hilt_version = '2.45'
repositories {
google()
mavenCentral()
Expand All @@ -12,7 +12,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.3.1'
classpath 'com.android.tools.build:gradle:7.4.1'
classpath "com.jaredsburrows:gradle-spoon-plugin:1.6.0"
classpath "io.github.gradle-nexus:publish-plugin:1.1.0"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
Expand All @@ -24,7 +24,7 @@ apply plugin: "io.github.gradle-nexus.publish-plugin"


ext {
targetSdkVersion = 32
targetSdkVersion = 33
buildToolsVersion = '33.0.0'
minSdkVersion = '14'

Expand Down Expand Up @@ -96,5 +96,70 @@ subprojects { subproject ->
}


}

ext.addLibraryPublishing = { pomName ->

apply plugin: 'signing'
apply plugin: 'maven-publish'
android {
namespace = "io.cucumber.android.${subproject.name.replace("-","_")}"

publishing {
singleVariant('release') {
withSourcesJar()
withJavadocJar()
}
}

}

afterEvaluate{

publishing {
publications {
release(MavenPublication) {
from components.release

pom {
name = pomName
packaging = 'aar'
// optionally artifactId can be defined here
description = 'Android support for Cucumber-JVM'
url = 'https://github.com/cucumber/cucumber-android'

scm {
connection = 'scm:git:https://github.com/cucumber/cucumber-android.git'
developerConnection = 'scm:git:[email protected]:cucumber/cucumber-android.git'
url = 'https://github.com/cucumber/cucumber-android'
}

licenses {
license {
name = 'MIT License'
url = 'http://www.opensource.org/licenses/mit-license'
}
}

developers {
developer {
id = 'lsuski'
name = 'Łukasz Suski'
email = '[email protected]'
}
}
}
}
}
}
signing {
def signingKey = findProperty("signingKey")
def signingPassword = findProperty("signingPassword")
useInMemoryPgpKeys(signingKey, signingPassword)
sign publishing.publications.release
}
}


}
}
21 changes: 21 additions & 0 deletions cucumber-android-hilt/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

addAndroidConfig()
addLibraryPublishing('Cucumber-JVM: Android Hilt')

dependencies {
api project(':cucumber-android')
implementation project(':cucumber-junit-rules-support')
api "com.google.dagger:hilt-android:$hilt_version"
api "com.google.dagger:hilt-android-testing:$hilt_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"

testImplementation "org.robolectric:robolectric:4.9.2"
kaptTest "com.google.dagger:hilt-android-compiler:$hilt_version"

}

2 changes: 2 additions & 0 deletions cucumber-android-hilt/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package dagger.hilt.android.internal.testing

internal object HiltExposer {


fun getTestComponentData(clazz:Class<*>): TestComponentData? {
return runCatching { TestComponentDataSupplier.get(clazz) }.getOrNull()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package io.cucumber.android.hilt

import dagger.hilt.android.internal.testing.HiltExposer
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
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.concurrent.Executors

@HiltAndroidTest
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?>()

override fun start() {

rulesExecutor = TestRulesExecutor(listOf(TestRulesData(false, this, listOf(ruleAccessor()))), executor)

rulesExecutor.startRules(testDescription)
}

private fun ruleAccessor(): TestRuleAccessor {
val hiltRule = HiltAndroidRule(this)
return object : TestRuleAccessor {
override fun getRule(obj: Any?): TestRule = hiltRule

override fun getOrder(): Int = 0
}
}

override fun stop() {
rulesExecutor.stopRules()
objects.clear()
}

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

override fun <T : Any?> getInstance(glueClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return objects.getOrPut(glueClass){
val instance = createInstance(glueClass)
injectWithHilt(glueClass, instance)
instance
} as T
}

private fun <T : Any?> injectWithHilt(glueClass: Class<T>, instance: T) {
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
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
cucumber.object-factory=io.cucumber.android.hilt.HiltObjectFactory
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package io.cucumber.android.hilt

import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.HiltTestApplication
import org.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

@HiltAndroidTest
@RunWith(RobolectricTestRunner::class)
@Config(application = HiltTestApplication::class)
class HiltObjectFactoryTest {

private var hiltObjectFactory = HiltObjectFactory()

@Test
fun `add class returns true`() {
assertTrue(hiltObjectFactory.addClass(String::class.java))
}

@Test
fun `injects into fields with proper scope`() {

hiltObjectFactory.start()

val someSteps = hiltObjectFactory.getInstance(SomeSteps::class.java)
val someOtherSteps = hiltObjectFactory.getInstance(SomeOtherSteps::class.java)

someSteps.doSomething()
someOtherSteps.doSomething()

assertSame(someOtherSteps.someSingletonDependency,someSteps.someSingletonDependency)
assertNotSame(someOtherSteps.someDependency,someSteps.someDependency)

hiltObjectFactory.stop()
}

@Test
fun `returns the same instance of steps`() {

hiltObjectFactory.start()

val someSteps1 = hiltObjectFactory.getInstance(SomeSteps::class.java)
val someSteps2 = hiltObjectFactory.getInstance(SomeSteps::class.java)


assertSame(someSteps1,someSteps2)
assertSame(someSteps1.someSingletonDependency,someSteps2.someSingletonDependency)
assertSame(someSteps1.someDependency,someSteps2.someDependency)

hiltObjectFactory.stop()
}

@Test
fun `returns new instance for second scenario`() {

hiltObjectFactory.start()

val someSteps1 = hiltObjectFactory.getInstance(SomeSteps::class.java)

hiltObjectFactory.stop()
hiltObjectFactory.start()

val someSteps2 = hiltObjectFactory.getInstance(SomeSteps::class.java)


assertNotSame(someSteps1,someSteps2)
assertNotSame(someSteps1.someSingletonDependency,someSteps2.someSingletonDependency)

hiltObjectFactory.stop()

}

@Test
fun `creates instance without hilt dependencies`() {
hiltObjectFactory.start()

val someSteps1 = hiltObjectFactory.getInstance(SomeStepsWithoutHilt::class.java)

val someSteps2 = hiltObjectFactory.getInstance(SomeStepsWithoutHiltAndDependencies::class.java)


assertSame(someSteps1.someStepsWithoutHiltAndDependencies,someSteps2)

hiltObjectFactory.stop()

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

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


class SomeDependencies @Inject constructor() {
fun doSomething() {

}
}


@Singleton
class SomeSingletonDependency @Inject constructor() {
fun doSomething() {

}
}

class SomeDependency @Inject constructor() {
fun doSomething() {

}
}
Loading