loading...
Cover image for End-to-End Testing with TestCafe Book Excerpt: Hooks

End-to-End Testing with TestCafe Book Excerpt: Hooks

dennmart profile image Dennis Martinez Originally published at dev-tester.com on ・12 min read

This article is an excerpt from one of the chapters of my upcoming book, End-to-End Testing with TestCafe. If you're interested in learning more about the book, visit https://testingwithtestcafe.com.


In the previous chapter, you began learning how to organize your tests in separate files, each with its fixture, to set up the tests in a consistent manner. Identifying your tests and grouping them by a common theme, like the functionality under test, helps with the long-term maintenance of your test suite. You'll know where your tests are, and anyone can have a birds-eye view of the test suite by glancing at the test results.

However, the tests written so far could use some improvement. In particular, you may have noticed that the tests written so far contain some repetitive code that handles the login functionality for your tests. While the tests work and pass successfully, having duplicate functionality across multiple tests can lead to problems in the future. For example, if the login functionality for TeamYap changes, you'll need to make the changes for all of the previous tests you've written so far. Besides being a tedious task, it introduces the risk of missing a spot and causing your tests to fail.

Since all the existing tests have to perform the same steps to log in before running the code for validating the Feed section functionality, you can move this logic to a single place to keep your tests tidy and organized. With TestCafe, you can use hooks to write your setup and cleanup tasks, so you don't have to write the same code repeatedly across different tests.

In this chapter, you'll learn about the different types of hooks included in TestCafe. Then, you'll put this knowledge into practice by refactoring and organizing your existing tests.

What are hooks?

While building your test suite, you'll often find yourself needing to perform some initial setup before your tests run, so they execute successfully. You may also need to run a few tasks after the tests finish to ensure future test runs start from a clean slate. You can write the functionality to complete these tasks before and after each test, but you'll find yourself with lots of duplicate code and a maintenance nightmare.

Instead of trying to manage extra code, you can apply hooks to keep your tests manageable. Hooks are functions that allow you to run code before a test or a group of tests begins to run. You can also use hooks to execute code after a single test or a group of tests completes its test run. Keeping the same initialization and cleanup tasks separate will help keep your tests short and focused on what it should validate.

TestCafe has two kinds of hooks for you to use, each helping you in different situations.

Test hooks

The first type of hook functions provided by TestCafe are test hooks. You can use test hooks to define a function that will run before and after test execution. Test hooks are the ideal place to write any setup or cleanup code your tests need to keep your scenarios organized and consistent. A few common uses for test hooks in TestCafe are:

  • Arranging repetitive code in your tests that needs to occur before or after a test, such as the login functionality in your existing tests.
  • Accessing the initial state of your application and storing information that you can share with other tests.
  • Ensuring that the application under test resets to a useable state, so your current test doesn't interfere with subsequent test runs.

Using test hooks

You can define a test hook either when initializing a fixture object or a test object. Both of these objects expose different methods to allow you to set up the hook.

On a fixture object, the beforeEach method allows you to define the code you want to execute before each of the tests inside the fixture run. The afterEach method lets you set what you want to execute after each of the tests finish. Keep in mind that the code defined in these test hook functions run before and after each test case.

On a test object, you can use the before method for any process you want to carry out before the individual test begins to execute, and the after method for defining code to run after the test finishes up. Typically, these methods are used for overriding the beforeEach and afterEach methods described in the fixture.

Besides the different method names, defining a test hook is identical for both fixtures and individual tests. The test hook requires an asynchronous anonymous function containing the code to use for the initialization or cleanup. The function includes TestCafe's test controller as an argument that allows you to access and use TestCafe's testing API. These functions are similar to defining a test case using the test function, so they will look familiar to you:

fixture("Fixture with test hooks")
  .beforeEach(async t => {
    // The code inside this function runs
    // before every test in this fixture
    // except where overridden.
  })
  .afterEach(async t => {
    // The code inside this function runs
    // before every test in this fixture
    // except where overridden.
  });

test
  .before(async t => {
    // The code inside this function runs
    // before this test, ignoring the code
    // defined in the beforeEach function
    // in the fixture.
  })
  .after(async t => {
    // The code inside this function runs
    // after this test, ignoring the code
    // defined in the afterEach function
    // in the fixture.
  })
  ("Test with test hooks", async t => {
    // This test only executes the code
    // defined in the before and after
    // functions in the test object.
  });

test("Test without test hooks", async t => {
  // This test executes the code defined in
  // the beforeEach and afterEach functions
  // in the fixture.
});

test("Another test without test hooks", async t => {
  // This test also executes the code defined
  // in the beforeEach and afterEach functions
  // in the fixture.
});

In the example code above, we have one fixture containing three tests. The fixture defines both the initialization and cleanup for the fixture tests by chaining together the beforeEach and afterEach methods to the fixture object. One of the tests contains its own initialization and cleanup functions defined in the before and after methods. This test will not execute the code from the fixture methods. The other tests that don't contain the before and after methods will run the fixture's initialization and cleanup code.


Note

A common mistake when using the beforeEach and afterEach functions is including too much functionality that executes before and after every individual test under the fixture. If you include more than you need, you risk your test suite running slowly and inconsistently, making it difficult to debug any issues that arise due to the extra code.

An example is if you have a test hook defined for a fixture that logs in a user. In the beforeEach function for the fixture, the test goes through the user login process, which takes a couple of extra sections before the test begins. However, if you have a few tests under the fixture that don't require a logged-in user to execute the test successfully, you and your team will waste plenty of time waiting for the test suite to perform actions it doesn't have to do every time.

Prevent your tests from slowing down by only including what you need in the beforeEach and afterEach functions, and splitting up tests into separate fixtures when required. Even if you only have one or two tests in a fixture that don't need to execute the test hook, it's worth fixing the problem as soon as you can so it doesn't get worse as time goes on.


Sharing information between tests using test hooks

Besides executing code, test hooks allow you to share information between the hooks and the tests. You can use this functionality to grab any information from the actions performed in your test hooks, store it in a variable, and use that variable inside any of the fixture's tests. It's useful when you carry out an action before the test starts that returns data required in the middle or after a test run.

TestCafe's test controller contains a test context object that you can access in the test hooks and any test scenario. To access the test context object, use the t.ctx property, where t is TestCafe's test controller. The test context object serves as both a setter and a getter method. You can define new properties to the object and later read them to retrieve the actual value. You can also modify existing properties if needed.

Here's an example showing how the test context object works when using test hooks for a fixture:

fixture("Fixture using test context")
  .beforeEach(async t => {
    // Adding the property 'loginName' to the
    // test context object and setting a value.
    t.ctx.loginName = "Dennis Martinez"
  });

test("Accessing test context", async t => {
  // Read the 'loginName' property set in the
  // beforeEach function for the fixture.
  await t
    .expect(t.ctx.loginName)
    .eql("Dennis Martinez");
});

You can also assign data to the test context object inside an individual test. However, keep in mind that any assigned values in this manner are valid only for the test where the value gets defined in the test hook. You can't access any data or properties from a local test context object in another test case, as shown in the example below:

test.before(async t => {
  t.ctx.loginName = "New User";
})
("Accessing test context", async t => {
  // This test passes because the test
  // context property is defined in the
  // test hook.
  await t
    .expect(t.ctx.loginName)
    .eql("New User");
});

test("Checking if context still works", async t => {
  // This test will fail because the test
  // context property does not define the
  // loginName property in the fixture of
  // test.
  await t
    .expect(t.ctx.loginName)
    .eql("New User");
});

You can also override the values of existing properties for a test context object in test hooks for both fixtures and tests. However, if a test case doesn't override a property, it will use the assigned value from the fixture test hook:

fixture("Fixture using test context")
  .beforeEach(async t => {
    t.ctx.loginName = "Dennis Martinez"
  });

test.before(async t => {
  t.ctx.loginName = "New User";
})
("Accessing local test context", async t => {
  // The test context object value comes from
  // the before test hook for the test.
  await t
    .expect(t.ctx.loginName)
    .eql("New User");
});

test("Accessing fixture test context", async t => {
  // The test context object value comes from
  // the beforeEach test hook for the fixture.
  await t
    .expect(t.ctx.loginName)
    .eql("Dennis Martinez");
});

Using the test context object is useful in many cases. For instance, you can grab some information during initialization that you need to use later, like reading from a third-party API or saving a piece of information from the page at its existing state. You can save that information as a property in the test context, and easily read it later when you can use it for your tests.


Note

One thing to keep in mind is that test hooks for initializing your tests - beforeEach for fixtures, before for test cases - run after TestCafe loads the starting page for the test. It allows you to have access to the initial page and perform any necessary actions on the first page that loads in the test. You can interact with the page in the same manner as you do when writing tests.

This functionality is useful if you want to save any information about the application's initial state, which you can use store in the test context and use later in your tests. However, it can also introduce confusion if you change the initial state in the test hook and don't expect it in a test. For instance, if you perform an action that changes the page in any way, it will remain in the altered state when the test run begins., which may cause confusion if you're expecting the initial page you defined in the fixture.

Often, you want to maintain your initial starting point on the page you define in the fixture for your tests. But as you'll see when you refactor your tests in a bit, it's also okay to allow actions in your test hooks to change the state of the starting page in the application. Be aware of what your test hooks do to the application under test to avoid any problems in each test case.


Fixture hooks

The other type of hook function included in TestCafe are fixture hooks. As the name suggests, these are functions that are only defined on a fixture object. Unlike test hooks, fixture hooks will run only once - before any tests in the fixtures begin, and after all tests in the fixture end. Fixture hooks are often used for one-time tasks related to the application under test, often to perform server-side operations.

  • Make a request to the server to set up the initial set of data needed in the application for the tests to execute.
  • Call an API endpoint to drop a database after a set of tests completes.
  • Access any artifacts left behind in the application by the test runs, like uploaded files or generated reports.

Using fixture hooks

You can define a fixture hook when initializing a fixture object by using the before method. The method definition is similar to the beforeEach test hook, requiring an asynchronous anonymous function containing the code you need to execute. The main difference with fixture hooks is that the function doesn't include TestCafe's test controller like it does with test hooks. Instead, the function gives you access to the fixture context.

Fixture hooks can run alongside test hooks. The code defined in fixture hooks will execute before and after any defined test hooks in the fixture:

fixture("Fixture and test hooks")
  .before(async ctx => {
    // The code inside this function runs only
    // once, before the beforeEach test hook.
  })
  .beforeEach(async t => {
    // The code inside this function runs after
    // the before function and before each test.
  })
  .afterEach(async t => {
    // The code inside this function runs before
    // the after function and after each test.
  })
  .after(async ctx => {
    // The code inside this function runs only
    // once, after the afterEach test hook.
  });

For the example above, we have one fixture containing both fixture hooks and test hooks. The before and after methods execute their code only once at the start and end of the test execution for this fixture, respectively. The first beforeEach test hook will run as soon as the before fixture hook finishes, and the last afterEach test hook will perform its code first, before the after fixture hook. Test hooks will run before and after each test, as usual.


Note

Fixture hooks share the same method name with test hooks for individual tests. It's easy to confuse these methods when writing tests, so double-check which kind of hook you need for your fixtures when defining them.


Sharing information between tests using fixture hooks

Fixture hooks also allow you to share any data between the hooks and the tests belonging to the fixture. Any properties assigned in a fixture context become accessible in test hooks and individual test cases. However, sharing data between a fixture hook and a test is slightly different than when using test hooks, since you won't have access to the test controller object.

The context provided by the function defined in the fixture hook works similar to using t.ctx for the test context object seen before with test hooks. You can set new properties to the object and retrieve any assigned value at a later time. The difference is that you can now assign properties directly on the fixture context:

fixture("Using fixture context")
  .before(async ctx => {
    // Adding the property 'loginName' to the
    // fixture context object and setting a value.
    ctx.loginName = "Dennis Martinez"
  });

Accessing the values of the fixture context inside a test is also different. TestCafe's test controller object includes a method to allow you to access the fixture context, using t.fixtureCtx:

fixture("Using fixture context")
  .before(async ctx => {
    ctx.loginName = "Dennis Martinez"
  });

test("Accessing fixture context", async t => {
  // Read the 'loginName' property set in the
  // before function for the fixture.
  await t
    .expect(t.fixtureCtx.loginName)
    .eql("Dennis Martinez");
});

Since test hooks also have access to the same test context object, you can access the fixture context in the same way:

fixture("Using fixture context")
  .before(async ctx => {
    ctx.loginName = "Dennis Martinez"
  });

test.before(async t => {
  // Read the 'loginName' property from the
  // fixture context and assign it to the
  // 'fixtureLoginName' in the test context
  // with an update.
  t.ctx.fixtureLoginName = t.fixtureCtx.loginName + "!!!";
})("Accessing fixture context", async t => {
  // Read the 't.ctx.fixtureLoginName' property
  // set in the local test hook.
  await t
    .expect(t.ctx.fixtureLoginName)
    .eql("Dennis Martinez!!!");
});

You can override fixture context properties inside test hooks and test cases. However, it's more useful to assign a property to the test context object instead of replacing the value in the fixture context.


Note

Another significant difference between the different kinds of hooks is that the before fixture hooks do not have access to the page defined by the fixture. You don't have access to the initial page and can't interact with the page inside these fixture hooks. You should use fixture hooks for interacting with your application in other ways, like using an API.


End-to-End Testing with TestCafe

If you found this article useful and want to know when the End-to-End Testing with TestCafe book is available, visit https://testingwithtestcafe.com and sign up to the mailing list. You'll receive the first three chapters of the book for free. You'll also receive exclusive updates and be among the first to know when the book is available for purchase for a discount.

Discussion

pic
Editor guide