統合テスト
テンプレートから生成されるすべてのプロジェクトには、配線されて実行可能な <YourProject>.Tests.Integration プロジェクトが含まれています。そのテストはモックではなく 実際のDataverse環境 に対して行われます。なぜなら、ポータルで壊れるほとんどの要素はモックが置き換える継ぎ目(クエリ翻訳、メタデータ、フォーマット、権限)に存在しているからです。ベースクラスは両側それぞれに2つずつ用意されています。
プロジェクトの内容
テンプレートはこれらのファイルを出荷します。リポジトリ内の通常のソースソースなので、自由に変更してください。
| ファイル | 目的 |
|---|---|
PortalEndpointTestBase.cs |
メモリでポータルを起動し、それに対して HttpClient を公開します。これを基にエンドポイントや権限テストを導き出します。 |
TestBase.cs |
認証済みのDataverse接続を持つサービスコンテナを構築します。そこから自分のサービスやロジックを直接呼び出すために派生します。 |
TestAuthentication.cs |
テスト認証スキームと、リクエストが誰として実行されるかを決定する TestUserContext です。 |
appsettings.json, xunit.runner.json |
非秘密設定や、コレクションをシリアルに保つxunitランナーの設定などです。 |
ExampleTests.cs, README.md |
両方の基地の実例(実際の記録を指すまで飛ばし)、そして簡単なローカルな参照。 |
基地の選択
この二つは補完的で、異なる層を試し、健康的なスイートでは両方を活用します。
PortalEndpointTestBase実際のProgramを実行し、リクエストは登録した通りの実際のミドルウェアの順序、実際の認可、権限ハンドラーを通過します。テーブル権限、フレームワークの/api/*表、追加したエンドポイントに使ってください。これはあなたのアプリケーション なので 、ここでテストが本番環境から逸脱することはありません。TestBaseホストを起動しません。コンテナを構築しDataverseクライアントを渡すため、プラグインスタイルのロジック、ドメインサービス、クエリ形状の安価な選択肢となります。HTTPは認識しないため、ミドルウェアや認可、権限ハンドラーは適用されません。
構成と秘密
D365:Url、テストがappsettings.jsonで実行されるべき環境、つまり開発環境や使い捨て環境を指し示します。テストはレコードを作成・削除します。認証情報はユーザーシークレットから来ており、テストプロジェクトは意図的にウェブプロジェクトと同じUserSecretsIdを宣言しているため、すでにローカルで動作しているポータルにはこれ以上の処理は不要です。明示的に設定するには:
dotnet user-secrets set "D365:ClientId" "<application (client) id>"
dotnet user-secrets set "D365:ClientSecret" "<client secret value>"
ソースはユーザーシークレット appsettings.json →環境変数→レイヤー化されているため、ビルドエージェントは環境変数(D365__ClientSecret、ダブルアンダースコアを使用)を通じてファイルに触れずにすべてを提供できます。クライアントシークレットはコミットされている appsettings.jsonに入れてはいけません。
注記
テストを実行するのにライセンスキーは必要ありません。インメモリホストはループバックアドレスでリスニングしており、ループバックはループバックなしでライセンスされています — Portal Websiteのレコードもテストダブルも必要ありません。
エンドポイントと権限のテスト
PortalEndpointTestBaseから派生し、Clientでリクエストを行います。リクエストは匿名で開始されます。これはエンドポイントが匿名発信者を拒否する際の正しいデフォルトです。リダイレクトは実行されないため、テストは到達するページではなく302上で主張できます。
public class AccountPermissionTests : PortalEndpointTestBase
{
[Fact]
public async Task AnonymousCallersAreRefused()
{
// No sign-in: the request is anonymous.
var response = await this.Client.GetAsync("/api/table/account/records");
response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
}
[Fact]
public async Task SignedInContactCanRead()
{
this.SignInAs(contactId);
var response = await this.Client.GetAsync("/api/table/account/records");
response.StatusCode.Should().Be(HttpStatusCode.OK);
}
}
ユーザーとしての行動
SignInAs が手を伸ばすべきものです。認証情報のステップだけをスタブします。発行するプリンシパルは実際のサインインと同じ主張を含んでいるため、認可、権限ハンドラー、すべてのクレーム読み取り拡張は本番環境と同じように動作します。パスワードもプロビジョニングもなく、単一のテストでリクエスト間のアイデンティティを変更できます。
LoginAsync ログインエンドポイントに本物の認証情報を投稿し、サインインパス自体がテストされている時のためにクッキーを保持します。パスワードと確認済みのメールを持つ連絡先が必要なため、処理は遅くなります。両者は共存し、なりすましユーザーが設定されていない場合はベースが本物のクッキーに戻ります。
// Impersonate — fast, no password, still runs the real authorization path.
this.SignInAs(contactId);
this.SignInAs(contactId, "Administrator", "Manager"); // with role claims
this.SignInAs(userId, PortalUserType.SystemUser); // an internal user
this.SignOut(); // anonymous again
// Or sign in for real, when the sign-in path itself is what's under test.
var login = await this.LoginAsync("someone@example.com", "password");
注記
LoginAsyncReactおよびBlazor WebAssembly/Autoホスト向けにマッピングされたJSON認証エンドポイントが必要です。サーバーレンダリングされたBlazorプロジェクトはフォーム投稿を通じてRazor Accountページにサインインするため、/api/auth/loginはそこにマッピングされず、LoginAsyncは404 —SignInAsを使い、すべてのホストで動作します。
セットアップ、解体および代替
通常のニーズを満たすフックは3つあります。 ConfigureTestServices はアプリケーション自身の登録後に動作するため、そこにある Replace が Program.cs に勝つことになります。これはアウトバウンド依存関係の偽物に立ち上がる方法です。 OnInitializedAsync と OnDisposingAsync は各テストをブラケットで区切り、シーディングとクリーンアップを行い、テストが失敗しても分解作業が行われます。
public class OrderTests : PortalEndpointTestBase
{
private Guid _contactId;
// Replace a service in the running host — a fake for an outbound dependency, say.
protected override void ConfigureTestServices(IServiceCollection services)
{
services.Replace(ServiceDescriptor.Transient<IShippingQuotes, FakeShippingQuotes>());
}
// Seed what the test needs. Runs after the host starts.
protected override async Task OnInitializedAsync()
{
_contactId = await this.CreateTestContactAsync();
}
// Always runs, including after a failing test.
protected override async Task OnDisposingAsync()
{
await this.DeleteContactAsync(_contactId);
}
}
重要
これらのテストはポータルを起動させるため、起動を妨げるものはポータルも停止させます。特に、強化認可が有効の場合、ホストは起動時に設定済み のウェブサイトID を検証し、実際の
powerpagesiteIDが提供されるまでガイダンスで失敗します。これはポータルを起動する前提条件と同じです。
テストサービスとロジック
TestBaseから導き出し、テスト対象のコードが必要とするものを登録し、それを解決します。Dataverseクライアントはすでに認証されています。
public class InvoiceLogicTests : TestBase
{
// Register what the code under test needs; always call base first.
protected override void RegisterServices(IServiceCollection services)
{
base.RegisterServices(services);
services.AddMyPortalLogic();
}
[Fact]
public async Task PostingAnInvoiceSetsTheBalance()
{
var logic = this.ServiceProvider.GetRequiredService<InvoiceLogic>();
// A Dataverse client, already authenticated. IOrganizationService resolves to the
// same connection, so resolving either doesn't open a second one.
using var service = this.CreateOrganizationService();
var invoice = await service.Queryable("invoice")
.FirstOrDefaultAsync(i => i.GetAttributeValue<string>("invoicenumber") == "INV-1");
invoice.Should().NotBeNull();
}
}
ユーザーIDを CreateOrganizationService(userId) に渡してなりすましする — Dataverseはそのユーザー自身の権限を強制し、エンドポイントを介さずデータ側からレコードレベルのアクセスを確認する方法を教えます。
CreateWebApiHttpClientAsync() Dataverse Web API(/api/data/v9.2/)に対して認証されたクライアントを返し、SDKよりもODataで表現しやすい主張を返します。設定時にクライアントIDやシークレットのほか Azure:TenantId が必要です。
テストの進行方法
dotnet testで実行するか、IDEのテストエクスプローラーから実行してください。多くの特徴を書く前に知っておく価値のあるいくつかの特徴があります:
- コレクションはシリアルで実行されます。 それらは1つのDataverse環境を共有し、同じレコードへの並列書き込みは失敗の再現を難しくします。これは
xunit.runner.jsonに設定されています。 - xunitは各テストメソッドごとに新しいテストクラスのインスタンスを構築します。 認証済みのDataverse接続はプロセス全体でキャッシュされ、軽量クローンとして配布されるため、テストごとに1回のサインインではなく1回のサインインが発生します。しかし、構築体に追加する高価なものはすべてのテストで支払われます。
- 作成したものを整理しましょう。 レコードを残すテストは、次の実行の主張が前回の主張に依存します。固定されたテストよりも一意の値(名前フィールド内のGUID)を優先し、繰り返し実行が衝突しないようにしましょう。
ベストプラクティス
スイートは本番環境ではなく、開発環境に向けてください。これらは実際のデータに対する書き込みであり、作成したものを削除するテストは、もしそれを一つに狙い定めれば実際のレコードも削除されます。
