DEV Community

Oliver Flint
Oliver Flint

Posted on • Originally published at oliverflint.co.uk on

D365 TypeScript Web Resources - Part 5 - Unit Testing

Before you get stuck into this make sure you've checked out any previous parts to the series. Each part in this series follows on from the previous, so you may need to grab the code from the previous part if you haven't been following.

Unit Tests? But my code is next level!

Nowadays there is no excuse to not unit test your code. It's becoming easy and more valuable, so don't not do it!

In this post I'm going to provide a basic guide to get you started with unit tests and typescript webresources for Dynamics 365... err Dataverse... err Power Apps... or whatever we are calling it today!

Configure Jest

First of all lets install the jest npm packages we require:

npm install jest @types/jest ts-jest --save-dev
Enter fullscreen mode Exit fullscreen mode

Next we'll create a basic config via:

npx ts-jest config:init
Enter fullscreen mode Exit fullscreen mode

Finally we'll install xrm-mock and sinon to help us a little:

npm install xrm-mock sinon @types/sinon --save-dev
Enter fullscreen mode Exit fullscreen mode

Ensure your package.json is setup to run jest via npm run test makes sure you have the following script defined:

"scripts": {
    "test": "jest",
    ...
Enter fullscreen mode Exit fullscreen mode

Now we are ready to write some tests! :-D

Writing our first test

Create a folder in the root of the project called "tests" and then create a new file called "first.test.ts".

Paste the following into the new file:

import { NavigationStaticMock, XrmMockGenerator } from "xrm-mock";
import * as sinon from "sinon";
import { Pointless } from "../src/sample";
describe("sample test", () => {
  describe("Pointless", () => {
    test("Should display alert", () => {
      XrmMockGenerator.initialise();
      const stub = sinon.stub(
        NavigationStaticMock.prototype,
        "openAlertDialog"
      );

      const msg = "a pointless test message";
      Pointless(msg);

      expect(stub.called).toBeTruthy();
      expect(stub.calledOnce).toBeTruthy();
      expect(stub.firstCall.args[0].text).toBe(msg);
      expect(stub.firstCall.args[0].title).toBe("A Pointless Message");
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Lets have a closer look at the test...

I'm not going to tell you how to use jest and sinon as these already have great documentation of their own.

First we'll "Arrange" our test...
Initialise our global Xrm object:

XrmMockGenerator.initialise();
Enter fullscreen mode Exit fullscreen mode

Stub openAlertDialog(). As I expect you know, openAlertDialog() displays a dialog in D365. Stubbing the function enables the code to execute without error given we don't have the UI and we can then test/assert the stubs properties etc.:

const stub = sinon.stub(NavigationStaticMock.prototype, "openAlertDialog");
Enter fullscreen mode Exit fullscreen mode

Now lets "Act" by calling our function:

const msg = "a pointless test message";
Pointless(msg);
Enter fullscreen mode Exit fullscreen mode

Finally we'll "Assert" our tests
Has the openAlertDialog() stub been called?:

expect(stub.called).toBeTruthy();
Enter fullscreen mode Exit fullscreen mode

The openAlertDialog() stub should have only been called once:

expect(stub.calledOnce).toBeTruthy();
Enter fullscreen mode Exit fullscreen mode

Was the openAlertDialog() stubs alertStrings.text parameter as expected?:

expect(stub.firstCall.args[0].text).toBe(msg);
Enter fullscreen mode Exit fullscreen mode

Was the openAlertDialog() stubs alertStrings.title parameter as expected?:

expect(stub.firstCall.args[0].title).toBe("A Pointless Message");
Enter fullscreen mode Exit fullscreen mode

It's quite a rudimentary test but it demonstrates the basics.

So, lets run the tests!

Execute npm run test

The output should look something like this:

> jest

 PASS  tests/first.test.ts (10.199 s)
  sample test
    Pointless
      √ Should display alert (34 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        10.811 s
Ran all test suites.
Enter fullscreen mode Exit fullscreen mode

Fancy a crack at mocking the WebApi?

So, we've written a function that calls Xrm.WebApi, but when we try to test that function it'll fail as the Api doesn't exist... We need to mock the Api call with a stub.

A function that calls Xrm.WebApi...

First of all we'll create a function called CreateAccount (yep you guessed it, it'll create an account!)

export async function CreateAccount(account: any): Promise<Xrm.Lookup> {
  const response = await Xrm.WebApi.createRecord("account", account);
  return response;
}
Enter fullscreen mode Exit fullscreen mode

It's a simple example but it should get the point across.

Testing a function that calls Xrm.WebApi

So here's the test. This can be added to the first.test.ts inside describe("sample test", () => {

It's not a great example, but it should demonstrate how we can stub the WebApi ;-)

describe("CallTheWebApi", () => {
  test("Should return a valid Xrm.Lookup", async () => {
    const stub = sinon
      .stub(WebApiMock.prototype, "createRecord")
      .withArgs("account", sinon.match.object)
      .resolves({
        entityType: "account",
        id: "9bce6686-48d5-4d6f-85a2-da0eea30984d",
        name: "Jest Account",
      } as LookupValueMock);

    const result: LookupValueMock = await CreateAccount({
      name: "Jest Account",
      creditonhold: false,
      address1_latitude: 47.639583,
      description: "This is the description of the sample account",
      revenue: 5000000,
      accountcategorycode: 1,
    });

    expect(stub.calledOnce).toBeTruthy();
    expect(result).toBeTruthy();
    expect(result.name).toBe("Jest Account");
  });
});
Enter fullscreen mode Exit fullscreen mode

You'll notice the absence of XrmMockGenerator.initialise();. I moved this to the beforeEach within describe("sample test", () => { like so

describe("sample test", () => {
  beforeEach(() => {
    XrmMockGenerator.initialise();
  });
  ...
Enter fullscreen mode Exit fullscreen mode

Lets have a closer look again

First we stub the createRecord function via the WebApiMock object provided by xrm-mock

const stub = sinon.stub(WebApiMock.prototype, "createRecord");
Enter fullscreen mode Exit fullscreen mode

Then we define the arguments for that stub (optional). Notice the use of the sinon.match to match an object.

  .withArgs("account", sinon.match.object)
Enter fullscreen mode Exit fullscreen mode

NOTE If we didn't do the above the stub would be executed for all calls to Xrm.WebApi.createRecord

Finally we tell the stub what we would like it to return, or in this case as it's a async/promise we tell it what to resolve.

  .resolves({
    entityType: "account",
    id: "9bce6686-48d5-4d6f-85a2-da0eea30984d",
    name: "Jest Account",
  } as LookupValueMock);
Enter fullscreen mode Exit fullscreen mode

OK OK! Lets run the bloody tests!

Execute npm run test and you should now get something similar to the following output

> jest

 PASS  tests/first.test.ts
  sample test
    Pointless
      √ Should display alert (7 ms)
    CallTheWebApi
      √ Should return a valid Xrm.Lookup (2 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        2.34 s, estimated 27 s
Ran all test suites.
Enter fullscreen mode Exit fullscreen mode

That's all folks!

Remember to take a look at jest, sinon, and xrm-mock

I hope that has been useful!

You can download a copy of the source code for this blog post here

In the next part we'll take a look at how we can integrate Azure Application Insights into the Webresources.

Thanks for reading.
Ollie

Top comments (0)