Automation Framework - Part 1

A bare minimum automation framework

·

7 min read

Introduction

This article will provide a high-level overview of creating the framework from scratch. It explains how to set up the project and the tools needed to make the framework as effective as possible.


Prerequisites

This framework depends on the following tools and concepts.

  • Visual Studio Code (or any IDE of your choice)

  • NodeJS

  • NPM

  • Selenium WebDriver


Structure

Any framework you design should be organized for better maintenance.

It is important to understand the overall project structure, below is the folder structure of this project.

config - all configurations will reside here

logs - store all types of logs

reports - reports are saved here

pages - to store locators and other details using the page object design

specs - contains test cases

utils - all global scripts

data - to store all relevant test data (optional in our case)

A typical project structure is as below.

image.png


Implementation

Step 1 - Initialize Project

Create a project directory and open it with an IDE.

mkdir e2e && cd e2e && code .

We will be using npm as the default package manager. So, let's initialize the project root.

npm init -y

The "-y" flag will initialize and create a package.json file with default values as below.

{
  "name": "e2e",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

Step 2 - Install Dependencies

Install the following dependencies.

npm install selenium-webdriver selenium-standalone winston config chai mocha mochawesome

It will include all packages as a project dependency in the package.json file.

"dependencies": {
    "chai": "^4.3.6",
    "config": "^3.3.7",
    "mocha": "^10.0.0",
    "mochawesome": "^7.1.3",
    "selenium-standalone": "^8.2.0",
    "selenium-webdriver": "^4.4.0",
    "winston": "^3.8.1"
  }

Dependency

chai - an assertion library

config - to handle global configurations

mocha - test framework

mochawesome - custom reporter in combination with the test framework

selenium-standalone - NodeJS based cli for launching selenium server

selenium-webdriver - browser automation library

winston - logger to write custom logs


Step 3 - Create Folder Structure

Let's create the required folders.

mkdir config pages specs utils logs reports

Step 4 - Start Scripting

The utils folder will store generic scripts used globally.

So, under utils create a file driver.js to initialize the "driver" as a global variable.

Note: Dynamic variables are used to read data from the config file instead of hardcoded values.

const { Builder } = require("selenium-webdriver");
const log = require("./logger");
const config = require("config");

const browser = process.env.BROWSER || config.get("browser");
const protocol = process.env.PROTOCOL || config.get("protocol");
const host = process.env.HOST || config.get("host");
const port = process.env.PORT || config.get("port");
const seleniumAddress =
  process.env.seleniumAddress || `${protocol}://${host}:${port}`;

let driver = new Builder()
  .forBrowser(browser)
  .usingServer(seleniumAddress)
  .build();

class Driver {
  constructor() {
    global.driver = driver;
    log.info("Driver has Initialized");
    log.info(`Opening ${browser} browser`);
  }
}

module.exports = new Driver();

Next, add a logger.js file in the utils folder to generate logs.

const { createLogger, format, transports } = require("winston");
const { combine, timestamp, printf, colorize } = format;

const logFormat = printf(({ message, timestamp }) => {
  return `${timestamp} => ${message}`;
});

const logger = createLogger({
  format: combine(
    colorize(),
    timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
    logFormat
  ),
  transports: [
    new transports.File({ filename: "./logs/automation.log" }),
  ],
});

module.exports = logger;

Now, we will write generic functions to handle selenium methods.

Let's add a common.js file under utils folder.

require("./driver");
require("chai").should();
const { By, Key, until } = require("selenium-webdriver");
const config = require("config");
const log = require("./logger");
const env = process.env.NODE_ENV || "prod";

const baseURL = config.get("url");

class Common {
  async getURL() {
    await driver.get(baseURL);
    log.info(`The test will run on ${env} environment`);
    log.info(`The url is: ${baseURL}`);
  }

  async setImplicitWait() {
    await driver.manage().setTimeouts({ implicit: 10000 });
    log.info("Set Implicit Wait Globally");
  }

  async getMaximizeWindow() {
    await driver.manage().window().maximize();
    log.info("Browser window maximized");
  }

  async getDeleteAllCookies() {
    await driver.manage().deleteAllCookies();
    log.info("Deleted Browser Cookies");
  }

  async getWindowTitle(windowTitle) {
    let title = await driver.getTitle().then(function (value) {
      return value;
    });

    await title.should.equal(windowTitle);
    log.info(`The title on browser window is ${windowTitle}`);
  }

  async getElementLocator(locator, locatorType) {
    let element = null;
    let timer = 30000;
    if (locatorType === "id") {
      element = await driver.wait(until.elementLocated(By.id(locator)), timer);
    } else if (locatorType === "xpath") {
      element = await driver.wait(
        until.elementLocated(By.xpath(locator)),
        timer
      );
    } else if (locatorType === "name") {
      element = await driver.wait(
        until.elementLocated(By.name(locator)),
        timer
      );
    } else if (locatorType === "css") {
      element = await driver.wait(until.elementLocated(By.css(locator)), timer);
    } else if (locatorType === "linkText") {
      element = await driver.wait(
        until.elementLocated(By.linkText(locator)),
        timer
      );
    } else if (locatorType === "partialLinkText") {
      element = await driver.wait(
        until.elementLocated(By.partialLinkText(locator)),
        timer
      );
    }
    return element;
  }

  async getSendKeys(locator, locatorType, locatorName, sendValue) {
    let element = await this.getElementLocator(locator, locatorType);
    if (element.isDisplayed()) {
      await element.clear();
      await element.sendKeys(sendValue);
      log.info(`Entered value into ${locatorName}`);
    } else {
      log.error(`${locatorName} not found`);
    }
  }

  async getClick(locator, locatorType, locatorName) {
    let element = await this.getElementLocator(locator, locatorType);
    if (element.isEnabled()) {
      await element.click();
      log.info(`Clicked ${locatorName}`);
    } else {
      log.error(`${locatorName} is disabled`);
    }
  }

  async getQuitBrowser() {
    await driver.quit();
    log.info("Browser Closed");
  }
}

module.exports = new Common();

Test cases will evolve over time. So, as a best practice, we should initialize and quit the browser sessions once per every test case or once per group of test cases.

Let's handle it, create a file startup.js under the utils folder.

const common = require("./common");

before(async () => {
  await common.getMaximizeWindow();
  await common.getDeleteAllCookies();
  await common.getURL();
  await common.setImplicitWait();
});

after(async () => {
  await common.getQuitBrowser();
});

Now, let's extend the utils capabilities to design our page object.

The pages in scope are Login & Logout features. So, create two files, loginPage.js & logoutPage.js under pages folder.

const common = require("../utils/common");
const config = require("config");

const username = config.get("username");
const password = config.get("password");

const USERNAME = "username";
const PASSWORD = "password";
const LOGIN = "//form/button[@type='submit']";

class LoginPage {
  async loginToApp() {
    await common.getSendKeys(USERNAME, "id", "username textbox", username);
    await common.getSendKeys(PASSWORD, "id", "password textbox", password);
    await common.getClick(LOGIN, "xpath", "login button");
  }
}

module.exports = new LoginPage();
const common = require("../utils/common");

const LOGOUT = "//div[@class='example']/a";

class LogoutPage {
  async logoutFromApp() {
    await common.getClick(LOGOUT, "xpath", "logout button");
  }
}

module.exports = new LogoutPage();

Finally, it is time to write test cases using the page object logic.

We will create two test cases, loginSpec.js & logoutSpec.js under specs folder.

const common = require("../utils/common");
const loginPage = require("../pages/loginPage");
require("../utils/startup");

describe("title test", function () {
  it("verify the title on login page", async function () {
    await common.getWindowTitle("The Internet");
  });
});

describe("login Test", function () {
  it("verify user is logged in successfully!", async function () {
    await loginPage.loginToApp();
  });
});
const logoutpage = require("../pages/logoutPage");
require("../utils/startup");

describe("logout test", function () {
  it("verify user is logged out successfully!", async function () {
    await logoutpage.logoutFromApp();
  });
});

Create a file default.json under the config folder.

Note: This is a sample login page used in this framework, if you want to know more check out this atGitHub

{
  "url": "https://the-internet.herokuapp.com/login",
  "username": "tomsmith",
  "password": "SuperSecretPassword!",
  "browser": "chrome",
  "protocol": "http",
  "host": "localhost",
  "port": 4444
}

Next, we are using mocha as the test framework, add a conf.js file at the project root.

Note: This framework supports parallel execution, it is set to false as the test cases are limited.

module.exports = {
  timeouts: 60000,
  exit: true,
  bail: true,
  slow: 1000,
  recursive: true,
  parallel: false,

  spec: ["./specs/loginSpec.js", "./specs/logoutSpec.js"],

  reporter: "mochawesome",
  require: "mochawesome/register",
  reporterOption: [
    "reportDir=reports",
    "reportTitle=Automation Report",
    "reportFilename=report",
  ],
};

It's time for execution, but before that, we need to handle some prerequisites in the scripts section under the package.json file.

"scripts": {
    "setup": "selenium-standalone install > ./logs/driver.log &",
    "start": "selenium-standalone start > ./logs/server.log &",
    "test": "mocha --config conf.js"
  },

Step 5 - Execute Test Cases

Let's execute our test cases.

npm run setup

npm-run-setup.gif

Start the selenium-standalone server, the process will run in the foreground.

npm start

npm-start.gif

Run the test!

npm test

The above command will:

  • open browser

  • execute test cases

  • generate HTML report

Console Output

npm-test.gif

Execution in Action

npm-test-running.gif

HTML Report

image.png

As a best practice, we should persist all logs for reference. So, we will write it to multiple files under the logs folder.

image.png


Conclusion

The selenium-standalone server runs in the foreground and holds the TTY till it's terminated manually.

There are several solutions, one of them being PM2 to run the process in the background, which we will discuss in Part 2.

The source code is available at GitHub.

If you find this article useful or have any suggestions, reach out to me on LinkedIn.

Thank you and keep learning!