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

TypeScript オントロジー SDK コードをユニットテストする

試験的

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

@osdk/unit-testing パッケージを使用すると、Foundry にリクエストを送信せずに、Foundry 関数を含む、オントロジー SDK の Client を受け取るコードをユニットテストできます。このパッケージには、次の機能があります。

  • 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>" です。

返されるモックは、実際のオントロジー SDK インスタンスと同じ形式です。

フィールド説明
$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 に存在しない場合はエラーをスローします。

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

テスト環境では、オントロジー SDK は指定されたオブジェクトタイプのどのプロパティがタイトルかを認識していません。テスト対象のコードが 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 }

一致する $primaryKey を持つ配列要素がない場合、fetchOne(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> を渡せる場所ならどこにでも渡せる 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() は、オントロジー SDK の Client インターフェースを満たす値と、スタブを設定するための4個の追加メソッドを返します。

  • client.when(callback): クライアントを起点とする呼び出しをスタブ化します。テスト対象のコードが実行するチェーン(たとえば stub(Employee).where(...).fetchPage())を再現するコールバックを渡します。呼び出しの形式に応じた .thenReturn* マッチャーを持つビルダーを返します。
  • client.whenObjectSet(set, callback): 特定の MockObjectSet(createMockObjectSet で作成)に対する呼び出しをスタブ化します。コードにオブジェクトセットが直接渡される場合や、モックオブジェクトセットに基づいている many-link の場合に、この方法を使用します。
  • 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 } }, ]);

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

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

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

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

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 });

その後、コードが ObjectSet<Employee> を想定する任意の箇所に同じ empSet を渡せます。テスト対象の関数に渡すことも、親モックオブジェクトの many-link の対象として使用することもできます。

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) はその Promise を拒否します。異なるパラメーターオブジェクトを個別にスタブ化できます。

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 を通ります。