Chapter 5:

Improving the Tests

The main learning goal of this chapter is to get familiar with how to improve a set of end-to-end tests. This will be achieved with the help of test setup and teardown. Also, we will take a look at different command-line settings to run tests. The testing techniques that we will cover in this chapter are universal and can be reused to write automated tests for any web project. By the end of the chapter, we will have an improved test suite and will learn how to run it with command-line options.

In this chapter, we're going to cover the following main topics:

  • Executing selected tests.
  • Exploring test setup and teardown.
  • Adding setup and teardown to the test project.
  • Running tests with command-line settings.

Technical requirements

All of the code examples for this chapter can be found on GitHub at https://github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch5.

Executing selected tests

Quite often, when writing or extending a set of tests, we need to concentrate on one specific test while omitting all others. Tests are usually organized into sets (groups of tests are also known as fixtures). Luckily, TestCafe provides the fixture.only and test.only methods to specify that only a selected test or fixture should be executed and all others should be skipped. Let's review it using our set of tests in a simplified form, with all the test actions commented out:

// ...fixture('Redmine log in tests')    .page('http://demo.redmine.org/');test.only('Create a new user', async (t) => { /* ... */ });test('Log in', async (t) => { /* ... */ });test('Log out', async (t) => { /* ... */ });fixture('Redmine entities creation tests')    .page('http://demo.redmine.org/');test('Create a new project', async (t) => { /* ... */ });test('Create a new issue', async (t) => { /* ... */ });test('Verify that the issue is displayed on a project page', async (t) => { /* ... */ });test('Upload a file', async (t) => { /* ... */ });fixture('Redmine entities editing tests')    .page('http://demo.redmine.org/');test('Edit the issue', async (t) => { /* ... */ });test('Verify that the updated issue is displayed on a project page', async (t) => { /* ... */ });test('Search for the issue', async (t) => { /* ... */ });fixture.only('Redmine entities deletion tests')    .page('http://demo.redmine.org/');test('Delete the issue', async (t) => { /* ... */ });test('Delete the file', async (t) => { /* ... */ });

As you can see in the example, test.only is used in the Create a new user test, and fixture.only is used in the Redmine entities deletion tests fixture, so only the Create a new user, Delete the issue, and Delete the file tests will be executed.

Note

If several tests (or fixtures) are marked with test.only (or fixture.only), all the marked tests and fixtures will be executed.

In addition to that, TestCafe allows you to use the test.skip and fixture.skip methods to specify a test or a fixture to skip when tests run:

// ...fixture('Redmine log in tests')    .page('http://demo.redmine.org/');test('Create a new user', async (t) => { /* ... */ });test.skip('Log in', async (t) => { /* ... */ });test.skip('Log out', async (t) => { /* ... */ });fixture.skip('Redmine entities creation tests')    .page('http://demo.redmine.org/');test('Create a new project', async (t) => { /* ... */ });test('Create a new issue', async (t) => { /* ... */ });test('Verify that the issue is displayed on a project page', async (t) => { /* ... */ });test('Upload a file', async (t) => { /* ... */ });fixture('Redmine entities editing tests')    .page('http://demo.redmine.org/');test('Edit the issue', async (t) => { /* ... */ });test.skip('Verify that the updated issue is displayed on a project page', async (t) => { /* ... */ });test.skip('Search for the issue', async (t) => { /* ... */ });fixture.skip('Redmine entities deletion tests')    .page('http://demo.redmine.org/');test('Delete the issue', async (t) => { /* ... */ });test('Delete the file', async (t) => { /* ... */ });

As demonstrated in the preceding example, only the Create a new user and Edit the issue tests will be executed.

Now that we have learned how to execute a particular test or fixture, skipping all the others, let's see how test setup and teardown can be done.

Exploring test setup and teardown

As tests can be quite long and contain a lot of repetitive actions, TestCafe has a way to optimize this with test setup and teardown.

Setup is usually done by executing a number of specific functions (also known as hooks) before a fixture or test starts (including fixture.before, fixture.beforeEach, and test.before).

Teardown is usually done by executing a number of specific functions after a fixture or test is completed (including fixture.after, fixture.afterEach, and test.after).

There are six methods for using hooks in TestCafe.

The first two (fixture.before and fixture.after) do not have access to the tested page and thus should be used to perform server-side operations, such as preparing the tested application's server or pre-creating some test data in the database:

  • fixture.before can be used to specify actions that should be executed before the first test in a fixture starts (https://devexpress.github.io/testcafe/documentation/reference/test-api/fixture/before.html). In the following example, the createTestData function will be called before the first test of the My first set of tests fixture:

    fixture('My first set of tests')    .page('https://test-site.com')    .before(async (t) => {        await createTestData();    });

  • fixture.after can be used to specify actions that should be executed after the last test in a fixture is finished (https://devexpress.github.io/testcafe/documentation/reference/test-api/fixture/after.html). In the following example, the deleteTestData function will be called after the last test of the My first set of tests fixture:

    fixture('My first set of tests')    .page('https://test-site.com')    .after(async (t) => {        await deleteTestData();    });

The next four methods (fixture.beforeEach, fixture.afterEach, test.before, and test.after) are launched when the tested web page is already loaded, so you can execute test actions and other test API methods inside these test hooks:

  • fixture.beforeEach can be used to specify actions that should be executed before each test in a fixture (https://devexpress.github.io/testcafe/documentation/reference/test-api/fixture/beforeeach.html). In the following example, the click action will be performed before each test of the My first set of tests fixture:

    fixture('My first set of tests')    .page('https://test-site.com')    .beforeEach(async (t) => {        await t.click('#log-in');    });

  • fixture.afterEach can be used to specify actions that should be executed after each test in a fixture (https://devexpress.github.io/testcafe/documentation/reference/test-api/fixture/aftereach.html). In the following example, the click action will be performed after each test of the My first set of tests fixture:

    fixture('My first set of tests')    .page('https://test-site.com')    .afterEach(async (t) => {        await t.click('#delete-test-data');    });

  • test.before can be used to specify actions that should be executed before a particular test (https://devexpress.github.io/testcafe/documentation/reference/test-api/test/before.html). In the following example, the click action will be performed before the My first Test test:

    test     .before(async (t) => {        await t.click('#log-in');    })    ('My first Test', async (t) => { /* ... */ });

  • test.after can be used to specify actions that should be executed after a particular test (https://devexpress.github.io/testcafe/documentation/reference/test-api/test/after.html). In the following example, the click action will be performed after the My first Test test:

    test     .after(async (t) => {        await t.click('#delete-test-data');    })    ('My first Test', async (t) => { /* ... */ });

    Note

    If a test runs in several browsers, test hooks are executed in each browser. If both the fixture.beforeEach and test.before (or fixture.afterEach and test.after) hooks are used together, the most specific hook will overrule. So, test.before (or test.after) will be executed and fixture.beforeEach (or fixture.afterEach) will be omitted and will not run for this test.

You can read more about the hooks at https://devexpress.github.io/testcafe/documentation/guides/basic-guides/organize-tests.html#initialization-and-clean-up.

In this section, we went through the types of hooks that are available in TestCafe. Now, let's put this knowledge into practice by applying it to our set of tests.

Adding setup and teardown to the test project

In this section, we will see how to optimize our test project code with setup and teardown blocks.

As we saw in the Exploring test setup and teardown section, fixture.beforeEach can specifically be useful when each of the tests needs a user to be logged in before the test. That's exactly our case, so let's add the beforeEach block to the Redmine entities creation tests fixture:

// ...fixture('Redmine entities creation tests')    .page('http://demo.redmine.org/')    .beforeEach(async (t) => {        await t.click('.login')            .typeText('#username', `test_user_testcafe_poc${randomDigits1}@sharklasers.com`)            .typeText('#password', 'test_user_testcafe_poc')            .click('[name="login"]');    });

Let's also remove the login actions from all the tests of the Redmine entities creation tests fixture, as these actions will be executed in the beforeEach block. So, the Create a new project test will look like this:

test('Create a new project', async (t) => {    await t.click('#top-menu .projects')        .click('.icon-add')        .typeText('#project_name', `test_project${randomDigits1}`)        .click('[value="Create"]')        .expect(Selector('#flash_notice').innerText).eql('Successful creation.')        .expect(getPageUrl()).contains(`/projects/test_project${randomDigits1}/settings`);});

After all login actions were moved to the beforeEach block, the Create a new issue test will look like this:

test('Create a new issue', async (t) => {    await t.click('#top-menu .projects')        .click('.icon-add')        .typeText('#project_name', `test_project${randomDigits2}`)        .click('[value="Create"]')        .click('#top-menu .projects')        .click(`[href*="/projects/test_project${randomDigits2}"]`)        .click('.new-issue')        .typeText('#issue_subject', `Test issue   ${randomDigits2}`)        .typeText('#issue_description', `Test issue description ${randomDigits2}`)        .click('#issue_priority_id')        .click(Selector('#issue_priority_id option').withText('High'))        .click('[value="Create"]')        .expect(Selector('#flash_notice').innerText).contains('created.');});

And the Verify that the issue is displayed on a project page test, without login actions, will look like this:

test('Verify that the issue is displayed on a project page', async (t) => {    await t.click('#top-menu .projects')        .click('.icon-add')        .typeText('#project_name', `test_    project${randomDigits3}`)        .click('[value="Create"]')        .click('#top-menu .projects')        .click(`[href*="/projects/test_project${randomDigits3}"]`)        .click('.new-issue')        .typeText('#issue_subject', `Test issue ${randomDigits3}`)        .typeText('#issue_description', `Test issue description ${randomDigits3}`)        .click('#issue_priority_id')        .click(Selector('#issue_priority_id option').withText('High'))        .click('[value="Create"]')        .click('#top-menu .projects')        .click(`[href*="/projects/test_project${randomDigits3}"]`)        .click('#main-menu .issues')        .expect(Selector('.subject a').innerText).

contains(`Test issue ${randomDigits3}`);});

And finally, the Upload a file test without login actions will look like this:

test('Upload a file', async (t) => {    await t.click('#top-menu .projects')        .click('.icon-add')        .typeText('#project_name', `test_project${randomDigits8}`)        .click('[value="Create"]')        .click('#top-menu .projects')        .click(`[href*="/projects/test_project${randomDigits8}"]`)        .click('.files')        .click('.icon-add')        .setFilesToUpload('input.file_selector', './uploads/test-file.txt')        .click('[value="Add"]')        .expect(Selector('.filename').innerText).eql('test-file.txt')        .expect(Selector('.digest').innerText).eql('d8e8fca2dc0f896fd7cb4cb0031ba249');});

Now, let's add the beforeEach block to the Redmine entities editing tests fixture:

fixture('Redmine entities editing tests')    .page('http://demo.redmine.org/')    .beforeEach(async (t) => {        await t.click('.login')            .typeText('#username', `test_user_testcafe_poc${randomDigits1}@sharklasers.com`)            .typeText('#password', 'test_user_testcafe_poc')            .click('[name="login"]');    });

Let's also remove the login actions from all the tests of the Redmine entities editing tests fixture, as this action will now be executed in the beforeEach block. So, the Edit the issue test will look like this:

test('Edit the issue', async (t) => {    await t.click('#top-menu .projects')        .click('.icon-add')        .typeText('#project_name', `test_project${randomDigits4}`)        .click('[value="Create"]')        .click('#top-menu .projects')        .click(`[href*="/projects/test_project${randomDigits4}"]`)        .click('.new-issue')        .typeText('#issue_subject', `Test issue ${randomDigits4}`)        .typeText('#issue_description', `Test issue description ${randomDigits4}`)        .click('#issue_priority_id')        .click(Selector('#issue_priority_id option').withText('High'))        .click('[value="Create"]')        .click('#top-menu .projects')        .click(`[href*="/projects/test_project${randomDigits4}"]`)        .click('#main-menu .issues')        .click(Selector('.subject a').withText(`Test issue ${randomDigits4}`))        .click('.icon-edit')        .selectText('#issue_subject')        .pressKey('delete')        .typeText('#issue_subject', `Issue ${randomDigits4} updated`)        .click('#issue_priority_id')        .click(Selector('#issue_priority_id option').withText('Normal'))        .click('[value="Submit"]')        .expect(Selector('#flash_notice').innerText).eql('Successful update.');});

All login actions were moved to the beforeEach block, so the Verify that the updated issue is displayed on a project page test will look like this:

test('Verify that the updated issue is displayed on a project page', async (t) => {    await t.click('#top-menu .projects')        .click('.icon-add')        .typeText('#project_name', `test_project${randomDigits5}`)        .click('[value="Create"]')        .click('#top-menu .projects')        .click(`[href*="/projects/test_project${randomDigits5}"]`)        .click('.new-issue')        .typeText('#issue_subject', `Test issue ${randomDigits5}`)        .typeText('#issue_description', `Test issue description ${randomDigits5}`)        .click('#issue_priority_id')        .click(Selector('#issue_priority_id option').withText('High'))        .click('[value="Create"]')        .click('#top-menu .projects')        .click(`[href*="/projects/test_project${randomDigits5}"]`)        .click('#main-menu .issues')        .click(Selector('.subject a').withText(`Test issue ${randomDigits5}`))        .click('.icon-edit')        .selectText('#issue_subject')        .pressKey('delete')        .typeText('#issue_subject', `Issue ${randomDigits5} updated`)        .click('#issue_priority_id')        .click(Selector('#issue_priority_id option').         withText('Normal'))        .click('[value="Submit"]')        .click('#main-menu .issues')        .expect(Selector('.subject a').innerText).eql(`Issue ${randomDigits5} updated`);});

The Search for the issue test without all login actions will look like this:

test('Search for the issue', async (t) => {    await t.click('#top-menu .projects')        .click('.icon-add')        .typeText('#project_name', `test_project${randomDigits6}`)        .click('[value="Create"]')        .click('#top-menu .projects')        .click(`[href*="/projects/test_project${randomDigits6}"]`)        .click('.new-issue')        .typeText('#issue_subject', `Test issue ${randomDigits6}`)        .typeText('#issue_description', `Test issue description ${randomDigits6}`)        .click('#issue_priority_id')        .click(Selector('#issue_priority_id option').         withText('High'))        .click('[value="Create"]')        .navigateTo('http://demo.redmine.org/search')        .typeText('#search-input', `Test issue ${randomDigits6}`)        .click('[value="Submit"]')        .expect(Selector('#search-results').innerText).contains(`Test issue ${randomDigits6}`);});

Now, let's add the beforeEach block to the Redmine entities deletion tests fixture:

fixture('Redmine entities deletion tests')    .page('http://demo.redmine.org/')    .beforeEach(async (t) => {        await t.click('.login')            .typeText('#username', `test_user_testcafe_poc${randomDigits1}@sharklasers.com`)            .typeText('#password', 'test_user_testcafe_poc')            .click('[name="login"]');});

Let's also remove the login actions from all the tests of the Redmine entities deletion tests fixture, as these actions will now be executed in the beforeEach block. So, the Delete the issue test will look like this:

test('Delete the issue', async (t) => {    await t.click('#top-menu .projects')        .click('.icon-add')        .typeText('#project_name', `test_  project${randomDigits7}`)        .click('[value="Create"]')        .click('#top-menu .projects')        .click(`[href*="/projects/test_project${randomDigits7}"]`)        .click('.new-issue')        .typeText('#issue_subject', `Test issue ${randomDigits7}`)        .typeText('#issue_description', `Test issue description ${randomDigits7}`)        .click('#issue_priority_id')        .click(Selector('#issue_priority_id option').withText('High'))        .click('[value="Create"]')        .click('#top-menu .projects')        .click(`[href*="/projects/test_project${randomDigits7}"]`)        .click('#main-menu .issues')        .click(Selector('.subject a').withText(`Test issue ${randomDigits7}`))        .setNativeDialogHandler(() => true)        .click('.icon-del')        .expect(Selector('.subject a').withText(`Test issue ${randomDigits7}`).exists).notOk()        .expect(Selector('.nodata').innerText).eql('No data to display');});

And the Delete the file test, after all login actions were moved to the beforeEach block, will look like this:

test('Delete the file', async (t) => {    await t.click('#top-menu .projects')        .click('.icon-add')        .typeText('#project_name', `test_  project${randomDigits9}`)        .click('[value="Create"]')        .click('#top-menu .projects')        .click(`[href*="/projects/test_project${randomDigits9}"]`)        .click('.files')        .click('.icon-add')        .setFilesToUpload('input.file_selector', './uploads/test-file.txt')        .click('[value="Add"]')        .click('#top-menu .projects')        .click(`[href*="/projects/test_

         project${randomDigits9}"]`)        .click('.files')        .setNativeDialogHandler(() => true)        .click(Selector('.filename a').withText('test-file.txt').parent('.file').find('.buttons a').withAttribute('data-method', 'delete'))        .expect(Selector('.filename').withText('test-file.txt').exists).notOk()        .expect(Selector('.digest').withText('d8e8fca2dc0f896fd7cb4cb0031ba249').exists).         notOk();});

Note

You can also review and download this file on GitHub at https://github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch5/test-project/tests/basic-tests17.js.

As we have integrated setup and teardown blocks, let's see how to run tests with command-line settings.

Running tests with command-line settings

As we already learned in Chapter 3, Setting Up the Environment, when you trigger tests by executing the testcafe command, TestCafe reads settings from the .testcaferc.json configuration file, if this file exists, and then applies the settings from the command line on top of it. Command-line settings overrule values from the configuration file if they differ. TestCafe outputs information about each overridden property to the console.

Note

If the browsers and src properties are provided in the configuration file, you can omit them in the command line.

Let's review some of the main command-line settings that can be used with the testcafe command while launching the tests:

You can read more about all the command-line options at https://devexpress.github.io/testcafe/documentation/reference/command-line-interface.html.

It is good practice to keep all the major settings in the .testcaferc.json configuration file, overriding them with command-line settings when needed – for example, a combination of --debug-on-fail --speed 0.8 will be quite convenient for debugging.

To sum up, in this section we learned about some of the main command-line settings and how they can be used when launching tests.

Summary

In this chapter, we explored how to execute tests selectively, as well as how to generalize some test actions with the help of test setup and teardown. Also, we reviewed some command-line settings to run tests. Now we have an improved test suite and know how to run it with command-line options.

In the next chapter, we will continue to refine our test suite by moving some test logic to separate functions and refactoring tests with PageObjects.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.223.160.61