OfficeDev / Office-Addin-Scripts

A set of scripts and packages that are consumed in Office add-ins projects.
MIT License
153 stars 93 forks source link

Unit testing Word Office Add-ins #781

Open gdangelo opened 1 year ago

gdangelo commented 1 year ago

Hello everyone,

My team and I are working on an Office Word Add-in which interacts with the document to insert text, images, tables, etc.

We've implemented a few helper functions to make those insertions with the Word JS API. Here's how one looks like:

export const insertTagIntoDoc = async (tag: Tag) => {
    const { path, type } = tag;

    try {
        await Word.run(async context => {
            const selectionRange = context.document.getSelection();
            let insertedContent: Word.Range | Word.InlinePicture | null = null;

            // Insert content into the document
            if (type === 'text') {
                insertedContent = selectionRange.insertText(`{{ ${path} }}`, Word.InsertLocation.replace);
            } else {
                insertedContent = selectionRange.insertInlinePictureFromBase64(tag.base64, Word.InsertLocation.replace);
                insertedContent.altTextDescription = tag.altText ?? '';
                if (tag.height) {
                    insertedContent.height = tag.height;
                }
                if (tag.width) {
                    insertedContent.width = tag.width;
                }
            }

            // Move cursor to the end of the inserted content
            insertedContent.select('End');

            await context.sync();
        });
    } catch (error) {
        console.log(error)
    }
};

However, it is not clear to us how to unit test those functions with the office-addin-mock. We know there is a section in the documentation about using that mock but it is very succinct and we don't know how to apply it to our use case.

More specifically, it's not clear what should be added the the mock object data and how to test that after calling the insertTagIntoDoc function, the document contains the data as expected, in this case, either some text or an image. Also, we want to test that the inserted context replace the current selection in the document, and that the cursor is move to the end of the inserted content.

import { OfficeMockObject } from 'office-addin-mock';

// Create the seed mock object
export const mockData = {
    context: {
        document: {
            // what to add here?
        },
    },
    InsertLocation: {
        replace: 'replace',
        before: 'before',
        after: 'after',
    },
    async run(callback: (context: unknown) => Promise<void>) {
        await callback(this.context);
    },
};

// Create the final mock object from the seed object
export const wordMock = new OfficeMockObject(mockData);

// Define and initialize the Word object
global.Word = wordMock as unknown as typeof Word;

Here's what we get when log the wordMock object in the console in our test:

it('should insert an image tag', async () => {
    await insertTagIntoDoc(imgTag);

    console.log(wordMock);

    // TODO: check document data
});
OfficeMockObject {
  _properties: Map(2) {
    'context' => OfficeMockObject {
      _properties: [Map],
      _loaded: false,
      _host: undefined,
      _value: 'Error, property was not loaded',
      _valueBeforeLoaded: undefined,
      _isObject: true,
      document: [OfficeMockObject]
    },
    'InsertLocation' => OfficeMockObject {
      _properties: [Map],
      _loaded: false,
      _host: undefined,
      _value: 'Error, property was not loaded',
      _valueBeforeLoaded: undefined,
      _isObject: true,
      replace: 'Error, property was not loaded',
      before: 'Error, property was not loaded',
      after: 'Error, property was not loaded'
    }
  },
  _loaded: false,
  _host: undefined,
  _value: 'Error, property was not loaded',
  _valueBeforeLoaded: undefined,
  context: OfficeMockObject {
    _properties: Map(1) { 'document' => [OfficeMockObject] },
    _loaded: false,
    _host: undefined,
    _value: 'Error, property was not loaded',
    _valueBeforeLoaded: undefined,
    _isObject: true,
    document: OfficeMockObject {
      _properties: Map(0) {},
      _loaded: false,
      _host: undefined,
      _value: 'Error, property was not loaded',
      _valueBeforeLoaded: undefined,
      _isObject: true
    }
  },
  InsertLocation: OfficeMockObject {
    _properties: Map(3) {
      'replace' => [OfficeMockObject],
      'before' => [OfficeMockObject],
      'after' => [OfficeMockObject]
    },
    _loaded: false,
    _host: undefined,
    _value: 'Error, property was not loaded',
    _valueBeforeLoaded: undefined,
    _isObject: true,
    replace: 'Error, property was not loaded',
    before: 'Error, property was not loaded',
    after: 'Error, property was not loaded'
  },
  run: [AsyncFunction: run]
}

Thanks for your help.

Greg

timbeese commented 10 months ago

I'm not associated with Microsoft, but I've been looking into a better way to unit test our usage of the office.js functions for quite a while now. It still looks like we have to mock pretty much everything, even with the office-addin-mock library. The one thing that library gives us that is helpful is the ability to make sure that properties are loaded before they are accessed.

If you're doing any sort of bulk processing that interacts with the Office document, it can be a real challenge to create a mock that validates what you did. At one point in an office-addin-mock presentation they mentioned that they were working on some templates that we could use that would help get things started, but I haven't seen those come out yet.

I've created some mock objects for Excel and if I'm trying to test anything with more than 3 or 4 function calls, it can turn into quite a lot of mocking. In your example, I think you'd need your document to contain a mock function for getSelection(), which would return a mock object that simulates a Word.Range. That object would need to have a mock insertText and insertInlinePictureFromBase64 function on it. It would also need a mock for Select and some way to keep the setting around. Then, once you finish your function, your unit test could look at the object you mocked, and make sure that certain properties are where they should be. Such as the Select('end') actually created some mock property that you can check. Also your insertText and insertInlinePicture mock objects would need to set some property that you can test.

Not the answer you were looking for, but hopefully it helps a little bit...