AndroidTestingBox


Source link: https://github.com/RoRoche/AndroidTestingBox

AndroidTestingBox

Android project to experiment various testing tools. It targets Java and Kotlin languages. Priority is given to fluency and ease of use. The idea is to provide a toolbox to write elegant and intelligible tests, with modern techniques like behavior-driven testing frameworks or fluent assertions.

AndroidTestingBox in the news

System under test (SUT)

Simple Java class

public class Sum {

  public final int a;
  public final int b;
  private final LazyInitializer<Integer> mSum;

public Sum(int a, int b) {

this.a = a;

this.b = b;

mSum = new LazyInitializer<Integer>() {

 @Override

 protected Integer initialize() throws ConcurrentException {

  return Sum.this.a + Sum.this.b;

 
}

}
;
  
}

public int getSum() throws ConcurrentException {

return mSum.get();

  
}
 
}

Android Activity

Here stands the layout file:

<?xml version="1.0" encoding="utf-8"?> <RelativeLayout
  android:id="@+id/activity_main"
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent">

<TextView

android:id="@+id/ActivityMain_TextView"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_centerInParent="true"

android:text="@string/app_name"/>

<Button

android:id="@+id/ActivityMain_Button"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_below="@id/ActivityMain_TextView"

android:layout_centerHorizontal="true"

android:text="@string/click_me"/> </RelativeLayout> 

and here stands the corresponding Activity:

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

setContentView(R.layout.activity_main)

 val textView: TextView = findViewById(R.id.ActivityMain_TextView) as TextView

val button = findViewById(R.id.ActivityMain_Button)

button.setOnClickListener({
 view: View -> textView.setText(R.string.text_changed_after_button_click) 
}
)
  
}
 
}

JUnit

Fluent assertions: truth

Alternative: AssertJ

Frutilla

@RunWith(value = org.frutilla.FrutillaTestRunner.class) public class FrutillaSumTest {

@Frutilla(

 Given = "two numbers a = 1 and b = 3",

 When = "computing the sum of these 2 numbers",

 Then = "should compute sum = 4"
  )
  @Test
  public void test_addition_isCorrect() throws Exception {

given("two numbers", () -> {

 final int a = 1;

 final int b = 3;

  when("computing the sum of these 2 numbers", () -> {

  final Sum sum = new Sum(a, b);

then("should compute sum = 4", () -> assertThat(sum.getSum()).isEqualTo(4));

 
}
);

}
);

  
}
 
}

Fluent test method names

Specifications framework: Spectrum

import static com.google.common.truth.Truth.assertThat; import static com.greghaskins.spectrum.Spectrum.describe; import static com.greghaskins.spectrum.Spectrum.it;  @RunWith(Spectrum.class) public class SpectrumSumTest {

  {

describe("Given two numbers a = 1 and b = 3", () -> {

 final int a = 1;

 final int b = 3;

  it("computing the sum of these 2 numbers, should compute sum = 4", () -> {

  final Sum sum = new Sum(a, b);

assertThat(sum.getSum()).isEqualTo(4);

 
}
);

}
);

  
}
 
}

Alternative: Oleaster

Hierarchies in JUnit: junit-hierarchicalcontextrunner

@RunWith(HierarchicalContextRunner.class) public class HCRSumTest {

public class GivenTwoNumbers1And3 {

private int a = 1;

private int b = 3;

 @Before

public void setUp() {

 a = 1;

 b = 3;

}

 public class WhenComputingSum {

 private Sum sum;

  @Before

 public void setUp() {

  sum = new Sum(a, b);

 
}

  @Test

 public void thenShouldBeEqualTo4() throws ConcurrentException {

  assertThat(sum.getSum()).isEqualTo(4);

 
}

}

 public class WhenMultiplying {

 private int multiply;

  @Before

 public void setUp() {

  multiply = a * b;

 
}

  @Test

 public void thenShouldBeEqualTo3() throws ConcurrentException {

  assertThat(multiply).isEqualTo(3);

 
}

}

  
}
 
}

Novelty to consider: JUnit 5 Nested Tests

BDD tools

Cucumber

  • Define the .feature file:
Feature: Sum computation
 Scenario Outline: Sum 2 integers
  Given two int <a> and <b> to sum
  When computing sum
  Then it should be <sum>

Examples: 

|  a |  b | sum | 

|  1 |  3 |
4 | 

| -1 | -3 |  -4 | 

| -1 |  3 |
2 |
  • Define the corresponding steps:
public class SumSteps {

  Sum moSum;
  int miSum;

@Given("^two int (-?\\d+) and (-?\\d+) to sum$")
  public void twoIntToSum(final int a, final int b) {

moSum = new Sum(a, b);

  
}

@When("^computing sum$")
  public void computingSum() throws ConcurrentException {

miSum = moSum.getSum();

  
}

@Then("^it should be (-?\\d+)$")
  public void itShouldBe(final int expected) {

Assert.assertEquals(expected, miSum);

  
}
 
}
  • Define the specific runner:
@RunWith(Cucumber.class) @CucumberOptions(

features = "src/test/resources/" ) public class SumTestRunner {
 
}
  • Relevant tools:

JGiven

public class JGivenSumTest extends SimpleScenarioTest<JGivenSumTest.TestSteps> {

@Test
  public void addition_isCorrect() throws ConcurrentException {

given().first_number_$(1).and().second_number_$(3);

when().computing_sum();

then().it_should_be_$(4);

  
}

public static class TestSteps extends Stage<TestSteps> {

private int mA;

private int mB;

private Sum mSum;

 public TestSteps first_number_$(final int piA) {

 mA = piA;

 return this;

}

 public void second_number_$(final int piB) {

 mB = piB;

}

 public void computing_sum() {

 mSum = new Sum(mA, mB);

}

 public void it_should_be_$(final int piExpected) throws ConcurrentException {

 assertThat(mSum.getSum()).isEqualTo(piExpected);

}

  
}
 
}

Mutation testing: Zester plugin

For this sample project, define a new "Run configuration" with Zester such as:

Target classes: com.guddy.android_testing_box.zester.* Test class: com.guddy.android_testing_box.zester.ZesterExampleTest 

It generates an HTML report in the build/reports/zester/ directory, showing that 2 "mutants" survived to unit tests (so potential bugs, and in this case, yes it is).

Alternative to JUnit: TestNG

Kotlin

Fluent assertions: Kluent

Alternative: Expekt

Specifications framework: Spek

@RunWith(JUnitPlatform::class) class SpekSumTest : Spek({

given("two numbers a = 1 and b = 3") {

val a: Int = 1

val b: Int = 3

 on("computing the sum of these 2 numbers") {

 val sum: Sum = Sum(a, b)

  it("should compute sum = 4") {

  sum.sum shouldBe 4

 
}

}

  
}
 
}
)

Android

Fluent assertions: AssertJ Android

Robotium

@RunWith(AndroidJUnit4.class) public class MainActivityTest {

  //region Rule
  @Rule
  public final ActivityTestRule<MainActivity> mActivityTestRule = new ActivityTestRule<>(MainActivity.class, true, false);

  //endregion

//region Fields
  private Solo mSolo;
  private MainActivity mActivity;
  private Context mContextTarget;
  //endregion

//region Test lifecycle
  @Before
  public void setUp() throws Exception {

mActivity = mActivityTestRule.getActivity();

mSolo = new Solo(InstrumentationRegistry.getInstrumentation(), mActivity);

mContextTarget = InstrumentationRegistry.getTargetContext();

  
}

@After
  public void tearDown() throws Exception {

mSolo.finishOpenedActivities();

  
}

  //endregion

//region Test methods
  @Test
  public void testTextDisplayed() throws Exception {

given("the main activity", () -> {

  when("launching activity", () -> {

  mActivity = mActivityTestRule.launchActivity(null);

then("should display 'app_name'", () -> {

final boolean lbFoundAppName = mSolo.waitForText(mContextTarget.getString(R.string.app_name), 1, 5000L, true);

assertThat(lbFoundAppName);

  
}
);

 
}
);

}
);

  
}

  //endregion 
}

Espresso

Robolectric

 testCompile 'org.robolectric:robolectric:3.2.2'
  testCompile 'org.robolectric:shadows-multidex:3.2.2'
  testCompile 'org.robolectric:shadows-support-v4:3.2.2'
  testCompile 'org.khronos:opengl-api:gl1.1-android-2.1_r1'
@RunWith(RobolectricTestRunner.class) @Config(constants = BuildConfig.class) public class RobolectricMainActivityTest {

@Test
  public void test_clickingButton_shouldChangeText() throws Exception {

 given("The MainActivity", () -> {

 final MainActivity loActivity = Robolectric.setupActivity(MainActivity.class);

 final Button loButton = (Button) loActivity.findViewById(R.id.ActivityMain_Button);

 final TextView loTextView = (TextView) loActivity.findViewById(R.id.ActivityMain_TextView);

  when("clicking on the button", () -> {

  loButton.performClick();

then("text should have changed", () -> assertThat(loTextView.getText().toString()).isEqualTo("Text changed after button click"));

 
}
);

}
);

  
}
  
}

Cucumber support

  • Configure the build.gradle file:
android {

  defaultConfig {

testApplicationId "com.guddy.android_testing_box.ui"

testInstrumentationRunner "com.guddy.android_testing_box.ui.CucumberInstrumentationRunner"
  
}

 sourceSets {

androidTest {

 assets.srcDirs = ['src/androidTest/assets']

}

  
}
 
}
  • Write features in the src/androidTest/assets directory, for example this main.feature file:
Feature: Main activity
 Scenario: Click on the button
  Given the initial state is shown
  When clicking on the button
  Then the text changed to "Text changed after button click"
  • Define the corresponding steps:
@CucumberOptions(features = "features") public class CucumberMainActivitySteps extends ActivityInstrumentationTestCase2<MainActivity> {

public CucumberMainActivitySteps() {

super(MainActivity.class);

  
}

@Given("^the initial state is shown$")
  public void the_initial_main_activity_is_shown() {

// Call the activity before each test.

getActivity();

  
}

@When("^clicking on the button$")
  public void clicking_the_Click_Me_button() {

onView(withId(R.id.ActivityMain_Button)).perform(click());

  
}

@Then("^the text changed to \"([^\"]*)\"$")
  public void text_$_is_shown(final String s) {

onView(withId(R.id.ActivityMain_TextView)).check(matches(withText(s)));

  
}
 
}
  • Define the specific runner:
public class CucumberInstrumentationRunner extends MonitoringInstrumentation {

private final CucumberInstrumentationCore mInstrumentationCore = new CucumberInstrumentationCore(this);

@Override
  public void onCreate(Bundle arguments) {

super.onCreate(arguments);

 mInstrumentationCore.create(arguments);

start();

  
}

@Override
  public void onStart() {

super.onStart();

 waitForIdleSync();

mInstrumentationCore.start();

  
}
 
}

JGiven support

@RunWith(AndroidJUnit4.class) public class EspressoJGivenMainActivityTest extends

SimpleScenarioTest<EspressoJGivenMainActivityTest.Steps> {

@Rule
  @ScenarioState
  public ActivityTestRule<MainActivity> activityTestRule = new ActivityTestRule<>(MainActivity.class);

@Rule
  public AndroidJGivenTestRule androidJGivenTestRule = new AndroidJGivenTestRule(this.getScenario());

@Test
  public void clicking_ClickMe_changes_the_text() {

given().the_initial_main_activity_is_shown()

  .with().text("AndroidTestingBox");

when().clicking_the_Click_Me_button();

then().text_$_is_shown("Text changed after button click");

  
}

public static class Steps extends Stage<Steps> {

@ScenarioState

CurrentStep currentStep;

 @ScenarioState

ActivityTestRule<MainActivity> activityTestRule;

 public Steps the_initial_main_activity_is_shown() {

 // nothing to do, just for reporting

 return this;

}

 public Steps clicking_the_Click_Me_button() {

 onView(withId(R.id.ActivityMain_Button)).perform(click());

 return this;

}

 public Steps text(@Quoted String s) {

 return text_$_is_shown(s);

}

 public Steps text_$_is_shown(@Quoted String s) {

 onView(withId(R.id.ActivityMain_TextView)).check(matches(withText(s)));

 takeScreenshot();

 return this;

}

 private void takeScreenshot() {

 currentStep.addAttachment(

Attachment.fromBinaryBytes(ScreenshotUtils.takeScreenshot(activityTestRule.getActivity()), MediaType.PNG)

  .showDirectly());

}

  
}
 
}

IDE configuration

Nota Bene

A relevant combination of Dagger2 and mockito is already described in a previous post I wrote: http://roroche.github.io/AndroidStarter/

Bibliography

Interesting repositories

Interesting articles

Resources

Logo credits

Science graphic by Pixel perfect from Flaticon is licensed under CC BY 3.0. Made with Logo Maker

Resources

A lightweight wrapper around realm-java which introduces reactive stream semantics to SQL operations.

Dexter is an Android library that simplifies the process of requesting permissions at runtime.

android-utils contains a lot of useful utility classes for Android development.

Character picker view.

A simple, yet powerful wrapper around the SupportMapFragment with support for clustering and built-in support for querying and rendering a Realm results.

A simple and powerful EditText and RecyclerView UI component. It's build on top of RealmRecyclerView and RealmBasedRecyclerViewAdapter.

Topics


2D Engines   3D Engines   9-Patch   Action Bars   Activities   ADB   Advertisements   Analytics   Animations   ANR   AOP   API   APK   APT   Architecture   Audio   Autocomplete   Background Processing   Backward Compatibility   Badges   Bar Codes   Benchmarking   Bitmaps   Bluetooth   Blur Effects   Bread Crumbs   BRMS   Browser Extensions   Build Systems   Bundles   Buttons   Caching   Camera   Canvas   Cards   Carousels   Changelog   Checkboxes   Cloud Storages   Color Analysis   Color Pickers   Colors   Comet/Push   Compass Sensors   Conferences   Content Providers   Continuous Integration   Crash Reports   Credit Cards   Credits   CSV   Curl/Flip   Data Binding   Data Generators   Data Structures   Database   Database Browsers   Date &   Debugging   Decompilers   Deep Links   Dependency Injections   Design   Design Patterns   Dex   Dialogs   Distributed Computing   Distribution Platforms   Download Managers   Drawables   Emoji   Emulators   EPUB   Equalizers &   Event Buses   Exception Handling   Face Recognition   Feedback &   File System   File/Directory   Fingerprint   Floating Action   Fonts   Forms   Fragments   FRP   FSM   Functional Programming   Gamepads   Games   Geocaching   Gestures   GIF   Glow Pad   Gradle Plugins   Graphics   Grid Views   Highlighting   HTML   HTTP Mocking   Icons   IDE   IDE Plugins   Image Croppers   Image Loaders   Image Pickers   Image Processing   Image Views   Instrumentation   Intents   Job Schedulers   JSON   Keyboard   Kotlin   Layouts   Library Demos   List View   List Views   Localization   Location   Lock Patterns   Logcat   Logging   Mails   Maps   Markdown   Mathematics   Maven Plugins   MBaaS   Media   Menus   Messaging   MIME   Mobile Web   Native Image   Navigation   NDK   Networking   NFC   NoSQL   Number Pickers   OAuth   Object Mocking   OCR Engines   OpenGL   ORM   Other Pickers   Parallax List   Parcelables   Particle Systems   Password Inputs   PDF   Permissions   Physics Engines   Platforms   Plugin Frameworks   Preferences   Progress Indicators   ProGuard   Properties   Protocol Buffer   Pull To   Purchases   Push/Pull   QR Codes   Quick Return   Radio Buttons   Range Bars   Ratings   Recycler Views   Resources   REST   Ripple Effects   RSS   Screenshots   Scripting   Scroll Views   SDK   Search Inputs   Security   Sensors   Services   Showcase Views   Signatures   Sliding Panels   Snackbars   SOAP   Social Networks   Spannable   Spinners   Splash Screens   SSH   Static Analysis   Status Bars   Styling   SVG   System   Tags   Task Managers   TDD &   Template Engines   Testing   Testing Tools   Text Formatting   Text Views   Text Watchers   Text-to   Toasts   Toolkits For   Tools   Tooltips   Trainings   TV   Twitter   Updaters   USB   User Stories   Utils   Validation   Video   View Adapters   View Pagers   Views   Watch Face   Wearable Data   Wearables   Weather   Web Tools   Web Views   WebRTC   WebSockets   Wheel Widgets   Wi-Fi   Widgets   Windows   Wizards   XML   XMPP   YAML   ZIP Codes