GTM Templates Test API tips and tricks

GTM Templates Test API tips and tricks

Intro

If you haven’t written tests for GTM templates yet, please read the great Simo’s post about what Test API is and how to use it. Also please have a look at the Test API documentation. In this post I assume that you already play with tests, and I will just share some cool and practical (I hope) examples. Let’s go and have some test fun.

Get the data fast

If you build a Template with a complex table or a Template with a huge list of dependent fields - you can save your time if you add a few lines in your code:

1
2
const log = require("logToConsole");
log("data =", data);

And after that in the Template Preview section fill the fields with dummy values and click the Run Code button. Here’s an example for my Firestore Web Tag:

Firestore Web Tag

That’s all, now you can copy the value from the console and use it for your tests, slightly changing it for different cases as needed.

Use the Setup

Don’t ignore the Setup section of the Test Tab interface. The good idea is to set the default values for all your tests inside Setup to keep your tests DRY. These values can be:

  • let mockData = {...} with the most common state. And in each test you can change only one or two properties needed to be changed and pass the value to runCode method. For example like this:
1
2
mockData.serverUrl = 'https://127.0.0.1#incorrect-url'; 
runCode(mockData);
  • Add all libraries you will use in tests. That’s right you can require some libraries inside tests (but not all). For example:
1
2
3
4
5
const log = require("logToConsole");
const Promise = require("Promise");
const makeString = require("makeString");
const Object = require("Object");
// and so on
  • Define functions you will use in a few tests, for example functions which mock Promise, Firestore or Cookie logic, we will cover these API later in the post.

  • If you use getTimestampMillis inside your code and later check it value or compare it with the input values don’t forget to mock the time:

1
mock("getTimestampMillis", 1653117602428);
  • For Server-Side Tags it’s a good idea to set a default event state like this:
1
2
3
4
5
let mockEvent = {
  version: 1,
  event_name: "firestore",
.
};

And later in a test you can change some property and mock getAllEventData API

1
2
3
4
mockEvent.version = 2;
mock("getAllEventData", () => {
  return mockEvent;
});
  • For a Server-Side Client tests don’t forget to mock claimRequest like this:
1
mock('claimRequest', ()=>{});

Otherwise, if you try Run All tests only the first one will work and the others will return errors: claimRequest error example

fail

Short trick, if for a test case you want to be sure that some method wouldn’t be fired you can mock it with a fail inside, like this:

1
mock("setResponseStatus", (event)=>{ fail("Unexpected setResponseStatus call"); });

In this example If I have a bug in the logic and setResponseStatus will be fired the test fails.

assertThat

assertThat is a very handy little API we will use it practically everywhere inside mocks, if we want to check parameters passed into a function.

1
assertThat(eventParam).isEqualTo(expectedEventParam);

You will see a lot of examples later in the post.

API examples

Ok and now let’s go to the API examples, for each case I’ll show the code and the test example, we will start from the easiest API and leave the Firestore API for the last.

DataLayer push

Code

1
2
3
4
5
6
const createQueue = require("createQueue");
const dataLayerPush = createQueue("dataLayer");
const dataLayerPushObject = {
    event:"test",
};
dataLayerPush(dataLayerPushObject);

Test

1
2
3
4
5
6
7
const expectedDataLayerEvent = {"event":"test"};
mock("createQueue", function () {
  return function (event) {
    assertThat(event).isEqualTo(expectedDataLayerEvent);
  };
});
runCode(mockData);

Comment:

It is useful when you expect that at the end a Template will make a dataLayer push with a result state. This way we can check the result is correct.

Cookies

Code

1
2
3
4
5
6
7
8
const setCookie = require('setCookie');
const options = {
  'domain': 'www.example.com',
  'path': '/',
  'max-age': 60*60*24*365,
  'secure': true
};
setCookie('info', 'xyz', options);

Test

1
2
3
4
5
6
7
8
9
mock("setCookie", (cookieName, cookieValue, cookieSettings) => {
  if (cookieName === "info")
    assertThat(cookieValue).isEqualTo("xyz");
  else if (cookieName === "test")
    assertThat(cookieValue).isEqualTo("value");
  else {
    fail("Unexpected cookie: " + cookieName);
  }
});

Comment

In this example we check cookieName, and for different names assert different values and raise failure for unexpected cookies. It’s also a good idea to add all possible variants and move this mock to the Setup section.

SHA256

Code

1
2
3
4
5
6
7
const sendPixel = require('sendPixel');
const sha256 = require("sha256");

sha256('test', (hashed) => {
  sendPixel('https://example.com/collect?id=' + hashed);
  data.gtmOnSuccess();
}, data.gtmOnFailure, {outputEncoding: 'hex'});

Test

1
2
3
4
5
6
7
8
mock("sha256", (name, callback) => {
  return callback(name + "_hashed");
});

const expectedServerUrl = "https://example.com/collect?id=test_hashed";
mock('sendPixel', function(url, onSuccess, onFailure) {
    assertThat(url).isEqualTo(expectedServerUrl);
});

Comment

This is an example of callback testing. The code hashes some value and passes it as a parameter to sendPixel API. Again, sha256 API after hashing will execute a callback and pass a hashed value to this callback. So if we want to test this case, we can mock sha256 with a function which runs a callback straight away and passes a hashed value in this callback. For a hashing algorithm we can use very simple logic - just add _hashed at the end of the string. And as a second step we mock sendPixel and expect it will be fired with the URL with test_hashed at the end, because unhashed value was test and after our mock hashing it becomes test_hashed

Code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
const sendPixel = require("sendPixel");
const isConsentGranted = require("isConsentGranted");
const addConsentListener = require("addConsentListener");

const finalUrl = "https://domain.com/";

const onSuccess = () =>{data.gtmOnSuccess();};
const onFailure = () =>{data.gtmOnFailure();};

if (!isConsentGranted("ad_storage")) {
  let wasCalled = false;
  addConsentListener("ad_storage", (consentType, granted) => {
    if (wasCalled || !granted) return;
    wasCalled = true;
    sendPixel(finalUrl, onSuccess, onFailure);
  });
} else {
  sendPixel(finalUrl, onSuccess, onFailure);
}

Test

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const expectedServerUrl = "https://domain.com/";
mock('sendPixel', function(url, onSuccess, onFailure) {
    assertThat(url).isEqualTo(expectedServerUrl);
});

mock('isConsentGranted', function(consent){
  if(consent==="ad_storage") return false;
});
mock('addConsentListener', function(consent, callback){
  assertThat(consent).isEqualTo("ad_storage");
  callback("ad_storage", "granted");
});

// Call runCode to run the template's code.
runCode(mockData);

Comment

Here in the code we check ad_storage and if it hasn’t been granted we add the listener. In our test we want to check a case – at first the ad_storage isn’t granted, but it will be granted later.

For this case, in the test we mock isConsentGranted to return ad_storage equal false, and mock addConsentListener to return granted. But the funny thing here is how we work with callbacks. Please notice in the code, we pass the callback function to addConsentListener:

1
addConsentListener("ad_storage", (consentType, granted) => {..})

This callback function has two parameters: the consentType and the consent status.

In the test we mock addConsentListener with a function which immediately calls callback and pass values we expected “ad_storage” and “granted” like this:

1
2
3
mock('addConsentListener', function(consent, callback){
  callback("ad_storage", "granted");
});

localStorage

Code

1
2
3
4
5
6
7
8
9
const sendPixel = require('sendPixel');
const getType = require('getType');
const localStorage = require('localStorage');

const ls = getType(localStorage) === "function" ? localStorage() : localStorage;

const key = 'my_key';
const value = ls.getItem(key);
sendPixel('https://example.com/collect?id=' + value);

Test

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
mock("localStorage", () => {
  return {
    getItem: (name) => {
      return "value";
    }
  };
});

const expectedServerUrl = "https://example.com/collect?id=value";
mock('sendPixel', function(url, onSuccess, onFailure) {
    assertThat(url).isEqualTo(expectedServerUrl);
});

Comment

This one was really interesting for me. Unfortunately you can’t actually mock localStorage API, so we need a little trick here.

In the test we mock localStorage with a function which returns an object with getItem property, and this property itself is a function which returns our expected value.

In the code we check a type of the localStorage and if it’s a function it means we are inside a test, and initialize ls with the result of the localStorage function, otherwise we are not in the test and use localStorage API as always.

1
const ls = getType(localStorage) === "function" ? localStorage() : localStorage;

Later in the code we always use ls instead of localStorage, this way in tests we can inject localStorage values we need.

Promise API

For Promise API I created a separate blog post as this API is very handy and beautiful, and deserves a bit more time to play with. Please have a look at the link provided.

One important note about Promise and callbacks – unfortunately assertApi doesn’t work for API methods inside promises, you have to use assertThat inside mock if you want to check a method was called.

Firestore API

Code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const getType = require("getType");
const Firestore = require("Firestore");
const projectId = "123";
const path = "users/1234";
 
const firestore = getType(Firestore) === "function" ? Firestore() : Firestore;
 
firestore
 .runTransaction(
   (transaction) => {
     const transactionOptions = {
       projectId: projectId,
       transaction: transaction,
     };
     return firestore.read(path, transactionOptions).then(
       (result) => {
         const newInputCount = result.data.inputCount + 1;
         const input = { key1: "value1", inputCount: newInputCount };
         return firestore.write(path, input, transactionOptions);
       },
       (error) => {
         if (error.reason === "not_found") {
           //not_found case
         } else {
           // other case
         }
       }
     );
   },
   {
     projectId: projectId,
   }
 )
 .then((ids) => {
   data.gtmOnSuccess();
 }, data.gtmOnFailure);

Test

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const log = require("logToConsole");
const Promise = require("Promise");

const transactionID = 1;
const expectedPath = "users/1234";

const userStateBefore = { inputCount: 10 };
const userStateBeforeFormatted = { data: userStateBefore };
const userWriteState = { key1: 'value1', inputCount: 11 };

const mockExistingUser = () => {
  mock("Firestore", () => {
    return {
      runTransaction: (func, conf) => {
        func("transaction");
        return Promise.create((resolve, reject) => {
          resolve(transactionID);
        });
      },
      read: (path, transactionOptions) => {
        log("Firestore read path", path);
        assertThat(path).isEqualTo(expectedPath);
        log("Firestore read return", userStateBeforeFormatted);
        return Promise.create((resolve, reject) => {
          resolve(userStateBeforeFormatted);
        });
      },
      write: (path, userState, transactionOptions) => {
        log("Firestore write path", path);
        assertThat(path).isEqualTo(expectedPath);
        log("Firestore write userState", userState);
        assertThat(userState).isEqualTo(userWriteState);
        return Promise.create((resolve, reject) => {
          resolve(transactionID);
        });
      },
    };
  });
};


mockExistingUser();

Comment

For Firestore API I use tricks from localStorage and Promise API so please read them first. The important note, if your code uses Firestore API – you have to mock it, or your tests will always try to make requests to Firestore, and you get access errors or even worse, can corrupt your data, please be very careful.

In this example I show the most interesting case when you use transactions. In the code at first we do the same trick as for Promise API, it helps us to inject mocked Firestore.

1
const firestore = getType(Firestore) === "function" ? Firestore() : Firestore;

After that I use the sample code from the documentation, it gets value from Firestore and increases inputCount by one. In the code we use Firestore.runTransaction, Firestore.read, Firestore.write - and we need to mock all these methods.

In the test I create a mockExistingUser function, you can move it to the Setup sections and later call in all test cases. This function mock all three Firestore methods, for read method it returns userStateBeforeFormatted object, for write methods it compares input value with userWriteState object. In our test case the read method returns inputCount equal 10 and the write method expects inputCount should be 11.

If you need to test a scenario in which a Template requests not existing value from the Firestore, in this case a mock for the read method should return a promise which call a reject, something like this:

1
reject({ reason: "not_found" });

You can find more Firestore test examples in my Firestore Server-Side Tag. If you want to learn more about this Template please read the post on how to get / set user state using Firestore and GTM.

That’s all for now. I hope this post will help you to test GTM Templates, happy testing!

If you have any questions or ideas, please message me on LinkedIn.