Skip to content

Unit Test Guidelines

AE ( ͡ಠ ʖ̯ ͡ಠ) edited this page Oct 3, 2022 · 1 revision

Unit Test Guidelines

Jest is used for unit tests. Check to see if there is an already existing unit test file (*.spec.ts) in the tests folder for your capability. If there is, add your tests there. If there is not, please create a new test file.

Unit Test Breakdown

  1. Initialization
  2. Call the function you want to test
  3. Confirm the function has been called
  4. If the function is sending a message over the SDK boundary, then you should mock the message coming back to the function from the boundary
  5. Confirm that the function you are testing behaved the way you expected (e.g., returned the value or threw the error you expected)

Validating Error thrown from async code

This is the same as validating an Error thrown from inside a Promise.

Use async/await and expect(...).rejects.toThrowError(errorMessage);

Note: prefer async/await to using return expect(...).rejects.toThrowError(errorMessage) (note the return before expect) because it more closely resembles how app developers would call the function.

See jest expect.rejects documentation

Example 1

Code to be tested

export function getCloudStorageFolders(channelId: string): Promise<CloudStorageFolder[]> {
  return new Promise<CloudStorageFolder[]>(resolve => {
    ensureInitialized(FrameContexts.content);

    // test should validate this throws Error on null channelId
    if (!channelId || channelId.length === 0) {
      throw new Error('[files.getCloudStorageFolders] channelId name cannot be null or empty');
    }

    resolve(sendAndHandleError('files.getCloudStorageFolders', channelId));
  });
}

Test

it('should not allow calls with null channelId', async () => {
  await utils.initializeWithContext('content');
  // use toThrowError
  await expect(files.getCloudStorageFolders(null)).rejects.toThrowError(
    '[files.getCloudStorageFolders] channelId name cannot be null or empty',
  );
});

Validating SdkError thrown from async code

This is the same as validating an SdkError is thrown from inside a Promise.

Use async/await and expect(...).rejects.toEqual(expectedSdkErrorObject);

See jest toEqual documentation. Pay special attention to the note about toEqual not performing a deep equality check for two errors. Since SdkError is an object, we can use toEqual to validate all the properties match.

Example 2

Code to be tested

export function getLocation(
  props: LocationProps,
  callback?: (error: SdkError, location: Location) => void),
): Promise<Location> {
  ensureInitialized(FrameContexts.content, FrameContexts.task);
  return callCallbackWithErrorOrResultFromPromiseAndReturnPromise<Location>(
    getLocationHelper,
    callback,
    props,
  );
}

function getLocationHelper(props: LocationProps): Promise<Location> {
  return new Promise<Location>(resolve => {
    if (!isApiSupportedByPlatform(locationAPIsRequiredVersion)) {
      throw { errorCode: ErrorCode.OLD_PLATFORM }; // validate this throws SdkError
    }
    if (!props) {
      throw { errorCode: ErrorCode.INVALID_ARGUMENTS };
    }
    resolve(sendAndHandleError('location.getLocation', props));
  });
}

Test

it('getLocation call in default version of platform support fails', async () => {
  await mobilePlatformMock.initializeWithContext(FrameContexts.task);
  mobilePlatformMock.setClientSupportedSDKVersion(originalDefaultPlatformVersion);
  // use toEqual
  await expect(location.getLocation(defaultLocationProps)).rejects.toEqual({
    errorCode: ErrorCode.OLD_PLATFORM,
  });
});

Validating an Error is thrown from synchronous code:

This is the same as validating an Error is thrown from outside a Promise.

Use expect(() => ...).toThrowError(errorMessage);

See jest toThrowError documentation. Since toEqual doesn't perform a deep equality check on two Error objects, we need to use toThrowError instead.

Example 3

Code to be tested

export function authenticate(authenticateParameters?: AuthenticateParameters): Promise<string> {
  const isDifferentParamsInCall: boolean = authenticateParameters !== undefined;
  const authenticateParams: AuthenticateParameters = isDifferentParamsInCall ? authenticateParameters : authParams;
  if (!authenticateParams) {
    throw new Error('No parameters are provided for authentication'); // validate this throws Error
  ... // omitted for clarity
}

Test

it('should not allow authentication.authenticate calls with no auth parameters', () => {
  // use toThrowError
  expect(() => authentication.authenticate()).toThrowError(
    'No parameters are provided for authentication',
  );
});

When to use toMatchObject vs toEqual vs toThrowError

Generally speaking, use toThrowError to validate an Error is thrown, either in synchronous or async code.

If an SdkError is thrown, prefer to use toEqual to validate all properties match, either in synchronous or async code. (We cannot use toEqual on an Error because it does not perform a deep equality check on the two Error objects.)

toMatchObject will only check the object's properties that are passed to the toMatchObject function, so in general, prefer greater specificity with toEqual.

When to use expect.assertions(n)

If you have a test that contains an assertion (e.g., expect(...).toEqual(...)) that might not be executed, inside a catch, for example, it will ensure that 'n' number of assertions were tested and will fail the test if that is not the case.

Note: We prefer to validate exceptions by the methods above (toThrowError, rejects.toThrowError, rejects.toEqual, etc.) rather than expect.assertions with validation in a catch block.

See jest expect.assertions documentation.

Example 4

Code to be tested

export interface MyError {
  errorCode: ErrorCode;
}

export enum ErrorCode {
  MY_ERROR_CODE = 1;
}

export function myFunction(arg: string): void {
  if (arg === "myArgument") {
    throw { errorCode: ErrorCode.MY_ERROR_CODE };
  }
}

Test

it('should throw exception', () => {
  expect.assertions(1);
  expect(myFunction("myArgument"))
    .catch(e => expect(e).toEqual({ errorCode: MY_ERROR_CODE });
}

Code Coverage

We do not currently have official code coverage metrics. If you are running the Jest Visual Studio Code plugin, you can toggle the Coverage feature on, which highlights the lines of code that are not covered by the jest unit tests.

🔥 Hot Tip 🔥: Use this to help you decide if your unit tests are comprehensive!

  1. Open the VS Code Command Palette (Ctrl+Shift+P on Windows, ⌘+Shift+P on Mac)
  2. Type "Jest: Toggle Coverage"

In the status bar at the bottom of VS Code, you should now see this symbol Jest status bar symbol and when you open a source file, you should be able to see the lines highlighted when they are missing full or partial coverage: VS code screenshot with code coverage highlighted