Android App Testing with Continuous Integration
Learn how to create automated tests for an Android app using BDD and Appium, and integrate them into your continuous delivery pipeline using TeamCity.
Published on January 16, 2020
by
Filed under operations
There are two terms you need for this blog — behaviour driven development and continuous integration. Well, there are a few more, but these two stand out. In case you are not really familiar with them, here is a short abstract.
Behaviour Driven Development (BDD) is a software development methodology that helps stakeholders, technical teams and non-technical teams communicate better. The primary purpose of BDD is to make sure that each feature is correctly understood by all members of the team, before the development process starts. Basically, when doing automated testing, there is the option of writing a code or the option of using BDD. With BDD, everyone should be able to understand a product's behaviour scenarios.
Continuous integration (CI) is a software development practice in which each member of a development team integrates code changes into a shared repository a few times a day. Each integration triggers a build during which tests are run.
Using CI for developers is a great practice when working in a team. While frequently committing to a shared repository using a version control system, they are avoiding problems such as merge conflicts, bugs, code strategy divergence, duplicated code and so on. The point of using CI is to get feedback as soon as you make changes.
If you decide to implement CI, here are some guidelines for it:
Commit code frequently
Do not commit broken code
Fix broken builds immediately
Write automated tests
All tests and inspections must pass
Run private builds
Avoid getting broken code
Using these two practices, we made our Appium integration happen. To see the steps, details, and elements, keep on scrolling!
Here is a short overview of our workflow while creating automated tests:
Specifying Expected Behavior → Creating Page Object Model → Adding Step Definitions → Using Desired Capabilities → Hooks → Running the test → Gradle task
For the first part, you will need your favourite IDE. Other necessities can be installed throughout the project.
Pictured below is an overview of the project structure after following the steps in this chapter. Essentially, this is what you will end up with. 👇
To specify expected behaviour, we used the Cucumber testing framework which supports Behaviour Driven Development (BDD). Cucumber helps define application behaviour in plain text, using Gherkin — a business readable, domain-specific language that helps describe your software’s behaviour without going into the details of implementation.
To start writing behaviour steps, we needed to create a feature file in the resource bundle. Below is an example of a feature file with a scenario for a successful login into an Android application.
@regression
Feature: User Login and Logout
Scenario Outline: Successful Login and Logout
When user enter username <user>
And enter password <password>
And tap on login
Then user is successfully logged in
Examples:
|user | password |
|test | 12345 |
After that, you need to start the UI Automation, which is not a difficult task. Just find UI elements and perform operations on them.
The best way to organise your UI tests is to create a separate class file for finding, filling and verifying the elements.
We created the class LoginPage that is located in the package pageobjects. This design pattern is called Page Object Model and it has become very popular in test automation.
package pageobjects;
import io.appium.java_client.android.AndroidDriver;
import io.appium.java_client.android.AndroidElement;
import io.appium.java_client.pagefactory.AndroidFindBy;
import io.appium.java_client.pagefactory.AppiumFieldDecorator;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import settings.Driver;
public class LoginPage {
@AndroidFindBy(id = "editLogin")
private AndroidElement txtUsername;
@AndroidFindBy(id = "editPasswordLogin")
private AndroidElement txtPassword;
@AndroidFindBy(id = "login")
private AndroidElement btnLogin;
@AndroidFindBy(accessibility = "open")
private AndroidElement btnOpen;
private WebDriverWait wait = new WebDriverWait(Driver.instance(), 10);
public LoginPage(AndroidDriver driver) {
PageFactory.initElements(new AppiumFieldDecorator(driver), this);
}
public void enterUsername(String email) {
txtUsername.sendKeys(email);
}
public void enterPassword(String password) {
txtPassword.sendKeys(password);
}
public void clickOnLoginButton() {
btnLogin.click();
}
public void checkLogging() {
wait.until(ExpectedConditions.visibilityOf(btnOpen));
}
}
The next step is linking Step Definition and Gherkin steps. Step Definition is a method in a class with an annotation above it. This annotation is what links the Step Definition to all the matching Gherkin steps, stored in the resources feature file. We needed to add a step definition method for every step in the feature file. Because we were associating methods to steps, we ensured that Cucumber executed every method for the matching step in the scenario.
package stepmethods;
import cucumber.api.java.en.And;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;
import pageobjects.LoginPage;
import settings.Driver;
public class LoginSteps {
private LoginPage loginPage;
@When("^user enter username (.*)$")
public void userEnterUsername(String username) {
loginPage = new LoginPage(Driver.instance());
loginPage.enterUsername(username);
}
@And("^enter password (.*)$")
public void enterPassword(String password) {
loginPage.enterPassword(password);
}
@And("^tap on login$")
public void tapOnLogin() {
loginPage.clickOnLoginButton();
}
@Then("^user is successfully logged in$")
public void userIsSuccessfullyLoggedIn() {
loginPage.checkLogging();
}
}
Using desired capabilities, our tests can communicate with an Appium server by sending a POST request with a set of keys and values. They tell the server what kind of session needs to be started.
In the same class, we defined a method which initializes the Android driver by setting up the URL where the Appium server will run and by adding capabilities that we have previously defined.
package settings;
import io.appium.java_client.android.AndroidDriver;
import io.appium.java_client.remote.MobileCapabilityType;
import org.openqa.selenium.remote.DesiredCapabilities;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
public class Driver {
private static AndroidDriver driver;
public static AndroidDriver instance() {return driver;}
private DesiredCapabilities capabilities;
private File filePath = new File(System.getProperty("user.dir"));
private File appDir = new File(filePath, "/app");
private File app = new File(appDir, "TestApp.apk");
public void setCapabilities() {
capabilities = DesiredCapabilities.android();
capabilities.setCapability(MobileCapabilityType.PLATFORM_NAME, "Android");
capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "Nexus");
capabilities.setCapability(MobileCapabilityType.PLATFORM_VERSION, "6.0.1");
capabilities.setCapability(MobileCapabilityType.FULL_RESET, "true");
capabilities.setCapability(MobileCapabilityType.NEW_COMMAND_TIMEOUT, "120");
capabilities.setCapability(MobileCapabilityType.APP, app.getAbsolutePath());
}
public void initializeDriver() throws MalformedURLException {
String url = "http://0.0.0.0:4723/wd/hub";
driver = new AndroidDriver(new URL(url), capabilities);
}
public static void quit() { driver.quit(); }
}
Cucumber supports hooks, blocks of code that allow us to better manage the code workflow and help us reduce code redundancy. Methods annotated with the @Before annotation are going to be executed before every scenario. Similar to that, methods annotated with @After run after every scenario. Regardless of whether the scenario passes or fails, the hooks always run.
package stepmethods;
import cucumber.api.java.After;
import cucumber.api.java.Before;
import settings.Driver;
import java.net.MalformedURLException;
public class Hooks {
@Before
public void before() throws MalformedURLException {
Driver driverSetup = new Driver();
driverSetup.setCapabilities();
driverSetup.initializeDriver();
}
@After
public void after() {
if (Driver.instance() != null) {
Driver.quit();
}
}
}
After defining and implementing all of the tests, we needed to add a runner to our project. Cucumber uses the JUnit framework to run tests. The class in charge of running tests is named RunTest and it contains the following annotations:
@RunWith() – tells JUnit what the test runner class is
@CucumberOptions() – annotation where we specify the path to feature files, the path to step definitions, defining plugins options, tags, etc.
package runtest;
import cucumber.api.CucumberOptions;
import cucumber.api.junit.Cucumber;
import org.junit.runner.RunWith;
@RunWith(Cucumber.class)
@CucumberOptions(
features = {"src/test/resources"},
glue = {"stepmethods"},
plugin = {
"pretty",
"html:build/cucumber-report/results.html"}
)
public class RunTest {
}
You can run your test using the gradle command. In the build script called build.gradle, we created a task called runTest which includes starting and stopping the Appium server. We also added a path to the runner class with which we run test scenarios. Also, before adding the gradle task, we needed to add missing plugins and dependencies to execute our tests successfully.
plugins {
id 'java'
id "com.zasadnyy.appium" version "1.0.1"
}
group 'co.arsfutura.songbook'
version '1.0-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
testCompile group: 'junit', name: 'junit', version: '4.12'
compile group: 'io.appium', name: 'java-client', version: '7.0.0'
testCompile "info.cukes:cucumber-junit:1.2.5"
testCompile "info.cukes:cucumber-java:1.2.5"
}
tasks.withType(Test) {
systemProperties = System.getProperties() as Map<String,?>
systemProperties.remove("java.endorsed.dirs")
}
task runTest(type: Test) {
appium {
address "0.0.0.0"
port 4723
}
include '**/runtest/RunTest.class'
outputs.upToDateWhen { false }
}
Great, now we have our automated tests! Let’s move on to CI integration.
TeamCity is a user-friendly continuous integration (CI) server that supports building and deploying different types of projects. It is intended for professional developers, QA engineers and DevOps engineers.
If you get stuck while executing tests in the CI chain, the next few steps will provide tips to integrate your automation tests with TeamCity.
As this previous test has been written for testing an Android application, we will assume that you have already created a project in TeamCity. This project will build your Android application and stores the application .apk file as a build artifact. In our case, that build is called BuildAPKs and the path to the artifact is app/build/outputs/apk.
In the first step, we needed to create a build configuration that will be used to run our tests. Go to your project screen in TeamCity → General Settings and click Create Build Configuration.
In the next screen, you will be asked to pick the VCS repository where our code is stored and provide a username and password if you need to authenticate.
Now, if you click on Proceed, you will be informed that the connection to the VCS repository has been verified. You can name this build configuration and proceed to the next step.
In the next step, you need to add a new build step which will start the gradle task runTest that we implemented in our test project. So, after clicking on Add Build Step, from the Runner type dropdown you will need to select Gradle then enter "runTest" in the Step name text box. Then, most importantly, you need to specify the name of gradle task (runTest in our test project). Once the build step has been created, you can run your build.
After creating your build configuration, you can always manually trigger the build by clicking the Run button. If you want to automatically run builds, you can do that with the help of Triggers.
When you click on Add new trigger, in the pop-up window:
Select Finish Build Trigger from the drop-down
In the Build configuration drop-down select the configuration that is used for building applications .apk file
Click on the radio button to enable option Trigger after successful build only
Leave Branch Filter as default
In the previous step, we added a trigger which will add a build step called Run UI Test to the queue and run it after the build step Build APKs finishes successfully. As of now, we are building an .apk file for our Android application and automatically starting the test.
If your runTest build needs to be a part of some bigger pipeline, you will need to add it to your build chain. On the Dependencies tab of the Run UI Test build step, click on the Add New Snapshot Dependency button and select the Build APKs build step. You can leave other options as they are, for now.
The final step is to run our test using the .apk that is produced in the latest Build APKs build. To do that, we need to add Artifact Dependencies which allows us to use the output of the Build APKs build in the Run UI Test build. When the build Build APKs is successfully finished, the necessary artifacts (.apk file) will be downloaded to the agent before Run UI Test build starts. To add artifact dependencies, click on the Add New Artifact Dependency on the Dependencies tab of Run UI Test. On the pop-up window select:
Depend on: BuildAPKs – specify the build configuration for the current build configuration to depend on.
Get artifacts from: Build from the same chain – specify the type of build whose artifacts are to be taken.
Artifact rules: app/build/outputs/apk/release/app-release.apk=>app – specify the path of the artifacts (app/build/outputs/apk/release/app-release.apk), path of the source build to be downloaded, and the location (app) on the agent they will be downloaded to before the depends build starts.
There you have it! It might have a few steps, but it is nothing you cannot handle. If you get stuck in the process, feel free to reach out to us and ask. Good luck! 🍀
Join our newsletter
Like what you see? Why not put a ring on it. Or at least your name and e-mail.
Have a project on the horizon?