Best Practices

This article describes best TestCafe strategies and common user mistakes. It covers the following subjects:

Test Scope

To make the most out of TestCafe, use the framework for its intended purpose — end-to-end testing.

  • End-to-end tests are different from unit or integration tests. Like the name suggests, they test your application as a whole.
  • Do not use TestCafe to test individual components of your software. Instead, write tests that simulate common user scenarios.
  • Do not test exceptions and edge cases - that is what unit and integration tests are for. Test the general business logic of your application.
  • Write fewer E2E tests. End-to-end tests are slow by nature, so the number of tests should be drastically lower than that of unit or integration tests.

Common Assertion Mistakes

The purpose of assertions is to compare the actual state of your application to your expectations. If you compare two static values, you incur the risk of test failure. To avoid this outcome, do not use the await keyword when you define assertion variables.

Selector Example

Do not await a Selector statement when you want to copy it and pass it to an assertion. If you preface a Selector query with the await keyword, TestCafe immediately executes the query. Your new variable will contain the Selector’s return value, instead of the Selector query itself.

In the following example, the developerName variable contains the return value of a Selector query. The assertion fails because the DOM changed after the variable declaration:

import { Selector } from 'testcafe';

fixture `My fixture`
    .page `http://devexpress.github.io/testcafe/example/`;

test('Assertion with Selector', async t => {
    const developerNameInput = Selector('#developer-name');

    // Populate the developer name field
    await t.typeText(developerNameInput, 'Peter');

    // The await keyword precedes the Selector declaration.
    // TestCafe executes the Selector immediately, and passes its return value ("Peter") to the variable.
    const developerName = await Selector('#developer-name').value;

    await t
            .expect(developerName).eql('Peter')
            .typeText(developerNameInput, 'Jack') // Update the content of the field
            .expect(developerName).eql('Jack'); // The "developerName" variable contains outdated data. The assertion fails.
});

Without the await keyword, the developerName variable becomes a copy of the Selector query. The assertion at the end of the test executes the query, and does not fail.

import { Selector } from 'testcafe';

fixture `My fixture`
    .page `http://devexpress.github.io/testcafe/example/`;

test('Assertion with Selector', async t => {
    const developerNameInput = Selector('#developer-name');

    // Populate the developer name field
    await t.typeText(developerNameInput, 'Peter');

    // The await keyword does not precede the Selector declaration.
    // TestCafe passes the Selector query itself, and not its return value, to the variable.
    const developerName = Selector('#developer-name').value;

    await t
            .expect(developerName).eql('Peter')
            .typeText(developerNameInput, 'Jack') // Update the content of the field
            .expect(developerName).eql('Jack'); // TestCafe executes the Selector query from the developerName variable. The asserion succeeds.
});

Client Function Example

This rule also applies to Client Functions.

Do not await a Client Function when you want to copy it and pass it to an assertion. If you preface a Client Function with the await keyword, TestCafe immediately executes the function. Your new variable will contain the Client Function’s return value, instead of the Client Function query itself.

In the following example, the interfaceValue variable contains the return value of the getInterface query. The assertion fails because the DOM changed after the variable declaration:

import { Selector, ClientFunction } from 'testcafe';

fixture `My fixture`
    .page `http://devexpress.github.io/testcafe/example/`;

test('Assertion with ClientFunction', async t => {
    const interfaceSelect = Selector('#preferred-interface');
    const interfaceOption = interfaceSelect.find('option');

    const getInterface        = ClientFunction(() => document.getElementById('preferred-interface').value);

    // The await keyword precedes the call of the client function.
    // TestCafe executes the Client Function and passes its return value to the variable.
    const interfaceValue           = await getInterface(); 

    await t
        .click(interfaceSelect)
        .click(interfaceOption.withText('JavaScript API')) // Change the value of the field.
        .expect(interfaceValue).eql('JavaScript API'); // The "value" variable contains outdated data. The assertion fails.
});

Without the await keyword, the interfaceValue variable becomes a copy of the client function. The assertion at the end of the test executes this function, and does not fail.

import { Selector, ClientFunction } from 'testcafe';

fixture `My fixture`
    .page `http://devexpress.github.io/testcafe/example/`;

test('Assertion with ClientFunction', async t => {
    const getValue = ClientFunction(() => document.getElementById('preferred-interface').value)

    const interfaceSelect = Selector('#preferred-interface');
    const interfaceOption = interfaceSelect.find('option');
    const value           =  getValue();

    await t
        .click(interfaceSelect)
        .click(interfaceOption.withText('JavaScript API'))
        .expect(value).eql('JavaScript API')
});

Page Models

Incorporate Page Models into your test suite. Page model objects store Selector queries for important page elements, and define custom methods for frequent action combinations.

The following page model code snippet describes the TestCafe Example Page:

import {t, Selector } from 'testcafe';

class Page {
    constructor () {
        this.nameInput               = Selector('input').withAttribute('data-testid', 'name-input');
        this.importantFeaturesLabels = Selector('legend').withExactText('Which features are important to you:').parent().child('p').child('label');
        this.submitButton            = Selector('button').withAttribute('data-testid', 'submit-button');
    }

    async selectFeature(number) {
        await t.click(this.importantFeaturesLabels.nth(number));
    }

    async clickSubmit() {
        await t.click(this.submitButton);
    }

    async typeName(name) {
        await t.typeText(this.nameInput, name);
    }
}

export default new Page();

This useful abstraction improves the flexibility of your tests. If you change your application’s layout, you only need to update a single file. A test that uses this model can look like this:

import page from './page-model'

fixture `Use a Page Model`
    .page `https://devexpress.github.io/testcafe/example`;


test('Use a Page Model', async () => {

    await page.selectFeature(2);
    await page.typeName('Peter');
    await page.clickSubmit();

});

For more information, read the Page Model guide and view the page model example on GitHub.

Use Roles to Authenticate Users

Save authentication routines as Roles to simplify the log-in process, and switch between user accounts with a single line of code.

Store Role definitions in a separate file and activate them with the t.useRole method.

/tests/roles/roles.js:

import { Role } from 'testcafe';

const regularUser = Role('http://example.com/login', async t => {
    await t
        .typeText('#login', 'TestUser')
        .typeText('#password', 'testpass')
        .click('#sign-in');
});

const admin = Role('http://example.com/login', async t => {
    await t
        .typeText('#login', 'Admin')
        .typeText('#password', 'adminpass')
        .click('#sign-in');
});

export { regularUser, admin };

/tests/test_group1.js/test1.js:

import { regularUser, admin } from '../../roles/roles.js';

fixture `My Fixture`
    .page('../../my-page.html');

test('Regular user test', async t => {
    await t
        .useRole(regularUser);
});

test('Admin test', async t => {
    await t
        .useRole(admin);
});

Read the authentication guide to learn more about Roles.

File Structure

Follow these guidelines to optimize the structure of your test suite:

  • Use a page model to store frequently used Selectors and action combinations.
  • Store all the page model files in a single, separate directory. If your application consists of multiple components or subsystems, split up the associated page model objects into separate files.
  • Store Role definitions in a separate file to re-use them across tests.
  • Create a Configuration File in the root directory of the project. The configuration file simplifies test setting management.
  • Define one fixture per file. Multiple fixtures per test file may cause confusion as you scale.
  • Use fixtures to store groups of related tests. For example, place all authentication-related tests into a single fixture.
  • TestCafe tests are purely functional. As such, it is best to separate them from application code. Keep your test files in a separate, appropriately named directory (for example, tests).
  • Create subfolders for tests that cover different subsystems of your application.
  • Don’t write long tests. Shorter test scenarios are easier to debug and run concurrently.
  • Store reused data (for example, large sets of reference values or form inputs) in a dedicated, descriptively titled directory (for example, data).

If you apply all the suggestions above, your project’s file structure might look like this:

.
├── .testcaferc.json
└── tests
    ├── |- test_group1/
    │   └── |-test1.js
    │       |-test2.js
    ├── |- test_group2/
    │   └── |-test1.js
    │       |-test2.js
    ├── |- page_model/
    │   └── |- page1.js
    │       |- page2.js
    ├── |- helpers/
    │   └── |- helper1.js
    │       |- helper2.js
    ├── |- roles/
    │   └── |- roles.js
    └── |-data

Setup and Teardown

State management is an important part of web testing. Web tests produce unnecessary artifacts - database records, local storage records, cache data, and cookies. Create hooks to erase these artifacts after the test run.

If one of your tests downloads a file to local storage, you may want to delete this file after the fixture. Save this routine to an afterEach hook:

fixture `My fixture`
    .page `http://example.com`
    .afterEach( async t => {
        await cleanDir();
    });

test('My test', async t => {
    //test actions
});

The after and afterEach hooks are good for cleanup. But do not use these hooks to prepare your file system or database for the next test or fixture. TestCafe proceeds to the next test even when the after / afterEach hook fails. This approach can cause increased test failure rates.

Use before or beforeEach to prepare your environment for the test/fixture instead. If these hooks fail, the test does not run. This approach saves you time and effort.

fixture `Another fixture`
    .page `http://example.com`
    .beforeEach( async t => {
        await setupFileSystem();
    });

test('Another test', async t => {
    //test actions
});

Selector Strategy

Follow these guidelines to create reliable Selectors:

  • Do not create Selectors with a high level of specificity. Such Selectors can break when you make changes to your application. For example, the following query is unreliable because it references the order of page elements. If this order changes, the Selector breaks: Selector(‘body’).find(‘div’).nth(5).find(‘p’).nth(3).
  • Do not create Selectors that are too generic. These Selectors, too, can break when you change your application. For example, the following query doesn’t reference class names or other unique attribues. If you introduce a new div element, or a new button element, the Selector breaks: Selector(‘div > button’).
  • Do not create Selectors that reference dynamic attributes. For example, the following query references a CSS property. If you change the style of the element, the Selector breaks: Selector('[style*="background-color: red"]').
  • Create easy-to-read Selector queries. For example, the following query is difficult to understand, and, therefore, hard to maintain: Selector(‘div’).find(‘pre’).nextSibling(-1). Use class names and filter elements by content instead: Selector(‘#topContainer’).find(‘.child’).withText(‘Add item’).
  • Create meaningful Selectors. It’s a good idea to build selectors that identify elements from the end-user perspective. For instance, Selector(‘form’).find(‘[name=”btn-foo-123”]’) might be stable, but has no semantic meaning.
  • Mark important page elements with a custom element attribute (such as data-testid). Then, reference this attribute in your Selector queries. This attribute should persist as you develop your application, making it easier to write reliable TestCafe tests.
  • Store common Selector queries in a page model file. Page models increase test stability and help remove redundant code.
  • Use plugins to refrence components by name in Angular, React, Vue, and other frameworks.