I'm trying to launch tests with Koin DI help like in the example https://insert-koin.io/docs/2.0/documentation/koin-core/index.html#_making_your_test_a_koincomponent_with_kointest or https://insert-koin.io/docs/2.0/getting-started/junit-test/
but every time getting NoSuchMethodError. What I am doing wrong?
First I was using already created modules from main package, but there was this error. Then I created modules in test package, but error is still the same.
My code
class ComponentA
class ComponentB(val a: ComponentA)
class SignInTest : KoinTest {
val componentB : ComponentB by inject()
#Before
fun before() {
startKoin { modules(
module {
single { ComponentA() }
single { ComponentB(get()) }
}) }
}
#Test
fun test_test() {
val componentA = get<ComponentA>()
assertNotNull(componentA)
assertEquals(componentA, componentB.a)
}
#After
fun after() {
stopKoin()
}
java.lang.NoSuchMethodError: org.koin.core.definition.BeanDefinition.(Lorg/koin/core/qualifier/Qualifier;Lorg/koin/core/qualifier/Qualifier;Lkotlin/reflect/KClass;)V
at net.app.at.features.signin.SignInTest$before$1$1.invoke(SignInTest.kt:79)
at net.app.at.features.signin.SignInTest$before$1$1.invoke(SignInTest.kt:26)
at org.koin.dsl.ModuleKt.module(Module.kt:31)
at org.koin.dsl.ModuleKt.module$default(Module.kt:29)
at net.app.at.features.signin.SignInTest$before$1.invoke(SignInTest.kt:36)
at net.app.at.features.signin.SignInTest$before$1.invoke(SignInTest.kt:26)
at org.koin.core.context.GlobalContextKt.startKoin(GlobalContext.kt:72)
at net.app.at.features.signin.SignInTest.before(SignInTest.kt:35)
Please check versions of Koin libraries you are using.
I had same problem. It turns out that I had in my build.gradle:
implementation "org.koin:koin-android:2.0.0-beta-1"
and a few lines below:
testImplementation "org.koin:koin-test:2.0.0"
When I set version 2.0.0 in both places - it worked.
Related
I use hilt for dependency injection.
I use this articles for implements test.
https://developer.android.com/training/dependency-injection/hilt-testing
https://dagger.dev/hilt/testing.html
now wanna write test for my fragments.
I have a problem with add library.
this is my code
this class is my runner and use in gradle
class CustomTestRunner : AndroidJUnitRunner() {
override fun newApplication(
cl: ClassLoader?,
className: String?,
context: Context?
): Application {
return super.newApplication(cl, MyCustom_Application::class.java.name, context)
}
}
this is interface for hilt to generate an application class that i use in runner class
#CustomTestApplication(AndroidApplication::class)
interface MyCustom
this is my test class
#HiltAndroidTest
class SettingsActivityTest {
#get:Rule
var hiltRule = HiltAndroidRule(this)
// UI tests here.
#Test
fun test(){
// val activityScenario = launchActivity<MainActivity>()
}
}
my libraries
// For instrumented tests.
androidTestImplementation("com.google.dagger:hilt-android-testing:2.28-alpha")
// ...with Kotlin.
kaptAndroidTest("com.google.dagger:hilt-android-compiler:2.34.1-beta")
androidTestImplementation("junit:junit:4.12")
androidTestImplementation ("androidx.test.ext:junit:1.1.2")
androidTestImplementation ("androidx.test.espresso:espresso-core:3.3.0")
but my problem:
when i add this line
kaptAndroidTest("com.google.dagger:hilt-android-compiler:2.34.1-beta")
gradle show this error message:
error: SettingsActivityTest_TestComponentDataSupplier is not abstract and does not override abstract method get() in TestComponentDataSupplier
public final class SettingsActivityTest_TestComponentDataSupplier extends TestComponentDataSupplier {
^
and when i use this line
kaptTest("com.google.dagger:hilt-android-compiler:2.34.1-beta")
android studio cant generate MyCustom_Application
I have encountered this problem before. This problem occurs due to incompatibility of versions.
use :
androidTestImplementation("com.google.dagger:hilt-android-testing:2.34.1-beta")
kaptAndroidTest("com.google.dagger:hilt-android-compiler:2.34.1-beta")
implementation("com.google.dagger:hilt-android:2.34.1-beta") //hilt dependency
instead of this :
androidTestImplementation("com.google.dagger:hilt-android-testing:2.28-alpha")
kaptAndroidTest("com.google.dagger:hilt-android-compiler:2.34.1-beta")
In my KoinTest extends
#Before
fun setUp() {
startKoin { modules(KoinStarter.getModules()) }
}
#Test
fun `should inject my components`() {
val settingsStore: SettingsStore = get()
assertNotNull(settingsStore)
}
I get error No definition found for class:'android.content.Context'. Check your definitions!
But my module located in KoinStarter.getModules()
val localDataModule = module {
factory<Resources> { get<Context>().resources }
single<SettingsStore> { SettingsStore(get<Context>()) }
}
You need to provide the application context in the androidContext() method.
Since your question is about testing specifically, in my case I had to pass application context via ApplicationProvider:
startKoin {
androidContext(ApplicationProvider.getApplicationContext())
.....
}
Don't forget to add the androidx dependency:
testImplementation 'androidx.test:core:1.0.0'
I have several test classes implementing KoinTest interface, and in every one of them I have the same code:
#Before
fun setUp() {
startKoin { modules(appModule) }
}
#After
fun tearDown() {
stopKoin()
}
Is it possible to call startKoin() before all these tests, and after the tests call stopKoin(), so I can remove above code from every test class? Or maybe it would be strongly discouraged for some reason?
I see that in docs here they have written 'For each test, we start startKoin() and close Koin context closeKoin().', but I don't know if this is the only valid way to go.
You can use a TestRule. Create a test rule for Koin.
class KoinTestRule : TestRule {
override fun apply(base: Statement, description: Description): Statement {
return object : Statement() {
override fun evaluate() {
startKoin { modules(appModule) }
base.evaluate()
stopKoin()
}
}
}
}
Create BaseKoinTest that implements KoinTest interface and add the rule to this class. All the test classes that require Koin can extend from this class.
abstract class BaseKoinTest : KoinTest {
#get:Rule
val koinTestRule = KoinTestRule()
}
There are two modules in my android project, app module and lib module.
Both these two modules need Koin for D.I., so I call startKoin in MyApplication class in app module, and IninKointContentProvider in lib module as below.
// app module
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin(this, modules1)
}
}
// lib module
class InitKoinContentProvider : ContentProvider() {
override fun onCreate(): Boolean {
startKoin(context.applicationContext, modules2)
return true
}
}
Then app crashed and shown this message
Caused by: org.koin.error.BeanOverrideException: Try to override definition with Single [class='android.content.Context'], but override is not allowed. Use 'override' option in your definition or module.
I guess startKoin can be called only one time.
The solution I found is merging two koin modules then calling startKoin in MyApplication, but I don't like it. Lib module may be imported by other android project which doesn't use koin, in that case, I think calling startKoin in InitKoinContentProvider is better.
Any solution for this problem?? Thanks!
I found the best solution inspired by #laalto's answer, thanks!
Upgrade to koin 2.0, then use KoinApplication and customized KoinComponent to create a isolated koin context, it can let lib module using koin without any initializing call by app module, still start koin in ContentProvider. The whole code may like below.
// app module
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this#MyApplication)
modules(module{
viewModel { MainViewModel() }
})
}
}
}
class MainActivity: AppCompactActivity() {
private val viewModel: MainViewModel by viewModel()
}
// lib module
internal object MyKoinContext {
lateinit var koinApplication: KoinApplication
}
interface MyKoinComponent : KoinComponent {
override fun getKoin(): Koin {
return MyKoinContext.koinApplication.koin
}
}
class InitKoinContentProvider : ContentProvider() {
override fun onCreate(): Boolean {
MyKoinContext.koinApplication = koinApplication {
androidContext(context.applicationContext)
modules(module{
viewModel { FooViewModel() }
})
}
return true
}
}
class FooActivity: AppCompactActivity(), MyKoinComponent {
private val viewModel: FooViewModel by viewModel()
}
Ref:
https://insert-koin.io/docs/2.0/documentation/reference/index.html#_koin_context_isolation
In your library modules, use loadKoinModules() to load the module-specific koin modules. Docs.
You need to have run startKoin() prior to that, so the init order with content providers can be a little tricky.
To init extra koin modules on other project modules and get no duplicate loading issues (e.g. pressing home, than coming back to the activity), go to your module declaration file:
val myModule = module {
single { MyRepository(get()) }
viewModel { MyViewModel(get()) }
}
private val loadKoinModules by lazy {
loadKoinModules(myModule)
}
fun inject() = loadKoinModules
Then on your view:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
inject()
}
TL;DR
Use single/factory methods with override param set to true when providing your dependencies that are overriding those provided by the modules loaded before.
single<Manager>(override = true) { TestManager() }
I have faced a similar issue when I tried to override one of the dependencies for UI test purposes.
When I setup in Application.onCreate():
startKoin {
module {
single { Printer() }
}
}
and then in before method of test:
loadKoinModules(module {
single<Printer> { TestPrinter() }
})
I get a Runtime exception during the test:
org.koin.core.error.DefinitionOverrideException: Already existing definition or try to override an existing one
And the solution is to show Koin that you are intentionally overriding that dependency by using override param of single function like that:
loadKoinModules(module {
single<Printer>(override = true) { TestPrinter() }
})
By design startKoin is meant to be called from the Application class. You can provide a parameter in the lib whether to call startKoin or not. But I doubt that including such things as Koin in libs is a good practice. What if an application already includes Koin, but of different version?
This way worked perfect for me:
#KoinExperimentalAPI
class MainApplication : Application() {
override fun onCreate() {
super.onCreate()
initKoin()
}
private fun initKoin() {
startKoin {
androidLogger()
androidContext(this#MainApplication)
}
loadKoinModules(
myFeatureModule
)
}
}
And define you module in your feature as usual:
val myFeatureModule = module {
factory<...> { ...() }
single { ...() }
viewModel { ...(get() }
}
One of my classes has a dependency of type Context. Before adding Koin to my project, I initialized this with a hard dependency on my Application class:
class ProfileRepository(
private var _context: Context? = null,
private var _profileRestService: IProfileRestService? = null
) : IProfileRepository {
init {
if (_context == null) {
_context = MyApplication.getInstance().applicationContext
}
}
Now, I want to use Koin to inject this dependency. This is how I've defined the module:
object AppModule {
#JvmField
val appModule = module {
single<IProfileRestService> { ProfileRestService() }
single<IProfileRepository> { ProfileRepository(androidContext(), get()) }
}
}
I'm starting Koin in the onCreate method of my Application class (which is written in Java):
startKoin(singletonList(AppModule.appModule));
I want to test this class with an instrumented test and not a unit test because I want to use the real context and not a mock. This is my test:
#RunWith(AndroidJUnit4::class)
class MyTest : KoinTest {
private val _profileRepository by inject<IProfileRepository>()
#Test
fun testSomething() {
assertNotNull(_profileRepository)
}
The test is failing with an exception:
org.koin.error.BeanInstanceCreationException: Can't create definition for 'Single [name='IProfileRepository',class='com.my.app.data.profile.IProfileRepository']' due to error :
No compatible definition found. Check your module definition
I can get it to work with a unit test if I mock the context like so:
class MyTest : KoinTest {
private val _profileRepository by inject<IProfileRepository>()
#Before
fun before() {
startKoin(listOf(AppModule.appModule)) with mock(Context::class.java)
}
#After
fun after() {
stopKoin()
}
#Test
fun testSomething() {
assertNotNull(_profileRepository)
}
How can I make it work as an instrumented test with a real context?
In place of (in Application):
startKoin(applicationContext, modules)
Use a mocked Context:
startKoin(modules) with (mock(Context::class.java))
From https://insert-koin.io/docs/1.0/documentation/koin-android/index.html#_starting_koin_with_android_context_from_elsewhere
Apparently there's no way to start Koin from a Java class and inject the application context. What that means is if one of your classes needs to get the context from the container, you must use org.koin.android.ext.android.startKoin instead of org.koin.java.standalone.KoinJavaStarter.startKoin.
Since my Application class is still written in Java, I created an object called KoinHelper with one method:
#JvmStatic
fun start(application: Application) {
application.startKoin(application, listOf(AppModule.appModule))
}
Then I called this from the onCreate method of my Application class:
KoinHelper.start(this);
Now, the instrumented test I posted in my original answer runs just fine.
Please see this issue on GitHub for more info.
Please check this section in the documentation. It says
if you need to start Koin from another Android class, you can use the
startKoin() function and provide your Android Context instance with
just like:
startKoin(androidContext, myAppModules)
So in your instrumentation test, you can pass a context while starting the Koin.
#Before
fun before() {
startKoin(InstrumentationRegistry.getContext(), listOf(AppModule.appModule))
}
Or if you want an application level context
#Before
fun before() {
startKoin(InstrumentationRegistry.getTargetContext(), listOf(AppModule.appModule))
}
The referenced documentation is for Version 1.0.1
In terms of getting the Application context in the instrumented test, you can use androidx.test.core.app.ApplicationProvider or InstrumentationRegistry.targetContext.applicationContext.
#Before
fun setUp() {
stopKoin()
loadKoinModules(testModule) with ApplicationProvider.getApplicationContext<Application>()
}
...where testModule uses androidApplication() to retrieve the Application context:
val testModule = module {
single {
ToDoDatabase.newInstance(
androidApplication(),
memoryOnly = true
)
}
single { ToDoRepository(get()) }
}
Note that my stopKoin() call is there because I was having difficulty overriding an existing module created by startKoin() in my custom Application subclass. ¯\_(ツ)_/¯
#Before
fun setUp() {
stopKoin()
startKoin {
androidContext(app) // for example ApplicationProvider.getApplicationContext<TestApplication>()
modules(module1, module2)
}
}