Working with test data


Separating test data from the test logic

One of the widely recognized best practices in test automation is that of storing the test data separately from the test logic. There are two main reasons for this:

  1. The same piece of data is often used in several tests. Referencing the test data from an external data file helps avoid duplicating it, making it easier to maintain.

  2. Test data and the test logic tend to change at different paces. Separating them makes it easier to find and fix the errors.

Here’s an example of a test action that populates its locator argument from an external data file named locators.yaml:

- description: Type "react" in the search box and press Enter
  action: org.getopentest.selenium.SendKeys
  args:
      locator: $data("locators").searchBox
      text: react
      sendEnter: true

The syntax $data("locators").searchBox tells the test actor to look for a property named searchBox in the data file named locators.yaml.

Note
By convention, all data files must be placed in the data subdirectory of the test repo. This is why, when we ask for a data file using the $data() syntax, we only have to specify the path relative to the test-repo-path/data directory. We can also omit the .yaml extension of the data file. These conventions allow for a concise syntax and make the test definitions easier to read and troubleshoot.

Of course, for all this to work we must also create the locators.yaml data file in the data directory:

test-repo-path/data/locators.yaml
searchBox: { name: q }

The test repo directory should now look like this:

test-repo
    ├── data
    │   └── locators.yaml
    └── tests
        └── Sample test.yaml

In real-world test automation scenarios test data can get quite complex. OpenTest recognizes this and allows testers the freedom to organize test data to best fit their particular use case. Read on to learn about some of the features and techniques that help alleviate this problem.

Splitting test data across files and directories

The earlier example defined a locators.yaml data file that can be used to store the complete list of object locators. However, for a bigger application, you might want to think about splitting the data into multiple files - one file per each page, and put all these files in a directory called locators. This is even more useful for large teams because it helps avoiding merge conflicts in source control.

To split your object locator data in multiple files, you can create a directory structure similar to the one below:

test-repo
├── data
│   └── locators
│       ├── login.yaml
|       ├── home.yaml
|       └── products.yaml
└── tests
    └── ...

To reference these files, you can use the same $data() syntax, as long as you specify the correct relative path to the data file you want to access:

- description: Type "react" in the search box and press Enter
  action: org.getopentest.selenium.SendKeys
  args:
      locator: $data("locators/home").searchBox
      text: react
      sendEnter: true
Note
The argument passed to the $data() function is the path to the data file, relative to the data subdirectory of the test repo and excluding the .yaml extension.

Storing data in a multi-level hierarchy

Since data files are expressed in YAML, you can always use the whole set of features that YAML brings along with it. For example, if you decide not to split the object locators into multiple files, you can still group the locators by the page they belong to inside one single data file:

test-repo-path/data/locators.yaml
login:
  username: { id: username }
  password: { id: password }

home:
  searchBox: { name: q }

The locator of the search box element on the homepage can be reached by using the dot operator to navigate through this hierarchy:

$data("locators").home.searchBox

While data hierarchies can go as deep as you need them to, we recommend to avoid using more than two levels, to make data files easier to read and maintain.

Referencing data files from other data files

While this might not seem like a very useful thing to do, at times you’ll find yourself wanting to split your data properties in a way that requires being able to read a data property from a data file and use it, maybe with some slight alterations, in a different data file.

Let’s say you have a data file called "environment.yaml" that stores the environment settings for some API test suite:

test-repo-path/data/environment.yaml
baseUrl: https://my.domain.com
username: john_doe
password: sk67E8qL

You can then store the URLs for various API in a different data file, by concatenating the base URL from environment.yaml with the rest of the URL, like this:

test-repo-path/data/urls.yaml
customerApiUrl: $data("environment").baseUrl + "/customer"
productApiUrl: $data("environment").baseUrl + "/product"
Note
The expressions used to build the API URLs are written in JavaScript. The test actor will evaluate the JavaScript expressions and use the results as values for the corresponding properties. More on this in the scripting support section.

Generating test data dynamically

Since the value of a data property can be any arbitrary JavaScript expression, there’s a lot of flexibility as to what you can do to generate it. For example, to generate a random username, you could do something like this:

test-repo-path/data/environment.yaml
username: $script "user" + (1 + Math.floor(Math.random() * 999))

You may have noticed that the $script syntax is not valid JavaScript code. This is just a prefix that lets the test actor know that we want whatever follows after it to be interpreted as JavaScript code. If we don’t include the $script prefix, the test actor will think that we are specifying just an ordinary string literal. In the scripting support section we elaborate on the logic used by the test actor to distinguish between string literals and JavaScript expressions.