Avanade

coreex-test-api

"Write or update an integration test in a CoreEx *.Test.Api project. USE FOR: new XxxReadTests/XxxMutateTests partial classes, per-operation test files (Get/Query/Create/Update/Patch/Delete), OneTimeSetUp seeding + cache + outbox wiring, seed data (read-data.seed.yaml/mutate-data.seed.yaml), .res.json/.req.json resources, ETag/concurrency and soft-delete scenarios, outbox event assertions, inter-domain HTTP mocking. DO NOT USE FOR: Subscribe host tests (use coreex-test-subscribe), Outbox Relay host tests (use coreex-test-relay), pure unit tests with no infrastructure (validators/aggregates/adapters already cover their own Test.Unit guidance), the controller/endpoint implementation itself (use coreex-api)."

Avanade 28 8 Updated 1w ago

Resources

1
GitHub

Install

npx skillscat add avanade/coreex/coreex-test-api

Install via the SkillsCat registry.

SKILL.md

CoreEx: API Integration Test

Guides you through writing or extending *.Test.Api integration tests — the real host, real DB, real
cache, real outbox, over WithApiTester<{Solution}.Api.Program>. This is the canonical integration-test
setup shared with Subscribe host tests (coreex-test-subscribe links back here for the common parts).

When to Use

  • Adding a new entity's read/mutate API tests from scratch
  • Adding a test for a single new operation (Get/Query/Create/Update/Patch/Delete) on an existing entity
  • Adding an ETag/concurrency, soft-delete, idempotent-delete, or outbox-event scenario
  • Wiring OneTimeSetUp (DB migrate/seed, cache clear, outbox publisher, inter-domain HTTP mocks) for a new test class
  • Called standalone ("write the test for X") or as the hand-off step from coreex-api once an endpoint is implemented

When Not to Use

  • Subscribe host integration tests — use coreex-test-subscribe (shares this skill's DB/cache/outbox setup, differs in trigger mechanism)
  • Outbox Relay host tests — use coreex-test-relay (mostly templated, rarely extended)
  • Validator/aggregate/adapter unit tests (*.Test.Unit) — already covered by coreex-validator/coreex-aggregate/coreex-adapter
  • Implementing the controller/endpoint itself — use coreex-api

Resolve project-wide choices from state before asking. Read the solution-root AGENTS.md
Feature Configuration: data-provider (PostgreSQL vs SQL Server vs Cosmos) selects the migrate/seed and
outbox helper family used throughout — never mix them; outbox-enabled and messaging-provider
determine whether outbox-event assertions (ExpectXxxOutboxEvents) apply at all. Only prompt for
what is unrecorded; re-state resolved values for confirmation.

Quick Reference

  • One class pair per entity: XxxReadTests (seeds read-data.seed.yaml) / XxxMutateTests (seeds mutate-data.seed.yaml) — each a partial class, one operation per sub-file: Xxx{Read|Mutate}Tests.{Operation}.cs
  • [OneTimeSetUp] lives in the base partial file: migrate + seed (named-file overload — MigratePostgresDataAsync, MigrateSqlServerDataAsync, or MigrateCosmosDataAsync) → Test.ClearFusionCacheAsync() → provider-specific Test.UseExpected{Postgres|SqlServer|CosmosDb}OutboxPublisher() → HTTP mocks if the domain has adapters (if an adapter caches via IHybridCache, also add a per-test [SetUp] => Test.ClearFusionCacheAsync() — Redis is shared across tests and runs)
  • Seed → Tests → Resources, in that order — seed known ^N rows first, write tests referencing them, capture .res.json/.req.json from the actual run
  • Seed the precondition state directly — don't chain API calls to reach it. A test needing e.g. a cancelled booking should seed a row with that status, not Create → Confirm → Cancel through the API just to arrange it. Exceptions: the transition itself is under test (Cancel_Success), the value is only known at runtime (ETag — read via GET first), or the thing being asserted is a side effect of the action (an outbox event). See coreex-tests.instructions.md → "Prefer Seeded State Over API-Orchestrated Setup"
  • read-data.seed.yaml/mutate-data.seed.yaml are the preferred default, not the only option — the seed overload takes a list, so a class with data too distinct to fit cleanly (e.g. several precondition-state rows for one workflow) can add its own file alongside or instead of the shared pair
  • One seed row per destructive test (not per operation) — NUnit randomises order, so two mutate tests sharing a row collide non-deterministically
  • Test.Http() / Test.Http<T>() fluent chain: expectations → .Run(...) → assert
  • Expectation helpers auto-exclude from JSON compare: ExpectIdentifier(), ExpectETag(), ExpectChangeLogCreated()/Updated() — omit those fields from .res.json
  • 412 vs 409 vs 428: stale ETag → AssertPreconditionFailed() (412); duplicate/business conflict → AssertConflict() (409); no ETag supplied → 428 (auto on PUT/PATCH, or POST/DELETE opted in via ro.WithIfMatchRequired()) — assert with .Assert(HttpStatusCode.PreconditionRequired), no dedicated helper exists
  • Delete is idempotent: always AssertNoContent() (204), never AssertNotFound() on DELETE — the 404 belongs to the follow-up GET; only the first delete emits an outbox event
  • Verify persistence, not just the echo: every Create/Update/Patch _Success test explicitly asserts the specific changed property/properties on the mutation response (e.g. updated.Salary.Should().Be(60000.00m)), then ends with a follow-up GET + AssertValue(...) — the mutation response alone only proves the handler echoed the value back, not that the intended value was actually written
  • Outbox assertions are provider-specific — ExpectPostgresOutboxEvents/ExpectSqlServerOutboxEvents/ExpectCosmosDbOutboxEvents; .AssertWithValue(destination, subject) for value-carrying events (Create/Update), .AssertMetadata(destination, subject, key) for no-value events (Delete) — key is optional (AssertMetadata(destination, subject) checks routing only). If the event's payload type differs from the API's returned type (e.g. a leaner projection), use .AssertWithValue<TValue>(() => expectedPayload, destination, subject) instead of the plain form
  • Every event-publishing call needs an explicit expectation — UseExpectedXxxOutboxPublisher defaults to asserting zero events when none is declared; omitting it on a call that legitimately publishes fails the test. Prefer AssertMetadata/AssertWithValue (confirms which event) over a bare ExpectXxxOutboxEvents() (confirms only that some event was published)
  • Reference-data JSON name: non-Code suffix (gender, not genderCode) in .res.json/.req.json/inline bodies
  • HTTP client mocking: MockHttpClientFactory + MockHttpClientRequest fields configured in OneTimeSetUp, per-test .Respond.With(...)/.Respond.WithJsonResource(...), always .Verify()
  • Name tests {Entity}_{Action}_{Outcome} (e.g. Product_Create_Success)

For full workflow, decision trees, and code patterns see `references/workflow.md`.

Key References