Testing a Data Layer with Python

In a previous post, I wrote about using Python to automate the QA process when writing Adobe DTM rules. Although better than nothing, the process outlined there still has a single point of failure, me. Therefore, I would like to take my testing to its logical conclusion by removing myself and running unit tests. Just as before I will be using Python and Selenium for my automation and adding the Python unittest module which is part of the standard library.

This time I will not be testing DTM rules specifically but rather the values from a JavaScript data layer object on different pages. However, the method I use here can be used to test analytics rules, or anything else for that matter that can be evaluated in the JavaScript console.

For demonstration purposes I will be testing a data layer object I found “in the wild” on Ticketmaster.com. Looking at the Ticketmaster.com homepage with Tagtician and Omnibug reveal a lot of data. After some inspection, I see that Ticketmaster’s data layer object is called digitalData. I will be testing the values of this object along with a few props and conversion variables that I believe correspond with this object. In doing this I'm going to make a few assumptions: the values written to digital data are correct, the props and conversion variables being sent are correct and are being set using digitalData. Also, for no reason at all, I am going to assume that the digital data object itself is being written by DTM and thus I will be testing that rule as well.


First let's see what we are working with. Here is what the Ticketmaster homepage data object and page load looks like in JavaScript console, Omnibug and Tagtician. As you can see the values in the digitalData object seem to correspond with some of the analytics variables being sent. This is a good indication that my assumption that the variables are being sent using the digital data object as a reference is correct.

digitalData on the Ticketmaster Homepage
Ticketmaster Homepage Omnibug
Ticketmaster Homepage Tagtician (cropped)

Time to start testing. I am going to use the same Selenium driver set up as my previous article, but any proper driver should suffice. Its easiest to think of a driver as the browser that Selenium will use to execute your automations. This is how I set up my Chrome driver:

  • Webdriver is the selenium module that is used to create the browser object. This object will be used to traverse my list of links. I customize this object in the function, then return it.
  • Chrome Options allows me to add extensions. Note that Chrome extensions must be packaged before they can be used with Selenium. More on that here. I use get to access the dictionary in case the paths have not been added to the .env file.
  • Desired Capabilities is dict of Chrome preferences. I update it here to allow access to the browser’s javascript console output. I will not be accessing the console output here, but I always add this feature just in case.
  • I use a .env that is not in source control to store sensitive information. This approach is also easier when sharing the code since others can just make an .env file instead of adding stuff to their PATH. In my case specifically this approach makes it easier to share the script between Windows and Mac users.

With my driver set up I can now automate browser functions and just as important, I can run Javascript in the driver and return its output in Python. This is the crux of my unit tests. Navigate to a page, run JavaScript statement and test the value it returns against a known value. On a side note, I like to use triple quotes to differentiate Javascript statements in Python. For example:

>>> python_var = driver.execute_script(''' return 2+2; ''')
>>> print python_var
4

The driver will convert Javascript types to Python types when returning data. The python_var above will be an integer, Javascript true will become Python True, and even Javascript objects will become Python dictionaries. This is very convenient since we will be checking values in the digitalData object. I’ll make a function so we can evaluate digitalData as a dictionary whenever necessary. You will see this in action shortly.

def digital_data(driver):
return driver.execute_script(''' return digitalData; ''');

Setting up and running unit tests with Selenium is very similar to doing regular unit tests in Python. I’m not going to cover those conventions here, but for those who are interested I highly recommend this section of the Hitchhiker’s Guide to Python. First, the basics:

  • The setUp function is used to create an instance of our Chrome Driver assigned to self.driver. To access the driver in the test functions we can call self.driver.
  • tearDown function handles all the cleanup and closes the driver. You can also use driver.quit() in place of close().
  • The actual testing functions will go between the setUp and tearDown. Technically they can go anywhere in the class, but putting them between setUp and tearDown just makes it more readable for me. In this example I have a very simple test that navigates to the Ticketmaster homepage via Chrome/Selenium and evaluates whether the string ticket is in the URL. When running the script you will see Chrome open and close fairly quickly. Most of the time in the test is spent waiting for Chrome to load initially. The output of this test should look like so:
$ python -m unittest test_basic
Importing environment from .env...
.
--------------------------------------------------------------------
Ran 1 test in 6.452s
OK

If the test were to fail the output would look a little different:

$ python -m unittest test_basic
Importing environment from .env...
F
====================================================================
FAIL: test_driver (test_basic.TestBasic)
--------------------------------------------------------------------
Traceback (most recent call last):
File "test_ticketmaster.py", line 14, in test_driver
assert "foobar" in self.driver.current_url
AssertionError
--------------------------------------------------------------------
Ran 1 test in 6.566s
FAILED (failures=1)

Now that we have everything set up its time to start testing our data layer and Adobe metrics. Let’s test the pageName which is “TM_US: Home”. You can see this value in digitalData.page.pageInfo.pageName and prop1. Here I am assuming that the satellite variable named “page.pageInfo.pageName” is setting prop1:

Looks like everything checks out. I now have 2 tests so Chrome will open and close twice.

$ python -m unittest test_pagename
Importing environment from .env...
..
--------------------------------------------------------------------
Ran 2 tests in 14.052s
OK

Whats a common action that probably happens a lot on Ticketmaster? My guess is searching. I’ll search for “Detroit Red Wings”, lets see where it shows up:

They send my search term as eVar7/prop5
Satellite Data Element & Datalayer

Here is a test that automates the searching functionality and tests the variables that are stored with that behavior. Here I am going to the homepage, typing a query into the search box, hitting enter, and testing the values of the results page. I have simplified finding the HTML elements, by not catching exceptions that may occur. To make this more robust I would employ Selenium Waits.

Again, I am assuming that eVar7 and prop5 are being set using the data element “page.pageInfo.onsiteSearchTerm”.

$ python -m unittest test_search
Importing environment from .env...
...
--------------------------------------------------------------------
Ran 3 tests in 28.144s
OK

Although these examples are simple, the possibilities from here are endless. Anything that a user can do on your site, from a simple search to complex account creation flows, can be automated with Selenium and tested with Python. Therefore, any expected data layer values or DTM rules can be tested using the methods above.

An important part of unit testing is focusing on one aspect of functionality and proving it true. When using user behavior as a basis for testing it is also a good idea to automate the entire flow just as a user would experience it; i.e instead of just going straight to a search results page in my test_search_page function, I start at the home page and go from there.

Although it would be a considerable initial time investment, a good testing plan here would be to write a test function for each value in the data layer. Once the tests are written, you could make changes to your data layer, and be confident that your changes did not impact any other aspects of it.

References