End to End UI Automation using TestCafe

For one of our clients, TestCafe has been recently used for end-to-end(e2e) UI automation. TestCafe has a number of significant features, including simple setup, no web driver installation, support for all main operating systems, various browser support, reporting and HTTP Mocking.

In this article, I’d like to show how to create UI tests with TestCafe, as well as how to set up and write e2e tests with basic assertions using page object models. For this, we created a sample React Resume Builder Application, which we will use to write an e2e test in the sections that follow. If you are new to TestCafe, this article will help you understand the basic features of TestCafe as well as how to write selectors for a React application.

TestCafe Setup and Requirements

Pre-requisite: Install NodeJS and npm and check if the installation is correct by node -v and npm -v

TestCafe can be installed globally or locally in your project using npm.
For Global Installation on windows – npm install-g testing cafe
For Local Installation on windows – npm install --save-dev testcafe

Check the installation by running the command testcafe -v
The output looks like this:

$ testcafe -v
Using locally installed version of TestCafe.
1.15.1

Note: If TestCafe is installed both locally and globally, the TestCafe command will run the local version.

About our Web Application and Setup

For this tutorial, I am using this sample Resume Builder Application created using create-react-app. This application is built for our internal training purpose. You can find the source code here. This react application is used to create a resume by entering personal, educational, and professional experience details. Begin by cloning the base project structure

$ git clone https://github.com/yashrajb/Drail-Resume-Builder.git

From the root directory run npm start to start the project. The application will be deployed at http://localhost:3000 and a list of UI fields should appear. Once you’re comfortable with the application, start writing tests.

Before we get started, take a look at our sample Resume Builder application. It consists of two pages, main page with textboxes, drop down list, buttons etc., and a redirect page. In this tutorial, I am going to build a E2E tests for Resume builder application. For example, this test will go to the main web page, fill out the form, and verify if the filled in contents are displayed in the redirected page.

Main Page

Education and Experience fields in Main Page

Page Object Architecture

Using the Page Objects Pattern is one of the best pattern for architecting a testing project. Ideally, you should save all selectors or locators or any specific data that is unique to a specific page in a page object file. The main advantage of this pattern is that it will be easier to maintain the test because if some changes are required, the test itself will remain unchanged, and only the selectors or locators in one page will need to be modified. This pattern helps to make the code more understandable, maintainable, and reusable by writing the page methods. You can read here about page objects.

Writing Page Objects

For this testing, I used the page object model and created classes for each web page in the application. The Test scenarios include

  • filling out the basic information, educations details and experience details on the main page and
  • asserting the data in the redirect page.
  • In the project root folder, I created a folder called `/pages` and a folder called `/tests`, and started writing page objects and tests here. Lets start with a basic assertion. The directory structure looks like this:

    Project Directory Structure

    Create Main page model

    Make a file called “mainPage.js” in the pages folder, then declare a Page Object Class as shown below. This page consists of all the element locators and methods that we will use in the main page. To begin, you must import the Selector API from testcafe module. The TestCafe Selector function allows to use CSS selectors to locate the elements to test.

    import { Selector, t } from 'testcafe';
    const { ReactSelector } = require('testcafe-react-selectors');
    
    export default class mainPage {
        constructor() {   
             this.title = Selector('a');     
        }
    

    In the mainPage.js, I have simply added one Selector which will be accessed from the test.

    Creating our first Test

    Test scripts should be organized by module name under the “tests” folder. Each test is grouped into a fixture in TestCafe. The fixture is essentially a test category, and each test might have multiple fixtures. TestCafe uses a proxy server to inject scripts to the page, which we can then inspect.

    I created a ‘/E2ETest.js’ file in the `/tests` folder and this test starts by opening the application and checking the header of the page(we will extend this test further in our next sections). If you notice I have also defined a config file to store all the critical data required for the project. In automated testing, test data is usually a crucial component. Using hardcoded values in our tests, such as ‘url’, is not a good idea. So I have added these values to a config file and am reading from it.

    Our first test looks something like this.

    import mainPage from '../pages/mainPage';
    import config from '../config/config'
    
    fixture`E2E Fixture - Building a resume`
          .page`${config.testURL}`;
    
    // page header test
    test('Assert main page header', async t => {
          // The assertion      
          await t.expect(await mainPage.title.innerText).eql("Drail Resume Builder");
    });

    After importing the required modules, declare a test fixture and name it as `fixture ‘E2E Fixture – Building a resume’.` and define the start URL for all the tests in the fixture. In our situation, the URL is http://localhost:3000. Use keyword test to create a new test(). In our case, the test starts with asserting the URL of the home page header.

    Run the tests

    There are many ways to run a test in TestCafe. The simplest way is run using

    testcafe chrome ./tests/E2ETest.js

    Run Test

    Add actions to the page models

    Let’s now add some actions to the page object mainPage.js. Inside the page object functions, things alter a bit. This time we’re instructing TestCafe to select specific form elements and type using the typeText(). So our mainPage.js now becomes

    mainPage.js

    import { Selector, t } from 'testcafe';
    const { ReactSelector } = require('testcafe-react-selectors');
    
    class mainPage {
        constructor() {
            this.title = Selector('a');
            this.resume = ReactSelector('LinkAnchor')
            this.name = Selector('input[id=name]')
            this.email = Selector('input[id=email]')
            this.phone = Selector('input[id=phone]')
            this.headline = Selector('input[id=headline]')
            this.objective = Selector('textarea[id=objective]')
            this.skills = Selector('input[id=skills]')
            this.languages = Selector('input[id=lang]')
            this.college = ReactSelector('EducationForm').findReact('Input').withProps('id', 'college')
            this.degree = ReactSelector('EducationForm').findReact('Input').withProps('id', 'degree')
            this.fromDateEdu = ReactSelector('EducationForm').findReact('Input').withProps('id', 'from')
            this.toDateEdu = ReactSelector('EducationForm').findReact('Input').withProps('id', 'to')
            this.educationSubmit = ReactSelector('EducationForm').findReact('Button').withProps('children', 'Submit')
            this.redirect = Selector('h3')
            this.position = ReactSelector('ExperienceForm').findReact('Input').withProps('id', 'position')
            this.company = ReactSelector('ExperienceForm').findReact('Input').withProps('id', 'company')
            this.fromDateExp = ReactSelector('ExperienceForm').findReact('Input').withProps('id', 'from')
            this.toDateExp = ReactSelector('ExperienceForm').findReact('Input').withProps('id', 'to')
            this.responsibility = ReactSelector('ExperienceForm').findReact('Input').withProps('id', 'responsible')
            this.experienceSubmit = ReactSelector('ExperienceForm').findReact('Button').withProps('children', 'Submit')
        }
    
        // click the resume button for the given link
        async clickResumeButton(link) {
            await t.click(this.resume.withProps('children', link))
        };
    
        // fills basic details
        async fillBasicDetails(name, email, phone, headline, obj, skills, lang) {
            await t
                .typeText(await this.name, name)
                .typeText(await this.email, email)
                .typeText(await this.phone, phone)
                .typeText(await this.headline, headline)
                .typeText(await this.objective, obj)
                .typeText(await this.skills, skills)
                .typeText(await this.languages, lang)
        }
    
        // fill the education details in the page
        async fillEducationDetails(educationDetails) {
            await t
                .typeText(await this.college, educationDetails.college)
                .typeText(await this.degree, educationDetails.degree)
                .typeText(await this.fromDateEdu, educationDetails.fromDateEdu)
                .typeText(await this.toDateEdu, educationDetails.toDateEdu)
                .click(await this.educationSubmit)
        }
    
        // fills the experience details 
        async fillExperienceDetails(experienceDetails) {
            await t
                .typeText(await this.position, experienceDetails.position)
                .typeText(await this.company, experienceDetails.company)
                .typeText(await this.fromDateExp, experienceDetails.fromDateExp)
                .typeText(await this.toDateExp, experienceDetails.toDateExp)
                .typeText(await this.responsibility, experienceDetails.responsibility)
                .click(await this.experienceSubmit)
        }
    }
    
    export default new mainPage();
    

    In the above page, I have added the following:

  • Selector and ReactSelectors are used in the test in the mainPage class. TestCafe provides ReactSelectors plugin which provides selector extensions which helps to navigate a component structure and select component by name and state. The ReactSelector function takes in a root component name. You can use findReact() method to select the subcomponent or the component’s children
  • different page methods which will be used in our tests
  • defined different page actions using Test API to fill up the Education and Experience fields. To do this, different API functions such as .typeText(), .expect(), and .click() were used.
  • Similarly, the page methods for redirect page are to verify if the contents are displayed correctly.

    redirectPage.js

    const { ReactSelector } = require('testcafe-react-selectors');
    
    export default class redirect {
        constructor() {
            this.resume = ReactSelector('Resume')
            this.educationView = ReactSelector('EducationView')
            this.experienceView = ReactSelector('ExperienceForm')
        }
    
        // resume with specific name
        async checkName(name) {
            return this.resume.withProps('name', name)
        };
    
        // check education details
        async checkEducationDetails(educationDetails) {        
            return this.educationView.withProps(educationDetails)
        }
    
        // check experience details
        async checkExperienceDetails(Experiencedetails) {
            return this.experienceView.withProps(Experiencedetails)
        }
    
    };
    

    Complete E2E Test

    Now our test needs to be extended to make use of the methods in the page objects. By importing and accessing its selectors, I can now use this page model in the e2e test.

    I have added different tests for filling in personal, education and experience details and asserting the contents. I first imported all the page object classes needed, such as mainPage.js, redirectPage.js, as well as the config and data files. If you notice, I have also created a data folder to store data-specific information. With this in place, we can access our selectors through the page model’s properties and our test becomes like this

    import mainPage from '../pages/mainPage';
    import redirectPage from '../pages/redirectPage';
    const data = require('../data/mainPageData');
    import config from '../config/config'
    
    fixture`E2E Fixture - Building a resume`
          .page`${config.testURL}`;
    
    // page header test
    test('Assert main page header', async t => {         
          await t.expect(await mainPage.title.innerText).eql(`${data.basicInfo.title}`);
    });
    
    // fill the education and experience details page
    test('Fill the education and experience details', async t => {
          await mainPage.fillBasicDetails(data.basicInfo.name, data.basicInfo.email, data.basicInfo.phone, data.basicInfo.headline, data.basicInfo.objective, data.basicInfo.skills, data.basicInfo.languages)    
          await mainPage.fillEducationDetails(data.educationDetails)     
          await mainPage.fillExperienceDetails(data.experienceDetails)
          await mainPage.clickResumeButton('Resume')      
          const name = await redirectPage.checkRedirectName(data.basicInfo.name);
          await t.expect(name.exists).ok();     
          // check that we have the correct education details
          const educationDetails = await redirectPage.checkEducationDetails({
                college: data.educationDetails.college,
                degree: data.educationDetails.degree,
                from: data.educationDetails.fromDateEdu,
                to: data.educationDetails.toDateEdu
          })           
          await t.expect(educationDetails.exists).ok()
          // check that we have the correct experience details
          const experienceDetails = await redirectPage.checkExperienceDetails({
                position: data.experienceDetails.position,
                company: data.experienceDetails.company,
                from: data.experienceDetails.fromDateExp,
                to: data.experienceDetails.toDateExp,
                responsible: data.experienceDetails.responsibility
          })    
          await t.expect(experienceDetails.exists).ok()
    });
    

    Now, run the test and view the Test Results. There are many ways to run a test in TestCafe. This time I have added the testcafe command to the scripts section in package.json as shown below

    "scripts": {
        "test-resume": "testcafe chrome ./tests/E2ETest.js/"
    }
    

    $ npm run test-resume

    Test Output

    Running Tests using CLI commands

    TestCafe supports modern browsers and automatically detects the browsers installed on your machine. Listing out few CLI commands

    1. Use below command to view the list of all available browsers

    testcafe --list-browsers

    2. Use below command to run this test on selected browsers

    testcafe firefox, chrome, safari ./tests/E2ETest.js

    In this case, testcafe will open three instances together in three different browsers.

    3. Running on Google Chrome headless

    testcafe "chrome:headless" .tests/E2ETest.js

    4. Parallel Test Execution against multiple browsers

    testcafe -c 1 safari,chrome ./tests/E2ETest.js

    5. Set Test Execution speed

    testcafe chrome ./tests/E2ETest.js --speed 0.5

    Debugging the Tests

    There are different ways to debug the TestCafe Tests. One way is Debug mode. Using this mode, test execution is paused before the action or assertion so that you can open the developer tools and debug the state of the element and page. The execution is halted and you can resume test execution or skip to the next action or assertion.

    Debug Mode

    We can also enable Debug mode On Fail using CLI option, which will automatically enter into Debug mode when a test fails using below command

    testcafe chrome ./tests/E2ETest.js --debug-on-fail


    That’s all folks !! I have only touched the basics of what can be done with TestCafe, if you want more information on TestCafe you can visit official documentation


    References:

    1. TestCafe documentation
    2. End to End testing with TestCafe

    Leave a Reply

    Your email address will not be published. Required fields are marked *