Office 加载项中的单元测试

单元测试检查加载项的功能,而无需网络或服务连接,包括与 Office 应用程序的连接。 单元测试服务器端代码和 调用 Office JavaScript API 的客户端代码在 Office 外接程序中与在任何 Web 应用程序中相同,因此不需要特殊文档。 但调用 Office JavaScript API 的客户端代码难以测试。 为了解决这些问题,我们创建了一个库来简化单元测试中模拟 Office 对象的创建: Office-Addin-Mock。 该库通过以下方式简化了测试:

  • Office JavaScript API 必须在 Office 应用程序 (Excel、Word 等 ) 上下文中的 Webview 控件中初始化,因此无法在开发计算机上运行单元测试的过程中加载它们。 Office-Addin-Mock 库可以导入到测试文件中,这样就可以在运行测试的 Node.js 进程中模拟 Office JavaScript API。
  • 特定于应用程序的 API 具有负载同步方法,这些方法必须以相对于其他函数和彼此的特定顺序调用。 此外, load 必须使用某些参数调用 方法,具体取决于 稍后 要由要测试的函数中的代码读取的 Office 对象的哪些属性。 但单元测试框架本质上是无状态的,因此它们无法记录是否 load 调用 或 sync 或 被传递给 的参数 load。 使用 Office-Addin-Mock 库创建的模拟对象具有跟踪这些内容的内部状态。 这使模拟对象能够模拟实际 Office 对象的错误行为。 例如,如果正在测试的函数尝试读取未首先传递给 load的属性,则该测试将返回类似于 Office 返回的错误。

库不依赖于 Office JavaScript API,它可以与任何 JavaScript 单元测试框架一起使用,例如:

本文中的示例使用 Jest 框架。 Office-Addin-Mock 主页提供了使用 Mocha 框架的示例。

先决条件

本文假定你熟悉单元测试和模拟的基本概念,包括如何创建和运行测试文件,并且具有一些单元测试框架的经验。

提示

如果使用的是 Visual Studio,建议阅读在 Visual Studio 中对 JavaScript 和 TypeScript 进行单元测试 一文,了解有关 Visual Studio 中的 JavaScript 单元测试的一些基本信息,然后返回本文。

安装工具

若要安装库,请打开命令提示符,导航到外接程序项目的根目录,然后输入以下命令。

npm install office-addin-mock --save-dev

基本用法

  1. 项目将具有一个或多个测试文件。 (请参阅下面的示例中的测试框架和示例测试文件的说明。) 使用 或 import 关键字 (keyword) 将库require导入到具有调用 Office JavaScript API 的函数测试的任何测试文件,如以下示例所示。

    const OfficeAddinMock = require("office-addin-mock");
    
  2. 导入包含要使用 requireimport 关键字 (keyword) 测试的外接程序函数的模块。 下面是一个示例,假定测试文件位于包含加载项代码文件的文件夹的子文件夹中。

    const myOfficeAddinFeature = require("../my-office-add-in");
    
  3. 创建一个数据对象,该对象具有测试函数时需要模拟的属性和子属性。 下面是模拟 Excel Workbook.range.address 属性和 Workbook.getSelectedRange 方法的对象的示例。 这不是最终的模拟对象。 将其视为用于 OfficeMockObject 创建最终模拟对象的种子对象。

    const mockData = {
      workbook: {
        range: {
          address: "C2:G3",
        },
        getSelectedRange: function () {
          return this.range;
        },
      },
    };
    
  4. 将数据对象传递给 OfficeMockObject 构造函数。 有关返回 OfficeMockObject 的对象,请注意以下事项。

    • 它是 OfficeExtension.ClientRequestContext 对象的简化模拟。
    • mock 对象具有数据对象的所有成员,并且还具有 和 sync 方法的load模拟实现。
    • 模拟对象将模拟该对象的关键错误行为 ClientRequestContext 。 例如,如果正在测试的 Office API 尝试在不首先加载属性并调用 sync的情况下读取属性,则测试将失败并显示类似于在生产运行时中引发的错误:“错误,未加载属性”。
    const contextMock = new OfficeAddinMock.OfficeMockObject(mockData);
    

    注意

    有关该 OfficeMockObject 类型的完整参考文档位于 Office-Addin-Mock

  5. 在测试框架的语法中,添加 函数的测试。 使用 OfficeMockObject 对象来代替它模拟的对象,在本例中为 ClientRequestContext 对象。 下面继续 Jest 中的示例。 此示例测试假定要测试的外接程序函数称为 getSelectedRangeAddress,它采用 ClientRequestContext 对象作为参数,并且它旨在返回当前所选区域的地址。 本文 稍后将介绍完整示例。

    test("getSelectedRangeAddress should return the address of the range", async function () {
      expect(await getSelectedRangeAddress(contextMock)).toBe("C2:G3");
    });
    
  6. 根据测试框架和开发工具的文档运行测试。 通常,有一个 package.json 文件,其中包含用于执行测试框架的脚本。 例如,如果 Jest 是框架, package.json 将包含以下项:

    "scripts": {
      "test": "jest",
      -- other scripts omitted --  
    }
    

    若要运行测试,请在项目的根目录中的命令提示符中输入以下内容。

    npm test
    

示例

本节中的示例使用 Jest 及其默认设置。 这些设置支持 CommonJS 模块。 有关如何配置 Jest 和 Node.js 以支持 ECMAScript 模块和支持 TypeScript,请参阅 Jest 文档 。 若要运行这些示例中的任何一个,请执行以下步骤。

  1. 为相应的 Office 主机应用程序创建 Office 外接程序项目, (例如 Excel 或 Word) 。 快速执行此操作的一种方法是将 Yeoman 生成器用于 Office 加载项
  2. 在项目的根目录中, 安装 Jest
  3. 安装 office-addin-mock 工具
  4. 创建与示例中第一个文件完全相同的文件,并将其添加到包含项目的其他源文件(通常称为 \src)的文件夹。
  5. 创建源文件文件夹的子文件夹,并为其指定适当的名称,例如 \tests
  6. 创建与示例中的测试文件完全相同的文件,并将其添加到子文件夹中。
  7. test将脚本添加到 package.json 文件,然后运行测试,如基本用法中所述。

模拟 Office 通用 API

本示例假定任何支持 Office 通用 API ((例如 Excel、PowerPoint 或 Word) )的主机的 Office 加载项。 加载项在名为 my-common-api-add-in-feature.js的 文件中具有其功能之一。 下面显示了文件的内容。 函数addHelloWorldText将文本“Hello World!”设置为文档中当前选择的任何内容;例如,Word中的区域、Excel 中的单元格或 PowerPoint 中的文本框。

const myCommonAPIAddinFeature = {

    addHelloWorldText: async () => {
        const options = { coercionType: Office.CoercionType.Text };
        await Office.context.document.setSelectedDataAsync("Hello World!", options);
    }
}
  
module.exports = myCommonAPIAddinFeature;

名为 my-common-api-add-in-feature.test.js 的测试文件位于子文件夹中,相对于外接程序代码文件的位置。 下面显示了文件的内容。 请注意,顶级属性是 context,一个 Office.Context 对象,因此要模拟的对象是此属性的父级: 一个 Office 对象。 关于此代码,请注意以下几点:

  • 构造 OfficeMockObject 函数 不会 将所有 Office 枚举类添加到 mock Office 对象,因此 CoercionType.Text 必须在种子对象中显式添加外接程序方法中引用的值。
  • 由于未在节点进程中加载 Office JavaScript 库, Office 因此必须在外接程序代码中引用的对象进行声明和初始化。
const OfficeAddinMock = require("office-addin-mock");
const myCommonAPIAddinFeature = require("../my-common-api-add-in-feature");

// Create the seed mock object.
const mockData = {
    context: {
      document: {
        setSelectedDataAsync: function (data, options) {
          this.data = data;
          this.options = options;
        },
      },
    },
    // Mock the Office.CoercionType enum.
    CoercionType: {
      Text: {},
    },
};
  
// Create the final mock object from the seed object.
const officeMock = new OfficeAddinMock.OfficeMockObject(mockData);

// Create the Office object that is called in the addHelloWorldText function.
global.Office = officeMock;

/* Code that calls the test framework goes below this line. */

// Jest test
test("Text of selection in document should be set to 'Hello World'", async function () {
    await myCommonAPIAddinFeature.addHelloWorldText();
    expect(officeMock.context.document.data).toBe("Hello World!");
});

模拟 Outlook API

尽管严格来说,Outlook API 是通用 API 模型的一部分,但它们具有围绕 Mailbox 对象构建的特殊体系结构,因此我们为 Outlook 提供了一个独特的示例。 此示例假定 Outlook 在名为 my-outlook-add-in-feature.js的文件中具有其功能之一。 下面显示了文件的内容。 函数addHelloWorldText将文本“Hello World!”设置为邮件撰写窗口中当前选择的任何内容。

const myOutlookAddinFeature = {

    addHelloWorldText: async () => {
        Office.context.mailbox.item.setSelectedDataAsync("Hello World!");
      }
}

module.exports = myOutlookAddinFeature;

名为 my-outlook-add-in-feature.test.js 的测试文件位于子文件夹中,相对于外接程序代码文件的位置。 下面显示了文件的内容。 请注意,顶级属性是 context,一个 Office.Context 对象,因此要模拟的对象是此属性的父级: 一个 Office 对象。 关于此代码,请注意以下几点:

  • host模拟库在内部使用模拟对象上的 属性来标识 Office 应用程序。 这是 Outlook 的必需项。 它目前对任何其他 Office 应用程序没有任何用途。
  • 由于未在节点进程中加载 Office JavaScript 库, Office 因此必须在外接程序代码中引用的对象进行声明和初始化。
const OfficeAddinMock = require("office-addin-mock");
const myOutlookAddinFeature = require("../my-outlook-add-in-feature");

// Create the seed mock object.
const mockData = {
  // Identify the host to the mock library (required for Outlook).
  host: "outlook",
  context: {
    mailbox: {
      item: {
          setSelectedDataAsync: function (data) {
          this.data = data;
        },
      },
    },
  },
};
  
// Create the final mock object from the seed object.
const officeMock = new OfficeAddinMock.OfficeMockObject(mockData);

// Create the Office object that is called in the addHelloWorldText function.
global.Office = officeMock;

/* Code that calls the test framework goes below this line. */

// Jest test
test("Text of selection in message should be set to 'Hello World'", async function () {
    await myOutlookAddinFeature.addHelloWorldText();
    expect(officeMock.context.mailbox.item.data).toBe("Hello World!");
});

模拟特定于 Office 应用程序的 API

测试使用特定于应用程序的 API 的函数时,请确保模拟正确的对象类型。 有两个选项:

  • 模拟 OfficeExtension.ClientRequestObject。 当要测试的函数满足以下两个条件时执行此操作:

    • 它不调用 Hostrun 函数,例如 Excel.run
    • 它不引用 Host 对象的任何其他直接属性或方法。
  • 模拟 Host 对象,例如 ExcelWord。 在上述选项不可用时执行此操作。

下面各小节中提供了这两种类型的测试的示例。

注意

Office-Addin-Mock 库当前不支持模拟集合类型对象,这些对象是在模式 *集合上命名的特定于应用程序的 API 中的所有对象,例如 WorksheetCollection。 我们正在努力将此支持添加到库。

模拟 ClientRequestContext 对象

此示例假定 Excel 加载项在名为 my-excel-add-in-feature.js的文件中具有其功能之一。 下面显示了文件的内容。 请注意, getSelectedRangeAddress 是在传递给 的回调中调用的 Excel.run帮助程序方法。

const myExcelAddinFeature = {
    
    getSelectedRangeAddress: async (context) => {
        const range = context.workbook.getSelectedRange();      
        range.load("address");

        await context.sync();
      
        return range.address;
    }
}

module.exports = myExcelAddinFeature;

名为 my-excel-add-in-feature.test.js 的测试文件位于子文件夹中,相对于外接程序代码文件的位置。 下面显示了文件的内容。 请注意,顶级属性为 workbook,因此要模拟的对象是 的父 Excel.Workbook级 :a ClientRequestContext 对象。

const OfficeAddinMock = require("office-addin-mock");
const myExcelAddinFeature = require("../my-excel-add-in-feature");

// Create the seed mock object.
const mockData = {
    workbook: {
      range: {
        address: "C2:G3",
      },
      // Mock the Workbook.getSelectedRange method.
      getSelectedRange: function () {
        return this.range;
      },
    },
};

// Create the final mock object from the seed object.
const contextMock = new OfficeAddinMock.OfficeMockObject(mockData);

/* Code that calls the test framework goes below this line. */

// Jest test
test("getSelectedRangeAddress should return address of selected range", async function () {
  expect(await myOfficeAddinFeature.getSelectedRangeAddress(contextMock)).toBe("C2:G3");
});

模拟主机对象

本示例假定Word外接程序在名为 my-word-add-in-feature.js的 文件中具有其功能之一。 下面显示了文件的内容。

const myWordAddinFeature = {

  insertBlueParagraph: async () => {
    return Word.run(async (context) => {
      // Insert a paragraph at the end of the document.
      const paragraph = context.document.body.insertParagraph("Hello World", Word.InsertLocation.end);
  
      // Change the font color to blue.
      paragraph.font.color = "blue";
  
      await context.sync();
    });
  }
}

module.exports = myWordAddinFeature;

名为 my-word-add-in-feature.test.js 的测试文件位于子文件夹中,相对于外接程序代码文件的位置。 下面显示了文件的内容。 请注意,顶级属性是 context,一个 ClientRequestContext 对象,因此要模拟的对象是此属性的父对象:对象 Word 。 关于此代码,请注意以下几点:

  • OfficeMockObject 构造函数创建最终的模拟对象时,它将确保子 ClientRequestContext 对象具有 syncload 方法。
  • 构造OfficeMockObject函数不会向 mock Word 对象添加run函数,因此必须在种子对象中显式添加函数。
  • 构造OfficeMockObject函数不会将所有Word枚举类添加到 mock Word 对象,因此InsertLocation.end必须在种子对象中显式添加外接程序方法中引用的值。
  • 由于未在节点进程中加载 Office JavaScript 库, Word 因此必须在外接程序代码中引用的对象进行声明和初始化。
const OfficeAddinMock = require("office-addin-mock");
const myWordAddinFeature = require("../my-word-add-in-feature");

// Create the seed mock object.
const mockData = {
  context: {
    document: {
      body: {
        paragraph: {
          font: {},
        },
        // Mock the Body.insertParagraph method.
        insertParagraph: function (paragraphText, insertLocation) {
          this.paragraph.text = paragraphText;
          this.paragraph.insertLocation = insertLocation;
          return this.paragraph;
        },
      },
    },
  },
  // Mock the Word.InsertLocation enum.
  InsertLocation: {
    end: "end",
  },
  // Mock the Word.run function.
  run: async function(callback) {
    await callback(this.context);
  },
};

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

// Define and initialize the Word object that is called in the insertBlueParagraph function.
global.Word = wordMock;

/* Code that calls the test framework goes below this line. */

// Jest test set
describe("Insert blue paragraph at end tests", () => {

  test("color of paragraph", async function () {
    await myWordAddinFeature.insertBlueParagraph();  
    expect(wordMock.context.document.body.paragraph.font.color).toBe("blue");
  });

  test("text of paragraph", async function () {
    await myWordAddinFeature.insertBlueParagraph();
    expect(wordMock.context.document.body.paragraph.text).toBe("Hello World");
  });
})

注意

有关该 OfficeMockObject 类型的完整参考文档位于 Office-Addin-Mock

另请参阅