Server GTM Promise API. How to test?

Test Server GTM Promise API

GTM Template API is the greatest thing that has happened to GTM after GTM itself. Especially because of the built in Test API. As soon as we start to create something interesting we have to think about testing. Of Course TRUE TDD is hardly possible but at least we can try.

One note, to make this test I have to add require("Promise") at the very beginning but this line returns a warning. Maybe in the near future GTM Templates will have a special API for testing Promises but now this testing method is better than nothing.

Reminder

In this post we will show how to test Promise API based on an example from the previous post. For a quick recap, we did following steps:

  • Call sendHttpRequest to get user state
  • Prepare date
  • Call sendHttpRequest two times to update user state
  • Update event parameters
  • Call runContainer to generate new event with updated parameters from user state

What to test

We can test that runContainer will be called with new event parameters, event_name is changed and properties are increased.

Implementation

Let’s start from code example and after that go line by line

 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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
const Promise = require("Promise");

// Mock event data
const user_id = "123";
const mockEvent = {
  event_name: "web_event",
  other_value: "55",
  user_id: user_id,
};
// Set mock for all event data
mock("getAllEventData", () => {
  return mockEvent;
});

// Set mock for user_id 
mock("getEventData", (name) => {
  return user_id;
});


// Mock userData returned by API
const userDataBody =
  '[{"loginCounter": 11},{"sessionCounter": 4},{"userProperty": 3}]';
const endpoint_get = "https://some_url" + userId + "/data/";

// Mock all requests 
mock("sendHttpRequest", (endpoint, headers, body) => {
    if (endpoint === endpoint_get) {
      return Promise.create((resolve, reject) => {
        resolve({ body: userDataBody });
      });
    } else if (endpoint === endpoint_get + "sessionCounter") {
      return Promise.create((resolve, reject) => {
        resolve({ statusCode: 201 });
      });
    } else if (endpoint === endpoint_get + "loginCounter") {
      return Promise.create((resolve, reject) => {
        resolve({ statusCode: 201 });
      });
    }
  });

// Expected event data 
const result_event = {
  event_name: "login_init",
  other_value: "55",
  user_id: "123",
  sessionCounter: 5,
  loginCounter: 12,
  userProperty: 3,
};

// Mock runContainer
mock("runContainer", (event, func) => {
  assertThat(event).isEqualTo(result_event);
});

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

// Check API calls
assertApi("getEventData").wasCalled();
assertApi("getAllEventData").wasCalled();
assertApi("sendHttpRequest").wasCalled();

At first I mock incoming event

 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Mock event data
const user_id = "123";
const mockEvent = {
  event_name: "web_event",
  other_value: "55",
  user_id: user_id,
};
// Set mock for all event data
mock("getAllEventData", () => {
  return mockEvent;
});

// Set mock for user_id 
mock("getEventData", (name) => {
  return user_id;
});

Lines 9 - 13 we mock API methods getEventData and getAllEventData. Now these methods will return our fake data.

Next mock userData data returned by sendHttpRequest. The tricky part.

21
22
23
24
25
26
27
28
29
30
31
// Mock userData returned by API
const userDataBody =
  '[{"loginCounter": 11},{"sessionCounter": 4},{"userProperty": 3}]';
const endpoint_get = "https://some_url" + userId + "/data/";

// Mock all requests 
mock("sendHttpRequest", (endpoint, headers, body) => {
    if (endpoint === endpoint_get) {
      return Promise.create((resolve, reject) => {
        resolve({ body: userDataBody });
      });

Line 22 returns userDataBody - a fake response from the server. But we need to inject this response into sendHttpRequest.

In the previous post we used sendHttpRequest for two reasons to get user data and to update data. These scenarios have different endpoints. So we copy the GET endpoint from the previous post. If sendHttpRequest is called with this GET endpoint it means we are trying to get data, otherwise we are trying to update data.

Let’s remember how GET case works:

  sendHttpRequest(
    getGetEndpoint(),
    { headers: getHeaders(), method: "GET", timeout: 5000 },
    ""
  )
    .then((result) => {
      const userData = prepareBody(result.body);
      callback(userData);
    })
    .catch((error) => {
      logToConsole("getData error", error);
      data.gtmOnFailure();
    });

So sendHttpRequest returns a Promise and we need to pass our fake userDataBody into resolve function

27
28
29
30
31
mock("sendHttpRequest", (endpoint, headers, body) => {
    if (endpoint === endpoint_get) {
      return Promise.create((resolve, reject) => {
        resolve({ body: userDataBody });
      });

Lines 32 - 41 are about the second scenario. We are mocking the update case. The code was:

    Promise.all(
      requestsParams.map((requestsParam) =>
        sendHttpRequest(
          requestsParam.endpoint,
          { headers: getHeaders(), method: "PUT", timeout: 5000 },
          requestsParam.body
        )
      )
    )
      .then((results) => {
        if (
          results.filter((result) => result.statusCode === 200).length ===
          requestsParams.length
        ) {

This time we call sendHttpRequest twice and in success path we check only statusCode.

32
33
34
35
36
37
38
39
40
    } else if (endpoint === endpoint_get + "sessionCounter") {
      return Promise.create((resolve, reject) => {
        resolve({ statusCode: 201 });
      });
    } else if (endpoint === endpoint_get + "loginCounter") {
      return Promise.create((resolve, reject) => {
        resolve({ statusCode: 201 });
      });
    }

Now here we add two if statements with endpoints for each user property. And in both cases pass {statusCode:200} to the resolver.

Yes that’s all. Next the final part about runContainer. It will be easy.

43
44
45
46
47
48
49
50
51
52
53
54
55
56
// Expected event data 
const result_event = {
  event_name: "login_init",
  other_value: "55",
  user_id: "123",
  sessionCounter: 5,
  loginCounter: 12,
  userProperty: 3,
};

// Mock runContainer
mock("runContainer", (event, func) => {
  assertThat(event).isEqualTo(result_event);
});

So, we have just defined the result event. Please consider:

  • We changed event_name to login_init
  • We increased sessionCounter and loginCounter
  • We added new property userProperty from userData
  • We left other events properties untouched

In lines 53 - 64 we mock runContainer and use assertThat API to check if a new event has the expected value That’s practically all.

53
54
55
56
57
58
59
60
61
62
63
64
// Mock runContainer
mock("runContainer", (event, func) => {
  assertThat(event).isEqualTo(result_event);
});

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

// Check API calls
assertApi("getEventData").wasCalled();
assertApi("getAllEventData").wasCalled();
assertApi("sendHttpRequest").wasCalled();

We run test by using runCode and check getEventData, getAllEventData, sendHttpRequest were called.

Test or not to test

You may ask, what’s the profit of all these tests? If you collect real important data and marketing KPI really depends on how audiences and events are built - you for sure will continue to experiment and add more new properties and new logic. In this case you have to guarantee that a new code wouldn’t break the existing one.

May the Test API be with you.