I am developing an Android application using Kotlin programming language. I am adding instrumentation tests into my application. Now I am trying to test if an activity is started after some delay.
This is my activity code.
class MainActivity : AppCompatActivity() {
companion object {
val LAUNCH_DELAY: Long = 2000
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Handler().postDelayed({
this.startLoginActivity()
}, LAUNCH_DELAY)
}
protected fun startLoginActivity()
{
startActivity(Intent(this, LoginActivity::class.java))
}
}
I know how to write a simple test like this
#Test
fun itRendersCompanyName() {
onView(withId(R.id.main_tv_company_name)).check(matches(withText("App Name")))
}
But what I am trying to test here is if the LoginActivity is launched after some delay. How can I do it using Espresso framework?
You can get the visible Activity using ActivityManager:
inline fun <reified T : Activity> isVisible(): Boolean {
val am = ApplicationProvider.getApplicationContext<Context>().getSystemService(ACTIVITY_SERVICE)
as ActivityManager
val visibleActivityName = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
am.appTasks[0].taskInfo.topActivity.className
} else {
am.getRunningTasks(1)[0].topActivity.className
}
return visibleActivityName == T::class.java.name
}
Calling isVisible<LoginActivity>() will tell you that LoginActivity is visible or not.
Also, to wait until your LoginActivity visible, you can wait for this method to gets true. For example:
inline fun <reified T : Activity> waitUntilActivityVisible() {
val startTime = System.currentTimeMillis()
while (!isVisible<T>()) {
Thread.sleep(200)
if (System.currentTimeMillis() - startTime >= TIMEOUT) {
throw AssertionError("Condition unsatisfied after $TIMEOUT milliseconds")
}
}
}
You can use Intents.intended() for that.
Add following to your build.gradle file:
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.0'
In your test function, you can try following code:
Intents.init()
Intents.intended(hasComponent(LoginActivity::class.java!!.getName()))
You can read more about Espresso-Intents here.
It’s better to test this state with unit tests. Use architecture pattern (for example MVP/MVVM), mock presenter/view model and check what method which is responsible for activity start is triggered
I got it working using the following:
val expectedUrl = "https://yoururlhere"
Intents.init();
Matcher<Intent> expectedIntent = allOf(hasAction(Intent.ACTION_VIEW), hasData(expectedUrl));
intending(expectedIntent).respondWith(new Instrumentation.ActivityResult(0, null));
onView(withId(R.id.someViewId)).perform(click());
intended(expectedIntent);
Intents.release();
And remember to add "androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.0'" to your gradle dependency
Related
I have a composable with a button that launches one of the native activities (Google Settings).
To test this before compose (using Robolectric) I would do something like this:
My test:
#Test
fun `MyFragment - when button clicked - starts activity`() {
// ...
val shadowActivity: ShadowActivity = Shadow.extract(activity)
val nextStartedActivity = shadowActivity.nextStartedActivity
assertNotNull(nextStartedActivity)
assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, nextStartedActivity.action)
}
With compose tests (not using activity scenario) it's different. There is no activity handle, only a composeTestRule:
My test:
// ...
private val buttonNode get() = composeTestRule.onNodeWithContentDescription("Button")
#Test
fun `MyComposableToTest - when button clicked - starts activity`() {
composeTestRule.setContent {
MyComposableToTest()
}
buttonNode.assertExists().assertHasClickAction().assertIsEnabled().performClick()
// No possibility to get current activity
}
How can I assert that a new activity is started when testing a Composable?
Some context:
Android Gradle Plugin 7.0.3
Robolectric 4.7.3
Compose 1.1.0-beta04
You are able to fetch the context from the ComposeContentTestRule like this:
lateinit var context : Context
composeTestRule.setContent {
context = LocalContext.current
MyComposableToTest()
}
and then to assert the next started activity
val shadowActivity: ShadowActivity = Shadow.extract(context as ComponentActivity)
val nextStartedActivity = shadowActivity.nextStartedActivity
assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, nextStartedActivity.action)
This is how I did it for my instrumented test (NOT using Robolectric).
build.gradle[.kts]:
androidTestImplementation("androidx.test.espresso:espresso-intents:3.4.0")
The test class (in src/androidTest/... directory):
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
// ...
#RunWith(AndroidJUnit4::class)
class MainActivityInstrumentedTest {
#get:Rule val composeTestRule = createAndroidComposeRule<MainActivity>()
#Test fun testTheIntent() {
Intents.init() // IMPORTANT (to be able to use "intended" function)
composeTestRule.setContent {
MyAppTheme {
MainScreen()
}
}
composeTestRule.onNodeWithText("My Button").performClick()
intended(hasComponent(MySecondActivity::class.java.name))
Intents.release()
}
}
My test is never running to completion and I have absolutely no idea why. I can see the toast displayed on my phone's screen. There is absolutely nothing in the logs.
#RunWith(AndroidJUnit4::class)
#SmallTest
class BaseDataFragmentUITest
{
#Test
fun isDisplayingToastWhenFAILED_TO_UPDATE()
{
val fragmentScenario = launchFragmentInContainer<TestBaseDataFragmentImp>()
val toastString: String = context.resources.getString(com.developerkurt.gamedatabase.R.string.data_update_fail)
fragmentScenario.onFragment {
it.handleDataStateChange(BaseRepository.DataState.FAILED_TO_UPDATE)
onView(withText(toastString)).inRoot(withDecorView(not(it.requireActivity().getWindow().getDecorView()))).check(matches(isDisplayed()))
}
}
}
Apparently, Espresso assertions shouldn't be made inside of the onFragment block. So when I wrote the test like this it worked:
#Test
fun isDisplayingToastWhenFAILED_TO_UPDATE()
{
val fragmentScenario = launchFragmentInContainer<TestBaseDataFragmentImp>()
val toastString: String = context.resources.getString(com.developerkurt.gamedatabase.R.string.data_update_fail)
var decorView: View? = null
fragmentScenario.onFragment {
it.handleDataStateChange(BaseRepository.DataState.FAILED_TO_UPDATE)
decorView = it.requireActivity().getWindow().getDecorView()
}
onView(withText(toastString)).inRoot(withDecorView(not(decorView!!))).check(matches(isDisplayed()))
}
Please find below a function using a coroutine to replace callback :
override suspend fun signUp(authentication: Authentication): AuthenticationError {
return suspendCancellableCoroutine {
auth.createUserWithEmailAndPassword(authentication.email, authentication.password)
.addOnCompleteListener(activityLifeCycleService.getActivity()) { task ->
if (task.isSuccessful) {
it.resume(AuthenticationError.SignUpSuccess)
} else {
Log.w(this.javaClass.name, "createUserWithEmail:failure", task.exception)
it.resume(AuthenticationError.SignUpFail)
}
}
}
}
Now I would like to unit testing this function. I am using Mockk :
#Test
fun `signup() must be delegated to createUserWithEmailAndPassword()`() = runBlockingTest {
val listener = slot<OnCompleteListener<AuthResult>>()
val authentication = mockk<Authentication> {
every { email } returns "email"
every { password } returns "pswd"
}
val task = mockk<Task<AuthResult>> {
every { isSuccessful } returns true
}
every { auth.createUserWithEmailAndPassword("email", "pswd") } returns
mockk {
every { addOnCompleteListener(activity, capture(listener)) } returns mockk()
}
service.signUp(authentication)
listener.captured.onComplete(task)
}
Unfortunately this test failed due to the following exception : java.lang.IllegalStateException: This job has not completed yet
I tried to replace runBlockingTest with runBlocking but the test seems to wait in an infinite loop.
Can someone help me with this UT please?
Thanks in advance
As can be seen in this post:
This exception usually means that some coroutines from your tests were scheduled outside the test scope (more specifically the test dispatcher).
Instead of performing this:
private val networkContext: CoroutineContext = TestCoroutineDispatcher()
private val sut = Foo(
networkContext,
someInteractor
)
fun `some test`() = runBlockingTest() {
// given
...
// when
sut.foo()
// then
...
}
Create a test scope passing test dispatcher:
private val testDispatcher = TestCoroutineDispatcher()
private val testScope = TestCoroutineScope(testDispatcher)
private val networkContext: CoroutineContext = testDispatcher
private val sut = Foo(
networkContext,
someInteractor
)
Then in test perform testScope.runBlockingTest
fun `some test`() = testScope.runBlockingTest {
...
}
See also Craig Russell's "Unit Testing Coroutine Suspend Functions using TestCoroutineDispatcher"
In case of Flow testing:
Don't use flow.collect directly inside runBlockingTest. It should be wrapped in launch
Don't forget to cancel TestCoroutineScope in the end of a test. It will stop a Flow collecting.
Example:
class CoroutinesPlayground {
private val job = Job()
private val testDispatcher = StandardTestDispatcher()
private val testScope = TestScope(job + testDispatcher)
#Test
fun `play with coroutines here`() = testScope.runBlockingTest {
val flow = MutableSharedFlow<Int>()
launch {
flow.collect { value ->
println("Value: $value")
}
}
launch {
repeat(10) { value ->
flow.emit(value)
delay(1000)
}
job.cancel()
}
}
}
This is not an official solution, so use it at your own risk.
This is similar to what #azizbekian posted, but instead of calling runBlocking, you call launch.
As this is using TestCoroutineDispatcher, any tasks scheduled to be run without delay are immediately executed. This might not be suitable if you have several tasks running asynchronously.
It might not be suitable for every case but I hope that it helps for simple cases.
You can also follow up on this issue here:
https://github.com/Kotlin/kotlinx.coroutines/issues/1204
If you know how to solve this using the already existing runBlockingTest and runBlocking, please be so kind and share with the community.
class MyTest {
private val dispatcher = TestCoroutineDispatcher()
private val testScope = TestCoroutineScope(dispatcher)
#Test
fun myTest {
val apiService = mockk<ApiService>()
val repository = MyRepository(apiService)
testScope.launch {
repository.someSuspendedFunction()
}
verify { apiService.expectedFunctionToBeCalled() }
}
}
According to my understanding, this exception occurs when you are using a different dispatcher in your code inside the runBlockingTest { } block with the one that started runBlockingTest { }.
So in order to avoid this, you first have to make sure you inject Dispatchers in your code, instead of hardcoding it throughout your app. If you haven't done it, there's nowhere to begin because you cannot assign a test dispatcher to your test codes.
Then, in your BaseUnitTest, you should have something like this:
#get:Rule
val coroutineRule = CoroutineTestRule()
#ExperimentalCoroutinesApi
class CoroutineTestRule(
val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
) : TestWatcher() {
override fun finished(description: Description?) {
super.finished(description)
Dispatchers.setMain(testDispatcher)
}
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.resetMain()
testDispatcher.cleanupTestCoroutines()
}
}
Next step really depends on how you do Depedency Injection. The main point is to make sure your test codes are using coroutineRule.testDispatcher after the injection.
Finally, call runBlockingTest { } from this testDispatcher:
#Test
fun `This should pass`() = coroutineRule.testDispatcher.runBlockingTest {
//Your test code where dispatcher is injected
}
There is an open issue for this problem: https://github.com/Kotlin/kotlinx.coroutines/issues/1204
The solution is to use the CoroutineScope intead of the TestCoroutinScope until the issue is resolved, you can do by replacing
#Test
fun `signup() must be delegated to createUserWithEmailAndPassword()`() =
runBlockingTest {
with
#Test
fun `signup() must be delegated to createUserWithEmailAndPassword()`() =
runBlocking {
None of these answers quite worked for my setup due to frequent changes in the coroutines API.
This specifically works using version 1.6.0 of kotlin-coroutines-test, added as a testImplementation dependency.
#Test
fun `test my function causes flow emission`() = runTest {
// calling this function will result in my flow emitting a value
viewModel.myPublicFunction("1234")
val job = launch {
// Force my flow to update via collect invocation
viewModel.myMemberFlow.collect()
}
// immediately cancel job
job.cancel()
assertEquals("1234", viewModel.myMemberFlow.value)
}
If you have any
Channel
inside the launch, you must call to
Channel.close()
Example code:
val channel = Channel<Success<Any>>()
val flow = channel.consumeAsFlow()
launch {
channel.send(Success(Any()))
channel.close()
}
runBlockingTest deprecated since 1.6.0 and replaced with runTest.
You need to swap arch background executor with one that execute tasks synchronously. eg. For room suspend functions, live data etc.
You need the following dependency for core testing
androidTestImplementation 'androidx.arch.core:core-testing:2.1.0'
Then add the following at the top of test class
#get:Rule
val instantExecutor = InstantTaskExecutorRule()
Explanations
InstantTaskExecutorRule A JUnit Test Rule that swaps the background executor used by the
Architecture Components with a different one which executes each task
synchronously.
You can use this rule for your host side tests that use Architecture
Components
As I mentioned here about fixing runBlockingTest, maybe it could help you too.
Add this dependency if you don't have it
testImplementation "androidx.arch.core:core-testing:$versions.testCoreTesting" (2.1.0)
Then in your test class declare InstantTaskExecutorRule rule:
#get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
Today I started to use Espresso to test an Activity, I am practical with Junit tests but do not understand how to mock, I have the line cashPeriod that obviously gives me a NPE when I try to launch the activity with Espresso
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//no layout preview possible
setContentView(R.layout.activity_combi_book_balance)
title = getString(R.string.balance_details)
setupActionBarWithHomeEnabled(true)
val cashPeriod: CashPeriod = intent.extras.getParcelable(PERIOD_OBJECT_EXTRA)
val sections = showList(cashPeriod)
setAdapter(sections)
}
and my Espresso test is
#Test
fun appLaunchesSuccessfully() {
ActivityScenario.launch(CombiBookBalanceDetailActivity::class.java)
}
Now if I stub the cashPeriod in the onCreate giving some hardcoded values, the test passes, but I need to do that in the test class of course
How can I mock the line val cashPeriod: CashPeriod = intent.extras.getParcelable(PERIOD_OBJECT_EXTRA) in my espresso test? Is different by Junit, where I use Mockito/mockito-kotlin/mockk and give a behaviour with when /// return //that
You have to use ActivityTestRule
See this
A bit late, but maybe this is what you were looking for. You can add this inside your espresso test.
#Test fun testName() {
val intent = Intent(ApplicationProvider.getApplicationContext(), CombiBookBalanceDetailActivity::class.java)
intent.putExtra(PERIOD_OBJECT_EXTRA, "some test value")
val scenario = launchActivity<CombiBookBalanceDetailActivity>(intent)
}
I'm new to writing tests and using Mockito.
I've read the similar topics here on Stackoverflow and made the suggested changes, making sure that regarded classes / interfaces / methods are open.
I tried to follow this
Mocking the constructor injected dependencies
This is the test I came up with so far
class RegistrationPresenterTest {
#Test
fun testRegisterSuccess() {
val mockService = mock<IHerokuInteractor>()
val mockLocal = mock<ILocalStorageInteractor>()
val mockView = mock<RegisterView>()
val mockRegistrationResponse = HerokuRegisterResponse("hash")
val mockPair = ImeiPair("imei","hash")
val presenter = RegisterPresenterImpl(mockLocal,mockService)
whenever(mockService.register(any())).thenReturn(Observable.just(mockRegistrationResponse))
whenever(mockLocal.clearPreferences()).thenReturn(Observable.just(true))
whenever(mockLocal.putImeiPair(any())).thenReturn(Observable.just(true))
//whenever(presenter.writeImeiPairLocally(any())) How do I specify parameters since it uses a parameter from the register method?
presenter.bindView(mockView)
presenter.register("imei","male")
verify(mockService, times(1)).register(any())
verify(mockLocal,times(1)).clearPreferences()
verify(mockLocal,times(1)).putImeiPair(any())
verify(mockView,times(1)).moveToMain()
}
but the response I keep getting is
Wanted but not invoked:
registerPresenterImpl.writeImeiPairLocally(
<any com.company.appname.model.ImeiPair>
);
Actually, there were zero interactions with this mock.
I got this response even when I don't mention that method in the test.
This is my presenter register method. I've changed the classes / interfaces & methods involved to open (kotlin). I believe override methods are open by nature in kotlin.
open class RegisterPresenterImpl #Inject constructor(val localStorage : ILocalStorageInteractor, var herokuService : IHerokuInteractor)
override fun register(imei : String, gender : String){
subscription = herokuService.register(RegisterObject(imei,gender)).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()).subscribe(
{
registrationResult ->
Log.d(TAG,"${registrationResult}")
if(registrationResult.imei_hash != null){
writeImeiPairLocally(ImeiPair(imei,registrationResult.imei_hash))
}
else{
Log.e(TAG,"User already exists")
}
},
{
errorResponse -> Log.e(TAG,"Could not register user ${errorResponse.message}")
}
)
addSubscription(subscription)
}
and similarly the
open fun writeImeiPairLocally(pair : ImeiPair){
subscription = localStorage.clearPreferences().flatMap {
cleared -> localStorage.putImeiPair(pair)}.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()).subscribe(
{
booleanResult -> view?.moveToMain()
},
{
errorResponse -> Log.e(TAG,"Could not write ImeiPair to SharedPreferences ${errorResponse.message}")
}
)
addSubscription(subscription)
}
Here is interfaces
open interface ILocalStorageInteractor : ILocalStorage{
fun getImeiPair() : Observable<ImeiPair>
fun putImeiPair(pair: ImeiPair) : Observable<Boolean>
}
open interface ILocalStorage {
fun clearPreferences() : Observable<Boolean>
}
All help is appreciated.
If you are using plain jUnit, then your AndroidSchedulers.mainThread() is null. That's why onNext is not called.
You need to override Schedulers in a setUp() method with:
RxAndroidPlugins.getInstance().registerSchedulersHook(new RxAndroidSchedulersHook() {
#Override
public Scheduler getMainThreadScheduler() {
return Schedulers.immediate(); // or .test()
}
});
To avoid concurrency in tests, I would recommend to override Schedulers.io() like this:
RxJavaHooks.setOnIOScheduler(scheduler1 -> Schedulers.immediate());
If you are going to use TestScheduler, don't forget to call TestScheduler.triggerActions() method.
Also don't forget to unregister Schedulers in tearDown() like this:
RxJavaHooks.reset();
RxAndroidPlugins.getInstance().reset();
AndroidSchedulers.reset();
Schedulers.reset();