E2E Tests Project Structure Organizing

Maria Golubeva
JAGAAD Digital Solutions
6 min readMay 26, 2023

--

There are many lessons online on how to write E2E tests in different programming languages (my article “The first E2E test with TestCafe” is no exception). But there is not enough information about how to properly structure the project, distribute all the necessary and different in meaning information into folders, access and links between which will be logical. In this article, I will share with you some simple project organization principles to effectively maintain and update the tests.

Discussing with Jagaad team members the new E2E tests to implement

As a basis, we will take the already existing E2E test from this article. To begin with, I recommend that you follow the steps described in the article above, write a script in one file, run it and get the results.

import {Selector} from 'testcafe';

fixture("Wikipedia test set")
.page("https://www.wikipedia.org/")

test("Search test", async t => {
await t
//navigate to the Tenerife page using the search
.typeText('#searchInput', 'Tenerife')
.click('[type="submit"]')

//check the title of the page
.expect(Selector('#firstHeading').visible)
.ok('Tenerife page title is not visible')
.expect(Selector('#firstHeading').innerText)
.eql('Tenerife', 'Tenerife page title is not as expected')

//navigate to the search page
.click('[name="search"]')
.pressKey('enter')

//check the title of the search page
.expect(Selector('#firstHeading').visible)
.ok('Search page title is not visible')
.expect(Selector('#firstHeading').innerText)
.eql('Search', 'Search page title is not as expected')

//search Teide
.typeText('#searchText', 'Teide')
.click('.oo-ui-actionFieldLayout-button')

//check that the count of the found results is 20
.expect(Selector('.searchResultImage').count)
.eql(20, 'The count of the links in not as expected');
});

Next, we will split this scenario into two tests in order to show you the feature organization with a list of test cases:

  • Navigate to the article page using the main page search.
  • Articles results list on the search page.

Where to keep all the code related to E2E tests

  • If the tests are in the same project as the application under test. All lower-level folders should be kept separately in the tests folder in order to clearly delimit their space.
  • If your project only implies the presence of E2E tests that are run for an application located in another repository, then place the folders that we will talk about next in the src folder. This is just our case — a separate project.

Project organization

First, let’s create 1 main src folder. And put 4 folders in it:

  • fixtures — sets of tests, combined according to some principle, starting from a single URL.
  • helpers — functions that are often used as helpers in different tests.
  • object-repository — data (variables — strings, numbers) that are used in different tests.
  • pages — test page descriptions.
Folders structure

Now more about the contents of each of the above folders.

Object repository

Constant data used in our tests. It makes sense to divide them into 3 folders according to their meanings:

  • application-content — text/number constants that we expect to see on our application pages.
  • selectors.
  • test-data — text/number constants that we will use to run tests.

Application content

I always recommend separating application content into pages. Therefore, I propose to create two files. And then place and export constants from them that are used in our tests:

article-page-content.js

export const ARTICLE_PAGE_CONTENT = {
TENERIFE_TITLE: 'Tenerife',
};

search-page-content.js

export const SEARCH_PAGE_CONTENT = {
TITLE: 'Search',
};

Selectors

By the same principle, we separate the selectors by files. And we export an object with selectors as constant:

article-page-selectors.js

export const ARTICLE_PAGE_SELECTORS = {
TITLE: '#firstHeading',
};

main-page-selectors.js

export const MAIN_PAGE_SELECTORS = {
SEARCH_INPUT: '#searchInput',
SEARCH_BUTTON: '[type="submit"]',
};

search-page-selectors.js

export const SEARCH_PAGE_SELECTORS = {
TITLE: '#firstHeading',
SEARCH_INPUT: '#searchText',
SEARCH_BUTTON: '.oo-ui-actionFieldLayout-button',
ARTICLE_PREVIEW: '.searchResultImage',
};

Test data

We divide the necessary data into files united by a single topic. The following is relevant to us:

search-values.js

export const SEARCH_VALUES = {
TENERIFE: 'Tenerife',
TEIDE: 'Teide',
};

urls.js

export const BASE_URL = 'https://www.wikipedia.org/';
Object-repository folder structure

Pages

Here we will create three files to describe the pages on which we run our tests:

  • article-page.js
  • main-page.js
  • search-page.js

Within pages, we initialize elements in the constructor. We will also distribute the previously described actions that are performed on these elements by methods with names that describe more complex user actions.
In order to have access to test data from other folders and files, we must import them at the very beginning.

Let’s start with main-page.js:

import { Selector, t } from 'testcafe';
import { MAIN_PAGE_SELECTORS } from '../object-repository/selectors/main-page-selectors';

class MainPage {
constructor() {
this.searchInput = Selector(MAIN_PAGE_SELECTORS.SEARCH_INPUT);
this.searchButton = Selector(MAIN_PAGE_SELECTORS.SEARCH_BUTTON);
}

async navigateToArticlePage(searchText) {
await t
.typeText(this.searchInput, searchText)
.click(this.searchButton);
}

async navigateToSearchPage() {
await t
.click(this.searchInput)
.pressKey('enter');
}
}
export default new MainPage();

article-page.js

import { Selector, t } from 'testcafe';
import { ARTICLE_PAGE_SELECTORS } from '../object-repository/selectors/article-page-selectors';

class ArticlePage {
constructor() {
this.title = Selector(ARTICLE_PAGE_SELECTORS.TITLE);
}

async isDisplayed(expectedTitle) {
await t
.expect(this.title.visible)
.ok('Article page title is not visible')
.expect(this.title.innerText)
.eql(expectedTitle, 'Article page title is not as expected')
}

}
export default new ArticlePage();

search-page.js

import { Selector, t } from 'testcafe';
import { SEARCH_PAGE_SELECTORS } from '../object-repository/selectors/search-page-selectors';

class SearchPage {
constructor() {
this.title = Selector(SEARCH_PAGE_SELECTORS.TITLE);
this.searchInput = Selector(SEARCH_PAGE_SELECTORS.SEARCH_INPUT);
this.searchButton = Selector(SEARCH_PAGE_SELECTORS.SEARCH_BUTTON);
this.articlePreview = Selector(SEARCH_PAGE_SELECTORS.ARTICLE_PREVIEW);
}

async isDisplayed(expectedTitle) {
await t
.expect(this.title.visible)
.ok('Search page title is not visible')
.expect(this.title.innerText)
.eql(expectedTitle, 'Search page title is not as expected');
}

async searchArticle(text) {
await t
.typeText(this.searchInput, text)
.click(this.searchButton);
}

async checkCountResults(expectedCount) {
await t
.expect(this.articlePreview.count)
.eql(expectedCount, 'The count of the links in not as expected');
}

}
export default new SearchPage();

We also need to export the page object in order to be able to access class methods from test scripts.

Pages folder structure

Fixtures

I recommend placing only one fixture with tests in one file, even if multiple fixtures are allowed by TestCafe because there will be better visibility when distributing fixtures across files with the names of the functionality they test. We will create one search.js file.

What will we place inside this file?

import { BASE_URL } from '../object-repository/test-data/urls';

fixture("Search")
.page(BASE_URL)

test("Navigate to the article page using the main page search", async t => {
//methods
});

test("Articles results list on the search page", async t => {
//methods
});

Next, we fill the tests with methods on the pages described in the files of the pages folder.

Page methods are accessed by calling from the imported page object. We also use imported constant variables instead of the specified strings.

import { BASE_URL } from '../object-repository/test-data/urls';
import MainPage from '../pages/main-page';
import ArticlePage from '../pages/article-page';
import SearchPage from '../pages/search-page';

import { ARTICLE_PAGE_CONTENT } from '../object-repository/application-content/article-page-content';
import { SEARCH_PAGE_CONTENT } from '../object-repository/application-content/search-page-content';

import { SEARCH_VALUES } from '../object-repository/test-data/search-values';

fixture("Search")
.page(BASE_URL)

test("Navigate to the article page using the main page search", async t => {
await MainPage.navigateToArticlePage(SEARCH_VALUES.TENERIFE);
await ArticlePage.isDisplayed(ARTICLE_PAGE_CONTENT.TENERIFE_TITLE);
});

test("Articles results list on the search page", async t => {
await MainPage.navigateToSearchPage();
await SearchPage.isDisplayed(SEARCH_PAGE_CONTENT.TITLE);
await SearchPage.searchArticle(SEARCH_VALUES.TEIDE);
await SearchPage.checkCountResults(20);
});
Fixtures folder structure

Helpers

There will be no helpers in our project, the folder will remain empty. But what could be here? For example, a helper for finding elements on a page using XPath. Or a date converter to various formats. That is, helper functions that can be useful in different parts of our code.

Run the tests

Open the Terminal/Console, go to the src/fixtures and run the command:

testcafe chrome search.js

TestCafe will find the browser installed on the machine, launch it and run the tests.

Wikipedia search tests run
Test run results

--

--

Maria Golubeva
JAGAAD Digital Solutions

Belarusian | QA Engineer in Italian company | Mentor in Tech | Web Automation | Improving English and Italian levels | Travel lover