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
基本用法
项目将具有一个或多个测试文件。 (请参阅下面的示例中的测试框架和示例测试文件的说明。) 使用 或
import
关键字 (keyword) 将库require
导入到具有调用 Office JavaScript API 的函数测试的任何测试文件,如以下示例所示。const OfficeAddinMock = require("office-addin-mock");
导入包含要使用
require
或import
关键字 (keyword) 测试的外接程序函数的模块。 下面是一个示例,假定测试文件位于包含加载项代码文件的文件夹的子文件夹中。const myOfficeAddinFeature = require("../my-office-add-in");
创建一个数据对象,该对象具有测试函数时需要模拟的属性和子属性。 下面是模拟 Excel Workbook.range.address 属性和 Workbook.getSelectedRange 方法的对象的示例。 这不是最终的模拟对象。 将其视为用于
OfficeMockObject
创建最终模拟对象的种子对象。const mockData = { workbook: { range: { address: "C2:G3", }, getSelectedRange: function () { return this.range; }, }, };
将数据对象传递给
OfficeMockObject
构造函数。 有关返回OfficeMockObject
的对象,请注意以下事项。- 它是 OfficeExtension.ClientRequestContext 对象的简化模拟。
- mock 对象具有数据对象的所有成员,并且还具有 和
sync
方法的load
模拟实现。 - 模拟对象将模拟该对象的关键错误行为
ClientRequestContext
。 例如,如果正在测试的 Office API 尝试在不首先加载属性并调用sync
的情况下读取属性,则测试将失败并显示类似于在生产运行时中引发的错误:“错误,未加载属性”。
const contextMock = new OfficeAddinMock.OfficeMockObject(mockData);
注意
有关该
OfficeMockObject
类型的完整参考文档位于 Office-Addin-Mock。在测试框架的语法中,添加 函数的测试。 使用
OfficeMockObject
对象来代替它模拟的对象,在本例中为ClientRequestContext
对象。 下面继续 Jest 中的示例。 此示例测试假定要测试的外接程序函数称为getSelectedRangeAddress
,它采用ClientRequestContext
对象作为参数,并且它旨在返回当前所选区域的地址。 本文 稍后将介绍完整示例。test("getSelectedRangeAddress should return the address of the range", async function () { expect(await getSelectedRangeAddress(contextMock)).toBe("C2:G3"); });
根据测试框架和开发工具的文档运行测试。 通常,有一个 package.json 文件,其中包含用于执行测试框架的脚本。 例如,如果 Jest 是框架, package.json 将包含以下项:
"scripts": { "test": "jest", -- other scripts omitted -- }
若要运行测试,请在项目的根目录中的命令提示符中输入以下内容。
npm test
示例
本节中的示例使用 Jest 及其默认设置。 这些设置支持 CommonJS 模块。 有关如何配置 Jest 和 Node.js 以支持 ECMAScript 模块和支持 TypeScript,请参阅 Jest 文档 。 若要运行这些示例中的任何一个,请执行以下步骤。
- 为相应的 Office 主机应用程序创建 Office 外接程序项目, (例如 Excel 或 Word) 。 快速执行此操作的一种方法是将 Yeoman 生成器用于 Office 加载项。
- 在项目的根目录中, 安装 Jest。
- 安装 office-addin-mock 工具。
- 创建与示例中第一个文件完全相同的文件,并将其添加到包含项目的其他源文件(通常称为
\src
)的文件夹。 - 创建源文件文件夹的子文件夹,并为其指定适当的名称,例如
\tests
。 - 创建与示例中的测试文件完全相同的文件,并将其添加到子文件夹中。
-
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 枚举类添加到 mockOffice
对象,因此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。 当要测试的函数满足以下两个条件时执行此操作:
- 它不调用 Host。
run
函数,例如 Excel.run。 - 它不引用 Host 对象的任何其他直接属性或方法。
- 它不调用 Host。
下面各小节中提供了这两种类型的测试的示例。
注意
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
对象具有sync
和load
方法。 - 构造
OfficeMockObject
函数不会向 mockWord
对象添加run
函数,因此必须在种子对象中显式添加函数。 - 构造
OfficeMockObject
函数不会将所有Word枚举类添加到 mockWord
对象,因此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。
另请参阅
- Office-Addin-Mock npm 页面 安装点。
- 开放源代码存储库是 Office-Addin-Mock。
- 开玩笑
- 摩 卡
- 茉莉花