🐙

Microsoft Office アドイン プラットフォームとテストの話

2023/12/20に公開

ご覧いただきありがとうございます。

Microsoftのアドインプラットフォームと呼ばれる機能をご存じでしょうか?

Office アドイン プラットフォームの概要

この機能を利用することでMicrosoft製品の右側に独自のガジェットを表示することができます。

Outlook add-inの例

アドイン部分は、WebViewを使って実現されており、WebシステムがMicsrosoft製品に埋め込まれる形で実現しています。
マイクロソフトの製品上の情報を取得・変更できるAPIが JavaScriptライブラリ Office.js が提供されています。これを使ってMicrosoft製品上の情報をアドインのWebシステムと連携を実装することができます。

開発者も特定の言語やプラットフォームに特化した高度な技術ではなく標準的なWeb技術+αで拡張機能を実装することができます。HTMLサイトさえ構築できれば、PHPでもJavaでもC#でも、好きな技術スタックを利用することができます。

今回はMicrosoftのアドイン連携システムを実装する時の自動テストを書く難しさとどう解決しているのかについて書きたいと思います。

アドイン用JavaScriptライブラリの問題点

APIの呼び出しにはMicrosoft製品と一緒に利用するのが前提

Office.jsはMicrosoft製品上での動作を前提としています。ブラウザで、Office.jsを利用したサイトを動作させると、APIの評価結果の多くはエラーになったり、undefined になります。

通常のブラウザで開くことができない

Microsoft製品上での動作を前提としているため、通常のWebサイトと同様にブラウザで開くことができません。このため、自動ブラウザテストツール playwright を利用することができません。

ユニットテストは、Mockを活用してOffice.jsの呼び出しを偽装することができます。しかしブラウザを利用したインテグレーションテストではOffice.jsの呼び出しを偽装することができません。

どうやって解決しているか?

プログラム呼び出し境界を意識して設計する

Office.jsのAPIの呼び出しとWebサイトのJavaScriptの実装には明確な境界を意識して設計します。例えば次のようなReactコンポーネントで、Office.jsのAPIを呼び出しているコードを想定します。

const Subject = () => {
  const [subject, setSubject] = useState("");
  // Outlookのメールの件名を取得してsubjectを更新
  const onClick = useCallback(() => {
    Office.onReady(() => {
      const subject = Office.context.mailbox.item?.subject || "";
      setSubject(subject);
    });
  }, []);
  return (<div>
           <div>Subject: {subject}<div>
           <button onClick={onClick}>Get Subject</button>
	 </div>)
}

Office.jsの初期化後に、メールの件名を表示するReactコンポーネントですが、Office.jsのAPIを直接呼び出しています。このReactコンポーネントは、Office.jsが存在していること、マイクロソフト製品から読み込まれなければいけないという暗黙上の制約を持っています。

これは境界が明確に設計できていない例でした。これをJavaScriptの標準APIを使って境界を明確に分離します。境界を分離するためにCustomEventを利用します。

CustomEventで境界を分離する

const Subject = () => {
  const [subject, setSubject] = useState("");
  useEffect(() => {
    const handler = (e) => {
        Office.onReady(() => {
	  const subject = Office.context.mailbox.item?.subject || "";
	  e.detail.resolve(subject);
	});
    };
    document.addEventListener("GetSubject", handler);
    return () => document.removeEventListener("GetSubject", handler);
  }, []);
  // Outlookのメールの件名を取得してsubjectを更新
  const onClick = useCallback(() => {
    const getSubject = new Promise((resolve, reject) => {
      document.dispatchEvent(new CustomEvent(
        "GetSubject",
	{detail: {resolve, reject}}
    });
    getSubject.then(subject => setSubject(subject);
  }, []);
  return (<div>
           <div>Subject: {subject}<div>
           <button onClick={onClick}>Get Subject</button>
	 </div>)
}

Office.jsのAPIを呼び出すときには CustomEventとPromiseを経由して呼び出すように変更しました。一見複雑なだけにも見えますが、うれしいこともあります。

  • playwrightを使ったテストができる
  • playwrightのpage.evaluateを利用してCustomEventを偽装できる

テスト時はCustomEventを偽装する

Office.jsのAPIをCustomEvent越しに実行するようになったのでplaywrightのpage.evaluateを利用してCustomEventを偽装します。

const eventInstall = await page.evaluate(async () => {
  const h = (e) => e.detail.resolve("hello");
  document.addEventHandler("GetSubject", h);
});
// ボタンを押すとhelloが表示されることを確認
await page.click("button");
await expect(page.locator("div div")).toHaveText("Subject: hello");

これによってReact Componentのインテグレーションテストをplaywrightで書けるようになります。

『偽装している時点でインテグレーションテストではない』そんな原理的主義的な声も聞こえてきそうです。テストピラミッドの分類ではミディアムテストになるでしょうか。しかし、今回のような工夫をすることでインテグレーションにできるだけ近い自動テストを行うことができるようになります。

JavaScriptで、CustomEvent/Promiseを使って依存を分離する実装例を紹介してみました。

Discussion