Android project to experiment various testing tools
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.
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();
}
}
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) })
}
}
@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));
});
});
}
}
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);
});
});
}
}
@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);
}
}
}
}
@Nested
and @DisplayName
annotations allow developers to reach an elegant "given/when/then" canvas.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 |
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);
}
}
@RunWith(Cucumber.class)
@CucumberOptions(
features = "src/test/resources/"
)
public class SumTestRunner {
}
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);
}
}
}
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).
@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
}
}
}
})
@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
}
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"));
});
});
}
}
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']
}
}
}
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"
@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)));
}
}
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();
}
}
@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());
}
}
}
A relevant combination of Dagger2 and mockito is already described in a previous post I wrote: http://roroche.github.io/AndroidStarter/
Science graphic by Pixel perfect from Flaticon is licensed under CC BY 3.0. Made with Logo Maker