Skip to content

Instantly share code, notes, and snippets.

@OrenBochman
Last active November 18, 2018 07:46
Show Gist options
  • Save OrenBochman/facbdd30be48aeccd91a791494322526 to your computer and use it in GitHub Desktop.
Save OrenBochman/facbdd30be48aeccd91a791494322526 to your computer and use it in GitHub Desktop.
Agile Android - Testing

Agile Android - Testing

Planning

To write agile android - testing is essential otherwise it quickly beocomes impossible to sperate code.

Testing Atoms

  • Rule for launching activity
  • Rule for launching service
  • Testing a view - using Espresso onView perform ``
  • Testing a spinner/listview/adapterView by itereating its values - using Espresso onData
  • Testing a RecyclerView - using Espresso RecyclerViewActions
  • Testing RX code

Blocks

  • Testing Contracts with - Mocks, Doubles, Stubs, Fakes and Shadows.
  • Testing Behaviour not impleminatation -the Robot Patten.
  • Dependency Injection.
  • MVP - Desinging for testablilty.
  • MVVM - Desinging for testablilty.

The Robot pattern Overview

Robot is based on Page objects for web. It is a testing architecture for use with espresso.

The robot class is used to decouple the test code from the UI by abstracting the ui interactions from the test. When the UI changes threr is a minimal changes needed to maintain the tests.

  • ScreenRobot - the generic abstract robot since we will need a robot per fragment/activity.
  • LoginRobot - the robot for our activity/fragment. Each user operation is abstracted by a single method in the robot. e.g. ** username enrty, ** password entry, ** remeber me, ** login button click.
  • LoginActivityTest - the test that uses the robot or roborts.

The MVP pattern

The MVP places an interface infront of the view and pulls out all non UI Code from Android activity into a Presenter greatly simplifing and speeding up testing on the JVM.

testing intents

Depend on

  • androidTestImplementation 'com.android.support.test.espresso:espresso-intents:3.0.2'

The rule

@Rule public IntentsTestRule<MyActivity> intentsTestRule = new IntentsTestRule<>(MyActivity.class);

Testing the API access

References

//shows intent validation that uses existing intent matchers that matches an outgoing intent that starts a browser
intended(allOf(
hasAction(equalTo(Intent.ACTION_VIEW)), // verify action
hasCategories(hasItem(equalTo(Intent.CATEGORY_BROWSABLE))), // verify category
hasData(hasHost(equalTo("www.google.com"))), // verify data
hasExtras(allOf( // verify extras
hasEntry(equalTo("key1"), equalTo("value1")),
hasEntry(equalTo("key2"), equalTo("value2")))),
toPackage("com.android.browser")));
@Test
public void activityResult_DisplaysContactsPhoneNumber() {
// Build the result to return when the activity is launched.
Intent resultData = new Intent();
String phoneNumber = "123-345-6789";
resultData.putExtra("phone", phoneNumber);
ActivityResult result = new ActivityResult(Activity.RESULT_OK, resultData);
// Set up result stubbing when an intent sent to "contacts" is seen.
intending(toPackage("com.android.contacts"))
.respondWith(result);
// User action that results in "contacts" activity being launched.
// Launching activity expects phoneNumber to be returned and displayed.
onView(withId(R.id.pickButton))
.perform(click());
// Assert that the data we set up above is shown.
onView(withId(R.id.phoneNumber))
.check(matches(withText(phoneNumber)));
}
/**
* Atomic Testing Template for Lists/Spinners/Recyclers etc
*/
@RunWith(AndroidJUnit4.class)
public class SpinnerSelectionTest {
//SCAFFOLDING
@Rule
public ActivityTestRule mActivityRule = new ActivityTestRule<>(MainActivity.class);
/**
* Iterate through the spinner, selecting each item and
* checking to see if it matches the string in the array.
*/
@Test
public void iterateSpinnerItems() {
//GIVEN
String[] myArray = mActivityRule
.getActivity()
.getResources()
.getStringArray(R.array.labels_array);
// Iterate through the spinner array of items.
int size = myArray.length;
for (int i=0; i<size; i++) {
//WHEN - the spinner is found and clicked.
onView(withId(R.id.label_spinner))
.perform(click());
// and the spinner item is found and clicked.
onData(is(myArray[i]))
.perform(click());
//THEN - check the TextView contains the spinner item.
onView(withId(R.id.text_phonelabel))
.check(matches(withText(containsString(myArray[i]))));
}
}
}
package com.sqisland.tutorial.recipes.test;
import android.support.annotation.IdRes;
import android.support.annotation.StringRes;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.isSelected;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.not;
public abstract class ScreenRobot<T extends ScreenRobot> {
public T checkIsHidden(@IdRes int... viewIds) {
for (int viewId : viewIds) {
onView(withId(viewId))
.check(matches(not(isDisplayed())));
}
return (T) this;
}
public T checkViewHasText(@IdRes int viewId, @StringRes int stringId) {
onView(withId(viewId))
.check(matches(withText(stringId)));
return (T) this;
}
public T checkIsSelected(@IdRes int... viewIds) {
for (int viewId : viewIds) {
onView(withId(viewId))
.check(matches(isSelected()));
}
return (T) this;
}
}
package com.sqisland.tutorial.recipes.test;
import android.content.Intent;
import android.support.annotation.StringRes;
import android.support.test.InstrumentationRegistry;
import android.support.test.rule.ActivityTestRule;
import com.sqisland.tutorial.recipes.R;
import com.sqisland.tutorial.recipes.data.local.InMemoryFavorites;
import com.sqisland.tutorial.recipes.injection.TestRecipeApplication;
import com.sqisland.tutorial.recipes.ui.recipe.RecipeActivity;
import org.junit.Before;
public class RecipeRobot extends ScreenRobot<RecipeRobot> {
private final InMemoryFavorites favorites;
public RecipeRobot() {
TestRecipeApplication app = (TestRecipeApplication)
InstrumentationRegistry.getTargetContext().getApplicationContext();
favorites = (InMemoryFavorites) app.getFavorites();
favorites.clear();
}
public RecipeRobot launch(ActivityTestRule rule) {
rule.launchActivity(null);
return this;
}
public RecipeRobot launch(ActivityTestRule rule, String id) {
Intent intent = new Intent();
intent.putExtra(RecipeActivity.KEY_ID, id);
rule.launchActivity(intent);
return this;
}
public RecipeRobot noTitle() {
return checkIsHidden(R.id.title);
}
public RecipeRobot description(@StringRes int stringId) {
return checkViewHasText(R.id.description, stringId);
}
public RecipeRobot setFavorite(String id) {
favorites.put(id, true);
return this;
}
public RecipeRobot isFavorite() {
return checkIsSelected(R.id.title);
}
}
package com.sqisland.tutorial.recipes.ui.recipe;
import android.content.Intent;
import android.support.test.InstrumentationRegistry;
import android.support.test.rule.ActivityTestRule;
import com.sqisland.tutorial.recipes.R;
import com.sqisland.tutorial.recipes.data.local.InMemoryFavorites;
import com.sqisland.tutorial.recipes.injection.TestRecipeApplication;
import com.sqisland.tutorial.recipes.test.RecipeRobot;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.isSelected;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.CoreMatchers.not;
import static org.junit.Assert.*;
public class RecipeActivityTest {
private static final String CARROTS_ID = "creamed_carrots";
@Rule
public ActivityTestRule<RecipeActivity> activityRule
= new ActivityTestRule<>(
RecipeActivity.class, true, false);
@Test
public void recipeNotFound() {
new RecipeRobot()
.launch(activityRule)
.noTitle()
.description(R.string.recipe_not_found);
}
@Test
public void clickToFavorite() {
launchRecipe(CARROTS_ID);
onView(withId(R.id.title))
.check(matches(withText("Creamed Carrots")))
.check(matches(not(isSelected())))
.perform(click())
.check(matches(isSelected()));
}
@Test
public void alreadyFavorite() {
new RecipeRobot()
.setFavorite(CARROTS_ID)
.launch(activityRule, CARROTS_ID)
.isFavorite();
}
private void launchRecipe(String id) {
Intent intent = new Intent();
intent.putExtra(RecipeActivity.KEY_ID, id);
activityRule.launchActivity(intent);
}
}
public class RetrofitMockClient implements Client {
private String jsonResponse;
private int statusCode = 200;
private String reason;
private static final String MIME_TYPE = "application/json";
public RetrofitMockClient(int statusCode, String reason, String jsonResponse) {
this.statusCode = statusCode;
this.reason = reason;
this.jsonResponse = jsonResponse;
}
@Override
public Response execute(Request request) throws IOException {
return createDummyJsonResponse(request.getUrl(), statusCode, reason, jsonResponse);
}
private Response createDummyJsonResponse(String url, int responseCode, String reason, String json) {
return new Response(url, responseCode, reason, Collections.EMPTY_LIST,
new TypedByteArray(MIME_TYPE, json.getBytes()));
}
}
public class RestServiceMockUtils {
public static String convertStreamToString(InputStream is) throws Exception {
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
StringBuilder sb = new StringBuilder();
String line = null;
while ((line = reader.readLine()) != null) {
sb.append(line).append("\n");
}
reader.close();
return sb.toString();
}
public static String getStringFromFile(Context context, String filePath) throws Exception {
final InputStream stream = context.getResources().getAssets().open(filePath);
String ret = convertStreamToString(stream);
//Make sure you close all streams.
stream.close();
return ret;
}
public static RetrofitMockClient getClient(Context context, final int httpStatusCode, String reason, String responseFileName) throws Exception {
return new RetrofitMockClient(httpStatusCode, reason, getStringFromFile(context, responseFileName));
}
}
public class QuoteOfTheDayServiceTests extends InstrumentationTestCase {
public static final String TAG = "QODServiceTest";
public static final String BASE_API_URL = "http://api.theysaidso.com/";
@SmallTest
public void testAuthoriseFailsForIncorrectSessionId() throws Exception {
String jsonResponseFileName = "quote_401_unauthorised.json";
int expectedHttpResponse = 401;
String reason = "Unauthorised";
//Create RetrofitMockClient with the expected JSON response and code.
RetrofitMockClient retrofitMockClient = RestServiceMockUtils.getClient(getInstrumentation().getContext(), expectedHttpResponse, reason, jsonResponseFileName);
RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint(BASE_API_URL)
.setClient(retrofitMockClient)
.setConverter(new JacksonConverter())
.setErrorHandler(new RetrofitErrorHandler())
.build();
RestService restServiceClient = restAdapter.create(RestService.class);
//Run test code - you can test anything you want to here, test the correct response, the way the UI displays for certain mock JSON.
String incorrectApiKey = "incorrectApiKey";
try {
String quote = restServiceClient.getQuoteOfTheDay(incorrectApiKey);
Assert.fail("Should have thrown unauthorised exception");
} catch (UnauthorizedException unauthorised) {
Log.d(TAG, "Successfully caught unauthorised exception when incorrect API key was passed");
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment