Dynamic Tests Pt.1 JavaScript Title

Test Design: Dynamic Tests Pt. 1 – JavaScript

Over the next two articles, we’re going to identify the potential to make our tests dynamic. The first part looks at supplementing our scripts with JavaScript-based steps, while the latter covers some uses for Conditional Steps in Ghost Inspector. Our aim is to more closely approximate user behaviour, increasing the value of our test coverage.

Static vs. dynamic tests

If we run a static test (one which is identical in every run) in a controlled environment (one where we know the state of the system before every test run), we can be very detailed in our expectations. This allows us to set strict verification steps, therefore easily spot deviations from the expected behaviour. However, this is likely to provide a very narrow scope of coverage. Also, we might not be able to control the state of the environment on which we run our tests, requiring at least a level of flexibility from our scripts.

Our software under test must cope with many permutations of states. We’re unlikely to be able to provide test coverage for all permutations due to the setup and maintenance time investment. Rather than having a large suite of many static scripts, we could use fewer scripts which take a slightly different path each time they are run. Through this approach we introduce the potential for unexpected behaviour, which may enable us to uncover issues that a static/fixed path would not.

Behaving like the end user

We should use what we know about our end users when thinking about how to make our tests more valuable. For example, if we’re testing the search function for an ecommerce site, we most likely have an idea of the most popular search terms used. Why not use a pool of the most popular search terms to use in our testing to more closely match the actions of our end user?

There are likely several areas where we can use data to inform our tests. At a basic level, we know that our ecommerce customers don’t all purchase the exact same product. Therefore, the example we’ll work through will be to arrive at a random product for use in a purchase journey.

Worked Example

We’ll be using johnlewis.com as the basis for our dynamic journey example. The goal is to navigate through the site navigation structure until we can select a specific product and then add that product to the cart.

Top-level navigation

johnlewis.com Top-level Navigation Screenshot
the current johnlewis.com top-level desktop navigation at the time of writing

If we’re using the navigation in our tests, we need to be careful to use flexible selectors, as we know the navigation is likely to change over time. Of the top-level navigation items on johnlewis.com, all have a common structure except for the “Brands” option, which lacks a sub-category navigation. Ideally, to reduce scripting complexity, we want to keep to a predictable structure, so we should ignore the unique nav elements. Fortunately, in this case, the Brands item lacks the a.navigation-item-forward class. Therefore, we can just use the subset of nav items using this class (assuming this is always at the end of the regular nav items).

johnlewis.com Top-level Navigation Source Screenshot
the Brands menu item lacks the navigation-item-forward* class

In order to choose a random top-level item, we need to count the number of items (allowing for this to change over time) and randomly choose one from that number. To do this, we’ll use a JavaScript step in Ghost Inspector, so we need to write a small amount of JS. We’re going to take a very similar approach throughout the navigation process:

  • Find a selector to isolate one of the items from which we want to select randomly
  • Write some basic JS to count how many of those items there are
  • Pick a number between 1 and the total number of items
  • Use that number to select the item

For the top-level, we’re using the following selector: a[class*=”navigation-item-forward”]. We’re going to be using a partial/contains attribute selector quite frequently. This is because johnlewis.com has an ID appended to the end of all class names, and we don’t want to risk our tests being broken if a rebuild causes these ID’s to change.

var max = jQuery('a[class*="navigation-item-forward"]').length; var min = 1; return Math.floor(Math.random()*(max-min+1)+min);
Code language: JavaScript (javascript)

We’ll use the above JS to return the randomly selected number to be used in a variable. We can then use this variable (‘navitem’) in our selection in the click action – ul[class*=”navigation-navBar”] li:nth-child({{navitem}}) a

Sub-category selection

Now we have a top-level item selected, we now need to apply a similar approach to sub-category selection. We can see that the second tier of navigation actually has two levels of hierarchy – the more granular categories are grouped into a sub-category. As the number of these groups can differ between navigation section, we need to take a two-stage approach to our random selection. First, we will choose a group at random, then a link within that group at random.

johnlewis.com Second Tier Navigation Screenshot

TIP: As the sub-category menu is injected on the page by JavaScript, here’s a handy tip – with Dev Tools open, pause the JS by using F8 – this way you can inspect the menu easily to get your selector without it disappearing.

Part 1 – Picking a random sub-category group

When we look for our selector, we notice that things start to get messy due to a lack of specific data in the DOM we can use to isolate elements. Our selectors are going to be fairly complex, which also increases the likelihood of changes causing our selectors to fail. Encountering this on a project where you are part of the development team should trigger collaboration with developers to introduce something to increase the ease of testing. There’s a tendency to use ‘data’ attributes for this purpose, with many now using these in preference to ID’s. You’ll see examples of this on the John Lewis site later with the ‘data-test’ attribute used for this purpose.

In order to count the groups, we’ll use the selector ul[class^=”navigation-items”] > li[class=”navigation-item-item–level1″]:not([class=”navigation-item-mobile”])

var max = document.querySelectorAll('ul[class^="navigation-items"] > li[class*="navigation-item-item--level1"]:not([class*="navigation-item-mobile"])').length; var min = 1; return Math.floor(Math.random()*(max-min+1)+min);
Code language: JavaScript (javascript)

The selector we need to use here is particularly verbose, which serves to demonstrate the need to collaborate in order to ensure a site can be effectively tested:

var max = document.querySelectorAll('ul[class^="navigation-items"] > li[class*="navigation-item-item--level1"]:not([class*="navigation-item-mobile"]):nth-child({{subcategory}}) div[class^="navigation-navigationPane"] li:not([class*="navigation-item-mobile"]) a').length; var min = 1; return Math.floor(Math.random()*(max-min+1)+min);
Code language: JavaScript (javascript)

We store the outcome of the two above scripts as variables – ‘subcategory’ and ‘subcategorylink’. To select our random category, we use the following selector for our Click step: ul[class^=”navigation-items”] > li[class=”navigation-item-item–level1″]:not([class=”navigation-item-mobile”]):nth-child({{subcategory}}) div[class^=”navigation-navigationPane”] li:not([class*=”navigation-item-mobile”]):nth-child({{subcategorylink}}) a

Product selection

Now we have selected our final category page, we can focus on selecting a product. There is an additional level of complexity here – we should land on a list of products, but in a small number of cases it may be a content page with an additional category choice before hitting the products list. Let’s ignore this for now – we’ll pick this up in Part 2, where we use conditional steps to handle different permutations.

Assuming we land on a product listing page, we want to select a product that we know can be purchased. For this reason, we want to apply the ‘hide out of stock items’ filter prior to selecting a product. Note the presence of the data-test attribute in our selectors from this point, keeping them much more concise and robust.

johnlewis.com Hiding Out of Stock Items Screenshot
a[data-test=”oos-switch”] is our selector

Now we have our list of buyable products, we need to pick a random product from those on the page, using a similar technique to our navigation selection process. Fortunately, there are ‘data-‘ attributes to make our selectors straight forward. section[data-test=”product-card”] allows us to count the number of products displayed.

var max = document.querySelectorAll('section[data-test="product-card"]').length; var min = 1; return Math.floor(Math.random()*(max-min+1)+min);
Code language: JavaScript (javascript)

We store the outcome of the above in a variable called ‘product’, using the following selector to access the product page – div[data-test=”component-grid-container”] > div:nth-of-type({{product}}) a.product__image. We now have the full end-to-end process covered for selecting a random product using the main site navigation.

Configuring the tests in Ghost Inspector

Suite setup

Ghost Inspector Script Steps Screenshot
we only have a single script, which will eventually become a module to be inherited in journeys

Script steps example

the script for sub-category selection makes use of the Extract from JavaScript step type

Script run example

Ghost Inspector Script Run Screenshot
in action, we see the random numbers selected and their use in the :nth-child() argument

Leave a Comment

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.