Write a synthetic test

edit

This functionality is in beta and is subject to change. The design and code is less mature than official GA features and is being provided as-is with no warranties. Beta features are not subject to the support SLA of official GA features.

After setting up a project, you can start writing synthetic tests that check critical actions and requests that an end-user might make on your site.

Syntax overview

edit

To write synthetic tests for your application, you’ll need to know basic JavaScript and Playwright syntax.

Playwright is a browser testing library developed by Microsoft. It’s fast, reliable, and features a modern API that automatically waits for page elements to be ready.

The synthetics agent exposes an API for creating and running tests, including:

journey

Tests one discrete unit of functionality. Takes two parameters: a name (string) and a callback (function). Learn more in Create a journey.

step

Actions within a journey that should be completed in a specific order. Takes two parameters: a name (string) and a callback (function). Learn more in Add steps.

beforeAll

Runs a provided function once, before any journey runs. If the provided function is a promise, the runner will wait for the promise to resolve before invoking the journey. Takes one parameter: a callback (function). Learn more in Set up and remove a global state.

before

Runs a provided function before a single journey runs. Takes one parameter: a callback (function). Learn more in Set up and remove a global state.

afterAll

Runs a provided function once, after all the journey runs have completed. Takes one parameter: a callback (function). Learn more in Set up and remove a global state.

after

Runs a provided function after a single journey has completed. Takes one parameter: a callback (function). Learn more in Set up and remove a global state.

monitor

The monitor.use method allows you to determine a monitor’s configuration on a journey-by-journey basis. If you want two journeys to create monitors with different intervals, for example, you should call monitor.use in each of them and set the schedule property to different values in each. Note that this is only relevant when using the push command to create monitors in Kibana. Learn more in Configure individual monitors.

Create a journey

edit

Create a new file using the .journey.ts or .journey.js file extension or edit one of the example journey files.

A journey tests one discrete unit of functionality. For example, logging into a website, adding something to a cart, or joining a mailing list.

The journey function takes two parameters: a name and a callback. The name helps you identify an individual journey. The callback argument is a function that encapsulates what the journey does. The callback provides access to fresh Playwright page, params, browser, and context instances.

journey('Journey name', ({ page, browser, context, params, request }) => {
  // Add steps here
});

name

A user-defined string to describe the journey.

page

A page object from Playwright that lets you control the browser’s current page.

browser

A browser object created by Playwright.

context

A browser context that doesn’t share cookies or cache with other browser contexts.

params

User-defined variables that allow you to invoke the Synthetics suite with custom parameters. For example, if you want to use a different homepage depending on the env (localhost for dev and a URL for prod). See Work with params and secrets for more information.

request

A request object that can be used to make API requests independently of the browser interactions. For example, to get authentication credentials or tokens in service of a browser-based test. See Make API requests for more information.

Add steps

edit

A journey consists of one or more steps. Steps are actions that should be completed in a specific order. Steps are displayed individually in the Uptime app along with screenshots for convenient debugging and error tracking.

A basic two-step journey would look like this:

journey('Journey name', ({ page, browser, client, params, request }) => {
    step('Step 1 name', () => {
      // Do something here
    });
    step('Step 2 name', () => {
      // Do something else here
    });
});

Steps can be as simple or complex as you need them to be. For example, a basic first step might load a web page:

step('Load the demo page', () => {
  await page.goto('https://elastic.github.io/synthetics-demo/'); 
});

See the page.goto reference for more information.

A more complex step might wait for a page element to be selected and then make sure that it matches an expected value.

For example, on a page using the following HTML:

<header class="header">
  <h1>todos</h1>
  <input class="new-todo"
    autofocus autocomplete="off"
    placeholder="What needs to be done?">
</header>

You can verify that the input element with class new-todo has the expected placeholder value (the hint text for input elements) with the following test:

step('Assert placeholder text', async () => {
  const placeholderValue = await page.getAttribute(
      'input.new-todo',
      'placeholder'
  ); 
  expect(placeholderValue).toBe('What needs to be done?'); 
});

Find the input element with class new-todo and get the value of the placeholder attribute. See the page.getAttribute reference for more information.

Use the assertion library provided by the Synthetics agent to look for the expected value. See Jest expect docs for more information.

If you want to generate code by interacting with a web page directly, you can use the Synthetics Recorder.

The recorder launches a Chromium browser that will listen to each interaction you have with the web page and record them internally using Playwright. When you’re done interacting with the browser, the recorder converts the recorded actions into JavaScript code that you can use with Elastic Synthetics or Heartbeat.

For more details on getting started with the Synthetics Recorder, refer to Use the Synthetics Recorder.

Make API requests

edit

You can use the request parameter to make API requests independently of browser interactions. For example, you could retrieve a token from an HTTP endpoint and use it in a subsequent webpage request.

step('make an API request', async () => {
  const response = await request.get(params.url);
  // Do something with the response
})

The Elastic Synthetics request parameter is similar to other request objects that are exposed by Playwright with a few key differences:

  • The Elastic Synthetics request parameter comes built into the library so it doesn’t have to be imported separately, which reduces the amount of code needed and allows you to make API requests in inline journeys.
  • The top level request object exposed by Elastic Synthetics has its own isolated cookie storage unlike Playwright’s context.request and page.request, which share cookie storage with the corresponding BrowserContext.
  • If you want to control the creation of the request object, you can do so by passing options via --playwright-options or in the synthetics.config.ts file.

For a full example that shows how to use the request object, refer to the Elastic Synthetics demo repository.

The request parameter is not intended to be used for writing pure API tests. Instead, it is a way to support writing plain HTTP requests in service of a browser-based test.

Set up and remove a global state

edit

If there are any actions that should be done before or after journeys, you can use before, beforeAll, after, or afterAll.

To set up global state or a server that will be used for a single journey, for example, use a before hook. To perform this setup once before all journeys, use a beforeAll hook.

before(({ params }) => {
  // Actions to take
});

beforeAll(({ params }) => {
  // Actions to take
});

You can clean up global state or close a server used for a single journey using an after hook. To perform this cleanup once after all journeys, use an afterAll hook.

after(({ params }) => {
  // Actions to take
});

afterAll(({ params }) => {
  // Actions to take
});

Import NPM packages

edit

You can import and use other NPM packages inside journey code. Refer to the example below using the external NPM package is-positive:

import { journey, step, monitor, expect } from '@elastic/synthetics';
import isPositive from 'is-positive';

journey('bundle test', ({ page, params }) => {
  step('check if positive', () => {
    expect(isPositive(4)).toBe(true);
  });
});

When you create a monitor from a journey that uses external NPM packages, those packages will be bundled along with the journey code when the push command is invoked.

However there are some limitations when using external packages:

  • Bundled journeys after compression should not be more than 800 Kilobytes.
  • Native node modules will not work as expected due to platform inconsistency.

Sample synthetic test

edit

A complete example of a basic synthetic test might look like this:

import { journey, step, expect } from '@elastic/synthetics';

journey('Ensure placeholder is correct', ({ page }) => {
  step('Load the demo page', async () => {
    await page.goto('https://elastic.github.io/synthetics-demo/');
  });
  step('Assert placeholder text', async () => {
    const placeholderValue = await page.getAttribute(
      'input.new-todo',
      'placeholder'
    );
    expect(placeholderValue).toBe('What needs to be done?');
  });
});

You can find more complex examples in the Elastic Synthetics demo repository.

Test locally

edit

As you write journeys, you can run them locally to verify they work as expected. Then, you can create monitors to run your journeys at a regular interval.

To test all the journeys in a project, navigate into the directory containing the synthetics project and run the journeys in there. By default, the @elastic/synthetics runner will only run files matching the filename *.journey.(ts|js)*.

# Run tests on the current directory. The dot `.` indicates
# that it should run all tests in the current directory.
npx @elastic/synthetics .
Test an inline monitor
edit

To test an inline monitor’s journey locally, pipe the inline journey into the npx @elastic/synthetics command.

Assume, for example, that your inline monitor includes the following code:

step('load homepage', async () => {
    await page.goto('https://www.elastic.co');
});
step('hover over products menu', async () => {
    await page.hover('css=[data-nav-item=products]');
});

To run that journey locally, you can save that code to a file and pipe the file’s contents into @elastic-synthetics:

cat path/to/sample.js | npx @elastic/synthetics --inline

And you’ll get a response like the following:

Journey: inline
   ✓  Step: 'load homepage' succeeded (1831 ms)
   ✓  Step: 'hover over products menu' succeeded (97 ms)

 2 passed (2511 ms)