Automated web shop testing with WebdriverIO

We’ve recently migrated our website to a new environment. While we were at it, we thought we could complement our simple uptime-Testing with some automated web shop testing, so that we can sleep at night knowing our site is OK. We’ve implemented some tests with WebdriverIO and thought we’d share the procedure, in case you’re looking for a quick guide. The entire test script is available for download here.

Our test scenario

When somebody buys Ghostlab from our website, they have to do several things: add Ghostlab to the cart, go to the cart page, click the checkout button, provide all required information in the checkout form, pay for the order, and finally they will expect to receive their license information via E-mail. And that’s exactly the procedure we want to pack into an automated test run.

  • Automated test steps
  • Go to Ghostlab landing page
  • Click “Buy” link to get to WooCommerce product page
  • Add Ghostlab to the cart
  • Click on “View cart” to see the cart contents
  • Proceed to checkout
  • Complete checkout form
  • Pay – we’ll do that with a trick
  • On order completion, receive E-mail with license key

 

Set up WebdriverIO project

Before you can get started, you have to set up a WebdriverIO project. It’s really easy, just follow the WebdriverIO guide. What you’re doing is basically:

  • set up your project directory
  • download selenium server and driver – be sure to start the server after download.
  • install webbdriverio via npm

Once you’re set up, let’s directly create a test runner for our project. Just follow the instructions for how to complete the testrunner setup dialog (WebdriverIO guide, “Let’s get serious”). Note that as of today (January 13 2017), there is one additional setup step in the dialog that is not described in the guide – I just selected the default value.

I’m assuming that you now have your test/specs directory. Now create a JavaScript file there that lets you write your test. You should end up with a setup comparable to the one in the image below.

# To run your test(s), simply execute the 
# following from the command line in the 
# project directory. 
./node_modules/.bin/wdio wdio.conf.js
Why WebdriverIO?
There are many tools to create automated tests. When we set up our tests, we were looking for something that was powerful, well-documented, easy to setup, and locally available (i.e. no SaaS). We gave several tools a quick spin, and WebdriverIO convinced us because the time to your first hello world is really short, and it is very well documented.

Now on to the actual test steps…we will only be looking at a small set of features of the Webdriver API – for the full reference, see WebdriverIO API Docs.

1: Navigate to landing page, follow to WooCommerce product page, add Ghostlab to cart

We want to start from our product landing page, so we need to point the browser to that location.

browser.url('https://www.vanamco.com/ghostlab');

On this page, there is a Buy button. We’d expect visitors who want to buy Ghostlab to click it and get to the WooCommerce product page. After clicking the button, we need to wait for the WooCommerce product page to load before we can perform our further actions. Therefore, we’re waiting for an element to be present that was not on the landing page, but is on the WooCommerce product page, namely the “Add to cart” button.

browser.click('=Buy');
browser.waitForExist('.single_add_to_cart_button.button', 5000);

Both the click and waitForExist function expect a Selector as first argument. A selector is used to specify the element in question – the one that we want to click on, or the one we’re waiting for to be present.

We’re specifying a timeout of 5000 milliseconds for waitForExist – in case the page does not load within this time, the test will fail. So make sure you specify a timeout long enough for your page to load.

Selectors
There’s a wide range of possible selectors, we’ve found the link text (‘=Buy’, on anchor elements) and CSS selectors to be easy to handle. Of course, which selectors best suit you depends on the content of your page. As soon as you start using selectors, be aware that changes in your page might break your test. For example, if we were to change the text of the buy button to “Get one now”, the test would no longer work. See the Developer Guide for a full reference of available selectors.

As soon as the WooCommerce product page has loaded, there will be an element to add the product to the cart, and our test run will continue. Next we actually want to add 1x Ghostlab to the cart by pressing the button. When this is done, the page will show a confirmation message that we’ve added it to the cart – and there will be a link that takes us to our shopping cart. So we wait for the confirmation to appear, then follow the cart link.

browser.click('.single_add_to_cart_button.button');
browser.waitForExist('.woocommerce-message > a', 5000);
browser.click('.woocommerce-message > a');

2: Enter coupon code so we don’t have to provide payment information, continue to checkout

As soon as the cart is displayed, we enter a coupon code we’ve prepared for the test that gives us 100% off. This means that our order totals 0$ and we can perform the checkout without entering payment information.  We detect whether the cart has loaded by checking for the presence of the coupon code field. We then wait for the confirmation message WooCommerce writes to the page to show that the code was applied, and verify that it was successfully applied by examining the content of the message.

browser.waitForExist('input[name="coupon_code"]');
browser.setValue('input[name="coupon_code"]', config.couponCode);
browser.click('input[name="apply_coupon"]');
browser.waitForExist('.woocommerce-message', 5000);
var msg = browser.getText('.woocommerce-message').trim();
assert(msg === 'Coupon code applied successfully.', 'Did not see coupon code applied successfully message');

Once the coupon code is applied, we go to the checkout form by clicking the proceed to checkout link. We detect the load of the checkout form by checking for the presence of the place order button – which we will use in the end to actually place our order.

browser.click('.wc-proceed-to-checkout > a');
browser.waitForExist('input[name="woocommerce_checkout_place_order"]');

3: Complete checkout form

Now we can fill out the checkout form. We’ve stored the field selectors and the associated values in a separate array over which we can iterate – of course, you could also solve this differently. One field we have to treat specially is the country selector – this is not a simple select, but a JavaScript-enhanced select component where you can search for values, then select them by clicking them. We are unable to simply set the value as with a regular input field, so we need to perform the click-search-select pattern that a real user would use.

 

config.checkoutForm = [
 {s: 'input[name="billing_first_name"]', v: 'Automatic'} /*, ... */
];

for (var i = 0; i < config.checkoutForm.length; i++) {
    var item = config.checkoutForm[i];
    browser.setValue(item.s, item.v);
 }
browser.click('.country_select');
myWait(1000);
browser.setValue('#s2id_autogen1_search', 'Switzerland');
myWait(500);
browser.click('.select2-result-label');

 

We are using the myWait function. This will simply make the test wait for a specified number of milliseconds by using the waitUntil function in combination with some time math. We use it on the JavaScript select box to make sure that the page has had the time to first render the search box, then filter down the results. We’ll later use the myWait function before trying to fetch the license e-mail.

/*
 * Simple function to wait for a specified number of milliseconds (roughly)
 */
function myWait(ms) {
    console.log('Waiting for ' + (ms/1000) + ' seconds');
    var now = new Date().getTime();
    browser.waitUntil(function(){
        return (new Date().getTime() - now) > ms;
    }, ms + 1000)
}

4: Place order, verify order confirmation, extract order number

Now that the form is completely filled out, we can place the order. For that, we simply click the place order button. After the order has been processed, we will see a confirmation page. We verify that we have reached it by waiting for the thank you note, and by examining the content of the message. Also, we extract the order number assigned to our order by the system. We need this to check if we have received the license e-mail.

browser.click('input[name="woocommerce_checkout_place_order"]');
browser.waitForExist('.woocommerce-thankyou-order-received');
msg = browser.getText('.woocommerce-thankyou-order-received').trim();
assert(msg === 'Thank you. Your order has been received.', 'Did not see order has been received message.');

console.log('Extracting order number...');
var orderNumber = browser.getText('.woocommerce-thankyou-order-details.order_details > li:first-child > strong').trim();

 

5: Verify that license e-mail has been sent

On order placement, our backend should have generated a license key for Ghostlab and sent it to the e-mail address we have provided in the checkout form. In order to complete the test, we need to see if that e-mail has actually been delivered. For that, we use a node imap package to connect to the e-mail account we provided on purchase. Before we try and fetch the e-mail, we wait for 30 seconds, otherwise, it might not have been delivered yet.

Mocha timeouts
By default, mocha (which we’re using in our example) has a timeout of 10s – so when we would wait for 30s, that would fail, as the global timeout would apply after 10s. You can increase the mocha timeout in the mocha options (mochaOpts.timeout), this can be found in the file wdio.conf.js, which is automatically created for your project.

We know that the Subject text of that license e-mail is unique – it is some static text combined with the order number (that’s the unique part). So we can simply search for a message with the expected subject, and will be satisfied if the number of messages returned by the search is exactly 1. Of course, we could go on to examine the content of the license email, maybe even try to activate the software product with the license – but we’ll leave that for some other time.

// Let's wait a bit before we try to fetch the E-mail
myWait(10 * 1000);

var subject = 'Ghostlab 2 license for your order ' + orderNumber + ' (1 x Ghostlab)'
var foundEmail = false;
imap.once('ready', function(){
  imap.openBox('INBOX', true, function(err, box){
    imap.search([ 'UNSEEN', ['SUBJECT', subject] ], function(err, results) {
      if (err) 
        throw err;
       var resultsLength = results.length;
       assert(resultsLength === 1, 'Did not find exactly one e-mail with license information');
       foundEmail = true;
      imap.end();
    }); 
  });
});
imap.connect();

// Wait for e-mail to be confirmed.
browser.waitUntil(function() { return foundEmail; }, 10000);

Note that the call to imap.connect() is asynchronous – so how do we “notify” our test that e-mail fetching has been successful? We use the waitUntil function and pass it a function that returns a variable. In the e-mail handling, we set the variable to true if we successfully fetch the e-mail.

The entire test script is available for download here.