How to unit test custom Application class? - android

I would like to write some unit tests for my custom Application class but my tests fail immediately after I try to create Application object with this error:
java.lang.RuntimeException: Stub!
at android.content.Context.<init>(Context.java:20)
at android.content.ContextWrapper.<init>(ContextWrapper.java:21)
at android.app.Application.<init>(Application.java:36)
...
This is my Application class:
class MainApplication : Application() {
#Inject lateinit var statLogger: StatLogger
override fun onCreate() {
super.onCreate()
inject()
setupStatLogger()
}
private fun inject() {
...
}
private fun configStatLogger() {
statLogger.config()
}
}
And this is my test:
class MainApplicationTest {
val app = MainApplication().apply {
statLogger = mock()
}
#Test
fun `logger is configured`() {
app.onCreate()
verify(app.statLogger).config()
}
}

You can modify your application class to something similar to MVP and/or test the testable components of your Application class in isolation.
You are injecting your StatLogger and the checking the config on it. You can get a copy of your StatLogger under test and then check the config outside of the Application class too.

Related

Android Test Koin NoBeanDefFoundException

I'm trying to do some Android Tests with Koin and so far, it is not a success.
I want to test a basic Activity with a ViewModel, injected by Koin.
I already read posts like NoBeanDefFoundException with Mock ViewModel, testing with Koin, Espresso but so far I still have the error.
Here is the code relative to the tests configuration
A specific app that start with no module.
class MyTestApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin { emptyList<Module>() }
}
}
A specific runner that uses the test app
class OccazioTestRunner : AndroidJUnitRunner() {
override fun newApplication(
cl: ClassLoader?,
className: String?,
context: Context?
): Application {
return super.newApplication(cl, MyTestApplication::class.java.name, context)
}
}
That is defined in my app build.gradle to be used as runner
android {
defaultConfig {
testInstrumentationRunner "fr.dsquad.occazio.occazio.OccazioTestRunner"
}
}
And now the code I want to test
In my MyActivity
class MyActivity : AppCompatActivity(R.layout.activity_my) {
private val myViewModel by viewModel<MyViewModel>()
// Some code
}
And the viewmodel
class MyViewModel(private val useCase: MyUseCase): ViewModel() {
// Some code
}
And finally, the test itself (in androidTest)
#LargeTest
class MyActivityTest : KoinTest {
private lateinit var mockUseCase: MyUseCase
#JvmField
#Rule
val activityRule = activityScenarioRule<MyActivity>()
#Before
fun setup() {
mockUseCase = mock(MyUseCase::class.java)
startKoin {
modules(module { viewModel { MyViewModel(mockUseCase) } })
}
// I've also tried this
loadKoinModules(
module { viewModel { MyViewModel(mockUseCase) } }
)
}
#After
fun cleanUp() {
stopKoin()
}
#Test
fun someTest() = runBlocking {
// Mock the usecase response
`when`(mockUseCase.doSomething()).thenReturn("taratata")
// Start the scenario
val scenario = activityRule.scenario
// Verify we call the getUserId
// Activity is supposed to call the view model that will call the method doSomethingAdterThat.
verify(mockUseCase, times(1)).doSomethingAfterThat()
return#runBlocking
}
}
And so far, everytime I run this code I have this error
org.koin.core.error.NoBeanDefFoundException:
No definition found for 'mypackage.MyViewModel' has been found. Check your module definitions.
What is interesting is that, when
I change the rule activityScenarioRule by the old deprecated ActivityTestRule(SplashScreenActivity::class.java, true, false)
I change val scenario = activityRule.scenario by val scenario = activityRule.launchActivity(null)
I use loadKoinModules and not startKoin in setUp
Two things happen
When my test is started alone (via Android Studio): it passes.
When my test is started with other tests (by the class or with connectedAndroidTest), only one of them passes and old the others are KO.
So I have two questions in fact here.
How can I make this test work with activityScenarioRule ?
How can I make them "all" work (and not start them one by one to make them work) ?
Ok, don't ask me how it works but I figured it out.
First of all, as I needed config I followed this https://medium.com/stepstone-tech/better-tests-with-androidxs-activityscenario-in-kotlin-part-1-6a6376b713ea .
I've done 3 things
First, I needed to configure koin before startup, to do that, I needed to use ActivityScenario.launch() with an intent that I defined earlier
private val intent = Intent(ApplicationProvider.getApplicationContext(), MyActivity::class.java)
var activityRule : ActivityScenario<MyActivity>? = null
// And then I can start my activity calling
activityRule = ActivityScenario.launch(intent)
Then "KoinApp was not started"... I just replaced the loadKoinModules block with the startKoin one in setUp
startKoin { modules(module { viewModel { MyViewModel(mockUseCase) } }) }
Finally, it worked for 1 test, but the others were failing because "KoinAppAlreadyStartedException" like the stopKoin() was not called. So I found out that I should extend AutoCloseKoinTest instead of KoinTest.. But no success.
In the end, I've put a stopKoin() before the startKoin and now, everything works like a charm.
Here is my complete code that works
#LargeTest
class MyActivityTest : KoinTest() {
private val intent = Intent(ApplicationProvider.getApplicationContext(), MyActivity::class.java)
var activityRule : ActivityScenario<MyActivity>? = null
private lateinit var mockUseCase: MyUseCase
#Before
fun setup() {
mockUseCase = mock(MyUseCase::class.java)
stopKoin()
startKoin {
modules(module { viewModel { MyViewModel(mockUseCase) } })
}
}
#After
fun cleanUp() {
activityRule?.close()
}
#Test
fun someTest() = runBlocking {
// Mock the usecase response
`when`(mockUseCase.doSomething()).thenReturn("taratata")
// Start the rule
val activityRule = ActivityScenario.launch(intent)
// Verify we call the getUserId
// Activity is supposed to call the view model that will call the method doSomethingAdterThat.
verify(mockUseCase, times(1)).doSomethingAfterThat()
return#runBlocking
}
}
Ho, I've also added this code to my two Applications
override fun onTerminate() {
super.onTerminate()
stopKoin()
}
Just to be sure !

Koin Unit testing how to verify whether a class method was called when injected by "by inject()"

I have a class for which I have written unit tests. The class injects 2 other classes via the constructor. However due to cyclic dependency issues, I had to inject one of the other dependencies via by inject().
My class looks as the follows:
class AuthUseCase(
private val accessTokenUseCase: AccessTokenUseCase,
private val refreshTokenRepo: RefreshTokenRepo
) : KoinComponent {
val notificationService: NotificationService by inject()
fun getSyncedAccessToken(loginResult: LoginResult): Token? {
return when (loginResult) {
is LoginResult.Success -> {
accessTokenUseCase.storeRefreshToken(loginResult.accessToken)
notificationService.init()
loginResult.accessToken.accessToken
}
is LoginResult.Failure -> {
null
}
}
}
}
I want to test if notificationService.init was fired or not in my test.
Normally it would be as simple as
verify(notificationService).init()
However I cannot understand how to mock this class. Any help would be highly appreciated.
Thanks to #Mariuz and through this post:
https://github.com/InsertKoinIO/koin/issues/197#issuecomment-429768448
The trick is to start an empty Koin container. And load mocked module in the test.
#Test
fun testGetValidAccessTokenIfInvalidAccessTokenIsPassed() {
notificationService = mock()
startKoin { }
loadKoinModules(module {
single {
notificationService
}
})
verify(notificationService).init()
}

Android Kotlin: integration tests using Dagger 2 and Mockito getting error, "zero interactions with this mock"

I am developing an Android application using Kotlin. I am writing integration tests for my application. Now I am having a problem mocking an injected dependency class that is injected using the Dagger 2 test if a method of the mocked object is called. Following is my code.
I have the TestAppComponent class with the following code
#Singleton
#Component(modules = [ TestAppModule::class ])
interface TestAppComponent
{
fun inject(app: ApplicationController)
fun inject(app: MockApplicationController)
}
As you can see in the class, I have two inject methods. One for the ApplicationController which basically is the application class used for the actual application. Another one for MockAppllication controller which is also the application class but it is used for the tests.
Following is the implementation of the ApplicationController class
open class ApplicationController: Application()
{
lateinit var appComponent: AppComponent
private fun initDagger(app: ApplicationController): AppComponent = DaggerAppComponent
.builder()
.appModule(AppModule(app)).build()
#Inject
lateinit var fileService: IFileService
override fun onCreate() {
super.onCreate()
appComponent = initDagger(this)
instance = this
this.appComponent.inject(this)
}
}
As you can see in the code, I am initializing the Dagger AppComponent class in the onCreate method of the application class. Then inject the dependency class.
This is the implementation of my MockApplicationController class which is used for the tests.
class MockApplicationController: ApplicationController()
{
private fun initDagger(app: MockApplicationController): AppComponent = DaggerTestAppComponent
.builder()
.testAppModule(TestAppModule(app))
.build()
override fun onCreate() {
super.onCreate()
//override the dagger app component appComponent
this.appComponent = initDagger(this)
instance = this
this.appComponent.inject(this)
}
}
This is my activity class.
class MainActivity: AppCompatActivity()
{
override fun onCreate(savedInstanceState: Bundle?) {
//rest of the code
button_save_file.setOnClickListener {
//rest of the code
ApplicationController.instance.fileService.saveFile(filePath)
}
}
}
As you can see, basically, what I am doing within the activity class is that I am just calling the saveFile of the IFileService interface.
Following is the definition of the IFileService interface
interface IFileService
{
fun saveFile(path: String)
}
There are two classes that are implementing the IFileService. One is FakeFileService class which will be used for the tests and the other one is FileService class which will be used for the actual application.
Following is the implementation of FakeFileService class
class FakeFileService: IFileService
{
fun saveFile(path: String)
{
//literally, this is doing nothing since we will only test that if the method is called
}
}
I also created two classes for the Dagger app modules. One for the tests and the other one for the actual application.
Following is the implementation of the TestAppModule class which will be used for tests.
#Module
open class TestAppModule (private val app: Application) {
private var fileService: IFileService? = null
#Singleton
#Provides
fun provideContext(): Context = app
//to override injecting the Mockito mock instance
fun injectFileService(fileService: IFileService) {
this.fileService = fileService
}
#Singleton
#Provides
open fun fileService(): IFileService {
if (this.fileService != null) {
return this.fileService as IFileService
}
return FakeFileService()
}
}
Note the injectFileService method. I created that method to inject the mock object/ instance mocked using Mockito within the tests.
This is my test class
#RunWith(AndroidJUnit4::class)
#LargeTest
class CameraTest: TestBuilder()
{
#get:Rule
var mainActivityRule: ActivityTestRule<MainActivity> = ActivityTestRule<MainActivity>(CameraActivity::class.java, true, false)
private lateinit var fileServiceMock: IFileIOService
#Before
fun setUp() {
this.fileServiceMock = mock(IFileService::class.java)
MockitoAnnotations.initMocks(this)
val instrumentation = InstrumentationRegistry.getInstrumentation()
val app = instrumentation.targetContext.applicationContext as MockApplicationController
var testModule = TestAppModule(app)
testModule.injectFileService(this.fileServiceMock)
app.appComponent = DaggerTestAppComponent
.builder()
.testAppModule(testModule)
.build()
app.appComponent.inject(app)
}
#Test
fun itSavesFileWhenButtonIsClicked() {
Intents.init()
var targetContext: Context = InstrumentationRegistry.getInstrumentation().targetContext
var intent: Intent = Intent(targetContext, MainActivity::class.java)
this.mainActivityRule.launchActivity(intent)
onView(withId(R.id.button_save_file)).perform(click())
verify(this.fileServiceMock).saveFile(any())
Intents.release()
}
}
As you can see, in my test class, I try to inject the mocked version of IFileService object before the test is run. Then in my test, I am asserting that the method is executed. When I run the test it is complaining that "zero interactions with this mock". How can I fix it? What is wrong with my code and how can I fix it?
This is my ApplicationController class
open class ApplicationController: Application()
{
lateinit var appComponent: AppComponent
private fun initDagger(app: ApplicationController): AppComponent = DaggerAppComponent
.builder()
.appModule(AppModule(app)).build()
#Inject
lateinit var fileService: IFileService
override fun onCreate() {
super.onCreate()
appComponent = initDagger(this)
instance = this
this.appComponent.inject(this)
}
}
When I put the loggin in each onCreate method of application controller classes and #Before method, they are called in the following order.
ApplicationController
MockApplicationController
The code to modify the MockApplicationController with the #Before method

Can't use injected value as constructor of another injection

I would like to define 2 injected classes, but one needs to use the second class method for the constructor. I am using Koin framework
class MainActivity : AppCompatActivity() {
private val connectionService : ConnectionService by inject()
private val resourcesHelper : ResourcesHelper by inject()
private val addressPropertyName = "connection.address"
private val portPropertyName = "connection.port"
private val appModule = module {
single { ResourcesHelperImpl(androidContext(), R.raw.config) }
single {
ConnectionServiceTcp(
resourcesHelper.getConfigValueAsString(addressPropertyName),
resourcesHelper.getConfigValueAsInt(portPropertyName)
)
}
}
And then I get an error because I cannot instantiate ConnectionServiceTcp using resourcesHelper. Is there a way to use injected field to inject another field?
Edit
Changing to get() helped, but now I struggle with module configuration.
I moved start koin to MainApplication class:
class MainApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this#MainApplication)
androidLogger()
modules(appModule)
}
}
}
And module to AppModule.kt
val appModule = module {
single { ResourcesHelperImpl(androidContext(), R.raw.drone) }
single {
ConnectionServiceTcp(
get<ResourcesHelper>().getConfigValueAsString(ResourcesHelper.droneAddressPropertyName),
get<ResourcesHelper>().getConfigValueAsInt(ResourcesHelper.dronePortPropertyName)
)
}
scope(named<MainActivity>()) {
scoped {
ConnectionServiceTcp(get(), get())
}
}
}
And then I try to inject some object to activities and I am getting
Caused by: org.koin.core.error.NoBeanDefFoundException: No definition found for has been found. Check your module definitions.
Okay, I encountered two problems, firstly I could not instantiate bean using other bean in constructor, it was resolved by changing my invoke to
ConnectionServiceTcp(
get<ResourcesHelper>().getConfigValueAsString(ResourcesHelper.droneAddressPropertyName),
get<ResourcesHelper>().getConfigValueAsInt(ResourcesHelper.dronePortPropertyName)
)
Secondly, there was a problem with NoBeanDefFoundException, it was due androidContext() in ResourcesHelperImpl, I needed there Context from the activity, not the koin context.

How do I inject a class that uses androidContext in an instrumented test with Koin?

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)
}
}

Categories

Resources