"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)."
Resources
1Install
npx skillscat add avanade/coreex/coreex-test-api Install via the SkillsCat registry.
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-apionce 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 bycoreex-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-enabledandmessaging-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(seedsread-data.seed.yaml) /XxxMutateTests(seedsmutate-data.seed.yaml) — each apartial 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, orMigrateCosmosDataAsync) →Test.ClearFusionCacheAsync()→ provider-specificTest.UseExpected{Postgres|SqlServer|CosmosDb}OutboxPublisher()→ HTTP mocks if the domain has adapters (if an adapter caches viaIHybridCache, also add a per-test[SetUp] => Test.ClearFusionCacheAsync()— Redis is shared across tests and runs)- Seed → Tests → Resources, in that order — seed known
^Nrows first, write tests referencing them, capture.res.json/.req.jsonfrom 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 → Cancelthrough the API just to arrange it. Exceptions: the transition itself is under test (Cancel_Success), the value is only known at runtime (ETag — read viaGETfirst), or the thing being asserted is a side effect of the action (an outbox event). Seecoreex-tests.instructions.md→ "Prefer Seeded State Over API-Orchestrated Setup" read-data.seed.yaml/mutate-data.seed.yamlare 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 viaro.WithIfMatchRequired()) — assert with.Assert(HttpStatusCode.PreconditionRequired), no dedicated helper exists - Delete is idempotent: always
AssertNoContent()(204), neverAssertNotFound()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
_Successtest explicitly asserts the specific changed property/properties on the mutation response (e.g.updated.Salary.Should().Be(60000.00m)), then ends with a follow-upGET+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) —keyis 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 —
UseExpectedXxxOutboxPublisherdefaults to asserting zero events when none is declared; omitting it on a call that legitimately publishes fails the test. PreferAssertMetadata/AssertWithValue(confirms which event) over a bareExpectXxxOutboxEvents()(confirms only that some event was published) - Reference-data JSON name: non-
Codesuffix (gender, notgenderCode) in.res.json/.req.json/inline bodies - HTTP client mocking:
MockHttpClientFactory+MockHttpClientRequestfields configured inOneTimeSetUp, 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
- `/.github/instructions/coreex-tests.instructions.md` — full, authoritative test conventions (auto-injected on any
*.Test*/**/*.csedit); this skill is a practical workflow layered on top of it, not a replacement - Related skills: `coreex-api` (the controller/endpoint implementation these tests exercise), `coreex-app-service` (Application logic behind the endpoint), `coreex-test-subscribe` (Subscribe host tests — shares this skill's setup foundations), `coreex-test-relay` (Outbox Relay host tests)
- Testing deep-dive — test architecture, data seeding, schema isolation, E2E runner; optional (after
/coreex-docs-sync) - Illustrative examples (CoreEx sample — not present in your project):
- Contoso.Products.Test.Api — PostgreSQL domain example (outbox + HTTP mock adapter)
- Contoso.Shopping.Test.Api — SQL Server domain example (outbox + ETag/concurrency)
- Contoso.Customers.Test.Api — Cosmos domain example (
MigrateCosmosDataAsync,UseExpectedCosmosDbOutboxPublisher)