注: 以下の翻訳の正確性は検証されていません。AIPを利用して英語版の原文から機械的に翻訳されたものです。
@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!1npm 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 14import { 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 12import 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 24import { 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つの処理を行います。
createMockClient() は、Client インターフェースを満たす MockClient を返します。コードが実際のクライアントを想定している場所ならどこにでも渡せます。createMockOsdkObject(Employee, { ... }) は、実際のインスタンスと同じ構造の Osdk.Instance を作成します。mockClient.when(stub => stub(Employee).fetchPage()).thenReturnObjects([...]) は、スタブを登録します。stub 引数は Client に似たファクトリーです。テスト対象のコードが実行するものと同じ呼び出しチェーンを再現します。createMockOsdkObject は、完全な形式の Osdk.Instance<T> を作成します。これは、テスト対象のコードに渡したり、.thenReturnObjects([...]) スタブ内に配置したりできます。以降のセクションでは、オブジェクトの形式、links オプション(単一、多数、エラー、モックオブジェクトセット)、および createMockAttachment について説明します。
createMockOsdkObject は3個の引数を取ります。
Copied!1createMockOsdkObject(objectType, properties, options);
objectType:**SDK から生成されたオブジェクトタイプの定数(たとえば Employee)です。モックは、この値から apiName と primaryKeyApiName を読み取ります。properties:**モックに設定するプロパティ値です。主キープロパティを含める必要があります。ほかのプロパティは任意で、コードがそれらを読み取る場合にのみ関係します。options:**3個の任意のフィールドです。
links:**オブジェクトの $link アクセサー用のモックデータです。リンクを参照してください。titlePropertyApiName:$title の元となるプロパティのAPI名です。タイトルプロパティを設定するを参照してください。$rid:**自動生成される $rid を上書きします。デフォルトは "ri.mock.main.object.<apiName>.<primaryKey>" です。返されるモックは、実際の OSDK インスタンスと同じ形式です。
| フィールド | 説明 |
|---|---|
$apiName, $objectType | オブジェクトタイプのAPI名です。 |
$primaryKey | properties 内の主キープロパティの値です。 |
$title | titlePropertyApiName プロパティの値です。設定されていない場合は undefined です。 |
$rid | 指定されている場合は options.$rid、そうでなければ自動生成されたモック RID です。 |
$objectSpecifier | "<apiName>:<primaryKey>" です。 |
$link | options.links を元にしたプロキシです。 |
$clone(updates?) | プロパティ値をマージした新しいモックを返します。 |
モックは $as および $__EXPERIMENTAL__NOT_SUPPORTED_YET__* アクセサーをモデル化しません。これらにアクセスするとエラーがスローされます。
Copied!1 2 3 4 5 6 7 8 9 10 11 12import { 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 7const 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 16const 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 13const 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 4const 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 24const 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 5const 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 15import { 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 10const 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 4import { 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 3const 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 を使った fetchPageCopied!1 2 3 4 5 6 7 8 9const 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 を使った fetchOneCopied!1 2 3mockClient .when((stub) => stub(Employee).fetchOne(1)) .thenReturnObject(emp);
thenReturnAggregation を使った aggregateCopied!1 2 3 4 5 6 7mockClient .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 10mockClient .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 19import { 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> を想定する任意の場所に渡せます。
クエリ(オントロジー上の関数)もスタブ化できます。
Copied!1 2 3 4import { 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 2mockClient.whenQuery(addOne, { n: 10 }).thenReturn(11); mockClient.whenQuery(addOne, { n: 20 }).thenReturn(21);
配列パラメーターを持つクエリも同じパターンに従います。コードが渡すパラメーターと一致させます。
Copied!1 2 3mockClient .whenQuery(queryTypeReturnsArray, { people: ["Alice", "Bob"] }) .thenReturn(["Alice - processed", "Bob - processed"]);
mockClient.clearStubs() は、クライアントに登録されたすべてのスタブを削除します。これは、複数の it ブロックでクライアントを再利用する場合に役立ちます。そうでない場合は、テストを分離するために、テストごとに新しい createMockClient() を構築します。
createMockClient がスタブ化するのは、オントロジー呼び出し(オブジェクトタイプ、クエリ、オブジェクトセット)のみです。@osdk/foundry.* と @osdk/internal.foundry.* の Foundry プラットフォーム API はインターセプトしません。これらの呼び出しは通常の fetch 経路を通ります。テストでネットワークにアクセスしないようにするには、MSW ↗ でインターセプトします。
ネットワークリクエストのスタブ化ライブラリを使って、プラットフォーム SDK のリクエストをスタブ化します。プレースホルダーのベース URL を使用します。
https://mock.invalid/
コードが行うすべてのプラットフォーム呼び出しは、このオリジンを基準に解決されます。関数が呼び出す特定のパスに対応する MSW ハンドラーを用意します。
MSW を開発用の依存関係としてインストールします。
Copied!1npm install --save-dev msw
テストファイル内に Node サーバーを設定します。標準の MSW ライフサイクルフックはテスト間でハンドラーをリセットするため、各 it ブロックが分離されます。
Copied!1 2 3 4 5 6 7 8 9import { 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 10import 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 63import 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 3import type { somePlatformFn } from "@osdk/foundry.<service>/<Resource>"; type ResponseShape = Awaited<ReturnType<typeof somePlatformFn>>;
次に、satisfies ResponseShape でレスポンス本文を検証します。
Copied!1 2 3HttpResponse.json( {/* ...response fields... */} satisfies ResponseShape, );
Awaited<ReturnType<typeof fn>> は、async 関数が返す Promise<T> を展開して T を直接取得します。これにより、モックのレスポンスが実際の呼び出しで返される内容と完全に一致することを保証します。
上の例では、このパターンを具体的に適用したものが type User = Awaited<ReturnType<typeof getCurrent>> です。エンドポイントが返すものに合わせてエイリアスに名前を付けます。
onUnhandledRequest: "error" を使います。 ハンドラーがないことを見つけるほうが、ハングしたテストをデバッグするより簡単です。setupServer() を1回呼び出し、次に各 it ブロック内で server.use(...) を呼び出してテストごとのハンドラーを設定します。afterEach(server.resetHandlers) を使ってハンドラーをクリアします。mockClient が両方の種類の呼び出しを同時に処理します。オントロジー呼び出しは when、whenObjectSet、whenQuery を通り、プラットフォーム呼び出しは MSW を通ります。