Setting the scene

We write JavaScript to perform business logic client-side on Dynamics forms.

  • Sometimes we want to dynamically alter the layout of a form (collapse tabs, hide sections etc.)
  • Sometimes we want to update field values
  • And sometimes we want to retrieve data from Dynamics using the Web API and then act on it

No matter why we're writing client-side scripts, we always want to write tests for them.

I've previously written in this blog post and others how basic Xrm functions can be tested against using xrm-mock. However, what about more advanced Xrm functions such as Xrm.WebApi?

Introducing the Web API with v9

Xrm.WebApi was introduced with Dynamics 365 version 9. In Microsoft's words, it: "Provides properties and methods to use Web API to create and manage records and execute Web API actions and functions in Customer Engagement".

My interpretation would be that it enables developers to interact with the Web API using shorthand code. For example, prior to version 9, one would write the following to create an account:

var req = new XMLHttpRequest()
req.open("POST",encodeURI(clientURL + "/api/data/v8.1/accounts"), true);
req.setRequestHeader("Accept", "application/json");
req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
req.setRequestHeader("OData-MaxVersion", "4.0");
req.setRequestHeader("OData-Version", "4.0");
req.onreadystatechange = function () {
 if (this.readyState == 4 /* complete */) {
  req.onreadystatechange = null;
  if (this.status == 204) {
   var accountUri = this.getResponseHeader("OData-EntityId");
   console.log("Created account with URI: "+ accountUri)
  }
  else {
   var error = JSON.parse(this.response).error;
   console.log(error.message);
  }
 }
};
req.send(JSON.stringify({ name: "Sample account" }));

Now, using Xrm.WebApi this can be rewritten as:

Xrm.WebApi.createRecord("account", { name: "Sample account" });

Faking Web API calls using xrm-mock

XrmMockGenerator.initialise() initialises an empty Xrm.WebAPI object. Calls to its methods such as createRecord throw a not implemented error. The current recommended approach is to therefore stub any API methods being called in the code under test, and force their return values. This allows you to:

  • control your test's expected behaviour
  • prevent direct calls to the Dynamics database via XMLHttpRequest or similar

Here's an example

This example demonstrates a basic client-side script running on a Contact form in Dynamics. When the form is loaded, the script:

  • gets the Id of the Contact's Parent Contact via Xrm.Page.getAttribute.getValue
  • retrieves the Parent Contact's name via Xrm.WebApi.retrieveRecord
  • sets the Contact's description to "My parent is called {parent contact's name}" via Xrm.Page.getAttribute.setValue

This example uses Sinon.JS to help create Web Api stubs.

"Standalone and test framework agnostic JavaScript test spies, stubs and mocks (pronounced "sigh-non", named after Sinon, the warrior)."

First, here's contact.ts, the script which will be run on the Contact form:

export default class Contact {
  public static onLoad(): Promise<void> {
    return Promise.resolve(this.describeParent());
  }

  private static async describeParent(): Promise<void> {
    const parentsName = await this.getParentsName();
    return Promise.resolve(Xrm.Page.getAttribute("description").setValue("My parent is called " + parentsName));
  }

  private static getParentsName(): Promise<string> {
    const parentId = Xrm.Page.getAttribute("primarycontactid").getValue()[0].id;

    return new Promise((resolve, reject) => {
      Xrm.WebApi.retrieveRecord("contact", parentId, "?$select=firstname").then((result) => {
        resolve(result.firstname);
      }).catch((error) => {
        reject(error);
      });
    });
  }
}

And here's contact.test.ts, the script we'll use to test contact.ts:

import * as sinon from "sinon";
import Contact from "../src/contact";
import { XrmMockGenerator } from "xrm-mock";

describe("Contact", () => {
  beforeEach(() => {
    XrmMockGenerator.initialise();
    XrmMockGenerator.Attribute.createString("description");
    XrmMockGenerator.Attribute.createLookup("primarycontactid", {
      entityType: "contact",
      id: "{00000000-0000-0000-0000-000000000001}",
      name: "Bob"
    });
  });

  it("should set description to parent contact's firstname", () => {
    const stub = sinon.stub(Xrm.WebApi, "retrieveRecord").resolves({
      firstname: "Bob"
    });

    return Contact.onLoad().then(() => {
      let description = Xrm.Page.getAttribute("description").getValue();
      expect(description).toBe("My parent is called Bob"); // Pass
    });
  });
});

Walkthrough: Understand the Code

// Import sinon. package.json should also contain @types/sinon as a dependency.
import * as sinon from "sinon";

// Import Contact module. This module is compiled to JavaScript and added to the Contact form in Dynamics.
import Contact from "../src/contact";

// Import XrmMockGenerator to run tests.
import { XrmMockGenerator } from "xrm-mock";

// Initialise a global Xrm object and create required attributes to the fake Xrm object.
XrmMockGenerator.initialise();
XrmMockGenerator.Attribute.createString("description");
XrmMockGenerator.Attribute.createLookup("primarycontactid", {
  entityType: "contact",
  id: "{00000000-0000-0000-0000-000000000001}",
  name: "Bob"
});

// Stub the retrieveRecord method. Instead of calling xrm-mock's default implementation (which throws an error), 
// our test will call this stub which resolves to return a JSON object.
const stub = sinon.stub(Xrm.WebApi, "retrieveRecord").resolves({
  firstname: "Bob"
});

// Invoke Contact.onLoad and assert the expect outcome (that the description attribute's value has been updated).
return Contact.onLoad().then(() => {
  let description = Xrm.Page.getAttribute("description").getValue();
  expect(description).toBe("My parent is called Bob");
});

Testing in action with Wallaby.js

And that's it

Using the example above you can see how to:

  • Create a fake Xrm object to use in client-side unit tests by using xrm-mock.
  • Stub a call to Xrm.WebApi to control your test's behaviour and not directly call the Dynamics database.
  • Write asynchronous TypeScript code for Dynamics.

This example and all its source code is available on xrm-mock's GitHub page here.