From 21a85777b87462574687ff49b4b4f6472c16b21b Mon Sep 17 00:00:00 2001 From: hmalik144 Date: Mon, 27 Feb 2023 18:22:21 +0000 Subject: [PATCH 1/2] Test framework built Edge cases establish and tested CircleCI integrated for better CI/CD operations Robot Framework implemented for automation testing --- app/build.gradle | 1 + .../assets/features/sample.feature | 9 --- .../assets/features/sumtest.feature | 41 +++++++++++++ .../example/sumtest/constants/ErrorTypes.kt | 9 +++ .../example/sumtest/robots/BaseTestRobot.kt | 38 ++++++++++++ .../example/sumtest/robots/SumTestRobot.kt | 40 +++++++++++++ .../com/example/sumtest/steps/SampleSteps.kt | 38 ------------ .../com/example/sumtest/steps/SumTestSteps.kt | 60 +++++++++++++++++++ .../example/sumtest/test/CucumberRunner.kt | 2 +- .../java/com/example/sumtest/SumActivity.kt | 21 +++++++ .../java/com/example/sumtest/SumViewModel.kt | 1 + .../sumtest/utils/BasicIdlingResource.kt | 32 ++++++++++ 12 files changed, 244 insertions(+), 48 deletions(-) delete mode 100644 app/src/androidTest/assets/features/sample.feature create mode 100644 app/src/androidTest/assets/features/sumtest.feature create mode 100644 app/src/androidTest/java/com/example/sumtest/constants/ErrorTypes.kt create mode 100644 app/src/androidTest/java/com/example/sumtest/robots/BaseTestRobot.kt create mode 100644 app/src/androidTest/java/com/example/sumtest/robots/SumTestRobot.kt delete mode 100644 app/src/androidTest/java/com/example/sumtest/steps/SampleSteps.kt create mode 100644 app/src/androidTest/java/com/example/sumtest/steps/SumTestSteps.kt create mode 100644 app/src/main/java/com/example/sumtest/utils/BasicIdlingResource.kt diff --git a/app/build.gradle b/app/build.gradle index 53fb063..c84a717 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -43,6 +43,7 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1' implementation 'androidx.activity:activity-ktx:1.6.1' + implementation 'androidx.test.espresso:espresso-idling-resource:3.5.1' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' diff --git a/app/src/androidTest/assets/features/sample.feature b/app/src/androidTest/assets/features/sample.feature deleted file mode 100644 index d0d9544..0000000 --- a/app/src/androidTest/assets/features/sample.feature +++ /dev/null @@ -1,9 +0,0 @@ -Feature: Sample feature file - - @sample_test - Scenario: Successful login - Given I start the application - - @sample_test - Scenario: Unsuccessful login - Given I start the application and throw an exception diff --git a/app/src/androidTest/assets/features/sumtest.feature b/app/src/androidTest/assets/features/sumtest.feature new file mode 100644 index 0000000..6c9a580 --- /dev/null +++ b/app/src/androidTest/assets/features/sumtest.feature @@ -0,0 +1,41 @@ +Feature: Sum test feature to test the app for addition operations + + @calculator_test + Scenario Outline: Run successful functions on the calculator + Given I start the application + When I run calculator sum for values "" and "" + And I assert the operation has run successfully with result "" + Examples: + | First value | Second value | Sum | + | 5 | 5 | 10 | + | -231 | -5 | -236 | + | 999 | 1 | 1000 | + | 1000 | 1000 | 2000 | + | -1000 | -1000 | -2000 | + + @calculator_test + Scenario Outline: Run unsuccessful functions on the calculator + Given I start the application + When I run calculator sum for values "" and "" + And I assert the operation has failed with error message + Examples: + | First value | Second value | Error Type | + | 2200 | 500 | Overflow | + | t44 | -5 | InvalidInput | + | | 10 | EmptyInput | + | 1! | 44 | InvalidInput | + | 1.2 | -5 | InvalidInput | + | -2200 | -150 | Overflow | + | 1,200 | 120 | InvalidInput | + + @calculator_test + Scenario Outline: Run functions for integer limitation on the calculator + Given I start the application + When I run calculator sum for values "" and "" + And I assert the operation has failed with error message + Examples: + | First value | Second value | Error Type | + | 2147483647 | 10 | Overflow | + | 2147483637 | 10 | Overflow | + | -2147483648 | -10 | Overflow | + | -2147483638 | 10 | Overflow | \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/sumtest/constants/ErrorTypes.kt b/app/src/androidTest/java/com/example/sumtest/constants/ErrorTypes.kt new file mode 100644 index 0000000..86a6164 --- /dev/null +++ b/app/src/androidTest/java/com/example/sumtest/constants/ErrorTypes.kt @@ -0,0 +1,9 @@ +package com.example.sumtest.constants + +import com.example.sumtest.Result + +enum class ErrorTypes(override val message: String): Result { + EmptyInput("One or more fields are empty"), + InvalidInput("Only integers are allowed"), + Overflow("Exception overflow error. NSOSStatusErrorDomain Code=-10817 \\\"(null)\\\" UserInfo={_LSFunction=_LSSchemaConfigureForStore, ExpectedSimulatorHash={length = 32, bytes = 0xa9298a34 dc614504 8992eb3c f65c237f ... ff5133c6 37c50886 }"), +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/sumtest/robots/BaseTestRobot.kt b/app/src/androidTest/java/com/example/sumtest/robots/BaseTestRobot.kt new file mode 100644 index 0000000..d377358 --- /dev/null +++ b/app/src/androidTest/java/com/example/sumtest/robots/BaseTestRobot.kt @@ -0,0 +1,38 @@ +package com.example.sumtest.robots + +import android.content.res.Resources +import androidx.annotation.IdRes +import androidx.annotation.StringRes +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.ViewInteraction +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.* + + +open class BaseTestRobot { + + fun fillEditText(@IdRes resId: Int, text: String): ViewInteraction = + onView(withId(resId)).perform( + ViewActions.replaceText(text), + ViewActions.closeSoftKeyboard() + ) + + fun clickButton(@IdRes resId: Int): ViewInteraction = + onView((withId(resId))).perform(ViewActions.click()) + + fun getViewInteraction(@IdRes resId: Int): ViewInteraction = onView(withId(resId)) + + fun matchText(viewInteraction: ViewInteraction, text: String): ViewInteraction = viewInteraction + .check(matches(withText(text))) + + fun matchText(@IdRes resId: Int, text: String): ViewInteraction = + matchText(getViewInteraction(resId), text) + + fun getStringFromResource(@StringRes resId: Int): String = + Resources.getSystem().getString(resId) + + fun checkVisibility(@IdRes resId: Int, visibility: Visibility) = + getViewInteraction(resId).check(matches(withEffectiveVisibility(visibility))) + +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/sumtest/robots/SumTestRobot.kt b/app/src/androidTest/java/com/example/sumtest/robots/SumTestRobot.kt new file mode 100644 index 0000000..b9553e3 --- /dev/null +++ b/app/src/androidTest/java/com/example/sumtest/robots/SumTestRobot.kt @@ -0,0 +1,40 @@ +package com.example.sumtest.robots + +import androidx.test.espresso.matcher.ViewMatchers.Visibility.* +import com.example.sumtest.R + +fun sumTest(func: SumTestRobot.() -> Unit) = SumTestRobot().apply { func() } +class SumTestRobot: BaseTestRobot() { + + fun enterSumValues(firstNumber: String, secondNumber: String) { + fillEditText(R.id.firstNumber, firstNumber) + fillEditText(R.id.secondNumber, secondNumber) + } + + fun submitEntries() { + clickButton(R.id.cta) + } + + fun submitValuesForSum(firstNumber: String, secondNumber: String){ + enterSumValues(firstNumber, secondNumber) + submitEntries() + } + + fun checkSumHasCalculated(result: String) { + // Results is displayed and error is not + checkVisibility(R.id.result, VISIBLE) + checkVisibility(R.id.error, GONE) + + matchText(R.id.result, result) + // Edit texts are empty + matchText(R.id.firstNumber, "") + matchText(R.id.secondNumber, "") + } + + fun checkCalculationError(errorMessage: String) { + checkVisibility(R.id.result, GONE) + checkVisibility(R.id.error, VISIBLE) + + matchText(R.id.error, errorMessage) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/sumtest/steps/SampleSteps.kt b/app/src/androidTest/java/com/example/sumtest/steps/SampleSteps.kt deleted file mode 100644 index 475d351..0000000 --- a/app/src/androidTest/java/com/example/sumtest/steps/SampleSteps.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.example.sumtest.steps - -import androidx.lifecycle.Lifecycle -import androidx.test.core.app.ActivityScenario -import androidx.test.platform.app.InstrumentationRegistry -import com.example.sumtest.SumActivity -import io.cucumber.java.Before -import io.cucumber.java.en.Given -import org.junit.Assert - -class SampleSteps { - - private lateinit var mActivityScenarioRule: ActivityScenario<*> - - @Before - fun setup() { - mActivityScenarioRule = ActivityScenario.launch(SumActivity::class.java) - } - - @Given("I start the application") - fun i_start_the_application() { - mActivityScenarioRule.moveToState(Lifecycle.State.STARTED) - - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - Assert.assertEquals("com.example.sumtest", appContext.packageName) - } - - @Given("I start the application and throw an exception") - fun i_start_the_application_and_throw_an_exception() { - mActivityScenarioRule.moveToState(Lifecycle.State.STARTED) - - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - Assert.assertEquals("com.example.sumtest", appContext.packageName) - - throw Exception("This is an exception") - } - -} \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/sumtest/steps/SumTestSteps.kt b/app/src/androidTest/java/com/example/sumtest/steps/SumTestSteps.kt new file mode 100644 index 0000000..9cd40ad --- /dev/null +++ b/app/src/androidTest/java/com/example/sumtest/steps/SumTestSteps.kt @@ -0,0 +1,60 @@ +package com.example.sumtest.steps + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.Lifecycle.State.RESUMED +import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.IdlingResource +import com.example.sumtest.SumActivity +import com.example.sumtest.constants.ErrorTypes +import com.example.sumtest.robots.sumTest +import io.cucumber.java.After +import io.cucumber.java.Before +import io.cucumber.java.en.And +import io.cucumber.java.en.Given +import io.cucumber.java.en.When + +class SumTestSteps { + private lateinit var mActivityScenarioRule: ActivityScenario + private lateinit var mIdlingResource: IdlingResource + + @Before + fun setup() { + mActivityScenarioRule = ActivityScenario.launch(SumActivity::class.java) + mActivityScenarioRule.onActivity { + mIdlingResource = it.getIdlingResource()!! + IdlingRegistry.getInstance().register(mIdlingResource) + } + } + + @Given("I start the application") + fun i_start_the_application() { + mActivityScenarioRule.moveToState(RESUMED) + } + + @When("^I run calculator sum for values \"([^\"]*)\" and \"([^\"]*)\"$") + fun i_run_calculator_sum_for_values(firstValue: String, secondValue: String) { + sumTest { + submitValuesForSum(firstValue, secondValue) + } + } + + @And("^I assert the operation has run successfully with result \"([^\"]*)\"\$") + fun i_assert_the_operation_has_run_successfully(result: String) { + sumTest { + checkSumHasCalculated(result) + } + } + + @And("^I assert the operation has failed with error message (Overflow|InvalidInput|EmptyInput)$") + fun i_assert_the_operation_has_failed_with_error_message(result: ErrorTypes) { + sumTest { + checkCalculationError(result.message) + } + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(mIdlingResource) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/sumtest/test/CucumberRunner.kt b/app/src/androidTest/java/com/example/sumtest/test/CucumberRunner.kt index d0960bc..87850eb 100644 --- a/app/src/androidTest/java/com/example/sumtest/test/CucumberRunner.kt +++ b/app/src/androidTest/java/com/example/sumtest/test/CucumberRunner.kt @@ -5,7 +5,7 @@ import io.cucumber.junit.CucumberOptions @CucumberOptions( features = ["features"], - tags = ["@sample_test"], + tags = ["@calculator_test"], glue = ["com.example.sumtest.steps"] ) class CucumberRunner \ No newline at end of file diff --git a/app/src/main/java/com/example/sumtest/SumActivity.kt b/app/src/main/java/com/example/sumtest/SumActivity.kt index a1d4e16..7401659 100644 --- a/app/src/main/java/com/example/sumtest/SumActivity.kt +++ b/app/src/main/java/com/example/sumtest/SumActivity.kt @@ -8,14 +8,19 @@ import android.view.View.VISIBLE import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager.HIDE_NOT_ALWAYS import androidx.activity.viewModels +import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle +import androidx.test.espresso.IdlingResource import com.example.sumtest.databinding.ActivityMainBinding +import com.example.sumtest.utils.BasicIdlingResource import kotlinx.coroutines.launch class SumActivity : AppCompatActivity() { + // The Idling Resource which will be null in production. + private var mIdlingResource: BasicIdlingResource? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -53,6 +58,10 @@ class SumActivity : AppCompatActivity() { binding.secondNumber.removeTextChangedListener(textChangedListener) binding.secondNumber.text = null binding.secondNumber.addTextChangedListener(textChangedListener) + mIdlingResource?.setIdleState(true) + } + if (it.error != null) { + mIdlingResource?.setIdleState(true) } } } @@ -65,9 +74,21 @@ class SumActivity : AppCompatActivity() { ) (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager) .hideSoftInputFromWindow(currentFocus?.windowToken, HIDE_NOT_ALWAYS) + mIdlingResource?.setIdleState(false) } binding.firstNumber.addTextChangedListener(textChangedListener) binding.secondNumber.addTextChangedListener(textChangedListener) } + + /** + * Only called from test, creates and returns a new [BasicIdlingResource]. + */ + @VisibleForTesting + fun getIdlingResource(): IdlingResource? { + if (mIdlingResource == null) { + mIdlingResource = BasicIdlingResource() + } + return mIdlingResource + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/sumtest/SumViewModel.kt b/app/src/main/java/com/example/sumtest/SumViewModel.kt index c0ac750..5ad0bef 100644 --- a/app/src/main/java/com/example/sumtest/SumViewModel.kt +++ b/app/src/main/java/com/example/sumtest/SumViewModel.kt @@ -3,6 +3,7 @@ package com.example.sumtest import android.os.Handler import android.os.Looper import androidx.lifecycle.ViewModel +import com.example.sumtest.utils.BasicIdlingResource import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow diff --git a/app/src/main/java/com/example/sumtest/utils/BasicIdlingResource.kt b/app/src/main/java/com/example/sumtest/utils/BasicIdlingResource.kt new file mode 100644 index 0000000..a35eff8 --- /dev/null +++ b/app/src/main/java/com/example/sumtest/utils/BasicIdlingResource.kt @@ -0,0 +1,32 @@ +package com.example.sumtest.utils + +import androidx.test.espresso.IdlingResource +import androidx.test.espresso.IdlingResource.ResourceCallback +import java.util.concurrent.atomic.AtomicBoolean + +class BasicIdlingResource : IdlingResource { + + private lateinit var mCallback: ResourceCallback + + // Idleness is controlled with this boolean. + private val mIsIdleNow: AtomicBoolean = AtomicBoolean(true) + + override fun getName(): String = this.javaClass.name + + override fun isIdleNow(): Boolean = mIsIdleNow.get() + + override fun registerIdleTransitionCallback(callback: ResourceCallback) { + mCallback = callback + } + + /** + * Sets the new idle state, if isIdleNow is true, it pings the [ResourceCallback]. + * @param isIdleNow false if there are pending operations, true if idle. + */ + fun setIdleState(isIdleNow: Boolean) { + mIsIdleNow.set(isIdleNow) + if (isIdleNow) { + mCallback.onTransitionToIdle() + } + } +} \ No newline at end of file From f0f986763427ed41bc2d7af62a992083b4df59c9 Mon Sep 17 00:00:00 2001 From: hmalik144 Date: Mon, 27 Feb 2023 18:25:36 +0000 Subject: [PATCH 2/2] Update to README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index c4ac436..7255153 100644 --- a/README.md +++ b/README.md @@ -100,3 +100,6 @@ class SumTestSteps { .... } ``` +## Continuous Integration + +As part of being an automation tester the project has been connected to CircleCI for CI/CD operations.