Espresso / How can I move button state between activities in Espresso tests? - android

I have experienced next scenario:
each test in my test-project uses separate activity
test1 (main screen) belong to activity#1, there is a button on this screen which can change state from STATE#1 (OFF) to STATE#2 (ON)
tap on button STATE#1 (from test1) cause raise of another screen which belong to activity#2 (there is test2 starting), then on act.#2 user perform some actions which should change button state to STATE#2
but in my test button STATE#2 wouldn't be refreshed and passed to the previous the activity#1
Do I need to sync test data in specific way? If button's state (which is on activity#1) can be changed from another test activity (activity#2)
Here is example of what I'm doing:
First of all test after which button state should be changed
TEST2 - test that should change button1 state
#RunWith(AndroidJUnit4.class)
public class ChangeBtnState extends MyIdle
{
#Rule
public ActivityTestRule<Activity#2> EspressoTestRule#2 = new ActivityTestRule<>(Activity#2.class, false, true);
private MyIdle IdlingRecourseActivity#2;
#Before
public void SetUpTest2()
{
EspressoTestRule#2.getActivity().getSupportFragmentManager().beginTransaction();
IdlingRecourseActivity#2 = (ESP_idling) EspressoTestRule#2.getActivity().getIdlingResource();
Espresso.registerIdlingResources(IdlingRecourseActivity#2);
}
#Test
public void StartTestRule2()
{
// Just some actions on activity#2 after which button1 state should be from STATE#1 to STATE#2
ViewInteraction ButtonSendtoOFF = Espresso.onView(allOf(ViewMatchers.withId(android.R.id.SomebuttonONact#2))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()));
ButtonSendtoOFF.preform(click()); // here click
}
}
#After
public void unregistereSetUpTest2()
{
if (IdlingRecourseActivity#2 != null)
{
Espresso.unregisterIdlingResources(IdlingRecourseActivity#2);
}
}
}
Now checking does button state changed on activity#1
#RunWith(AndroidJUnit4.class)
public class CheckBtnState extends MyIdle
{
#Rule
public ActivityTestRule<Activity#1> EspressoTestRule#1 = new ActivityTestRule<>(Activity#1.class, false, true);
private MyIdle IdlingRecourseActivity#1;
#Before
public void SetUpTest1()
{
EspressoTestRule#1.getActivity().getSupportFragmentManager().beginTransaction();
IdlingRecourseActivity#1 = (ESP_idling) EspressoTestRule#1.getActivity().getIdlingResource();
Espresso.registerIdlingResources(IdlingRecourseActivity#1);
}
#Test
public void StartTestRule1()
{
// Check some actions FROM activity#2 to change **button1** from STATE#1 to STATE#2
ViewInteraction ButtonSTATE = Espresso.onView(allOf(ViewMatchers.withId(android.R.id.Button1), withText("OFF"))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()));
//ButtonSTATE.preform(click()); // here click
}
#After
public void unregistereSetUpTest1()
{
if (IdlingRecourseActivity#1 != null)
{
Espresso.unregisterIdlingResources(IdlingRecourseActivity#1);
}
}
}
BTW, I have look through a lot of topics and even found examples with intents, but it doesn't work for me, perhaps there is should be specific structure.
If you need extra info please add your question in comments.

The problem is that you are writing two tests such that the second test relies on the state of the first one. This is incorrect test design. Every test should be able to run completely independent of any other tests. You need to rethink what each of your tests are testing. What happens in the second activity that affects the state of the first one? Are you using startActivityForResult()? If so, your test for the second activity should verify that the expected result is set from the second activity. This test should not rely on anything in the first activity.

Related

How do I make my activity use testing data?

I have an application which displays data (posts) from a web API.
A background service syncs this data at some unknown time and saves it.
When visiting my main activity it loads this data and displays it in a RecyclerView
The loading is handled via a singleton class
I currently test the main activity as follows
#Rule
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class);
#Test
public void testDataLoad() {
int postsTotal = DataSingleton.getInstance().getPostsCount();
ViewInteraction empty = onView(withId(R.id.empty_view));
ViewInteraction recycler = onView(withId(R.id.recycler_view));
if (postsTotal == 0) {
empty.check(matches(isDisplayed()));
recycler.check(matches(not(isDisplayed())));
} else {
empty.check(matches(not(isDisplayed())));
recycler.check(matches(isDisplayed()));
recycler.check(new RecyclerViewItemCountAssertion(greaterThan(postsTotal)));
}
}
I know that this can't be the right way to write tests. I want to be able to test both with an empty data set and a non-empty set so that the if-else is two separate tests. The only way I think I can achieve it is to mock the data.
Is there another way?
Can I use Mockito to make the MainActivity use mock data without modifying the production code? Is my only choice to make it inject either real or mocked data providers in place of my singleton?
Is it better to just uninstall and reinstall my app each time so there is no data to start with and then continue with real data testing?
Android Activity are heavyweight and hard to test. Because we don't have control over the constructor, it is hard to swap in test doubles.
The first thing to do is to make sure you are depending on an abstraction of the data-source rather than a concretion. So if you are using a singleton with a getPostsCount() method then extract an interface:
interface DataSourceAbstraction {
int getPostsCount();
}
Make a wrapper class that implements your interface:
class ConcreteDataSource implements DataSourceAbstraction {
#Override
int getPostsCount() {
return DataSingleton.getInstance().getPostsCount();
}
}
And make the Activity depend on that rather than the concrete DataSingleton
DataSourceAbstraction dataSourceAbstraction;
#Override
protected void onCreate(Bundle savedInstanceState) {
super(savedInstanceState);
injectMembers();
}
#VisibleForTesting
void injectMembers() {
dataSourceAbstraction = new ConcreteDataSource();
}
You can now swap in a test double by subclassing and overriding injectMembers that has relaxed visibility. It's a bad idea do this in enterprise development, but there are less options in Android Activities where you don't control the constructor of the class.
You can now write:
DataSourceAbstraction dataSource;
//system under test
MainActivity mainActivity
#Before
public void setUp() {
mockDataSource = Mockito.mock(DataSourceAbstraction.class);
mainActivity = new MainActivity() {
#Override
void injectMembers() {
dataSourceAbstraction = mockDataSource;
}
};
}

Toggling between multiple espresso tests on Android

I'm new to the automated testing, and using espresso to test my android App.
The problem is that I have multiple dynamic views depending on certain conditions :
My user has a boolean attribute, let's call it "isPremium"
when I click on a button my user is redirected to FragmentA if isPremuim == true , else he's redirected to FragmentB.
now for my tests I have
#Test public void testFragmentA();
and
#Test public void testFragmentB();
but when I run my tests based on my data, forcibly one of the two tests fails.
so should i make one test for both fragments like
private void testFragmentA();
private void testFragmentB();
#Test
public void myGlobalTest {
if(user.isPremium) testFragmentA();
else testFragmentB();
}
is this the right way to make my tests ? or there is another better way, because sincerly I'm not convinced with this method.
It would be best if you set value for premium at the beginning of each test (true for testFragmentA, false for testFragmentB). That way you will know what you are expecting and what each fragment depends on.
Also, if user is some global variable, you should keep its state in #Before and restore it in #After method.
boolean isPremium;
#Before
public void init() {
isPremium = User.isPremium();
}
#Test
public void testFragmentA(){
User.setPremium(true);
// test fragment A
}
#Test
public void testFragmentB(){
User.setPremium(false);
// test fragment B
}
#After
public void restore() {
User.setPremium(isPremium);
}

Correct way to use IdlingResource in Espresso Android

I'm writing UI tests with Espresso. App cooperates tightly with server, so in many cases, I need to wait for either value to be calculated, or data is got and displayed, etc. Espresso suggests using IdlingResource for this.
My IdlingResource classes look like this (simple and clear example).
public class IRViewVisible implements IdlingResource {
private View view;
private ResourceCallback callback;
public IRViewVisible(View view) {
this.view = view;
}
#Override
public String getName() {
return IRViewVisible.class.getName();
}
#Override
public boolean isIdleNow() {
if(view.getVisibility() == View.VISIBLE && callback != null) {
callback.onTransitionToIdle();
return true;
}
return false;
}
#Override
public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
this.callback = resourceCallback;
}
}
Please correct me if I'm wrong anywhere (as sometimes it seems to me that my IdlingResources do not work properly).
I register the idling resource in setUp() like this:
IRViewVisible ir = new IRViewVisible(View v);
Espresso.registerIdlingResources(ir).
Unregister it on tearDown().
I found this article (there is a section called "Register a component tied to an Activity instance") — I do not use his schema, but I checked hashcode of view that was set to IdlingResource after registering (in each method), and it's not the same view — all hashes are different.
Another question: One Test class (it's results) can't have any effect on another Test class, can it?
I'm guessing your problem stems from getName() returning the same name for all instances of IRViewVisible. This means you can only have one registered instance of it at a time - any subsequent registrations will fail (silently!).
You mention that you clear the IdlingResources at the end of each test, but if you are register multiple instances of it at once, you need to make sure each instance has a unique name. it's not clear from your question if you're registering multiple instances of IRViewVisible in a single test.
As to your final question: Yes, it is possible. Android doesn't completely shut down the Application between test runs - just the Activities. Common things which can cause problems:
Failing to clear persistent state (saved data).
Failing to clear global state - e.g. static variables/singletons
Not waiting for background threads to finish running.
As an aside, it's worth noting that you only call onTransitionToIdle() inside isIdleNow(). This works (thanks #Be_Negative for the heads up!) but it could slow down your tests a lot, since Espresso will only poll isIdleNow() every few seconds. If you call onTransitionToIdle() as soon as the view becomes visible, it should speed things up considerably.
I needed something similar to your IRViewVisible myself, here's my effort.
So the isIdleNow() method will never return true if you don't set a callback to the idlingResource?
I reckon it's better to refactor it like this:
#Override
public boolean isIdleNow() {
boolean idle = view.getVisibility() == View.VISIBLE;
if(idle && callback != null) {
callback.onTransitionToIdle();
}
return idle;
}
Well, first of all you shouldn't need to use Espresso IdlingResource to test server calls. If you use AsyncTasks in your server calls, Espresso will be able to know when to be idle and when not. If this is not enough: try to refactor your code in this way:
IRViewVisible idlingResource = new IRViewVisible(yourView);
IdlingPolicies.setMasterPolicyTimeout(waitingTime * 2, TimeUnit.MILLISECONDS);
IdlingPolicies.setIdlingResourceTimeout(waitingTime * 2, TimeUnit.MILLISECONDS);
// Now we wait
Espresso.registerIdlingResources(idlingResource);
// Stop and verify
// Clean up
Espresso.unregisterIdlingResources(idlingResource);
Hope to be helpful.

Espresso - How can I check if an activity is launched after performing a certain action?

the following is one of my Espresso test cases.
public void testLoginAttempt() {
Espresso.onView(ViewMatchers.withId(R.id.username)).perform(ViewActions.clearText()).perform(ViewActions.typeText("nonexistinguser#krossover.com"));
Espresso.onView(ViewMatchers.withId(R.id.username)).perform(ViewActions.clearText()).perform(ViewActions.typeText("invalidpassword"));
Espresso.onView(ViewMatchers.withId(R.id.login_button)).perform(ViewActions.click());
// AFTER CLICKING THE BUTTON, A NEW ACTIVITY WILL POP UP.
// Clicking launches a new activity that shows the text entered above. You don't need to do
// anything special to handle the activity transitions. Espresso takes care of waiting for the
// new activity to be resumed and its view hierarchy to be laid out.
Espresso.onView(ViewMatchers.withId(R.id.action_logout))
.check(ViewAssertions.matches(not(ViewMatchers.isDisplayed())));
}
Currently what I did was to check if a view in the new activity (R.id.action_logout) is visibible or not. If visible, I will assume tha the activity opened successfully.
But it doesn't seem to work as I expected.
Is there a better way to check if a new activity is successfully launched instead of checking a view in that activity is visible?
Thanks
You can use:
intended(hasComponent(YourExpectedActivity.class.getName()));
Requires this gradle entry:
androidTestCompile ("com.android.support.test.espresso:espresso-intents:$espressoVersion")
The import for the intended() and hasComponent()
import static android.support.test.espresso.intent.Intents.intended;
import static android.support.test.espresso.intent.matcher.IntentMatchers.hasComponent;
as mentioned by Shubam Gupta please remember to call Intents.init() before calling intended(). You can eventually call it in the #Before method.
Try:
intended(hasComponent(YourActivity.class.getName()));
Also, keep in mind
java.lang.NullPointerException is thrown if Intents.init() is not called before intended()
You may do it as follows:
#Test
public void testLoginAttempt() {
Espresso.onView(ViewMatchers.withId(R.id.username)).perform(ViewActions.clearText()).perform(ViewActions.typeText("nonexistinguser#example.com"));
Espresso.onView(ViewMatchers.withId(R.id.username)).perform(ViewActions.clearText()).perform(ViewActions.typeText("invalidpassword"));
Intents.init();
Espresso.onView(ViewMatchers.withId(R.id.login_button)).perform(ViewActions.click());
Intents.release();
}
java.lang.NullPointerException is thrown if Intents.init() is not called.
The problem is that your application performs the network operation after you click login button. Espresso doesn't handle (wait) network calls to finish by default. You have to implement your custom IdlingResource which will block the Espresso from proceeding with tests until IdlingResource returns back in Idle state, which means that network request is finished. Take a look at Espresso samples page - https://google.github.io/android-testing-support-library/samples/index.html
Make sure the Espresso intent library is in the gradle dependencies
androidTestImplementation "com.android.support.test.espresso:espresso-intents:3.0.1"
Then import these two in your test file
import static android.support.test.espresso.intent.Intents.intended
import static android.support.test.espresso.intent.matcher.IntentMatchers.hasComponent
Then add IntentsTestRule in your test class
#Rule
#JvmField
val mainActivityRule = IntentsTestRule(MainActivity::class.java)
Finally check the activity has launched intent
#Test
fun launchActivityTest() {
onView(ViewMatchers.withId(R.id.nav_wonderful_activity))
.perform(click())
intended(hasComponent(WonderfulActivity::class.java!!.getName()))
}
Try with
intended(hasComponent(new ComponentName(getTargetContext(), ExpectedActivity.class)));
Look at response from #riwnodennyk
I use this approach:
// The IntentsTestRule class initializes Espresso Intents before each test, terminates the host activity, and releases Espresso Intents after each test
#get:Rule
var tradersActivity: IntentsTestRule<TradersActivity> = IntentsTestRule(TradersActivity::class.java)
#get:Rule
var jsonViewActivity: IntentsTestRule<JsonViewActivity> = IntentsTestRule(JsonViewActivity::class.java)
#Test
fun scrollToItemAndClick() {
onView(withId(R.id.tradersRecyclerView)).perform(RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(ITEM_POS, click()))
// check is activity was started
intended(hasComponent(JsonViewActivity::class.java.getName()))
}
#RunWith(RobolectricTestRunner.class)
public class WelcomeActivityTest {
#Test
public void clickingLogin_shouldStartLoginActivity() {
WelcomeActivity activity = Robolectric.setupActivity(WelcomeActivity.class);
activity.findViewById(R.id.login).performClick();
Intent expectedIntent = new Intent(activity, LoginActivity.class);
assertThat(shadowOf(activity).getNextStartedActivity()).isEqualTo(expectedIntent);
}
}

Confused how to use Mockito for an android test

I'm trying to write a unit test for my android app but having trouble doing what I want with mockito. This is being used in conjunction with Robolectric which I have working just fine and have demonstrated that unit tests work.
I want to test whether or not a button will open a new activity depending on whether there is some bluetooth device connected. Obviously, there is no device connected with bluetooth in my test, however I want to pretend as though there is. The state of the bluetooth connection is stored in my Application class. There is no publicly accessible method to change this value.
So basically the logic in the app is like this:
HomeActivity.java:
//this gets called when the button to open the list is clicked.
public void openListActivity(View button) {
MyApplication myApplication = (MyApplication) getApplication();
if (myApplication.isDeviceConnected() {
startActivity(new intent(this, ListActivity.class));
}
}
So to test this I did the following:
TestHomeActivity.java:
#Test
public void buttonShouldOpenListIfConnected() {
FlexApplication mockedApp = Mockito.mock(MyApplication.class);
Mockito.when(mockedApp.isDeviceConnected()).thenReturn(true);
//listViewButton was setup in #Before
listViewButton.performClick();
ShadowActivity shadowActivity = Robolectric.shadowOf(activity);
Intent intent = shadowActivity.getNextStartedActivity();
assertNotNull(intent); //this fails because no new activity was opened. I debugged this and found that isDeviceConnected returned false.
ShadowIntent shadowIntent = Robolectric.shadowOf(intent);
assertThat(shadowIntent.getComponent().getClassName(), equalTo(ListActivity.class.getName()));
}
So my unit test fails because the call (in the activity) to isDeviceConnected returns false even though I thought I told it to return true with the mock framework. I want my test to have this method return true though. Isn't this what mockito does or am I totally mistaken on how to use mockito?
That's how mockito works, but the problem is: is your listViewButton using your mockedApp? Seems not, because you're creating mockedApp at the test method and never setting it anywhere. Mockito will not mock the method calls of all instances of Application, only from what you declared as a mock.
I personally don't know how android works with the Application class, but you will have to set it somewhere so listView use your mockedApp instead of what it receives normally.
EDIT
After the updated question, you can transform your getApplication in a protected method, spy you listViewButton and make it return your mockedApp. That smells a little bad, but it's one way if you can not set your application mocked object to listViewButton.
EDIT2
Example of using spy in your test using BDDMockito for readability :)
public HomeActivity {
...
protected MyApplication getApplication() {
// real code
}
...
}
public void TestHomeActivity {
private HomeActivity homeActivity;
#Before
public void setUp() {
this.homeActivity = spy(new HomeActivity());
}
#Test
public void buttonShouldOpenListIfConnected() {
// given
FlexApplication mockedApp = Mockito.mock(MyApplication.class);
Mockito.when(mockedApp.isDeviceConnected()).thenReturn(true);
// IMPORTANT PART
given(homeActivity.getApplication()).willReturn(mockedApp);
...
}
}
After that, your test should work as expected. But I reinforce: Use spy only if you can't inject your mockedApp inside HomeActivity.
Your mocked version isn't being called.
See that call, getApplication()? (below). That's returning a real copy of your MyApplication class, not your mocked version. You'd need to intercept the getApplication() call and pass in your mocked Application object.
HomeActivity.java:
//this gets called when the button to open the list is clicked.
public void openListActivity(View button) {
MyApplication myApplication = (MyApplication) getApplication(); // returns the real thing
if (myApplication.isDeviceConnected() {
startActivity(new intent(this, ListActivity.class));
}
}
I'm not sure this is possible with Mockito. Have you tried customizing the ShadowActivity#getApplication() method?

Categories

Resources