注: 以下の翻訳の正確性は検証されていません。AIPを利用して英語版の原文から機械的に翻訳されたものです。

TypeScript OSDK コードをユニットテストする

試験的

@osdk/unit-testing パッケージは試験的です。このパッケージは @osdk/unit-testing として公開されており、/experimental サブパス経由でのみエクスポートされます。安定した名前に移行するまでに、公開 API が変更される可能性があります。

@osdk/unit-testing パッケージを使用すると、Foundry にリクエストを送信せずに、Foundry 関数を含む、OSDK の Client を受け取るコードをユニットテストできます。データが事前に投入されたローカルのオントロジーサーバーに対して OSDK 呼び出しをテストする方法については、TypeScript OSDK コードを統合テストするを参照してください。

このパッケージは次の機能を提供します。

  • createMockClient: メソッドチェーン形式の .when、.whenObjectSet、.whenQuery マッチャーを使用してスタブを設定できる Client です。
  • createMockOsdkObject: $primaryKey、$title、$rid、$link、$clone を含む、完全な構造を持つ Osdk.Instance 値を作成します。
  • createMockObjectSet: 実際の ObjectSet を渡す場所ならどこにでも渡せる、スタンドアロンの ObjectSet です。集計用の多重リンクの対象としても使用できます。
  • createMockAttachment: 添付ファイルの値のプレースホルダーです。

インストールする

開発用の依存関係としてパッケージをインストールします。

Copied!
1 npm install --save-dev @osdk/unit-testing

このパッケージには次のピア依存関係があり、プロジェクトにすでにインストールされている必要があります。

  • @osdk/api
  • @osdk/client
  • @osdk/functions

このパッケージはテスト例で内部的に vitest を使用していますが、ご自身のコードでは任意のテストランナーを使用できます。

インポートする

すべてのエクスポートは /experimental サブパスから利用できます。

Copied!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import { createMockAttachment, createMockClient, createMockObjectSet, createMockOsdkObject, } from "@osdk/unit-testing/experimental"; import type { AggregateStubBuilder, FetchOneStubBuilder, FetchPageStubBuilder, QueryStubBuilder, StubBuilderFor, } from "@osdk/unit-testing/experimental";

最初のテストを書く

ページから最初の Employee を読み取る Foundry 関数を例に考えます。

Copied!
1 2 3 4 5 6 7 8 9 10 11 12 import type { Osdk } from "@osdk/api"; import type { Client } from "@osdk/client"; import { Employee } from "your-app-sdk"; export async function basicFetchPage( client: Client, ): Promise<Osdk.Instance<Employee>> { const objects = await client(Employee).fetchPage(); const object = objects.data[0]; if (object == null) throw new Error("No objects returned"); return object; }

モッククライアントを使用したユニットテストを次に示します。

Copied!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import { createMockClient, createMockOsdkObject, } from "@osdk/unit-testing/experimental"; import { describe, expect, it } from "vitest"; import { Employee } from "your-app-sdk"; import { basicFetchPage } from "./basicFetchPage.js"; describe("basicFetchPage", () => { it("returns the first Employee", async () => { const mockClient = createMockClient(); const mockEmployee = createMockOsdkObject(Employee, { employeeId: 1, fullName: "John", }); mockClient .when((stub) => stub(Employee).fetchPage()) .thenReturnObjects([mockEmployee]); const actual = await basicFetchPage(mockClient); expect(actual).toEqual(mockEmployee); }); });

このテストは次の3つの処理を行います。

  1. createMockClient() は、Client インターフェースを満たす MockClient を返します。コードが実際のクライアントを想定している場所ならどこにでも渡せます。
  2. createMockOsdkObject(Employee, { ... }) は、実際のインスタンスと同じ構造の Osdk.Instance を作成します。
  3. mockClient.when(stub => stub(Employee).fetchPage()).thenReturnObjects([...]) は、スタブを登録します。stub 引数は Client に似たファクトリーです。テスト対象のコードが実行するものと同じ呼び出しチェーンを再現します。

モックオブジェクトとリンク

createMockOsdkObject は、完全な形式の Osdk.Instance<T> を作成します。これは、テスト対象のコードに渡したり、.thenReturnObjects([...]) スタブ内に配置したりできます。以降のセクションでは、オブジェクトの形式、links オプション(単一、多数、エラー、モックオブジェクトセット)、および createMockAttachment について説明します。

引数

createMockOsdkObject は3個の引数を取ります。

Copied!
1 createMockOsdkObject(objectType, properties, options);
  1. **objectType:**SDK から生成されたオブジェクトタイプの定数(たとえば Employee)です。モックは、この値から apiName と primaryKeyApiName を読み取ります。
  2. **properties:**モックに設定するプロパティ値です。主キープロパティを含める必要があります。ほかのプロパティは任意で、コードがそれらを読み取る場合にのみ関係します。
  3. **options:**3個の任意のフィールドです。
    • **links:**オブジェクトの $link アクセサー用のモックデータです。リンクを参照してください。
    • titlePropertyApiName:$title の元となるプロパティのAPI名です。タイトルプロパティを設定するを参照してください。
    • **$rid:**自動生成される $rid を上書きします。デフォルトは "ri.mock.main.object.<apiName>.<primaryKey>" です。

返されるモックは、実際の OSDK インスタンスと同じ形式です。

フィールド説明
$apiName, $objectTypeオブジェクトタイプのAPI名です。
$primaryKeyproperties 内の主キープロパティの値です。
$titletitlePropertyApiName プロパティの値です。設定されていない場合は undefined です。
$rid指定されている場合は options.$rid、そうでなければ自動生成されたモック RID です。
$objectSpecifier"<apiName>:<primaryKey>" です。
$linkoptions.links を元にしたプロキシです。
$clone(updates?)プロパティ値をマージした新しいモックを返します。

モックは $as および $__EXPERIMENTAL__NOT_SUPPORTED_YET__* アクセサーをモデル化しません。これらにアクセスするとエラーがスローされます。

基本的な使い方

Copied!
1 2 3 4 5 6 7 8 9 10 11 12 import { createMockOsdkObject } from "@osdk/unit-testing/experimental"; import { Employee } from "your-app-sdk"; const emp = createMockOsdkObject( Employee, { employeeId: 1, fullName: "John Doe" }, { titlePropertyApiName: "fullName" }, ); emp.$primaryKey; // 1 emp.$title; // "John Doe" emp.$objectSpecifier; // "Employee:1"

主キープロパティを含める必要があります。createMockOsdkObject は objectType.primaryKeyApiName を読み取り、そのキーが properties に存在しない場合はエラーをスローします。

タイトルプロパティを設定する

テスト環境では、OSDK は特定のオブジェクトタイプのどのプロパティがタイトルなのかを認識していません。テスト対象のコードが obj.$title を読み取る場合は、titlePropertyApiName を渡して、そこに表示するプロパティをモックに指定する必要があります。

Copied!
1 2 3 4 5 6 7 const emp = createMockOsdkObject( Employee, { employeeId: 1, fullName: "John Doe" }, { titlePropertyApiName: "fullName" }, ); emp.$title; // "John Doe"

titlePropertyApiName には、実際に properties に含めたプロパティの名前を指定する必要があります。そのプロパティが存在しない場合、createMockOsdkObject はエラーをスローします。titlePropertyApiName を完全に省略した場合、$title は undefined になります。

リンク

links オプションは、オブジェクトタイプのリンクのAPI名に対応します。それぞれの値には、次のいずれかを指定できます。

リンクの多重度許可される値
単一モックオブジェクト、または Error インスタンス。
多数モックオブジェクトの配列、または MockObjectSet(モックオブジェクトセットを参照)。

リンク取得の成功

Copied!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const office = createMockOsdkObject(Office, { officeId: "nyc", name: "New York Office", }); const employee = createMockOsdkObject( Employee, { employeeId: 1, fullName: "John Doe" }, { links: { officeLink: office } }, ); await employee.$link.officeLink.fetchOne(); // → office (await employee.$link.officeLink.fetchOneWithErrors()).value; // → office

リンク取得の失敗

呼び出し元のコードの失敗時の分岐をテストするには、Error インスタンスを渡します。fetchOne() はそのエラーで拒否され、fetchOneWithErrors() は { error } に解決されます。

Copied!
1 2 3 4 5 6 7 8 9 10 11 12 13 const employee = createMockOsdkObject( Employee, { employeeId: 1 }, { links: { officeLink: new Error("link unavailable") } }, ); await expect(employee.$link.officeLink.fetchOne()).rejects.toThrow( "link unavailable", ); const result = await employee.$link.officeLink.fetchOneWithErrors(); result.error; // その Error result.value; // undefined

リンクがない場合

コードが $link.someLink にアクセスし、そのリンクを設定していない場合でも、アクセサー自体は存在します。fetchOne メソッドは拒否され、fetchOneWithErrors はリンク名、オブジェクトタイプ、主キーを含む { error } に解決されます。

Copied!
1 2 3 4 const employee = createMockOsdkObject(Employee, { employeeId: 1 }); await employee.$link.officeLink.fetchOne(); // 拒否されます

配列を使った多リンク

配列を渡すと、$link アクセサーは実際の多リンクと同じ呼び出し形式を公開します。

Copied!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const peep1 = createMockOsdkObject(Employee, { employeeId: 10, fullName: "Alice", }); const peep2 = createMockOsdkObject(Employee, { employeeId: 11, fullName: "Bob", }); const employee = createMockOsdkObject( Employee, { employeeId: 1 }, { links: { peeps: [peep1, peep2] } }, ); (await employee.$link.peeps.fetchPage()).data; // → [peep1, peep2] await employee.$link.peeps.fetchOne(11); // → peep2 ($primaryKey で一致) for await (const peep of employee.$link.peeps.asyncIter()) { // peep1, peep2 }

fetchOne(primaryKey) は、一致する $primaryKey を持つ配列要素がない場合にエラーをスローします。配列形式では aggregate() メソッドはサポートされていません。代わりに MockObjectSet を渡してください。

MockObjectSet を使った多リンク

多リンクでは、配列の代わりに MockObjectSet を元にすることもできます。コードがリンクに対して aggregate()、where()、またはほかのオブジェクトセットメソッドを呼び出す場合は、この方法を使用します。

Copied!
1 2 3 4 5 const employee = createMockOsdkObject( Employee, { employeeId: 1 }, { links: { peeps: peepsSet } }, );

peepsSet の作成方法と、それに対する呼び出しをスタブ化する方法については、モックオブジェクトセットを参照してください。

モックオブジェクトセット

createMockObjectSet(objectType) は ObjectSet<T> を返します。これは、実際のオブジェクトセットを渡せる場所であればどこにでも渡せます。テスト対象の関数に直接渡すことも、createMockOsdkObject の多リンクの値として渡すこともできます。モックオブジェクトセット自体はデータを保持しません。MockClient 上で、そのセットに対するスタブを登録して動作を設定します。

Copied!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import { createMockClient, createMockObjectSet, createMockOsdkObject, } from "@osdk/unit-testing/experimental"; const mockClient = createMockClient(); const peepsSet = createMockObjectSet(Employee); mockClient .whenObjectSet( peepsSet, (os) => os.aggregate({ $select: { $count: "unordered" } }), ) .thenReturnAggregation({ $count: 7 });

これで peepsSet.aggregate(...) は { $count: 7 } に解決されます。同じセットを、多リンクとして親のモックオブジェクトに関連付けることもできます。

Copied!
1 2 3 4 5 6 7 8 9 10 const employee = createMockOsdkObject( Employee, { employeeId: 1 }, { links: { peeps: peepsSet } }, ); const result = await employee.$link.peeps.aggregate({ $select: { $count: "unordered" }, }); result.$count; // 7

同じセットに fetchPage、where、およびほかの呼び出し形式を登録できます。ビルダーの完全なリファレンスについては、モックオブジェクトセットに対する呼び出しをスタブ化するを参照してください。

モック添付ファイル

Attachment 型の関数入力に対して、createMockAttachment はコードから呼び出せるインターフェースを備えたプレースホルダー値を返します。createMockOsdkObject と同じように使用します。

Copied!
1 2 3 4 import { createMockAttachment } from "@osdk/unit-testing/experimental"; const blob = new Blob(["hello world!"], { type: "text/plain" }); const attachment = createMockAttachment("ri.attachments.main.attachment.abc", blob);

クローンと更新

$clone はサポートされており、プロパティをマージした新しい凍結済みのモックを返します。主キーを異なる値に更新すると、エラーがスローされます。

Copied!
1 2 3 const updated = employee.$clone({ fullName: "Jane Doe" }); updated.$primaryKey; // 変更なし updated.fullName; // "Jane Doe"

クライアント呼び出しをスタブ化する

createMockClient() は、OSDK の Client インターフェースを満たす値に加え、スタブを設定するための4つのメソッドを返します。

  • client.when(callback): クライアントを起点とする呼び出しをスタブ化します。テスト対象のコードが実行するチェーンを再構築するコールバックを渡します(たとえば、stub(Employee).where(...).fetchPage())。呼び出しの形式に応じた .thenReturn* マッチャーを持つビルダーを返します。
  • client.whenObjectSet(set, callback): 特定の MockObjectSet(createMockObjectSet で作成)に対する呼び出しをスタブ化します。コードにオブジェクトセットが直接渡される場合や、モックオブジェクトセットに基づいている多対リンクの場合に、この方法を使います。
  • client.whenQuery(query, params?): クエリ呼び出し(オントロジー上の生成された関数)をスタブ化します。.thenReturn(value) と .thenThrow(error) を持つビルダーを返します。
  • client.clearStubs(): このクライアントに登録されたすべてのスタブを削除します。

各登録メソッドについて、以降のセクションで説明します。

クライアントを起点とする呼び出しをスタブ化する

client.when(callback) を使って、テスト対象のコードが実行する呼び出しチェーンを再構築します。引数は Client に似たファクトリーです。コードと同じように、where、aggregate、fetchPage、fetchOne などをチェーンします。

thenReturnObjects を使った fetchPage

Copied!
1 2 3 4 5 6 7 8 9 const mockClient = createMockClient(); const emp = createMockOsdkObject(Employee, { employeeId: 1, fullName: "John" }); mockClient .when((stub) => stub(Employee).fetchPage()) .thenReturnObjects([emp]); const page = await mockClient(Employee).fetchPage(); page.data; // [emp]

thenReturnObjects は asyncIter も設定します。コードがページネーションではなく反復処理を行う場合も、同じスタブが両方の呼び出し形式に対応します。

thenReturnObject を使った fetchOne

Copied!
1 2 3 mockClient .when((stub) => stub(Employee).fetchOne(1)) .thenReturnObject(emp);

thenReturnAggregation を使った aggregate

Copied!
1 2 3 4 5 6 7 mockClient .when((stub) => stub(Employee) .where({ employeeId: { $eq: 5 } }) .aggregate({ $select: { "employeeLocation:exactDistinct": "asc" } }) ) .thenReturnAggregation({ employeeLocation: { exactDistinct: 3 } });

$groupBy 集計も同じ方法でスタブ化します。グループ行の配列を返します。

Copied!
1 2 3 4 5 6 7 8 9 10 mockClient .when((stub) => stub(Employee).aggregate({ $select: { "employeeId:max": "unordered" }, $groupBy: { employeeId: "exact" }, }) ) .thenReturnAggregation([ { $group: { employeeId: 5 }, employeeId: { max: 5 } }, ]);

同じクライアント上の複数のスタブ

必要な数だけスタブを登録できます。スタブはコードが行う呼び出しと照合されます。登録順序は照合に影響しません。

モックオブジェクトセットに対する呼び出しをスタブ化する

コードが ObjectSet を直接受け取る場合(client(Type) から構築されていない場合)、または MockObjectSet に基づいている多対リンクの集計や取得の動作をスタブ化する場合は、セット自体に対してスタブを登録します。

Copied!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import { createMockObjectSet } from "@osdk/unit-testing/experimental"; const empSet = createMockObjectSet(Employee); const emp1 = createMockOsdkObject(Employee, { employeeId: 1, fullName: "Alice", }); const emp2 = createMockOsdkObject(Employee, { employeeId: 2, fullName: "Bob" }); mockClient .whenObjectSet(empSet, (os) => os.fetchPage()) .thenReturnObjects([emp1, emp2]); mockClient .whenObjectSet( empSet, (os) => os.aggregate({ $select: { $count: "unordered" } }), ) .thenReturnAggregation({ $count: 42 });

これにより、同じ empSet を、テスト対象の関数への引数や親モックオブジェクトの多対リンクの対象など、コードが ObjectSet<Employee> を想定する任意の場所に渡せます。

Foundry クエリをスタブ化する

クエリ(オントロジー上の関数)もスタブ化できます。

Copied!
1 2 3 4 import { addOne } from "your-app-sdk"; mockClient.whenQuery(addOne, { n: 5 }).thenReturn(6); mockClient.whenQuery(addOne, { n: 99 }).thenThrow(new Error("boom"));

thenReturn(value) はクエリの Promise を value で解決し、thenThrow(error) は拒否します。異なるパラメーターオブジェクトを個別にスタブ化できます。

Copied!
1 2 mockClient.whenQuery(addOne, { n: 10 }).thenReturn(11); mockClient.whenQuery(addOne, { n: 20 }).thenReturn(21);

配列パラメーターを持つクエリも同じパターンに従います。コードが渡すパラメーターと一致させます。

Copied!
1 2 3 mockClient .whenQuery(queryTypeReturnsArray, { people: ["Alice", "Bob"] }) .thenReturn(["Alice - processed", "Bob - processed"]);

テスト間でスタブをリセットする

mockClient.clearStubs() は、クライアントに登録されたすべてのスタブを削除します。これは、複数の it ブロックでクライアントを再利用する場合に役立ちます。そうでない場合は、テストを分離するために、テストごとに新しい createMockClient() を構築します。

MSW で Foundry プラットフォーム API をテストする

createMockClient がスタブ化するのは、オントロジー呼び出し(オブジェクトタイプ、クエリ、オブジェクトセット)のみです。@osdk/foundry.* と @osdk/internal.foundry.* の Foundry プラットフォーム API はインターセプトしません。これらの呼び出しは通常の fetch 経路を通ります。テストでネットワークにアクセスしないようにするには、MSW ↗ でインターセプトします。

仕組み

ネットワークリクエストのスタブ化ライブラリを使って、プラットフォーム SDK のリクエストをスタブ化します。プレースホルダーのベース URL を使用します。

https://mock.invalid/

コードが行うすべてのプラットフォーム呼び出しは、このオリジンを基準に解決されます。関数が呼び出す特定のパスに対応する MSW ハンドラーを用意します。

MSW を設定する

MSW を開発用の依存関係としてインストールします。

Copied!
1 npm install --save-dev msw

テストファイル内に Node サーバーを設定します。標準の MSW ライフサイクルフックはテスト間でハンドラーをリセットするため、各 it ブロックが分離されます。

Copied!
1 2 3 4 5 6 7 8 9 import { http, HttpResponse } from "msw"; import { setupServer } from "msw/node"; import { afterAll, afterEach, beforeAll } from "vitest"; const server = setupServer(); beforeAll(() => server.listen({ onUnhandledRequest: "error" })); afterEach(() => server.resetHandlers()); afterAll(() => server.close());

onUnhandledRequest: "error" を設定してください。ハンドラーのない意図しないプラットフォーム呼び出しを、何も通知せずに通過させるのではなく、検出できるようにします。

プラットフォーム呼び出しをスタブ化する

現在のユーザーを読み込み、ユーザー名の接尾辞に基づいて処理の可否を判定する関数です。

Copied!
1 2 3 4 5 6 7 8 9 10 import type { Client } from "@osdk/client"; import { Users } from "@osdk/foundry.admin"; export async function requireAdminUser(client: Client): Promise<string> { const user = await Users.getCurrent(client); if (!user.username.endsWith("@admin")) { throw new Error(`User ${user.username} is not an admin`); } return user.username; }

このテストでは、MSW でプラットフォームのエンドポイントをスタブ化し、Client に createMockClient() を使います。

Copied!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 import type { getCurrent } from "@osdk/foundry.admin/User"; import { createMockClient } from "@osdk/unit-testing/experimental"; import { http, HttpResponse } from "msw"; import { setupServer } from "msw/node"; import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; import { requireAdminUser } from "./requireAdminUser.js"; type User = Awaited<ReturnType<typeof getCurrent>>; const server = setupServer(); beforeAll(() => server.listen({ onUnhandledRequest: "error" })); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); describe("requireAdminUser", () => { it("resolves when the current user is an admin", async () => { server.use( http.get( "https://mock.invalid/api/v2/admin/users/getCurrent", () => HttpResponse.json( { id: "user-1", username: "alice@admin", givenName: "Alice", familyName: "Admin", realm: "default", status: "ACTIVE", attributes: {}, } satisfies User, ), ), ); const mockClient = createMockClient(); expect(await requireAdminUser(mockClient)).toBe("alice@admin"); }); it("rejects when the user is not an admin", async () => { server.use( http.get( "https://mock.invalid/api/v2/admin/users/getCurrent", () => HttpResponse.json( { id: "user-2", username: "bob@example.com", givenName: "Bob", familyName: "Example", realm: "default", status: "ACTIVE", attributes: {}, } satisfies User, ), ), ); const mockClient = createMockClient(); await expect(requireAdminUser(mockClient)).rejects.toThrow( "User bob@example.com is not an admin", ); }); });
実際のプラットフォームのデータ型に合わせてフィクスチャに型を付ける

MSW のレスポンス本文は JSON リテラルであるため、本文が実際の @osdk/foundry.* の形式からずれていても(フィールド名の変更、新しい必須プロパティなど)、実行時になって初めて失敗する場合があります。

フィクスチャを実際のデータ型に対応させたままにするには、Platform 関数自体からデータ型を導出します。

Copied!
1 2 3 import type { somePlatformFn } from "@osdk/foundry.<service>/<Resource>"; type ResponseShape = Awaited<ReturnType<typeof somePlatformFn>>;

次に、satisfies ResponseShape でレスポンス本文を検証します。

Copied!
1 2 3 HttpResponse.json( {/* ...response fields... */} satisfies ResponseShape, );

Awaited<ReturnType<typeof fn>> は、async 関数が返す Promise<T> を展開して T を直接取得します。これにより、モックのレスポンスが実際の呼び出しで返される内容と完全に一致することを保証します。

上の例では、このパターンを具体的に適用したものが type User = Awaited<ReturnType<typeof getCurrent>> です。エンドポイントが返すものに合わせてエイリアスに名前を付けます。

ヒント

  • onUnhandledRequest: "error" を使います。 ハンドラーがないことを見つけるほうが、ハングしたテストをデバッグするより簡単です。
  • ハンドラー間で1つのサーバーを再利用します。 モジュールスコープで setupServer() を1回呼び出し、次に各 it ブロック内で server.use(...) を呼び出してテストごとのハンドラーを設定します。afterEach(server.resetHandlers) を使ってハンドラーをクリアします。
  • オントロジースタブとプラットフォームスタブを組み合わせます。 1つの mockClient が両方の種類の呼び出しを同時に処理します。オントロジー呼び出しは when、whenObjectSet、whenQuery を通り、プラットフォーム呼び出しは MSW を通ります。