Magazine

Cucumber BDD with Selenium and Java

Take a wild ride through the ins and outs of automated testing with ­Cucumber 🎢

Introduction

Hello there, my name is Josip. I’m currently working on automation testing for a legendary company, Ars Futura. All jokes aside, I’ve always been interested in automation testing using Selenium. But, let's start from the beginning. When I was a young high school freshman, I learnt Java on my own. I always found it to be the most “logical” programming language (I wonder who’s triggered by that statement he he).

On a serious note, when I found out automated tests are a thing, I immediately started looking it up. Writing my first test - I liked it, executing my first test - I loved it. At first, I was only using basic Selenium with Java to create tests. When I started working at Ars Futura, my team lead introduced me to Cucumber and Behavior Driven Development which really takes automation to the next level.

In this blog, I will introduce you to all of the technologies used in creating clean coded, resilient to change, and easy-to-understand automated tests. Hopefully, you’ll learn a thing or two about automated software testing, or at least see the approach I take while creating it.

Selenium

What better to begin with than the open-source automation freeware that made all of this possible. Selenium is the most popular automation tool, used for authoring functional tests, without the need to learn a test scripting language (Selenium IDE). It also provides a test domain-specific language to write tests in a number of popular programming languages. These include JavaScript (Node.js), C#, Groovy, Java, Perl, PHP, Python, Ruby, and Scala. Browsers Mozilla Firefox, Google Chrome, Safari, and Internet Explorer are all supported by Selenium. Every example you’re about to see reading this post is going to be using Java as a programming language.

Selenium WebDriver

Selenium WebDriver is the most important component used for executing cross-browser automated tests. Selenium provides drivers specific to each browser. Without revealing the internal logic of browser functionality, the browser driver interacts with the respective browser by establishing a secure connection. These browser drivers are also specific to the language used for test case automation like C#, Python, or in this case, Java.

Let's go over a simple usage of Selenium WebDriver. In this example, the driver will open Google Chrome, maximize the window size, navigate to Google, and search for YouTube.

The first step is creating a WebDriver instance, in this case, it is for Google Chrome. Below, you can see a simple example of ChromeDriver creation.

System.setProperty("chrome.driver", "path-to-driver");
WebDriver driver = new ChromeDriver();
driver.manage().window().maximize()

In the first row, you tell the system where your previously downloaded Chrome driver is located. The following code initializes the driver to run as a Chrome driver. Once the browser is opened, its window is maximized.

The second step would be to open the URL to Google:

driver.manage().timeouts().wait(3000);
driver.get("https://google.com");

I believe you’ve all noticed the first row where wait is declared. Why wait? Well, that’s because the driver works fast and it would enter the URL before Chrome is even opened, and the test would fail. Therefore, implicit wait is created, which in this case is set to 3000 milliseconds. At this moment, we have Chrome open and the Google URL opened as shown in the picture above.

Now, let's put all of this on pause and talk about locators. A locator is a way to identify elements on a desired web page or application. The locator is the main argument passed to the finding elements method. Some of the different locators in Selenium are as follows: ID, CSS ClassName, name attribute, xpath, etc.

Now, knowing all that, let’s resume our automation. So, we are on Google and we need to tell Google to open YouTube. Once the Google search bar is inspected (as shown in the image below):

image-google

In the console we can see the element's attributes:

image-attributes

Now, using Selenium locators, we can use these attributes to pinpoint the exact element we want to interact with. In this scenario, let's use its name, which in this case is "q".

driver.findElement(By.name("q")).sendKeys("Youtube");

As seen above, we provide a locator (name) so the driver knows which element to interact with. Also, we are able to send the desired string in the same command using the sendKeys function. At this moment we have the "YouTube" value sitting in the Google search bar. Logically, now we want to press the search button. In the example below, I have previously saved the search button as an element itself so it can be used multiple times:

WebElement searchIcon = driver.findElement(By.name("btnK"));
searchIcon.click();

We could’ve used .click the same way we used .sendKeys in our previous step, but saving an element simplifies the interaction with it.

This was a simple example of a very short automation. I did my best to keep it as simple as possible. Surely, you learned the basics of Selenium functionality. In the following sections, we are going to take things up a notch, to make them more powerful and, hopefully, interesting.

Cucumber, Gherkin, and BDD

Cucumber is a testing tool that supports Behavior Driven Development (BDD). It offers a way to write tests that everybody can understand, regardless of their technical knowledge. In BDD, the first thing users do is write scenarios or acceptance tests that describe the behavior of the system.

Gherkin is a set of special keywords that give structure and meaning to executable functions. The simplest way to put it would be to say Gherkin is a language for writing test scenarios, using the English language. Yes, English! Let’s take a look at a simple example of a Cucumber scenario, written in Gherkin:

Scenario Login
 Given I'm on login page
 And I enter my email
 And I enter my password
 When I click login button
 Then I should be logged in

Now, imagine someone in a parallel universe (I’m a Flash fan, don’t judge) sends you the complete automation for a Web application, with a bunch of random code in it. And now you’re wondering which functionalities does it test? Because you read this blog post, you can simply open the feature file where scenarios are found and see exactly what it tests and which steps it takes.

You’re looking at the steps and wondering what’s exactly happening behind them? Don’t worry, I got you! Let’s be a bit more thorough. Behind every single step, there is its own step definition. Step definitions is a class that's connected to the steps file and it contains methods created in POMs. POM (Page Object Model) is a way of structuring the project so that every page from an application has its own class. For example, login and signup buttons are usually found on the landing page. So, once we find those 2 buttons and save them as web elements, we will save them into a Java class called, you guessed it, LandingPO (Landing Page Object). To make it as simple as possible I have used my Paint skills to create this masterpiece:

image-structure-graph

Now that we’ve covered everything, let's take a look at a bit more advanced scenario. The following example of a test scenario will cover Selenium, Cucumber, Gherkin, BDD, and POM all at once. So buckle up and, hopefully, enjoy.

Advanced usage with an example

For this example I created a full test automation using all the mentioned technologies. You’ll see how clean and readable the code is when Cucumber takes place. Our test will do the following:

  • Open Google Chrome
  • Search Stack Overflow
  • Login to Stack Overflow
  • Open user profile
  • Edit user profile
  • Change user display name
  • Save changes
  • Check success

As we can see, the test itself is not complicated or advanced. However, the way I’ll structure the project - is. Let’s begin. If you plan on following this example and creating the test yourself, these are your preconditions:

  • IntelliJ (or any preferred IDE)
  • Gradle installed
  • Java installed
  • Stack Overflow account

The first step is creating your project. Open your IDE and create one. Choose Gradle and pick your Java JDK. Pretty self-explanatory, right? Once created, you should see your project structure on the left side (if it’s on the right side, you’re loco):

image-project-structure

The next step would be to open build.gradle and add all the dependencies that will be used. You can find the dependencies by Googling the Maven repository. Here we can see my dependencies:

dependencies {
   testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
   testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2'
   testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
   testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2'
   implementation group: 'org.seleniumhq.selenium', name: 'selenium-java', version: '4.1.2'
   implementation group: 'org.seleniumhq.selenium', name: 'selenium-server', version: '3.141.59'
   implementation group: 'org.seleniumhq.selenium', name: 'selenium-support', version: '4.1.2'
   implementation group: 'org.seleniumhq.selenium', name: 'selenium-api', version: '4.1.2'
   implementation group: 'org.seleniumhq.selenium', name: 'selenium-chrome-driver', version: '4.1.2'
   implementation group: 'org.seleniumhq.selenium', name: 'selenium-firefox-driver', version: '4.1.2'
   testImplementation group: 'io.cucumber', name: 'cucumber-picocontainer', version: '7.2.3'
   testImplementation group: 'info.cukes', name: 'cucumber-junit', version: '1.2.6', ext: 'pom'
   implementation group: 'io.cucumber', name: 'cucumber-java', version: '7.2.3'
   implementation 'io.github.bonigarcia:webdrivermanager:5.1.1'
   testImplementation group: 'junit', name: 'junit', version: '4.13.2'
   implementation group: 'info.cukes', name: 'cucumber-jvm', version: '1.2.6', ext: 'pom'
   testImplementation 'com.codeborne:phantomjsdriver:1.5.0'
   implementation group: 'info.cukes', name: 'cucumber-core', version: '1.2.6'
   implementation group: 'info.cukes', name: 'cucumber-junit', version: '1.2.6'
}

After adding the dependencies, we open up our terminal and build our project using the gradle command:

./gradlew build

Awesome! Now that we’ve set up the core of our project, it’s time for the real deal. Following Cucumber and POM, let’s create 2 packages under src/test/java: PageObjects and StepMethods.

Now, we’ll create our test scenario steps. Let’s create a feature file and put it under resources. I will name my feature file ChangeDisplayName.feature. Our project hierarchy currently looks like this:

image-project-structure-2

Now, we create our test scenario using Gherkin. It can be done in multiple ways, you try yours and I’ll show you mine:

Feature: Changing data on Stack Overflow profile
 Scenario Outline: Check if display name change works correctly
   Given I'm on Stack Overflow web page
   And I click login button
   And I login using my "<email>" and "<password>"
   And I open my profile
   And I click edit profile
   When I change my display name to "<display_name>"
   And I save changes
   Then Changes should be saved successfully

   Examples:
   | email                  | password           | display_name |
   | notrealemail@gmail.com | forsecurityreasons | Funky Monkey |

This is my feature file for the mentioned test automation. I did say this will be a bit more advanced, so I’ve added an examples table. Basically, the examples table takes values defined in steps. If we add more rows to our examples table, our test would execute multiple times. For example, having 3 rows would make the test execute 3 times, until all given values in the example table are used. Pretty great, huh?

But Josip, why are my steps highlighted? Don’t worry, it’s because these steps still have no definition behind them. If we hover over the highlighted steps and click more actions -> create all step definitions we give them a definition. Let’s do so and call our class ChangeDisplayNameSteps.java. Also, put this class under the package StepMethods if you haven’t already.

Now we can see how things are put together, very cool in my opinion. Depending on your IDE and how your local project is set up, imports should be added automatically. Here’s a screenshot of my ChangeDisplayNameSteps.java class:

package StepMethods;

import io.cucumber.java.en.And;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;

public class ChangeDisplayNameSteps {
  
   @Given("I'm on Stack Overflow web page")
   public void iMOnStackOverflowWebPage() {
   }

   @And("I click login button")
   public void iClickLoginButton() {
   }

   @And("I login using my {string} and {string}")
   public void iLoginUsingMyAnd(String arg0, String arg1) {
   }

   @And("I open my profile")
   public void iOpenMyProfile() {
   }

   @And("I click edit profile")
   public void iClickEditProfile() {
   }

   @When("I change my display name to {string}")
   public void iChangeMyDisplayNameTo(String arg0) {
   }

   @And("I save changes")
   public void iSaveChanges() {
   }

   @Then("Changes should be saved successfully")
   public void changesShouldBeSavedSuccessfully() {
   }
}

As seen in the screenshot, now we can see our step definitions. In other words, the Gherkin steps can now see their definition. Following what I’ve just explained, if we go back to our feature file, we can see the steps are no longer highlighted, meaning there is a step definition behind them. Also, values in the example table are recognized. Looking at our step definitions class, we can see the steps where we used values from the example table are read as strings and sent as arguments. Now we change our String arguments to their real name in the example table. So, our arguments become email, password, and a display name.

@When("I change my display name to {string}")
public void iChangeMyDisplayNameTo(String display_name) {
}

Okay, cool, now let’s finally start using our Java coding skills. There is no automation without our Selenium WebDriver, so we should create one. The best way would be to create a class itself that will return our driver so we can use it globally in our project. I’ll start by creating a new Java class under StepMethods called Driver.

package StepMethods;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.WebDriverWait;

import java.time.Duration;

public class Driver {

   public void createChromeDriver(){
       System.setProperty("webdriver.chrome.driver", "StackDemo/chromedriver");
   }
   public static WebDriver driver = new ChromeDriver();
 public static WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(20));
}

As we can see, our driver class is now created. I believe this one is self-explanatory. A simple class from which we’re going to use our driver and the wait instance. Wait is created with an instance of the driver and a maximum number of seconds for which the driver will wait for an element. Considering we are going to use Google Chrome, we tell the driver to start with ChromeDriver and give it its path

Now, following the POM orientation, we shall create our Landing Page Object. Once we open our Stack Overflow web, we will be located on the Landing page. Let’s fill our LandingPO with elements we’re going to use, but in doing so, we will use the power of Selenium's FindBy method. Check this out:

public class LandingPO  {

   public LandingPO (){
       PageFactory.initElements(driver, this);
   }

   private String url = "https://stackoverflow.com/";

   @FindBy (xpath = "/html/body/header/div/ol[2]/li[3]/a")
   public WebElement loginBtn;

Firstly, we start with initializing the PageFactory for our LandingPO. Without PageFactory, our FindBy method and POM orientation would not work.

As we can see, we have our URL saved as a private string. Our login button is saved as a public WebElement using the FindBy method. Now we can use our URL and login button to create methods which we will call in step definitions. I’ve used the class name to find our login button which was, in this case, really long and ugly, but this is a real-life situation and sometimes it has to be like this. We could’ve also used xpath or other locators, but for this situation, the class name was the most stable one. Of course, if this element’s class name changes, our test will fail. To solve this problem, we can add data-testid to every element we use in our automation. Having a data-testid attribute on every element we use would make the tests resilient to change. However, since Stack is a public website, we can’t do that now.

Let’s create Java methods for calling our URL and clicking the login button.

public void openStackOverflowUrl(){
   driver.get(url);
   driver.manage().window().maximize();
}

public void clickLoginBtn(){
   wait.until(ExpectedConditions.visibilityOf(loginBtn)).click();
}

Would you look at that? This is so much easier to read and write compared to what you’ve seen at the beginning of this blog post. Basically, we are opening a URL, waiting for the visibility of the login button, and once visible, clicking it. Awesome, right?

Let’s put all of this together. We have our methods, our step definitions, and our steps. Currently, our step definitions are still empty, so let's change that by calling the methods we just created.

public class ChangeDisplayNameSteps {
   LandingPO landingPO;

   @Given("I'm on Stack Overflow web page")
   public void iMOnStackOverflowWebPage() {
       landingPO = new LandingPO();
       landingPO.openStackOverflowUrl();
   }

   @And("I click login button")
   public void iClickLoginButton() {
       landingPO.clickLoginBtn();
   }

As seen in the code above, our first and second steps have functionalities behind them. The functionalities we’ve created. Knowing all of this, we then create a LoginPO java class where we’re going to store all of the elements of our login page.

public class LoginPO extends Driver {

   public LoginPO(){
       PageFactory.initElements(driver, this);
   }

   @FindBy (id="email")
   public WebElement emailInputFld;

   @FindBy (id="password")
   public WebElement passwordInputFld;

   @FindBy (id= "submit-button")
   public WebElement loginBtn;

Because we have to pull strings from our example table, it’s time to get a bit more advanced. The methods for using values from the example table look something like this:

public void enterEmail(String argEmail) {
   wait.until(ExpectedConditions.visibilityOf(emailInputFld)).sendKeys(argEmail);
}

public void enterPassword(String argPassword) {
   wait.until(ExpectedConditions.elementToBeClickable(passwordInputFld)).click();
   passwordInputFld.sendKeys(argPassword);
}

public void clickLoginBtn(){
   wait.until(ExpectedConditions.elementToBeClickable(loginBtn)).click();
}

As I said, if we want to use values from the example table, we need to pass arguments and then use them as strings in step definitions:

@And("I login using my {string} and {string}")
public void iLoginUsingMyAnd(String email, String password) {
   loginPO = new LoginPO();
   loginPO.enterEmail(email);
   loginPO.enterPassword(password);
   loginPO.clickLoginBtn();
}

In doing so, we pull values from the example table and we have a lot of possibilities. Let’s not forget, the tests will run as many times as there are rows in the example table. Pretty awesome for testing purposes if you ask me.

Hopefully, now you get the idea and the structure of our project. I will speed things up from now on because there’s no need to repeat myself. Next, we open and edit our profile, change the display name and save changes.

This is what our project will look like in the end:

LandingPO:

package PageObjects;

import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.ui.ExpectedConditions;
import static StepMethods.Driver.driver;
import static StepMethods.Driver.wait;

public class LandingPO  {

   public LandingPO (){
       PageFactory.initElements(driver, this);
   }

   private String url = "https://stackoverflow.com/";

   @FindBy (xpath = "/html/body/header/div/ol[2]/li[3]/a")
   public WebElement loginBtn;

   @FindBy (xpath = "/html/body/header/div/ol[2]/li[2]/a/div[1]/img")
   public WebElement myProfileBtn;

   public void openStackOverflowUrl(){
       driver.get(url);
       driver.manage().window().maximize();
   }

   public void clickLoginBtn(){
       wait.until(ExpectedConditions.visibilityOf(loginBtn)).click();
   }

   public void openMyProfile(){
       wait.until(ExpectedConditions.visibilityOf(myProfileBtn)).click();
   }
}

LoginPO:

package PageObjects;

import StepMethods.Driver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.ui.ExpectedConditions;

public class LoginPO extends Driver {

   public LoginPO(){
       PageFactory.initElements(driver, this);
   }

   @FindBy (id="email")
   public WebElement emailInputFld;

   @FindBy (id="password")
   public WebElement passwordInputFld;

   @FindBy (id= "submit-button")
   public WebElement loginBtn;

   public void enterEmail(String argEmail) {
       wait.until(ExpectedConditions.visibilityOf(emailInputFld)).sendKeys(argEmail);
   }

   public void enterPassword(String argPassword) {
       wait.until(ExpectedConditions.elementToBeClickable(passwordInputFld)).click();
       passwordInputFld.sendKeys(argPassword);
   }

   public void clickLoginBtn(){
       wait.until(ExpectedConditions.elementToBeClickable(loginBtn)).click();
   }

}

Let me share a quick explanation of the enterPassword method which I created here. Sometimes, elements are not as responsive as they should be. If sending keys won’t work for you too, first try clicking the element and then sending the keys.

ProfilePO:

package PageObjects;

import StepMethods.Driver;
import org.junit.jupiter.api.Assertions;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.ui.ExpectedConditions;

public class ProfilePO extends Driver {

   public ProfilePO (){
       PageFactory.initElements(driver, this);
   }

   @FindBy(xpath = "//*[@id=\"mainbar-full\"]/div[1]/div[2]/a")
   public WebElement editProfileBtn;

   @FindBy (id = "displayName")
   public WebElement displayNameFld;

   @FindBy (xpath = "//*[@id=\"form-submit\"]/button")
   public WebElement saveChangesBtn;

   @FindBy (xpath = "//*[@id=\"mainbar-full\"]/div[1]/div[1]/div/div/div[1]")
   public WebElement displayNameElement;

   public void editMyProfile(){
       wait.until(ExpectedConditions.visibilityOf(editProfileBtn)).click();
   }

   public void changeDisplayName(String argDisplay_name){
       wait.until(ExpectedConditions.visibilityOf(displayNameFld));
       displayNameFld.clear();
       displayNameFld.sendKeys(argDisplay_name);
   }

   public void saveChanges(){
       wait.until(ExpectedConditions.elementToBeClickable(saveChangesBtn)).click();
   }

   public void checkNameChangeSuccess(){
      String actualDisplayName = wait.until(ExpectedConditions.visibilityOf(displayNameElement)).getText();
      String expectedDisplayName= "Funky Monkey";
       Assertions.assertEquals(expectedDisplayName, actualDisplayName);
       driver.quit();
   }
}

ProfilePO is a bit more complicated. We can see more coding put into this class.

The changeDisplayName method waits for the display name field to be visible. Naturally, there had to be some kind of a display name already set there, so I began with clearing the field and then sending the desired display name. Lastly, I save it.

In the final part, we check if our test has succeeded. The checkNameChangeSuccess method explanation is incoming.

The main functionality that our automation tested was if the display name change works. Accordingly, after saving the changes, we’re going to take the actual and expected display name and compare them. As you can see, using the FindBy method, I found an element where my display name is located and saved it as displayNameElement. Then, I put that same display name element into a string using getText(). This is done because we need to compare the two strings (in our case the actual and expected display name) to make sure our test has passed. Saving an element using the FindBy method, as the name says, saves our display name as a Web element, not as a string. Furthermore, I created a string containing our expected display name. Finally, using Assertions, those 2 strings are compared. Assertions are a function from junit which compare 2 strings (can be int, float, byte, long etc). If our 2 strings are exactly alike, assertions return true and make our test (step) pass. Otherwise, false is returned and our test fails.

Steps class:

package StepMethods;

import PageObjects.LandingPO;
import PageObjects.LoginPO;
import PageObjects.ProfilePO;
import io.cucumber.java.en.And;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;

public class ChangeDisplayNameSteps {
   LandingPO landingPO;
   LoginPO loginPO;
   ProfilePO profilePO;

   @Given("I'm on Stack Overflow web page")
   public void iMOnStackOverflowWebPage() {
       landingPO = new LandingPO();
       landingPO.openStackOverflowUrl();
   }

   @And("I click login button")
   public void iClickLoginButton() {
       landingPO.clickLoginBtn();
   }

   @And("I login using my {string} and {string}")
   public void iLoginUsingMyAnd(String email, String password) {
       loginPO = new LoginPO();
       loginPO.enterEmail(email);
       loginPO.enterPassword(password);
       loginPO.clickLoginBtn();
   }

   @And("I open my profile")
   public void iOpenMyProfile() {
       landingPO.openMyProfile();
   }

   @And("I click edit profile")
   public void iClickEditProfile() {
       profilePO = new ProfilePO();
       profilePO.editMyProfile();
   }

   @When("I change my display name to {string}")
   public void iChangeMyDisplayNameTo(String display_name) {
       profilePO.changeDisplayName(display_name);
   }

   @And("I save changes")
   public void iSaveChanges() {
       profilePO.saveChanges();
   }

   @Then("Changes should be saved successfully")
   public void changesShouldBeSavedSuccessfully() {
       landingPO.openMyProfile();
       profilePO.checkNameChangeSuccess();
   }
}

My steps class is pretty self-explanatory so there is no need to go into details.

build.gradle:

configurations {
   cucumberRuntime {
       extendsFrom testImplementation
   }
}

task runTestsChrome () {
   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/resources']
       }
   }
}

In my build.gradle file, you can see one configuration and one task. In order to run our feature file over gradle, we need to create a task. Firstly, we add a cucumber runtime to our gradle configuration. Without it, our tests would not be runnable over gradle. Secondly, we create a task which, when run, tells gradle exactly what to do. As you can see, we’re telling gradle to use Javaexec to run our feature files as a Java application with a given classpath. Now we can simply run our test by opening our command line and running

./gradlew runTestsChrome

Conclusion

This was a quick and easy example of Cucumber BDD with Selenium and Java. I tried my best to explain things and I truly hope you liked it, I know I did.

Test automation is a really interesting and powerful field in the software development process. Catching bugs (especially the ones that are hard to reproduce) improves Web or mobile application quality. In case the automated tests are configured properly, they can be executed daily (or even hourly) without the tester’s attention. We’ve just scratched the surface with this blog post and there are endless possibilities with test automation. For instance, using any continuous integration tool so the tests can be triggered by any event or set to any schedule. Of course, in order to achieve self-maintaining and code-change-resilient tests, a lot of work has to be done. API testing is also worth mentioning. It plays a powerful role in creating low or no-maintenance automated tests.

Now that I’ve mentioned CI (continuous integration), API testing, mobile testing, and automated tests that require little or no maintenance, I hope you felt a little tingle in your spine and you’d like to know more about it. Maybe in my next blog post, huh?

Josip, out.

*Mic drop sound*

Leave a comment Be the first!