Android MVP. Testing View (Instrumental tests or unit test using Robolectric) - android

Have the following doubts about testing View (Activity, Fragment). The first approach is to test view using robolectric with mocking Android components like the following code do:
private void setUp() {
fragment = new CustomFragment();
activity = Robolectric.setupActivity(MainActivity.class);
}
#Test
public void testOnCreateView() {
fragment.onCreateView(LayoutInflater.from(activity), (ViewGroup) activity.findViewById(R.id.container), null);
verify(repoInfoPresenter).onCreateView(null);
}
Don't like this approach, cause this tests executing takes long time. In my opinion unit tests should be executed on each merge request, so they shouldn't be as slow as tests like that.
Another variant is to write Instrumentation tests using Espresso. I know that it's already takes long time and real device, but I don't think that this test should executed as often as tests running on jvm.
Or maybe there is magical variant to mock(make stubs) for fragments or Activities which I don't know about?

Related

Android Testing custom views with mockito

I have a Customview class and I want to write a simple test for it. At first I want to check if the LayoutParams are set.
CustomView Class
public class CustomView extends FrameLayout {
public CustomView(#NonNull Context context) {
super(context);
initFrameLayout();
}
public void initFrameLayout() {
LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
this.setLayoutParams(layoutParams);
}
}
CustomViewTest class
public class CustomViewTest {
#Test
public void viewInitializedCorrectly() {
Context context = mock(Context.class);
CustomView customView = new CustomView(context);
int expectedViewWidth = FrameLayout.LayoutParams.MATCH_PARENT;
assertEquals(expectedViewWidth, customView.getLayoutParams().width);
}
The test fails with an NullPointerException. I inspected the method with the debugger and I note, that the FrameLayout Object exists but without parameter. Should I mock the CustomView.class too?
There are a number of different types of tests in Android. Local Unit Tests run in your IDE on your laptop or desktop and Instrumented Unit Tests run on a device.
Local Unit Tests do not ordinarily have access to Android SDK classes like FrameLayout. Instead, you get stubbed out versions of these classes that return null. This explains the NullPointerException
For getting around the error, you could mock FrameLayout manually or use something like Robolectric which is a framework that provides test doubles called "shadows" of Android classes like FrameLayout.
However, in general custom views are poorly suited for unit testing since they cannot have mocks injected easily (since they are inflated by the OS from XML attributes) and the tests can often degenerate into a reverse implementation of the class. If the custom view does require testing beyond "it looks right" a better option may be to write Espresso automated UI tests which are more suited for that sort of thing.

Espresso test passes individually, fails when run in the suite

I have the following Espresso test. It always passes if I run it by itself, but always fails when I run all the tests in the class together.
What's also a bit strange is that it used to work even as part of the suite. I'm not sure why now it stopped working. It must be something I've done but I don't know what.
#Test
public void itemHasImage_ShowsImage() {
closeSoftKeyboard();
if (mItem.getImageUrl() != null) {
onView(withId(R.id.edit_item_image)).perform(scrollTo())
.check(matches(isDisplayed()));
}
}
The error I'm getting is:
Error performing 'scroll to'...
...
Caused by: java.lang.RuntimeException: Action will not be performed
because the target view does not match one or more of the following
constraints:(view has effective visibility=VISIBLE and is descendant
of a: (is assignable from class: class android.widget.ScrollView...
The view is visible and a descendant of a scroll view, as evidenced by it passing when run on it's own.
When it gets to this test (in the suite) it just doesn't scroll. But when I run it by itself it scrolls just fine.
In another stack overflow question I asked recently Espresso not starting Activity the same for second iteration in parameterised test, I found out that onDestroy from the previous test was getting called after onResume in the current test, causing it to set a value to null and fail the test. Again in that situation, the problem was that the test passed by itself but not in the suite. I now have a similar problem but no obvious way to fix it. (Edit: the workaround for the other question can no longer be applied).
Any ideas anyone? Could it be reading the wrong Activity somehow? Like maybe it's looking at the one from the previous test. That sounds ridiculous but after that last problem I had it seems possible.
Edit: Ok it turns out that when running this test as part of the suite, the image is in fact not visible causing the test to fail. I found this using the debugger and scrolling the view manually. But why?
I think it's a bug and have logged an issue here:
https://code.google.com/p/android/issues/detail?id=235247
It can able to solve using Orchestrator android testing utility library, It will executing each test case independently, so there is no chance of breaking in test suite
When using AndroidJUnitRunner version 1.0 or higher, you have access to a tool called Android Test Orchestrator, which allows you to run each of your app's tests within its own invocation of Instrumentation.
Enabling using build.gradle
android {
defaultConfig {
...
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
testOptions {
execution 'ANDROID_TEST_ORCHESTRATOR'
}
}
dependencies {
androidTestImplementation 'com.android.support.test:runner:1.0.1'
androidTestUtil 'com.android.support.test:orchestrator:1.0.1'
}
For more info:https://developer.android.com/training/testing/junit-runner.html#using-android-test-orchestrator
I had a similar issue but the cause was different, #Rakshith-Kumar suggestion of using Orchestrator might work, also putting each test method in a TestSuite could also be a solution, since the flakiness could be addressed by running each test individually.
Also want to mention that if you're testing component that's using RxJava schedulers (io scheduler for example) to run some tasks in a background thread, the flakiness could happen because Espresso will not be able to synchronize with the background threads.
A solution can be injecting a the ObservableTransformer to do the subscribeOn and observeOn operations. And for Espresso tests substitute the ObservableTransformer instance with another one that uses Schedulers.from(AsyncTask.THREAD_POOL_EXECUTOR) for the io scheduler.
Something like that:
public <T> ObservableTransformer<T, T> ioTransformer() {
return observable -> observable.subscribeOn(AsyncTask.THREAD_POOL_EXECUTOR).observeOn(AndroidSchedulers.mainThread());
}
This way Espresso can synchronize with background threads and avoid flakiness.
More info:
https://blog.danlew.net/2015/03/02/dont-break-the-chain/
https://developer.android.com/training/testing/espresso/#sync

android espresso login once before running tests

I've been trying to cover my Android app with tests and have started using espresso recently. Pretty impressed with it so far. However most of my app's functionality requires that users are logged in. And since all tests are independent, this requires registering a new user for each test. This works fine however the time required for each test increases considerably because of this.
I am trying to find a way to register a user once in a class (of tests) and then use that same user account to perform all the tests in that class.
One way I have been able to do this is to actually have only one test (#Test) method that runs all the other tests in the order I want. However this is an all or nothing approach, since the gradle cAT task only outputs the results once at the end without providing info about the intermediate tests that may have passed/failed.
I also tried the #BeforeClass approach which however did not work (no gradle output from the class where I had used this even with the debug option and it seemed like it took a long time before it moved on to the next class of tests).
Is there a better approach to register a user once at start of a class and then logout once at the end of testing?
Any help appreciated.
Ideally you would test the login/logout functionality in a set of tests that just test different login/logout scenarios, and let the other tests focus on other use cases. However, since the other scenarios depend on the user being logged in, it sounds like one way to solve this would be to provide a mock version of the app component handling the login. For the other login dependent tests, you would inject this mock at the start and it would return mock user credentials that the rest of the app can work with.
Here's an example where Dagger, Mockito and Espresso is being used to accomplish this: https://engineering.circle.com/instrumentation-testing-with-dagger-mockito-and-espresso-f07b5f62a85b
I test an app that requires this same scenario. The easiest way I've gotten around this is to split up logging in and out into their own test classes. Then you add all your test classes to a suite, starting and ending with the login and logout suites respectively. Your test suites ends up looking kind of like this.
#RunWith(Suite.class)
#Suite.SuiteClasses({
LoginSetup.class,
SmokeTests.class,
LogoutTearDown.class
})
EDIT: Here is an example of both the LoginSetup and LogoutTearDown tests. This solution really should only be for end-to-end tests and comprise a small portion of your testing efforts. fejd provides a solution for a full testing stack which also needs to be considered.
#LargeTest
public class SmokeSetup extends LogInTestFixture {
#Rule
public ActivityTestRule<LoginActivity> mLoginActivity = new ActivityTestRule<>(LoginActivity.class);
#Test
public void testSetup() throws IOException {
onView(withId(R.id.username_field)).perform(replaceText("username"));
onView(withId(R.id.password_field)).perform(replaceText("password"));
onView(withId(R.id.login_button)).perform(click());
}
}
#LargeTest
public class LogoutTearDown extends LogInTestFixture {
#Rule
public ActivityTestRule<MainActivity> mMainActivity = new ActivityTestRule<>(MainActivity.class);
#Test
public void testLogout() throws IOException {
onView(withId(R.id.toolbar_menu)).perform(click());
onView(withId(R.id.logout_button)).perform(click());
}
}
The approach with logging in with #Before is nice but if your login is slow, your combined test time will be very slow.
Here's a great hack that works. The strategy is simple: run every test in order and fail every test before they get a chance to run if a certain test fails (in this case login test).
#RunWith(AndroidJUnit4.class)
#FixMethodOrder(MethodSorters.NAME_ASCENDING)
#LargeTest
public class YourTestsThatDependsOnLogin {
private static failEverything;
#Before
public void beforeTest() {
// Fail every test before it has a chance to run if login failed
if (failEverything) {
Assert.fail("Login failed so every test should fail");
}
}
#Test
public void test0_REQUIREDTEST_login() {
failEverything = true;
// Your code for login
// Your login method must fail the test if it fails.
login();
failEverything = false; // We are safe to continue.
}
// test1 test2 test3 etc...
}
Pros:
What you asked for works and it is fast (if your login is slow)
You can have multiple tests that depend on different logins, meaning you can do a bunch of tests for user1, then a bunch of tests for user2 etc.
Quick to set up.
Cons:
Not standard procedure and someone might wonder why so many tests
fail...
You should mock users instead of actually logging in. You should test your login separately and tests should not depend on each other.
Add the following function in your test file, replace the code in the try
block with yours that performs the login actions.
#Before
fun setUp() {
// Login if it is on the LoginActivity
try {
// Type email and password
Espresso.onView(ViewMatchers.withId(R.id.et_email))
.perform(ViewActions.typeText("a_test_account_username"), ViewActions.closeSoftKeyboard())
Espresso.onView(ViewMatchers.withId(R.id.et_password))
.perform(ViewActions.typeText("a_test_account_password"), ViewActions.closeSoftKeyboard())
// Click login button
Espresso.onView(ViewMatchers.withId(R.id.btn_login)).perform(ViewActions.click())
} catch (e: NoMatchingViewException) {
//view not displayed logic
}
}
With this #Before annotation, this setUp function will be executed before any other tests you have in this test file. If the app lands on Login Activity, do the login in this setUp function. The example here assumes there is an EditText for email and password, as well as a login button. It uses Expresso to type the email and password, then hit the login button. The try catch block is to ensure if you are not landing on the Login Activity, it will catch the error and do nothing, and if you didn't land on the Login Activity, then you are good to go on other tests anyway.
Note: this is Kotlin code, but it looks very similar to Java.
My Application also requires the user to be logged in through-out the test run.
However, I am able to login the first time and the application remembers my username/password throughout the test run. In fact, it remembers the credentials until I force it to forget them or uninstall and install the app again.
During a test run, after every test, my app goes to the background and is resumed again at the beginning of the next test. I am guessing your application requires a user to enter their credentials every time you bring it to the front from the background (banking application maybe?). Is there a setting in your application that will "Remember your credentials"? If yes, you can easily enable it right after you login for the first time in your test run.
Other than that, I think you should talk to the developers about providing you a way to remember your credentials.
If you are using #BeforeClass in Kotlin, you need to place it inside companion object. That way the block under #BeforeClass will be executed just once before the 1st test runs in the class. Also have #AfterClass inside companion object, so that it runs at the very end of the last test of the class.
Keep in mind that once the compiler moves from inside companion object to outside of it, the context of the app under test is lost. You can get back the context by launching the main activity(activity after login) of your app.
companion object {
#ClassRule
#JvmField
val activity = ActivityTestRule(Activity::class.java)
#BeforeClass
#JvmStatic
fun setUp() {
// login block
}
#AfterClass
#JvmStatic
fun tearDown() {
// logout block
}
}
#Test
fun sampleTest() {
activity.launchActivity(Intent())
}

Android espresso and account picker

I am having trouble making the instrumentation test using the Espresso.
I have an activity where account picker is popup-ed when app is started (main activity).
If customer clicks on cancel (in dialog), picker is popup up again; If user clicks on add, the result is picked up on activity result.
I dont know how to create a simple test with espresso which will include that picker.
When I create the Instrumentation test with the MainActivity, I got this message:
No activities in stage RESUMED...
public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity>{
MainActivity myActivity;
public MainActivityTest(){
super(MainActivity.class);
}
#Override
protected void setUp() throws Exception {
super.setUp();
getActivity();
}
public void testAccountPicker(){
onView(withText("Choose an account")).check(matches(isDisplayed()));
}
}
Does anybody had similar problem?
Thanx for your answers in advance.
That's a tough one :). The problem here is that once the flow leaves your application (Google Account Picker is an external application), Espresso ends the test. Account Picker is an activity from the package com.google.android.gms, thus external. Once it's started, your test is finished and you'll never be able to match anything in the dialog.
You have three possible solutions to make your tests feasible:
Using classpath substitution on your app to fake the intents; or
Fixing your app "testability"; or
Using dependency injection, like Dagger
I'll show how to use classpath substitution. The technique is really simple: you should isolate your Intent creation in a separate class, say IntentsFactory and, during tests, override that class.
Say your factory is in com.yourapp.factories.IntentsFactory and it is something like this:
public class IntentsFactory {
public static Intent getAccountPickerIntent (Context context) {
return AccountPicker.newChooseAccountIntent(null, null, new String[]{GoogleAuthUtil.GOOGLE_ACCOUNT_TYPE}, true, null, null, null, null);
}
}
You should create in your test app (say it is com.yourapp.tests) a package with the same name and methods, but that returns a different Intent, a mocked/dummy one:
public class IntentsFactory {
public static Intent getAccountPickerIntent (Context context) {
return new Intent(context, MyDummyAccountPickerActivity.class);
}
}
Whenever your tests execute, they will use the "nearest" class in the classpath, that is, the IntentsFactory from your tests. Instead of returning an intent that send the flow to another app, the flow will go to an class of your project and Espresso won't end the tests.
The only caveat here is that you'll have to create the MyDummyAccountPickerActivity which will return a result and a Bundle similar to the one returned by the framework class. The activity should exists in your app's manifest and you'll have to instruct your emulator Dalvik runtime to allow classpath (check it out this this and this links) substitution with the following command line:
adb shell setprop dalvik.vm.dexopt-flags v=n,o=v
adb shell stop installd
adb shell start installd
And execute your tests.
I had a similar problem to test the Camera and it's thoroughly discussed in Espresso forum
Seems, that you must operate on a root view which in your case the "account picker". Try this out:
public void testAccountPicker(){
onView(withText("Choose an account"))
.inRoot(withDecorView(not(is(getActivity().getWindow().getDecorView()))))
.check(matches(isDisplayed()));
}
There are several ways that you might be able to go about testing this using Espresso Intents https://google.github.io/android-testing-support-library/docs/espresso/intents/
You can verify that the Intent was sent to open the account picker by using the intended() syntax. You can also verify the behavior of your activity with the returned result from the picker using the intending().respondWith() syntax.
If you really want to interact with the picker directly, you may be able to using the UIAutomator API: https://developer.android.com/topic/libraries/testing-support-library/index.html#UIAutomator
UIAutomator can be used inside of Espresso tests.

Android unit testing and interfaces

I have been having quite a bit of trouble implementing unit testing on the Android. As a simple test, I've been trying to match a string retrieved from string resources:
String myString = myActivity.getResources().getString(R.string.testString));
However, when unit testing this invariably results in a null pointer exception. This includes robolectric as well as the Junit implementation delivered with the Android sdk.
One possible solution is to approach the retrieval of resources in a manner similar to a data access object. That is, create an interface through which string resources would be accessed. This would allow me to mock access the string resource. Similarly, I could separate the non-android dependent behavior of, say, an Activity, into a separate pojo class. This would allow me to run unit tests using standard Java testing tools. In fact, I could potentially delegate any Android infrastructure related activity to an interface.
This seems like a lot of jumping through hoops to get to unit testing. Is it worth it? Is there a more viable approach?
It turned out, the problem was that the activity has to be gotten in the actual test method. So, for example, my method now looks like this:
public void testGetActivityResourceString() {
Activity myActivity = this.getActivity();
String myString = myActivity.getResources().getString(R.string.hello);
Assert.assertNotNull(myString);
}
Whereas before I was creating activity in setup. This giveaway was in the docs:
"For each test method invocation, the Activity will not actually be created until the first time this method is called."
This was a real hassle to figure out. The example for HelloWorldTest doesn't work for the same reason.
Here's the full entry:
Public T getActivity ()
Since: API Level 3
Get the Activity under test, starting it if necessary.
For each test method invocation, the Activity will not actually be created until the first time this method is called.
If you wish to provide custom setup values to your Activity, you may call setActivityIntent(Intent) and/or setActivityInitialTouchMode(boolean) before your first call to getActivity(). Calling them after your Activity has started will have no effect.
NOTE: Activities under test may not be started from within the UI thread. If your test method is annotated with UiThreadTest, then your Activity will be started automatically just before your test method is run. You still call this method in order to get the Activity under test.
This works correctly:
public void testGetResourceString() {
assertNotNull(mActivity.getResources()
.getString(com.example.pkg.R.string.testString));
}
Because you haven't provided any of your code but only the getReousrces() line, I will guess what you are doing wrong:
you are not using the correct base class for your test, use ActivityInstrumentationTestCase2 because you need the system infrastructure
you are using the resources of your test project instead of your project under test, that's why in my example the id is com.example.pkg.R.string.testString.

Categories

Resources