This guide will help you setup and run automated tests in a local development environment running in Mac or Linux and using a free Microsoft development virtual machine to run tests.

Tools and Downloads

Step 1 - Virtual Machine

  • Install VirtualBox
  • Extract and Import the Windows Image
    • Change the following defaults:
    • Increase the CPU to at least 2, but 4 is better, IE runs too slowly otherwise.
    • Incraase the Memory and Video Memory as the defaults are also too low, move these into the green zone in VirtualBox and give it a good chunk of the total system, especially if your webapp is a hungry breast.
    • Network - I use Bridged, but as long as we can connected in both direction to and from the VM to your dev machine it should work.
    • Note: The download is approx 8Gb and once extracted and import it takes about 30Gb, make sure you have the dish space

Step 2 - Selenium and IEDriver

  • Download and install Java on the Windows VM
  • Download Selenium Grid on the windows VM
  • Download IEDriverServer an install in the path, I copied this directly into the c:/Windows/System32 folder
  • Create a script to start the hub and node for selenium grid

This VBS script runs on windows and start selenium as both the hub and node using the default settings and will find the IEDriverServer as long at it’s in the Path.

' Selenium.vbs

Dim WinScriptHost
Set WinScriptHost = CreateObject("WScript.Shell")
WinScriptHost.Run Chr(34) & "C:\Program Files\Java\jre1.8.0_281\bin\java.exe" & Chr(34) & " -jar selenium-server-4.0.0-beta-2.jar hub --log-level FINE", 1
WinScriptHost = Null

Set WinScriptHost2 = CreateObject("WScript.Shell")
WinScriptHost2.Run Chr(34) & "C:\Program Files\Java\jre1.8.0_281\bin\java.exe" & Chr(34) & " -jar selenium-server-4.0.0-beta-2.jar node --log-level FINE", 1
WinScriptHost = Null

Copy this to the desktop to make it easy to start each time, alternatively have it start on boot by placing it in the correct folder.

Step 3 - Setup Selenium-Webdriver and Jest

  • Install npm packages, I’m using selenium-webdriver, jest in a webpack project
  • Create the directory for tests and page objects
yarn add -D selenium-webdriver jest @types/selenium-webdriver @types/jest

mkdir -p tests/specs/
mkdir -p tests/pageObjects/

Add jest types to your TS config

{
  "compilerOptions": {
    ...
    "types": [..., "jest"]

  },
  ...
}

In the end 2 end tests in Jest use the beforeAll and afterAll hooks to open and close the connection to selenium

describe('Test My Web App', () => {
    let driver: WebDriver;
    beforeAll(() => new webdriver.Builder()
        .forBrowser('internet explorer')
        .usingServer('http://<VM ip address>:4444/')
        .build()
        .then((d) => {
            driver = d;
            return true;
        }));
    afterAll(() => {
        driver.quit();
    });
    /// ... Your Tests
})

Step 4 - Writing End-2-End testing

Create a base page Object class - See Martin Fowlers article on Page Objects on the benefits of this pattern.

export default class Page {
    protected driver: WebDriver;
    constructor(driver: WebDriver) {
        this.driver = driver;
    }
    protected open(path: string): Promise<Page> {
        return this.driver.get(`${this.BASE_URL}/${path}`).then(() => this);
    }
}

For each page in the app extend it to get each page component you will test

class LoginPage extends Page {
    /**
     * define selectors using getter methods
     */
    get inputUsername(): Promise<WebElement> {
        return this.driver.findElement(By.xpath('//[@data-testid="username"]'));
    }

    get inputPassword(): Promise<WebElement> {
        return this.driver.findElement(By.xpath('//[@data-testid="password"]'));
    }

    get btnSubmit(): Promise<WebElement> {
        return this.driver.findElement(By.xpath('//[@data-testid="submit"]'));
    }
}

Then write you test logic.

describe('Login Page', () => {
    let driver: WebDriver;

    beforeAll(() => new webdriver.Builder()
        .forBrowser('internet explorer')
        .usingServer('http://192.168.2.117:4444/')
        .build()
        .then((d) => {
            // eslint-disable-next-line no-param-reassign,@typescript-eslint/no-unused-vars -- needs to assign to test runner
            driver = d;
            return true;
        }));
    afterAll(() => {
        driver.quit();
    });

    it('Login with valid credentials', async (done) => {
        const login = await LoginPage(driver).open()
        await login.inputUsername.then(sendKeys("username"))
        await login.inputPassword.then(sendKeys("password"))
        await login.btnSubmit.then(click)
        return AnthenicatedPage(driver).heading(getText).then(authPageHeading => {
            // Make sure the test can actually fail using jests expect() for asserting
            expect(authPageHeading).toEqual("Dashboard")
            return true
        })
    });
})

The test above include a few little helper function to make the tests cleaner

export const getText = (element: WebElement): Promise<string> => element.getText();
export const click = (element: WebElement): Promise<void> => element.click();
export const sendKeys = (...args: Array<string|number|Promise<string|number>>) => (element: WebElement): Promise<void> => element.sendKeys(...args);