Custom JUnit Rule allows all tests to pass no matter what - android

I have a file that references some static methods:
class MyViewModel {
fun test() { }
companion object {
private val MY_STRING = ResourceGrabber.grabString(R.string.blah_blah)
}
}
In my JUnit test for this file, I write some code to mock my resource grabber in setup. This compiles and runs, and the following test fails as I'd expect it to:
#PrepareForTest(ResourceGrabber::class)
#RunWith(PowerMockRunner::class)
class MyViewModelTest {
private lateinit var viewModel: MyViewModel
#Before
fun setup() {
PowerMockito.mockStatic(ResourceGrabber::class.java)
val mockResourceGrabber = Mockito.mock(ResourceGrabber::class.java)
whenever(mockResourceGrabber.grabString(Mockito.anyInt())).thenAnswer { invocation ->
val res: Int? = invocation?.arguments?.get(0) as? Int
TestResourceGrabber.grabString(res)
}
viewModel = MyViewModel()
}
#Test
fun someTest() {
// Fails, as expected.
assertEquals(2, 3)
}
}
Here is where things get weird. I recently learned about custom JUnit rules that you can use to avoid some duplicated code across tests. In this case, I don't want to have to copy and paste my resource grabber work into every single test suite that uses it, so I made a custom rule:
class ResourceGrabberRule : TestRule {
override fun apply(base: Statement?, description: Description?): Statement {
return object : Statement() {
override fun evaluate() {
PowerMockito.mockStatic(ResourceGrabber::class.java)
val mockResourceGrabber = Mockito.mock(ResourceGrabber::class.java)
whenever(mockResourceGrabber.grabString(Mockito.anyInt())).thenAnswer { invocation ->
val res: Int? = invocation?.arguments?.get(0) as? Int
TestResourceGrabber.grabString(res)
}
}
}
}
}
Below is the implementation of that. The crazy thing is that now EVERY test is passing no matter what:
#PrepareForTest(ResourceGrabber::class)
#RunWith(PowerMockRunner::class)
class MyViewModelTest {
private lateinit var viewModel: MyViewModel
#Rule
#JvmField
val resourceGrabber = ResourceGrabberRule()
#Before
fun setup() {
viewModel = MyViewModel()
}
#Test
fun someTest() {
// PASSES!!!?!?!?!?!
assertEquals(2, 3)
}
}
I'm not sure where the problem lies. I've tried building and running tests from both Android Studio and the command line. I don't know if I've implemented my rule incorrectly, or if it's an issue with the JUnit Rule connected with Powermock, or if it's an issue with Kotlin annotation processing. The tests compile and run but just pass no matter what's inside the tests themselves.
I'm open to comments about the architecture here (I'm sure the community has plenty) but I'm really looking for an explanation as to why the rule I wrote passes every test.

In your custom TestRule, you need to call base.evaluate() to continue the chain of rules https://github.com/junit-team/junit4/wiki/rules#custom-rules

Related

Jetpack Compose instrument test with #HiltViewModel

So I want to test my jetpack compose project. It's easy enough running an instrument test following [these instructions]1 on android dev site, but when you add #HiltViewModel injection into the combination things get complicated.
I'm trying to test a pretty simple compose screen with a ViewModel that has an #Inject constructor.
The screen itself looks like this:
#Composable
fun LandingScreen() {
val loginViewModel: LoginViewModel = viewModel()
MyTheme {
Surface(color = MaterialTheme.colors.background) {
val user by loginViewModel.user.observeAsState()
if (user != null) {
MainScreen()
} else {
LoginScreen(loginViewModel)
}
}
}
}
and this is the view model:
#HiltViewModel
class LoginViewModel #Inject constructor(private val userService: UserService) : ViewModel() {
val user = userService.loggedInUser.asLiveData()
}
User service is of course backed by a room database and the loggedInUser property returns a Flow.
Things work as expected on standard run but when trying to run it in an instrument test it can't inject the view model.
#HiltAndroidTest
class LandingScreenTest {
#get:Rule
var hiltRule = HiltAndroidRule(this)
#get:Rule
val composeTestRule = createComposeRule()
#Inject
lateinit var loginViewModel: LoginViewModel
#Before
fun init() {
hiltRule.inject()
}
#Test
fun MyTest() {
composeTestRule.setContent {
MyTheme {
LandingScreen()
}
}
composeTestRule.onNodeWithText("Welcome").assertIsDisplayed()
}
}
Injection of an #HiltViewModel class is prohibited since it does not
create a ViewModel instance correctly. Access the ViewModel via the
Android APIs (e.g. ViewModelProvider) instead. Injected ViewModel:
com.example.viewmodels.LoginViewModel
How do you make that work with the ViewModelProvider instead of the #HiltViewModel?
Hilt needs an entry point to inject fields. In this case that would probably be an Activity annotated with #AndroidEntryPoint. You can use your MainActivity for that, but that would mean that you would then have to add code to every test to navigate to the desired screen which could be tedious depending on the size of your app, and is not feasible if your project is multimodule and your current Test file does not have access to MainActivity. Instead, you could create a separate dummy Activity whose sole purpose is to host your composable (in this case LoginScreen) and annotate it with #AndroidEntryPoint. Make sure to put it into a debug directory so it's not shipped with the project. Then you can use createAndroidComposeRule<Activity>() to reference that composable. You dont need to inject the ViewModel directly so get rid of that line too.
In the end your Test File should look like this:
#HiltAndroidTest
class LandingScreenTest {
#get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this)
#get:Rule(order = 1)
val composeRule = createAndroidComposeRule<LoginTestActivity>()
#Before
fun init() {
hiltRule.inject()
}
#Test
fun MyTest() {
composeTestRule.onNodeWithText("Welcome").assertIsDisplayed()
}
}
And your your dummy activity can look like this:
#AndroidEntryPoint
class LoginTestActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
LoginScreen()
}
}
}
And the debug directory would look like this:debug directory with dummy activity
Yes the debug directory has its own manifest and that is where you should add the dummy activity. set exported to false.
Try to do something like this:
#HiltAndroidTest
class LandingScreenTest {
#get:Rule
var hiltRule = HiltAndroidRule(this)
#get:Rule
val composeTestRule = createComposeRule()
// Remove this line #Inject
lateinit var loginViewModel: LoginViewModel
#Before
fun init() {
hiltRule.inject()
}
#Test
fun MyTest() {
composeTestRule.setContent {
loginViewModel= hiltViewModel() // Add this line
MyTheme {
LandingScreen()
}
}
composeTestRule.onNodeWithText("Welcome").assertIsDisplayed()
}
}
You must annotate any UI test that uses Hilt with #HiltAndroidTest. This annotation is responsible for generating the Hilt components for each test.
https://developer.android.com/training/dependency-injection/hilt-testing

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 !

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

Can't run 2 consecutive tests (Wanted but not invoked Actually, there were zero interactions with this mock)

There's actually a couple of similar questions here, but I've tried all the suggestions, and still facing this problem.
Wanted but not invoked:
observer.onChanged(<Repo matcher>);
-> at xxx.RepoViewModelTest.testRepoList(RepoViewModelTest.kt:51)
Actually, there were zero interactions with this mock.
class RepoViewModelTest {
#get:Rule var instantTaskExecutorRule = InstantTaskExecutorRule()
#Mock private lateinit var observer: Observer<DataWrapper<List<Repo>>>
#Mock private lateinit var repoRepository: RepoRepository
private lateinit var repoViewModel: RepoViewModel
#Before
fun setup() {
MockitoAnnotations.initMocks(this)
repoViewModel = RepoViewModel(RepoUseCase(repoRepository))
}
#Test
fun testError() {
`when`(repoRepository.getRepos("mine"))
.thenReturn(Observable.error(Exception("No internet")))
repoViewModel.observeRepos().observeForever(observer)
repoViewModel.getRepos("mine")
verify(observer).onChanged(
argThat(RepoMatcher(DataWrapper(Exception("No internet")))))
}
#Test
fun testRepoList() {
val listIn = listOf(Repo("Repo1"), Repo("Repo2"))
`when`(repoRepository.getRepos("mine"))
.thenReturn(Observable.just(listIn))
repoViewModel.observeRepos().observeForever(observer)
repoViewModel.getRepos("mine")
val listOut = listOf(Repo("Repo1"), Repo("Repo2"))
verify(observer)
.onChanged(argThat(RepoMatcher(DataWrapper(listOut))))
}
}
I can run each test individually just fine, but not the whole suite.
I've tried to reset the mocks after each test, create one observer per test, or instantiate each mock object inside each test, no dice.
Running tests in Android Studio 3.2.1, under app/xxx/test
mockito 2.8.47
java 1.8.0_191
Probably you need something like this
#BeforeClass
public static void setupClass() {
RxAndroidPlugins.setInitMainThreadSchedulerHandler(__ -> Schedulers.trampoline());
}

How to correctly mock ViewModel on androidTest

I'm currently writing some UI unit tests for a fragment, and one of these #Test is to see if a list of objects is correctly displayed, this is not an integration test, therefore I wish to mock the ViewModel.
The fragment's vars:
class FavoritesFragment : Fragment() {
private lateinit var adapter: FavoritesAdapter
private lateinit var viewModel: FavoritesViewModel
#Inject lateinit var viewModelFactory: FavoritesViewModelFactory
(...)
Here's the code:
#MediumTest
#RunWith(AndroidJUnit4::class)
class FavoritesFragmentTest {
#Rule #JvmField val activityRule = ActivityTestRule(TestFragmentActivity::class.java, true, true)
#Rule #JvmField val instantTaskExecutorRule = InstantTaskExecutorRule()
private val results = MutableLiveData<Resource<List<FavoriteView>>>()
private val viewModel = mock(FavoritesViewModel::class.java)
private lateinit var favoritesFragment: FavoritesFragment
#Before
fun setup() {
favoritesFragment = FavoritesFragment.newInstance()
activityRule.activity.addFragment(favoritesFragment)
`when`(viewModel.getFavourites()).thenReturn(results)
}
(...)
// This is the initial part of the test where I intend to push to the view
#Test
fun whenDataComesInItIsCorrectlyDisplayedOnTheList() {
val resultsList = TestFactoryFavoriteView.generateFavoriteViewList()
results.postValue(Resource.success(resultsList))
(...)
}
I was able to mock the ViewModel but of course, that's not the same ViewModel created inside the Fragment.
So my question really, has someone done this successfully or has some pointers/references that might help me out?
Also, I've tried looking into the google-samples but with no luck.
For reference, the project can be found here: https://github.com/JoaquimLey/transport-eta/
Within your test setup you'll need to provide a test version of the FavoritesViewModelFactory which is being injected in the Fragment.
You could do something like the following, where the Module will need to be added to your TestAppComponent:
#Module
object TestFavoritesViewModelModule {
val viewModelFactory: FavoritesViewModelFactory = mock()
#JvmStatic
#Provides
fun provideFavoritesViewModelFactory(): FavoritesViewModelFactory {
return viewModelFactory
}
}
You'd then be able to provide your Mock viewModel in the test.
fun setupViewModelFactory() {
whenever(TestFavoritesViewModelModule.viewModelFactory.create(FavoritesViewModel::class.java)).thenReturn(viewModel)
}
I have solved this problem using an extra object injected by Dagger, you can find the full example here: https://github.com/fabioCollini/ArchitectureComponentsDemo
In the fragment I am not using directly the ViewModelFactory, I have defined a custom factory defined as a Dagger singleton:
https://github.com/fabioCollini/ArchitectureComponentsDemo/blob/master/uisearch/src/main/java/it/codingjam/github/ui/search/SearchFragment.kt
Then in the test I replace using DaggerMock this custom factory using a factory that always returns a mock instead of the real viewModel:
https://github.com/fabioCollini/ArchitectureComponentsDemo/blob/master/uisearchTest/src/androidTest/java/it/codingjam/github/ui/repo/SearchFragmentTest.kt
Look like, you use kotlin and koin(1.0-beta).
It is my decision for mocking
#RunWith(AndroidJUnit4::class)
class DashboardFragmentTest : KoinTest {
#Rule
#JvmField
val activityRule = ActivityTestRule(SingleFragmentActivity::class.java, true, true)
#Rule
#JvmField
val executorRule = TaskExecutorWithIdlingResourceRule()
#Rule
#JvmField
val countingAppExecutors = CountingAppExecutorsRule()
private val testFragment = DashboardFragment()
private lateinit var dashboardViewModel: DashboardViewModel
private lateinit var router: Router
private val devicesSuccess = MutableLiveData<List<Device>>()
private val devicesFailure = MutableLiveData<String>()
#Before
fun setUp() {
dashboardViewModel = Mockito.mock(DashboardViewModel::class.java)
Mockito.`when`(dashboardViewModel.devicesSuccess).thenReturn(devicesSuccess)
Mockito.`when`(dashboardViewModel.devicesFailure).thenReturn(devicesFailure)
Mockito.`when`(dashboardViewModel.getDevices()).thenAnswer { _ -> Any() }
router = Mockito.mock(Router::class.java)
Mockito.`when`(router.loginActivity(activityRule.activity)).thenAnswer { _ -> Any() }
StandAloneContext.loadKoinModules(hsApp + hsViewModel + api + listOf(module {
single(override = true) { router }
factory(override = true) { dashboardViewModel } bind ViewModel::class
}))
activityRule.activity.setFragment(testFragment)
EspressoTestUtil.disableProgressBarAnimations(activityRule)
}
#After
fun tearDown() {
activityRule.finishActivity()
StandAloneContext.closeKoin()
}
#Test
fun devicesSuccess(){
val list = listOf(Device(deviceName = "name1Item"), Device(deviceName = "name2"), Device(deviceName = "name3"))
devicesSuccess.postValue(list)
onView(withId(R.id.rv_devices)).check(ViewAssertions.matches(ViewMatchers.isCompletelyDisplayed()))
onView(withId(R.id.rv_devices)).check(matches(hasDescendant(withText("name1Item"))))
onView(withId(R.id.rv_devices)).check(matches(hasDescendant(withText("name2"))))
onView(withId(R.id.rv_devices)).check(matches(hasDescendant(withText("name3"))))
}
#Test
fun devicesFailure(){
devicesFailure.postValue("error")
onView(withId(R.id.rv_devices)).check(ViewAssertions.matches(ViewMatchers.isCompletelyDisplayed()))
Mockito.verify(router, times(1)).loginActivity(testFragment.activity!!)
}
#Test
fun devicesCall() {
onView(withId(R.id.rv_devices)).check(ViewAssertions.matches(ViewMatchers.isCompletelyDisplayed()))
Mockito.verify(dashboardViewModel, Mockito.times(1)).getDevices()
}
}
In the example you provided, you are using mockito to return a mock for a specific instance of your view model, and not for every instance.
In order to make this work, you will have to have your fragment use the exact view model mock that you have created.
Most likely this would come from a store or a repository, so you could put your mock there? It really depends on how you setup the acquisition of the view model in your Fragments logic.
Recommendations:
1) Mock the data sources the view model is constructed from or
2) add a fragment.setViewModel() and Mark it as only for use in tests. This is a little ugly, but if you don't want to mock data sources, it is pretty easy this way.
One could easily mock a ViewModel and other objects without Dagger simply by:
Create a wrapper class that can re-route calls to the ViewModelProvider. Below is the production version of the wrapper class that simply passes the calls to the real ViewModelProvider which is passed in as a parameter.
class VMProviderInterceptorImpl : VMProviderInterceptor { override fun get(viewModelProvider: ViewModelProvider, x: Class<out ViewModel>): ViewModel {
return viewModelProvider.get(x)
}
}
Adding getters and setters for this wrapper object to the Application class.
In the Activity rule, before an activity is launched, swap out the real wrapper with a mocked wrapper that does not route the get ViewModel call to the real viewModelProvider and instead provides a mocked object.
I realize this is not as powerful as dagger but the simplicity is attractive.

Categories

Resources