Integration Testing

Every project generated from the templates includes a <YourProject>.Tests.Integration project, wired up and ready to run. Its tests go against a real Dataverse environment rather than mocks, because most of what breaks in a portal lives in the seams a mock replaces — query translation, metadata, formatting, permissions. Two base classes are provided, one for each side of that.

What's in the Project

The template ships these files; they're ordinary source in your repository, so change them freely.

File Purpose
PortalEndpointTestBase.cs Boots your portal in memory and exposes an HttpClient against it. Derive from this for endpoint and permission tests.
TestBase.cs Builds a service container with an authenticated Dataverse connection. Derive from this to call your own services and logic directly.
TestAuthentication.cs The test authentication scheme and the TestUserContext that decides who a request runs as.
appsettings.json, xunit.runner.json Non-secret settings, and the xunit runner configuration that keeps collections serial.
ExampleTests.cs, README.md Worked examples of both bases (skipped until you point them at real records), and a short local reference.

Choosing a Base

The two are complementary — they test different layers, and a healthy suite uses both.

Configuration and Secrets

Point D365:Url at the environment the tests should run against in appsettings.json — a development or throwaway environment, since tests create and delete records. Credentials come from user secrets, and the test project deliberately declares the same UserSecretsId as the web project, so a portal that already runs locally needs nothing further. To set them explicitly:

Sources are layered appsettings.json → user secrets → environment variables, so a build agent can supply everything through environment variables (D365__ClientSecret, using the double underscore) without touching a file. Never put the client secret in appsettings.json, which is committed.

Note

You don't need a license key to run tests. The in-memory host listens on a loopback address, and loopback is licensed without one — no Portal Website record and no test double required.

Testing Endpoints and Permissions

Derive from PortalEndpointTestBase and make requests with Client. Requests start anonymous, which is the right default for asserting that an endpoint turns anonymous callers away. Redirects are not followed, so a test can assert on a 302 rather than the page it would land on.

Acting as a User

SignInAs is the one to reach for. It stubs only the credential step — the principal it issues carries the same claims a real sign-in emits, so authorization, the permission handlers and every claim-reading extension behave as they do in production. No password, no provisioning, and it can change identity between requests in a single test.

LoginAsync posts real credentials to the login endpoint and keeps the cookie, for when the sign-in path itself is under test. It needs a contact with a password and a confirmed email, so it's slower; the two coexist and the base falls back to the real cookie whenever no impersonated user is set.

Note

LoginAsync needs the JSON auth endpoints, which are mapped for the React and Blazor WebAssembly/Auto hosts. A server-rendered Blazor project signs in through form posts to the Razor Account pages instead, so /api/auth/login isn't mapped there and LoginAsync returns 404 — use SignInAs, which works in every host.

Setup, Teardown and Substitutions

Three hooks cover the usual needs. ConfigureTestServices runs after your application's own registrations, so a Replace there wins over Program.cs — the way to stand in a fake for an outbound dependency. OnInitializedAsync and OnDisposingAsync bracket each test for seeding and cleanup, and the teardown runs even when a test fails.

Important

These tests start your portal, so anything that stops it starting stops them too. In particular, with enhanced authorization enabled the host validates the configured Website Id at startup and fails with guidance until a real powerpagesite id is supplied — the same precondition as running the portal.

Testing Services and Logic

Derive from TestBase, register what the code under test needs, and resolve it. The Dataverse client is already authenticated.

Pass a user id to CreateOrganizationService(userId) to impersonate — Dataverse then enforces that user's own privileges, which is how to check record-level access from the data side rather than through an endpoint.

CreateWebApiHttpClientAsync() returns a client authenticated against the Dataverse Web API (/api/data/v9.2/) for assertions that are easier to express in OData than through the SDK. It needs Azure:TenantId in configuration as well as the client id and secret.

How the Tests Run

Run them with dotnet test, or from your IDE's test explorer. A few characteristics are worth knowing before you write many:

Best practice

Point the suite at a development environment, never production. These are real writes against real data, and a test that deletes what it created will delete the real record if you aim it at one.