Web and Mobile Automation Inside CI Environments
Check out the amazing features the continuous integration environment has to offer.
Introduction
I’m back with another blog post (please, hold the applause 🤭 ). This time, I’m writing about the integration process of your mobile and web automation projects with a continuous integration cloud provider, creating a CI environment, and connecting it with Slack. I’ll cover more than just the CI, pipeline automation, cloud provider, or Slack integration, so stay tuned.
This blog is an addition to my first one, where I covered the basics of Cucumber BDD with Selenium and Java. If you still haven’t read it, I strongly advise you to, and not just because I wrote it, but because you’ll understand this one much better.
I think the quality of my jokes has decreased by 20% since my last blog post, so without further ado, let’s CI what I prepared for this one (get it?).
Continuous Integration
I’ll start by talking about the meaning and benefits of a Continuous Integration environment.
Continuous integration (CI) is a practice that involves software developers making small changes and checks to their code. Due to the scale of requirements and the number of steps involved, this process is automated. This is to ensure that teams can build, test, and package their applications in a reliable and repeatable way. This is what Mr. Google would say if you were to search “What is CI?”. Google is a bit formal so I’ll put it in my own words: “It is what it is” (read in meme voice).
Let’s say you have your mobile automation test that tests login functionality for your high-end iOS application. If you were to check your login functionality, you’d have to trigger and set everything up manually (probably every day). If you integrate that project inside a CI environment, you wouldn’t have to worry about that, your tests would trigger, run, and report feedback automatically, as easy as that. When you think about it, even the name says it, Continuous Integration.
There are many continuous integration tools DevOps and people in software development use today. From TeamCity, Jenkins, Azure DevOps, AWS CodePipeline, and CircleCi, to my personal favorites, GitHub Actions and Gitlab CI. In this blog, I’ll cover GitHub Actions. I find it very similar to Gitlab CI, but I’ll cover GitHub since the project from my first blog post is already on GitHub. However, if anybody asks, I prefer Gitlab CI for my test automation.
GitHub Actions
GitHub Actions is a continuous integration and continuous deployment (delivery) (CI/CD) platform that automates your build, test, and deployment pipeline. You can create workflows that build and test every pull request to your repository, or deploy merged pull requests to environments. In our case, GitHub Actions will be used to build and run our tests on a predefined schedule.
GitHub actions provide Linux, Windows, or MacOS machines for workflow runs. We’ll use MacOS.
Let’s talk about security. GitHub actions provide many security features, such as GitHub secrets. GitHub secrets are encrypted variables you can create in your repository environment. What does it mean? I don’t know (joking). It means you can keep any variable you want private, encrypted, and under a password. That variable can be anything, an environment password or something as simple as a String. A real-life scenario would be to keep your environment access ID and password secret. Here we won’t have that, so I’ll create a GitHub Secret for our Slack webhook, but more about that later.
CI Testing Cloud Providers
As bad as the name might be, CI Testing Cloud Providers says it all. The Cloud testing service providers provide an essential testing environment as per application under test requirements. As previously mentioned, the essential environment can be a web browser on a desktop or mobile device. Let’s not forget Android and iOS applications on their respective platforms. That device is usually run on a VM (Virtual Machine) that gets killed after each test which makes the continuous integration tests reliable and less fragile.
This goes without saying, Testing Cloud Providers offer a lot more than just desktop and mobile services. Depending on the provider, some other functionalities are:
- Virtual mobile devices cloud for mobile or web testing
- Real mobile devices for mobile or web testing
- CI integration with daily runs and schedules
- Bug tracking
- Test recording with test playbacks
- Parallel testing
- Reporting and analysis
- Debugging
These are just some of the many Testing Clouds offers. By now, it’s pretty clear why automated testers use them.
It’s time to mention some of the most reliable cloud providers currently on the market. I’ll start with SauceLabs and BrowserStack. They’re pretty similar and both offer many functionalities. There’s also SonarCloud, Testsigma, CloudQA and many more.
In this blog, I’ll cover my favorite and most frequently used cloud provider, SauceLabs.
SauceLabs
Okay, let’s talk Sauce. We have sour cream, sweet and sour cream, grilling sauce… No, wait, wrong chat…
Jokes aside, SauceLabs, in my opinion, is the best Testing Cloud provider on the market. It provides all the mentioned functionalities, has great documentation, and works on new integrations and services. SauceLabs service comes at a certain price, but if you’re working on a serious automation project, you should consider it. If you don’t prefer Sauces, perhaps try BrowserStack next.
Additionally, I’ll integrate my local project to run on the SauceLabs cloud. What does that mean? Well, it means that once I click run on my local machine, the browser won’t start locally. The browser will start on the SauceLabs cloud on a newly born VM, record video playback, output the console log, run the test, and finally, kill the VM. What more could you ask for!
Project structure
Before we start, I’ll cover my project structure and technologies.
So what does this tell us? Let’s break it down.
We have a local test automation project. In my case, the project is the same as in my first blog post. If you haven’t read it yet, now might be the time. Throughout this blog, we’ll cover everything else seen in the image.
I’ll connect my local project to SauceLabs which means my tests won’t run locally anymore. The tests will run on the SauceLabs cloud, as I explained earlier. SauceLabs cloud will provide live video playback, video reproduction, and console log output. Awesome.
Now let’s talk about GitHub. As can be seen in the image, GitHub does quite a lot of heavy lifting. I’ll create a workflow file that will trigger our tests to run on SauceLabs and put it on a schedule. Also, since I’m using Gradle and cucumber in my local project, GitHub itself will give us Gradle and cucumber console output. Finally, through the GitHub workflow file, I’ll create a connection to Slack so I can see notifications (pass/fail) immediately after the automated tests finish running. As you can see, when this is finished, you can relax and let your tests run for you.
I believe that’s everything. Let’s start coding!
Connecting to SauceLabs
So, my local project is Selenium with Cucumber and Java. Once we add the SauceLabs integration, our Driver (Hooks) Java class will look like this:
public class Hooks {
public static RemoteWebDriver driver;
public static WebDriverWait wait;
public Config config;
@Before
public void setup(Scenario scenario) throws IOException {
config = new Config();
String browserName = config.getBrowser();
String username = System.getenv("your sauce username");
String accessKey = System.getenv("your sauce access key");
String sauceUrl = "https://username:accesskey@ondemand.region.saucelabs.com:443/wd/hub";
MutableCapabilities capabilities;
MutableCapabilities sauceOpts = new MutableCapabilities();
switch (browserName) {
case "chrome":
capabilities = new ChromeOptions();
capabilities.setCapability("browserName", browserName);
capabilities.setCapability("browserVersion", "latest");
capabilities.setCapability("platformName", "Windows 10");
sauceOpts.setCapability("username", username);
sauceOpts.setCapability("accessKey", accessKey);
sauceOpts.setCapability("name", "CHROME TEST" + scenario.getName());
sauceOpts.setCapability("screenResolution" , "1920x1080");
capabilities.setCapability("sauce:options", sauceOpts);
URL url = new URL(sauceUrl);
driver = new RemoteWebDriver(url, capabilities);
wait = new WebDriverWait(driver, Duration.ofSeconds(70));
break;
case "safari":
capabilities = new SafariOptions();
capabilities.setCapability("browserName", browserName);
capabilities.setCapability("browserVersion", "14");
capabilities.setCapability("platformName", "macOS 11");
sauceOpts.setCapability("username", username);
sauceOpts.setCapability("accessKey", accessKey);
sauceOpts.setCapability("name", "SAFARI TEST: " + scenario.getName());
sauceOpts.setCapability("screenResolution", "1600x1200");
capabilities.setCapability("sauce:options", sauceOpts);
url = new URL(sauceUrl);
driver = new RemoteWebDriver(url, capabilities);
wait = new WebDriverWait(driver, Duration.ofSeconds(70));
break;
case "chromeMac":
capabilities = new ChromeOptions();
capabilities.setCapability("browserName", "chrome");
capabilities.setCapability("browserVersion", "latest");
capabilities.setCapability("platformName", "macOS 12");
sauceOpts.setCapability("username", username);
sauceOpts.setCapability("accessKey", accessKey);
sauceOpts.setCapability("name","CHROME MAC TEST: " + scenario.getName());
sauceOpts.setCapability("screenResolution" , "1600x1200");
capabilities.setCapability("sauce:options", sauceOpts);
url = new URL(sauceUrl);
driver = new RemoteWebDriver(url, capabilities);
wait = new WebDriverWait(driver, Duration.ofSeconds(70));
break;
default:
capabilities = new ChromeOptions();
capabilities.setCapability("browserName", browserName);
capabilities.setCapability("browserVersion", "latest");
capabilities.setCapability("platformName", "Windows 10");
sauceOpts.setCapability("username", username);
sauceOpts.setCapability("accessKey", accessKey);
sauceOpts.setCapability("name", "CHROME TEST: " + scenario.getName());
sauceOpts.setCapability("screenResolution" , "1920x1080");
capabilities.setCapability("sauce:options", sauceOpts);
url = new URL(sauceUrl);
driver = new RemoteWebDriver(url, capabilities);
wait = new WebDriverWait(driver, Duration.ofSeconds(70));
break;
}
}
@After
public void teardown (){
driver.quit();
}
@AfterStep
public void afterStep (Scenario scenario) {
if(scenario.isFailed()){
driver.executeScript("sauce:job-result=" + false);
}
else driver.executeScript("sauce:job-result=" + true);}
}
Okay, this is a lot of code at once so I’ll break everything down.
Roughly speaking, my Hooks class is mostly about creating a driver for SauceLabs. The driver will no longer be WebDriver, it will be RemoteWebDriver because we’re connecting to remote (SauceLabs cloud) via URL. The URL consists of your SauceLabs username, access key, and region. In my example, those values are hidden for security reasons.
I’m using Cucumber’s “Before” and “After” hooks. All the code inside the Before hook will execute before running test scenarios. Finally, the code inside the After hook will execute at the end of running test scenarios. We want our Sauce connection established before running our scenarios, so I put them inside the Before hook.
After the run of test scenarios, we want to finish the test and kill the driver, so that it’s inside the After hook.
The AfterStep hook is also provided by Cucumber. This hook is very important. In this hook, we have a simple “if” loop that puts scenario results to pass or fail. This is possible because Cucumber can give us results after each step, so we just add a driver script for SauceLabs, and voila.
SauceLabs capabilities are pretty self-explanatory, so I won’t go into detail. We set scenario names, browser names, browser versions, platform versions, resolutions, and so on.
In the beginning, I initialized the Config class. What is it? Config is another Java class I’ve created for navigating between browsers. Why? Well, because when my automated tests run on a schedule, they test all the browsers needed. How did I create this? Well, I did it my way. There are a lot of possibilities, but I’ll show you mine.
To switch between browsers you have to create a file where you’ll write properties, in our case, the browser name. So, I’ve created a file in the root of my project and named it gradle.properties. Finally, I’ve created a new Java class called, you guessed it, Config.java. It looks something like this:
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;
public class Config{
public Properties properties;
public Config() {
properties = new Properties();
try {
FileInputStream inputStream = new FileInputStream("gradle.properties");
properties.load(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
}
public String getBrowser() {
return properties.getProperty("browser");
}
}
As you can see, this class is simple and effective. I used the FileInputStream method pointed at a previously created gradle.properties file. Also, I’ve created a dynamic String method called getBrowser which will get a “browser” value from gradle.properties. For now, the gradle.properties file will remain empty because we don’t want to run our tests locally, therefore, we’ll write browser values using the Gradle task. More on that later on.
Gradle
As shown in my first blog post (see, you should’ve read it by now), I’m using the Gradle task for running my tests. Why? Well, because it is pretty easy to implement inside a CI/CD environment (I can run tests just by using a single CLI command). Now, let me show you how I write browser properties before running my tests. My Gradle task for running tests on Safari looks something like this:
task runTestsSafari (type: WriteProperties) {
outputFile = file('gradle.properties')
property 'browser', 'safari'
dependsOn assemble, testClasses
doLast {
javaexec {
main = "io.cucumber.core.cli.Main"
classpath = configurations.cucumberRuntime + sourceSets.main.output + sourceSets.test.output
args = [
'--plugin', 'pretty',
'--plugin', 'html:target/cucumber-report.html',
'--glue', 'stepMethods',
'src/test/resourcesSafari']
}
}
}
The task type is set to WriteProperties because, well, you know why. We need to write the property and value of “browser”. So, the first block of code will write “browser=safari” inside our gradle.properties file. With that, our Hooks class will enter our switch loop and select “case = safari”. Now, all I have to do is create as many tasks as I want for as many browsers and platforms as needed.
GitHub Actions
Let’s get to the part we’ve all been waiting for. I’ll show you how to configure the GitHub workflow file. First and foremost, I’ll upload my project to GitHub. Once that’s done, I’ll create a new folder in the root of my project called .github. Under .github I’ll add another folder called workflows. Finally, I’ll add an empty .yml file and name it main.
Inside my main.yml file I’ll add something like this:
name: Java CI
on:
schedule:
- cron: '45 9 * * 1-5'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'adopt'
- name: Validate Gradle wrapper
uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b
- name: Build with Gradle
uses: gradle/gradle-build-action@0d13054264b0bb894ded474f08ebb30921341cee
with:
arguments: runTestsSafari
Let’s break it down. In the first line, I just gave my workflow a basic name, Java CI. The following lines show I’ve put this workflow on a cron schedule. For more information about cron syntax, visit crontab.guru. My schedule is set every day from Monday till Friday at 9:45 AM. Be careful with time zones because GitHub uses UTC. I give my pipeline a job (build) and set it to run on the latest Ubuntu. Under steps, I’ve put some formal Java technologies and versions for my pipeline to use. After validating the Gradle wrapper, we can finally start building with Gradle and run our task. Set like this, my tests run daily at 9:45 and test Safari. At this point, I can see the Gradle and Cucumber output provided by GitHub, and I can see my test video playback on SauceLabs. Well, that’s not enough for me, I’m lazier than that. Let’s connect everything with Slack so I’m instantly notified if my tests have passed or failed.
Slack integration with GitHub Actions
To connect your GitHub project with Slack I’ll use a certain runner. I’ve tried many different runners provided by different organizations, but I only liked one. The runner I’m using is provided by rtCamp and can be found here.
Easy there, soldier. Before we edit our .yml file, we need to do a couple of things. Firstly, we need to add an incoming Webhook connection to our Slack workspace. You can see more details here. I can’t provide much information about this step because it’s mostly related to my organization’s Slack. Please check the link, it should be simple. When you’re finished creating your Webhook, copy it.
Next, open GitHub and click on secrets. We need to create a new secret on GitHub. So, we click to create a new secret and name it, in my case, SLACK_WEBHOOK_URL. Paste the copied value of your webhook in the previous step. After saving, GitHub will encrypt our webhook and keep it, you guessed it, a secret.
And finally, now we may edit our .yml GitHub workflow file:
- name: Send success
if: success()
uses: rtCamp/action-slack-notify@v2
env:
SLACK_CHANNEL: automated-tests-notifications
SLACK_LINK_NAMES: true
SLACK_COLOR: ${{ job.status }}
SLACK_ICON: https://github.com/rtCamp.png?size=48
SLACK_TITLE: Your tests have successfully passed
SLACK_USERNAME: roBot
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL}}
- name: Send failure
if: failure()
uses: rtCamp/action-slack-notify@v2
env:
SLACK_CHANNEL: automated-tests-notifications
SLACK_LINK_NAMES: true
SLACK_COLOR: ${{ job.status }}
SLACK_ICON: https://github.com/rtCamp.png?size=48
SLACK_TITLE: Danger, danger, automated tests have failed
SLACK_USERNAME: roBot
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL}}
This may seem pretty self-explanatory, but I’ve had many issues with the connection between GitHub Actions and Slack. What I’m saying is, try your best to use this as similar as I did, because even the smallest adjustments can break it.
As you can see, I’ve created two ifs. Both ifs act on the output of our Gradle runner. Now we’re getting to the best part of Gradle. Let’s not forget, our tests are running using the Gradle tasks. So, when the Gradle task finishes running, it creates an output that can also be read by our .yml file. It’s simple, but it took me a while to realize just how powerful and helpful that is. Moving on, now our .yml file knows which if loop to enter based on our test results. NICE!
As you can see, SLACK_WEBHOOK is also present here. We can get any encrypted value (secret) from our GitHub secrets using this syntax; ${{secrets.YOUR_SECRET_NAME}}
Knowing that we can use GitHub secrets to hide any sensitive information. I’ll let you try that yourself.
Conclusion
This was yet another quick explanation of how to connect all our technologies to work together as one big great machine.
Hopefully, this blog will help you in your daily struggles with web or mobile automation inside continuous integration environments and provide you with some new scalability, experiences and information. In my own experience, setting web/mobile automation inside a continuous integration environment was the pinnacle of many projects I worked on.
As I said, this blog is a bit more advanced than my previous one. I’m going to say it right away, I’ll be back with part three. Then I’ll explain more advanced and specific technologies. Not gonna lie, I did withhold some information in this one, but with a good purpose — I didn’t want to make the blog too long or complicated.
Stay tuned,
Josip, out 🫳 🎤
Mic drop sound