Your First Story

Start writing your own tests, and learn what makes a test useful.

What is a story?

As mentioned earlier, we will be building a simple task list app to learn how to test. Our first step would be to let users create tasks. Let’s call it a story because that’s the term used in Agile environments.

Each story must have an acceptance criteria. The acceptance criteria is a description of a feature that must be implemented to complete the story. In our case, it is described like this: user must be able to open an app, enter the name of the task, press enter and see his task in the list of tasks.

Why is it so important to have an acceptance criteria? This is for one very good reason: it is used to write the very first integration test. Based on the integration test, we write the unit tests. After the unit tests are written and fail, we begin writing code to fix the unit tests. Once all unit tests are fixed, the integration test should also pass.

Writing integration tests

Writing integration tests is actually pretty simple. You should just read off the acceptance criteria (or any English description of the feature), and translate it into JavaScript. To do this, you will need to know a few Selenium functions:

  • driver.get(url): Open a webpage.
  • driver.wait(condition, timeout): Wait for something. It’s typically used to wait for elements while the page is loading.
  • driver.findElement/driver.findElements: Find elements on a webpage.

And a few helper functions:

  • By: This function is used as an argument to find elements. For example:
    • By.id("textInput")
    • By.className("wrapper")
    • By.xpath("//input")
    • driver.findElement(By.tag("div"))
  • until: - This function is used as an argument for the wait function. For example:
    • until.elementLocated(By.id("submitBtn"))
    • until.titleContains("abacaba")
    • wait(until.elementLocated(By.name("wrapper")), 1000)

Now, we are ready to write the test. Rename the file sample.test.js to create-task.test.js in integration-tests. Rename the test to should create tasks and delete all lines except the driver.get("http://localhost:3000"). Add the new imports that we will be using:

import {until, By, Key} from 'selenium-webdriver';

Our first step for testing the task creation would be to wait until the task input is rendered. You can do this with this line:

await driver.wait(until.elementLocated(By.xpath("//input[@placeholder='Enter new task']")), 1000);

This instructs the driver to wait for one second (at most) until an input element with the desired placeholder is rendered.

We are using the XPath syntax to search for elements. If you are not comfortable with XPath, there will be a cheatsheet at the end of the lesson.

After the element is rendered, we want to send some text into it and press enter. It can be achieved with this line:

await driver.findElement(By.xpath("//input[@placeholder='Enter new task']"))
        .sendKeys('new todo!' + Key.ENTER);

This line finds the desired element and sends a sequence of keystrokes, followed by Enter.

After entering a new task, wait until it is rendered:

await driver.wait(until.elementLocated(By.xpath("//*[text()='new todo!']")), 1000);

This line queries the document for any element which contains text "new todo!". This is what you should end up with:

import {getDriver} from './helpers';
import {until, By, Key} from 'selenium-webdriver';

let driver;

beforeAll(() => {
    driver = getDriver();
});

afterAll(async () => {
    await driver.quit();
});

test('should create tasks', async  () => {
    await driver.get('http://localhost:3000');
    await driver.wait(until.elementLocated(By.xpath("//input[@placeholder='Enter new task']")), 1000);
    await driver.findElement(By.xpath("//input[@placeholder='Enter new task']"))
        .sendKeys('new todo!' + Key.ENTER);

    await driver.wait(until.elementLocated(By.xpath("//*[text()='new todo!']")), 1000);
});
Note that you can only run Selenium (integration) tests on your PC. The run button above runs unit tests

Since this is an integration test, you need to start the development server first with npm start and run the tests with npm run integration-tests. Here is the output you will be getting:

 RUNS  integration-tests/create-task.test.js

 FAIL  integration-tests/create-task.test.js (6.529s)owser/8c139df8-6f3f-4482-9d84
  × should create tasks (4755ms)

  ● should create tasks

    TimeoutError: Waiting for element to be located By(xpath, //input[@placeholder='Enter new task'])
    Wait timed out after 1041ms

      at node_modules/selenium-webdriver/lib/webdriver.js:842:17

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        7.332s
Ran all test suites matching /integration-tests/i.

Note the error message: TimeoutError: Waiting for element to be located By(...). This means that Selenium was waiting for the element to render, but it never did. This is expected, as we did not write it yet.

You might notice that this test is very simple; we do not make any assumptions about the page structure or the number of existing to-dos. This is precisely what you are looking for in an integration test. It should be as simple as possible, so the implementation of the app does not depend on the implementation of the test. The test must only describe the behaviour and leave the implementation open.

Quick recap

In this lesson, we learned how to translate a story from English into an integration test. We used functions from selenium-webdriver such as:

  • driver.wait
  • until and By
  • driver.findElement

In the next lesson, we will write unit tests from this integration test.

XPath cheatsheet

Selecting a tag:

  • //h1: Select all H1 elements.
  • //article//p: Select all P elements within ARTICLE elements.

Ordering:

  • //ul/li[1]: Select first LI child of UI elements.
  • //a[last()]: Slect the last A element of the whole document.

Attributes:

  • //*[@id='input']: Select element with id='input'.
  • //div[@class='wrapper']: Select all DIV with class wrapper.