I have an activity which is as simple as this:
class HomeActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
if (!this.userPreference.memberRegistered) {
goToActivity(AuthActivity::class.java)
}
}
}
So if the user is registered, it will stay at HomeActivity, otherwise it will route to AuthActivity.
userPreference is a wrapper around SharedPreference. Works well and well tested in another codebase.
I first tried whether RuntimeEnvironment from Robolectric could do the trick or not, and it works of course.
#Test
fun try_test() {
val userPreference = UserPreference(
Settings(SETTINGS_NAME, RuntimeEnvironment.application.applicationContext)
)
// default value should be false, PASS
assertEquals(false, userPreference.memberRegistered)
// change it to true, then it should be true, PASS
userPreference.memberRegistered = true
assertEquals(true, userPreference.memberRegistered)
}
Then I tried whether the route to AuthActivity works or not, and it works. The test pass.
#Test
fun should_go_to_AuthActivity_when_1st_start() {
val homeActivity = Robolectric.setupActivity(HomeActivity::class.java)
val expectedIntent = Intent(homeActivity, AuthActivity::class.java)
val actual = ShadowApplication.getInstance().nextStartedActivity
assertEquals(expectedIntent.component, actual.component)
}
Problem
Then the problem is I don't know how to check if the activity stays at HomeActivty when this.userPreference.memberRegistered = true. The following test failed due to the reason that actual is null. It is because there is no routing happen, so nextStartedActivity is null, but how to verify that it stays at this activity from the test?
#Test
fun should_stay_at_HomeActivity_when_already_member() {
val userPreference = UserPreference(
Settings(SETTINGS_NAME, RuntimeEnvironment.application.applicationContext)
)
userPreference.memberRegistered = true
val actual = ShadowApplication.getInstance().nextStartedActivity
assertEquals(
actual.component.shortClassName,
".ui.home.HomeActivity"
)
}
Related
ok I am working on concept idea my dad has pitched to me. I have an app that runs AdMobs. On the interstitial ads based off button. The idea of the app is you press the start button and you watch an ad. However, when the ad is closed out, the value should increase in the Ads Watched Field.
I have created a function that increases the TextView no problem. My issue is with AdMob functions, when I call the function in AdDismissed, it does not change the value. I can plug the function into the Start Button and it increases value, but when the Ad is dismissed it zeros out the textView.
I am showing the demo portion of the app, this is still experimental, but also learning with Admobs and the coding on functions. Any advice would be appreciated. Also the adCounter is in the stop button, that was just to make sure the increments where firing. Which it does work perfectly. My thing is when the ad ends keeping the value.
SO in example the Ads Watched: 167,897,256 should increment by one when the ad is dismissed. However placing adCount() in the dismissed section of the ad does not work it just zeros out that textView.
MainActivity
import android.content.Intent
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.google.android.gms.ads.*
import com.google.android.gms.ads.interstitial.InterstitialAd
import com.google.android.gms.ads.interstitial.InterstitialAdLoadCallback
class MainActivity : AppCompatActivity() {
lateinit var mAdView : AdView
private var mInterstitialAd: InterstitialAd? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
loadBanner()
loadInterAd()
val interAdBtnStart : Button = findViewById(R.id.btnStartAds)
val interAdBtnStop : Button = findViewById(R.id.btnStopAds)
interAdBtnStart.setOnClickListener {
showInterAd()
}
interAdBtnStop.setOnClickListener {
adCountInc()
}
}
fun adCountInc(){
val tvAdsAmount : TextView = findViewById(R.id.tvAdsAmount)
var i : Int = tvAdsAmount.text.toString().toInt()
tvAdsAmount.text = "${++i}"
}
private fun showInterAd() {
if (mInterstitialAd != null)
{
mInterstitialAd?.fullScreenContentCallback = object : FullScreenContentCallback(){
override fun onAdClicked() {
super.onAdClicked()
}
override fun onAdDismissedFullScreenContent() {
super.onAdDismissedFullScreenContent()
val intent = Intent(this#MainActivity, MainActivity::class.java)
startActivity(intent)
}
override fun onAdFailedToShowFullScreenContent(p0: AdError) {
super.onAdFailedToShowFullScreenContent(p0)
}
override fun onAdImpression() {
super.onAdImpression()
}
override fun onAdShowedFullScreenContent() {
super.onAdShowedFullScreenContent()
}
}
mInterstitialAd?.show(this)
}
else
{
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
}
}
private fun loadInterAd() {
var adRequest = AdRequest.Builder().build()
InterstitialAd.load(this,"ca-app-pub-3940256099942544/1033173712", adRequest, object : InterstitialAdLoadCallback() {
override fun onAdFailedToLoad(adError: LoadAdError) {
mInterstitialAd = null
}
override fun onAdLoaded(interstitialAd: InterstitialAd) {
mInterstitialAd = interstitialAd
}
})
}
private fun loadBanner() {
MobileAds.initialize(this) {}
mAdView = findViewById(R.id.adView)
val adRequest = AdRequest.Builder().build()
mAdView.loadAd(adRequest)
mAdView.adListener = object: AdListener() {
override fun onAdLoaded() {
// Code to be executed when an ad finishes loading.
}
override fun onAdFailedToLoad(adError : LoadAdError) {
// Code to be executed when an ad request fails.
}
override fun onAdOpened() {
// Code to be executed when an ad opens an overlay that
// covers the screen.
}
override fun onAdClicked() {
// Code to be executed when the user clicks on an ad.
}
override fun onAdClosed() {
// Code to be executed when the user is about to return
// to the app after tapping on an ad.
}
}
}
}
this is the full code to the app so far. Any advice will help. If i place the adCounter() anywhere in the ads section it will not update the textfield at all. Even after the textfield shows 1 then an ad is displayed it will always zero out the text field.
The value is not updated because you are opening the same Activity (MainActivity) on onAdDismissedFullScreenContent again.
First create a global TextView variable like:
private lateinit var tvAdsAmount : TextView`\
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
tvAdsAmount = findViewById(R.id.tvAdsAmount)
// Other things...
}
Then simply use:
override fun onAdDismissedFullScreenContent() {
val value = tvAdsAmount.text.toString().toInt()
// make sure that value is an Integer.
val updateValue = value++
tvAdsAmount.text = "$updatedValue"
}
Some points you should learn:
Every time you start a new Activity, you're getting a new TextView that has no memory of what was in a TextView of some previous Activity.
You should never use a UI component like TextView to store application state. It's just not reliable. UI components are intended to be a bridge between your application state and the user. They aren't supposed to be the application state themselves. This is the programming principle of separation of concerns.
Since you're not finishing the previous Activity, you're building up a large stack of duplicate Activities. The user will be surprised when they push the back button to see an outdated copy of the Activity, one after another.
Whenever there's a configuration change (such as a screen rotation, or the user changing some setting in the Android settings like the device language), Android destroys all of the Activities that you have open and creates new instances of them. So any application state you were holding in them is going to be lost. This is why there is a ViewModel class for holding state that will survive configuration changes.
To fix your app:
Change your logic so you aren't starting new Activities. Keep everything in the same Activity instance. If you want to load a new ad, just call the function that loads ads rather than creating a brand new Activity.
Create a ViewModel to hold your application state. In this case, it will just need a LiveData<Int> to hold your count. You can observe this LiveData in your Activity and update the value of the TextView in the observer function. Your ViewModel can have an increment function that increases the LiveData's integer value, and you'll call this after ads are dismissed.
Long term, you can consider backing up this value with SharedPreferences, so the value will persist between sessions of your app.
Points 2 and 3 have many tutorials online and questions on this site about them, so I'm not going to explain them in detail.
I'm using the new SplashScreen API for android 12 but I'm a bit confused on the login flow now. I have one activity and multiple fragments as advised by google , the mainActivity is where the splashScreen launches and the user's supposed to be directed to either login fragment or the homefragment.
my question is how do I implement this work flow with the new SplashAPI? What fragment should be the startDestination? I don't want to use the popTo attribute since it doesn't look pretty to always show the loginfragment and then direct the user to Homefragment.
If someone could explain this to me I'd be grateful.
Homefragment should be the startDestination. Conditionally navigate to loginfragment and pop back to Homefragment after authentication.
Refer to the following video by Ian Lake.
https://www.youtube.com/watch?v=09qjn706ITA
I have a workaround for that you can set the content of the activity after you check if the user is authorized or not and save the user state in a ViewModel or something, then set your content upon this state.
I leverage the power of setKeepOnScreenCondition function from core-splashscreen library.
SplashInstaller.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
SplashInstaller(
activity = this,
beforeHide = {
// Check if logged in or not asyncrounsly
delay(2000)
},
afterHide = {
setContent()
// start with you desired destination up on the check you did in beforeHide
})
}
/**
* #author Mohamed Elshaarawy on Oct 14, 2021.
*/
class SplashInstaller<A : ComponentActivity>(
private val activity: A,
visibilityPredicate: () -> Boolean = { BuildConfig.BUILD_TYPE != "debug" },
private val beforeHide: suspend A.() -> Unit = { delay(2000) },
private val afterHide: A.() -> Unit
) : CoroutineScope by activity.lifecycleScope {
private val isSplashVisibleChannel by lazy { Channel<Boolean>() }
private val isAfterCalled by lazy { Channel<Boolean>(capacity = 1) }
private val splashSuspensionJob = launch(start = CoroutineStart.LAZY) {
activity.beforeHide()
isSplashVisibleChannel.send(false)
}
init {
if (visibilityPredicate()) {
splashSuspensionJob.start()
installSplash()
} else afterSplash()
}
private fun installSplash() {
activity.installSplashScreen().setKeepOnScreenCondition {
val isVisible = isSplashVisibleChannel.tryReceive().getOrNull() ?: true
if (!isVisible) {
afterSplash()
}
isVisible
}
}
private fun afterSplash() {
if (isAfterCalled.tryReceive().getOrNull() != true) {
isAfterCalled.trySend(true)
activity.afterHide()
}
}
}
This solution uses
androidx.core:core-splashscreen:1.0.0-beta01
androidx.lifecycle:lifecycle-runtime-ktx:2.4.0
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2
My ViewModel has a method which returns a flow of PagingData. In my app, the data is fetched from the remote server, which is then saved to Room (the single source of truth):
fun getChocolates(): Flow<PagingData<Chocolate>> {
val pagingSourceFactory = { dao().getChocolateListData() }
return Pager(
config = PagingConfig(
pageSize = NETWORK_PAGE_SIZE,
maxSize = MAX_MEMORY_SIZE,
enablePlaceholders = false
),
remoteMediator = ChocolateRemoteMediator(
api,
dao
),
pagingSourceFactory = pagingSourceFactory
).flow
}
How do I test this method? I want to test if the returning flow contains the correct data.
What I've tried so far:
#InternalCoroutinesApi
#Test
fun getChocolateListReturnsCorrectData() = runBlockingTest {
val chocolateListDao: ChocolateListDao by inject()
val chocolatesRepository: ChocolatesRepository by inject()
val chocolateListAdapter: ChocolateListAdapter by inject()
// 1
val chocolate1 = Chocolate(
name = "Dove"
)
val chocolate2 = Chocolate(
name = "Hershey's"
)
// 2
// You need to launch here because submitData suspends forever while PagingData is alive
val job = launch {
chocolatesRepository.getChocolateListStream().collectLatest {
chocolateListAdapter.submitData(it)
}
}
// Do some stuff to trigger loads
chocolateListDao.saveChocolate(chocolate1, chocolate2)
// How to read from adapter state, there is also .peek() and .itemCount
assertEquals(listOf(chocolate1, chocolate2).toMutableList(), chocolateListAdapter.snapshot())
// We need to cancel the launched job as coroutines.test framework checks for leaky jobs
job.cancel()
}
I'm wondering if I'm on the right track. Any help would be greatly appreciated!
I found using Turbine from cashapp would be much much easier.(JakeWharton comes to rescue again :P)
testImplementation "app.cash.turbine:turbine:0.2.1"
According to your code I think your test case should looks like:
#ExperimentalTime
#ExperimentalCoroutinesApi
#Test
fun `test if receive paged chocolate data`() = runBlockingTest {
val expected = listOf(
Chocolate(name = "Dove"),
Chocolate(name = "Hershey's")
)
coEvery {
dao().getChocolateListData()
}.returns(
listOf(
Chocolate(name = "Dove"),
Chocolate(name = "Hershey's")
)
)
launchTest {
viewModel.getChocolates().test(
timeout = Duration.ZERO,
validate = {
val collectedData = expectItem().collectData()
assertEquals(expected, collectedData)
expectComplete()
})
}
}
I also prepare a base ViewModelTest class for taking care of much of setup and tearDown tasks:
abstract class BaseViewModelTest {
#get:Rule
open val instantTaskExecutorRule = InstantTaskExecutorRule()
#get:Rule
open val testCoroutineRule = CoroutineTestRule()
#MockK
protected lateinit var owner: LifecycleOwner
private lateinit var lifecycle: LifecycleRegistry
#Before
open fun setup() {
MockKAnnotations.init(this)
lifecycle = LifecycleRegistry(owner)
every { owner.lifecycle } returns lifecycle
}
#After
fun tearDown() {
clearAllMocks()
}
protected fun initCoroutine(vm: BaseViewModel) {
vm.apply {
setViewModelScope(testCoroutineRule.testCoroutineScope)
setCoroutineContext(testCoroutineRule.testCoroutineDispatcher)
}
}
#ExperimentalCoroutinesApi
protected fun runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) =
testCoroutineRule.runBlockingTest(block)
protected fun launchTest(block: suspend TestCoroutineScope.() -> Unit) =
testCoroutineRule.testCoroutineScope.launch(testCoroutineRule.testCoroutineDispatcher) { block }
}
As for extension function collectData() that's borrowed from answer from another post (Thanks #Farid!!)
And a slide show introducing turbine
There's basically two approaches to this depending on if you want pre-transformation or post-transformation data.
If you want to just assert the repository end, that your query is correct - you can just query PagingSource directly, this is pre-transform though so any mapping you do or filtering you do to PagingData in ViewModel won't be accounted for here. However, it's more "pure" if you want to test the query directly.
#Test
fun repo() = runBlockingTest {
val pagingSource = MyPagingSource()
val loadResult = pagingSource.load(...)
assertEquals(
expected = LoadResult.Page(...),
actual = loadResult,
)
}
The other way if you care about transforms, you need to load data from PagingData into a presenter API.
#Test
fun ui() = runBlockingTest {
val viewModel = ... // Some AndroidX Test rules can help you here, but also some people choose to do it manually.
val adapter = MyAdapter(..)
// You need to launch here because submitData suspends forever while PagingData is alive
val job = launch {
viewModel.flow.collectLatest {
adapter.submitData(it)
}
}
... // Do some stuff to trigger loads
advanceUntilIdle() // Let test dispatcher resolve everything
// How to read from adapter state, there is also .peek() and .itemCount
assertEquals(..., adapter.snapshot())
// We need to cancel the launched job as coroutines.test framework checks for leaky jobs
job.cancel()
}
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()))
}
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