Somehow I am not able to inject use to multiple classes that contains cucmber steps with inject of hilt, It works when I use inject is done just in one class containing steps. . How to inject to all classes containg steps whatever i need from hilt ?
#RunWith(AndroidJUnit4::class)
#WithJunitRule(useAsTestClassInDescription = true)
#HiltAndroidTest
class Givens {
#Rule(order = 0)
#JvmField
val hiltRule = HiltAndroidRule(this)
#Inject
lateinit var scanner: SensorScanner
#Before
fun init() {
hiltRule.inject()
}
.....
and another one similar:
#RunWith(AndroidJUnit4::class)
#WithJunitRule(useAsTestClassInDescription = true)
#HiltAndroidTest
class Whens {
#JvmField
val hiltRule = HiltAndroidRule(this)
#Inject
lateinit var scanner: SensorScanner
#Before
fun init() {
hiltRule.inject()
}
......
I tried mutltiple things it still fails in various ways, also could not find any report on SO, almost like if no one use it like this ?
Related
I am using Hilt for DI in my project. I am trying write unit test cases for LiveData object, but it's not coming under coverage.
ViewModel
#HiltViewModel
class HealthDiagnosticsViewModel #Inject constructor(
private var networkHelper: NetworkHelper
) : ViewModel() {
var result = MutableLiveData<Int>()
.....
}
My unit test class is as below:
HealthViewModelTest
#HiltAndroidTest
#RunWith(RobolectricTestRunner::class)
#Config(application = HiltTestApplication::class)
class HealthDiagnosticsViewModelTest{
#get:Rule
var hiltRule = HiltAndroidRule(this)
#Inject
lateinit var networkHelper: NetworkHelper
lateinit var healthDiagnosticsViewModel: HealthDiagnosticsViewModel
#Before
fun setUp() {
hiltRule.inject()
healthDiagnosticsViewModel = HealthDiagnosticsViewModel(networkHelper)
}
#Test
fun testGetResult() {
val result = healthDiagnosticsViewModel.result.value
Assert.assertEquals(null, result)
}
#Test
fun testSetResult() {
healthDiagnosticsViewModel.result.value = 1
Assert.assertEquals(1, healthDiagnosticsViewModel.result.value)
}
}
Test Cases are passed but it's not coming under method coverage.
I'll share with you the an example of my code that would solve your problem.
I'm usnig ViewModel with Dagger Hilt
You don't have to use Robelectric, you can use MockK library.
Replace your HiltRule with this Rule:
#get:Rule
var rule: TestRule = InstantTaskExecutorRule()
This is my ViewModel class
using MockK, you can mock the networkHelper class without Hilt.
So, your setup method will be like that:
lateinit var networkHelper: NetworkHelper
......
......
......
#Before
fun setUp() {
networkHelper = mockk<NetworkHelper>()
healthDiagnosticsViewModel = HealthDiagnosticsViewModel(networkHelper)
}
4)The most important part in your test is to Observe to the LiveData first.
#Test
fun testGetResult() {
healthDiagnosticsViewModel.result.observeForever {}
val result = healthDiagnosticsViewModel.result.value
Assert.assertEquals(null, result)
}
You can observe to the livedata for each unit test, but keep in mind to Observe first before change data.
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
Consider the below example
#Singleton
class LoginModel #Inject contractor(private val userModel:UserModel){
}
#Config(application = HiltTestApplication::class)
#HiltAndroidTest
class LoginModelTest {
#get:Rule
var hiltRule = HiltAndroidRule(this)
#Inject
lateinit var loginModel: LoginModel
#Before
open fun setup() {
hiltRule.inject()
}
}
How to mock UserModel inside the LoginModel without manually constructing LoginModel?
The solution is simple. You have to deliver two different versions of UserModel - production, and mock in tests.
You can do it by following these steps.
Provide UserModel thru hilt module, annotating by Inject is not enough
Accordingly to your needs :
Replace that module just in single test sheet https://developer.android.com/training/dependency-injection/hilt-testing#replace-binding-manually
Or
Replace in all tests
https://developer.android.com/training/dependency-injection/hilt-testing#replace-binding
In both cases you will end up with module which return mock in binding.
Cheers
Thanks, Jakub for the link. Here is my favorite way of doing it
#UninstallModules(AnalyticsModule::class)
#HiltAndroidTest
class SettingsActivityTest {
#BindValue #JvmField
val analyticsService: AnalyticsService = FakeAnalyticsService()
...
}
I am trying to migrate my tests for a ViewModel from JUnit 4 to JUnit 5, and use MockK in conjunction. In JUnit4, I have made use of rules--namely rules for RxJava2, LiveData, and Coroutines within one test, and it has worked well. Here's how I use them:
class CollectionListViewModelTest {
#get:Rule
val mockitoRule: MockitoRule = MockitoJUnit.rule()
#get:Rule
val taskExecutorRule = InstantTaskExecutorRule()
#get:Rule
val rxSchedulerRule = RxSchedulerRule()
#ExperimentalCoroutinesApi
#get:Rule
val coroutineRule = MainCoroutineRule()
#Mock
lateinit var getAllCollectionsUseCase: GetAllCollectionsUseCase
private lateinit var SUT: CollectionListViewModel
#Before
fun setUp() {
SUT = CollectionListViewModel(getAllCollectionsUseCase)
}
...
}
In trying to migrate to JUnit5, I learned that Rules are now Extensions, and after searching I have pieced together replacements for the previous rules I used, and having replaced Mockito with MockK, I have tried to replace Rules with Extensions in this manner:
#ExperimentalCoroutinesApi
#Extensions(
ExtendWith(InstantExecutorExtension::class),
ExtendWith(MainCoroutineExtension::class),
ExtendWith(RxSchedulerExtension::class)
)
class CollectionListViewModelTest {
#MockK
lateinit var getAllCollectionsUseCase: GetAllCollectionsUseCase
private lateinit var SUT: CollectionListViewModel
#Before
fun setUp() {
MockKAnnotations.init(this)
SUT = CollectionListViewModel(getAllCollectionsUseCase)
}
...
However, I am getting an error saying that the getMainLooper isn't mocked, which is the same error encountered if not using the InstantTaskExecutorRule in JUnit4:
java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked. See http://g.co/androidstudio/not-mocked for details.
What is the proper way of using multiple Extensions in Junit5?
Instead of using #Extension, I should have used #ExtendWith in this manner:
#ExtendWith(value = [InstantExecutorExtension::class, MainCoroutineExtension::class, RxSchedulerExtension::class])
Or in an even shorter manner:
#ExtendWith(InstantExecutorExtension::class, MainCoroutineExtension::class, RxSchedulerExtension::class)
I'm learning testing on Android with Mockito and Robolectric. I created very simple app in Kotlin with RxJava and Dagger2, using Clean Architecture. Everything works well on device, but I can't make my test pass. Here is my LoginPresenterTest:
#RunWith(RobolectricGradleTestRunner::class)
#Config(constants = BuildConfig::class)
public class LoginPresenterTest {
private lateinit var loginPresenter: LoginPresenter
#Rule #JvmField
public val mockitoRule: MockitoRule = MockitoJUnit.rule()
#Mock
private lateinit var mockContext: Context
#Mock
private lateinit var mockLoginUseCase: LoginUseCase
#Mock
private lateinit var mockLoginView: LoginView
#Mock
private lateinit var mockCredentialsUseCase: GetCredentials
#Before
public fun setUp() {
loginPresenter = LoginPresenter(mockCredentialsUseCase, mockLoginUseCase)
loginPresenter.view = mockLoginView
}
#Test
public fun testLoginPresenterResume(){
given(mockLoginView.context()).willReturn(mockContext)
loginPresenter.resume();
}
}
LoginPresenter contructor:
class LoginPresenter #Inject constructor(#Named("getCredentials") val getCredentials: UseCase,
#Named("loginUseCase") val loginUseCase: LoginUseCase) : Presenter<LoginView>
in loginPresenter.resume() i have:
override fun resume() {
getCredentials.execute(GetCredentialsSubscriber() as DefaultSubscriber<in Any>)
}
And, finally, GetCredentials:
open class GetCredentials #Inject constructor(var userRepository: UserRepository,
threadExecutor: Executor,
postExecutionThread: PostExecutionThread):
UseCase(threadExecutor, postExecutionThread) {
override fun buildUseCaseObservable(): Observable<Credentials> = userRepository.credentials()
}
The problem is, that every field in GetCredentials is null. I think I miss something (I took pattern from this project: https://github.com/android10/Android-CleanArchitecture), but I can't find what is it. Does anyone know what may cause this?
You're using a mock instance of GetCredentials (#Mock var mockCredentialsUseCase: GetCredentials) that's why you have nulls in its fields. It's rarely a good idea to mock everything apart from the main class under test (LoginPresenter). One way to think of this is to divide the dependencies into peers and internals. I would rewrite the test to something like:
inline fun <reified T:Any> mock() : T = Mockito.mock(T::class.java)
#RunWith(RobolectricGradleTestRunner::class)
#Config(constants = BuildConfig::class)
public class LoginPresenterTest {
val mockContext:Context = mock()
val mockLoginView:LoginView = mock().apply {
given(this.context()).willReturn(mockContext)
}
val userRepository:UserRepository = mock() // or some in memory implementation
val credentials = GetCredentials(userRepository, testThreadExecutor, testPostThreadExecutor) // yes, let's use real GetCredentials implementation
val loginUseCase = LoginUseCase() // and a real LoginUseCase if possible
val loginPresenter = LoginPresenter(credentials, loginUseCase).apply {
view = mockLoginView
}
#Test
public fun testLoginPresenterResume(){
given(mockLoginView.context()).willReturn(mockContext)
loginPresenter.resume();
// do actual assertions as what should happen
}
}
As usual you need to think about what you're testing. The scope of the test does not have to be limited to a single class. It's often easier to think of features you're testing instead of classes (like in BDD). Above all try to avoid tests like this - which in my opinion adds very little value as a regression test but still impedes refactoring.
PS. Roboelectric add helper functions for context