Simple.JsonApi.Testing
0.1.1
dotnet add package Simple.JsonApi.Testing --version 0.1.1
NuGet\Install-Package Simple.JsonApi.Testing -Version 0.1.1
<PackageReference Include="Simple.JsonApi.Testing" Version="0.1.1" />
<PackageVersion Include="Simple.JsonApi.Testing" Version="0.1.1" />
<PackageReference Include="Simple.JsonApi.Testing" />
paket add Simple.JsonApi.Testing --version 0.1.1
#r "nuget: Simple.JsonApi.Testing, 0.1.1"
#:package Simple.JsonApi.Testing@0.1.1
#addin nuget:?package=Simple.JsonApi.Testing&version=0.1.1
#tool nuget:?package=Simple.JsonApi.Testing&version=0.1.1
JsonApiKit.Testing
Test-client helpers for integration-testing a JSON:API HTTP API. Framework-agnostic and dependency-free: it drives plain HttpClient and parses with System.Text.Json.Nodes, so it works against any JSON:API server — not just ones built with JsonApiKit.
What's in it
Protocol/ — spec vocabulary
JsonApiMember— spec document member names (data,attributes,included,links.next, …) plus JsonApiKit's conventional pagination meta (total,pageCount), so tests navigate documents without magic strings.JsonApiMediaTypes—application/vnd.api+jsonandapplication/problem+jsonfor content-type assertions.
Documents/ — request bodies and expected documents
JsonApiDocuments— builders for both sides of the wire contract. Ids are strings, as the spec defines them; a suite for a server with numeric keys typically wraps these with converting overloads.- Write bodies:
Post,Patch, andLinkage(a to-one relationship update, or explicitdata: nullto clear). - Deliberately non-conformant bodies for the spec's rejection tests:
PostWithoutType(400),PostWithArrayData(400),PostWithClientGeneratedId(403),PatchWithDatalessRelationship(400). - Full expected response documents for
ShouldMatchExactly:Single,Page(with complete pagination links andtotal/pageCountmeta),Related,Linkage, andProblem(RFC 7807 body with ASP.NET's default type URIs).
- Write bodies:
ResourceExpectation— one expected resource object: type/id, attributes and relationships in declaration order,links.self, with nulls omitted mirroring JsonApiKit's serialization rules.Fields(...)trims it to a sparse fieldset. Wrap it in named per-resource builders that take the entities the test arranged, so expectations form a golden model independent of the production mapping code.
Http/ — driving the server
JsonApiHttpClientExtensionsGetDocumentAsync(url)— GET + parse toJsonNode; a non-success response throws with the status and the response body, so failing tests show the server's actual error. Also asserts theapplication/vnd.api+jsonmedia type — a spec invariant of successful responses — so no test checks it by hand.PostJsonApiAsync(url, document)/PatchJsonApiAsync(url, document)— send an anonymous object serialized with web (camelCase) conventions asapplication/vnd.api+jsonwith no media type parameters (a charset parameter would rightly draw a 415 from a spec-strict server).GetProblemAsync(url, expectedStatus)/ReadProblemAsync(response, expectedStatus)— for error paths: assert the status and theapplication/problem+jsonmedia type, verify atraceIdis present and strip it (it is per-request random, not contract), and return the parsed problem body for a full match.GetProblemAsynccovers GET error paths;ReadProblemAsynctakes theHttpResponseMessagea failed POST/PATCH/DELETE returned.FindIdAsync(collectionUrl, attribute, value)— id of the first resource whose string attribute matches, for locating seeded data without hard-coding ids. Inspects one page only.ShouldBeSuccessfulWrite()— asserts the spec's successful-write contract on anHttpResponseMessage: 200 OK with a response document, or 204 No Content.
Assertions/ — asserting on documents
JsonNodeAssertionsShouldMatch(expected)— asserts aJsonNodeagainst an anonymous object serialized with web (camelCase) conventions. Objects match as subsets: members the server returns beyond the expected ones are ignored. Arrays match element by element; scalars must be equal. On mismatch it throwsJsonApiMatchExceptionwith the JSON path of the first difference plus both payloads pretty-printed.ShouldMatchExactly(expected)— the strict variant: any member the server returns that the expectation does not declare is a mismatch too ($.data[0]: unexpected members: attributes), so one assertion covers the entire payload. Object member order stays insignificant; array order and count stay exact. Accepts an anonymous object or a prebuiltJsonNode, so expected documents can come from a builder.
JsonApiDocumentAssertions— spec-level assertions for conformance tests, also throwingJsonApiMatchException:ShouldBeSortedBy(field, descending)— the data array is ordered by an attribute, numerically for JSON numbers and ordinally for strings.ShouldHaveAvailableLink(key)/ShouldHaveUnavailableLink(key)— pagination link availability, honoring the spec's rule that an unavailable link may be either omitted or explicitly null.
Usage
All examples assume:
using JsonApiKit.Testing;
using static JsonApiKit.Testing.JsonApiMember; // or a using alias
Fetching and matching
var id = await client.FindIdAsync("/api/companies", "name", "Acme Manufacturing");
var document = await client.GetDocumentAsync($"/api/companies/{id}");
document[Data].ShouldMatch(new
{
type = "companies",
id,
attributes = new { name = "Acme Manufacturing" },
links = new { self = $"/api/companies/{id}" }
});
Creating a resource
JsonApiDocuments.Post builds the resource document — attributes as an anonymous object, relationships as (name, targetType, targetId) tuples. PostJsonApiAsync returns the raw HttpResponseMessage, so the test can assert the parts of a 201 the spec prescribes — status, Location header, and the created document:
var response = await client.PostJsonApiAsync("/api/contacts", JsonApiDocuments.Post("contacts",
new { firstName = "Beata", lastName = "Nowak" },
("company", "companies", companyId)));
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
var location = response.Headers.Location!.ToString();
var created = JsonNode.Parse(await response.Content.ReadAsStringAsync())!;
created[Data].ShouldMatch(new
{
type = "contacts",
attributes = new { firstName = "Beata", lastName = "Nowak" }
});
// The Location header must point at the new resource.
var fetched = await client.GetDocumentAsync(location);
fetched[Data].ShouldMatch(new { id = created[Data]![Id]!.GetValue<string>() });
Updating a resource
Omitted attributes and relationships keep their current values; a relationship tuple with a null id clears it:
var response = await client.PatchJsonApiAsync($"/api/contacts/{id}",
JsonApiDocuments.Patch("contacts", id, new { email = "beata.nowak@example.com" }));
// The spec's successful-write contract: 200 OK with a response document, or 204 No Content.
response.ShouldBeSuccessfulWrite();
Error paths
GetProblemAsync asserts the status and problem-details media type, strips the random traceId, and returns the body, so an exact match can pin the whole error contract:
var problem = await client.GetProblemAsync("/api/contacts/99999", 404);
problem.ShouldMatchExactly(new
{
type = "https://tools.ietf.org/html/rfc9110#section-15.5.5",
title = "Not found",
status = 404,
detail = "Contact '99999' does not exist."
});
For write error paths, feed the response a POST/PATCH/DELETE returned to ReadProblemAsync:
var response = await client.PostJsonApiAsync("/api/contacts",
JsonApiDocuments.Post("contacts", new { firstName = "" }));
var problem = await response.ReadProblemAsync(422);
problem.ShouldMatch(new { title = "Validation failed" });
Walking pagination
The JsonApiMember constants cover the pagination links and JsonApiKit's total/pageCount meta:
var page = await client.GetDocumentAsync("/api/contacts?page[size]=10");
page[Meta].ShouldMatch(new { total = 27, pageCount = 3 });
while (page[Links]?[Next]?.GetValue<string>() is { } next)
{
page = await client.GetDocumentAsync(next);
}
page.ShouldHaveUnavailableLink(Next); // last page
Spec-conformance assertions
JsonApiDocumentAssertions covers rules from the spec's fetching sections. Sorting, per "the sort order for each sort field MUST be ascending unless it is prefixed with a minus" — numbers compare numerically, strings ordinally:
var ascending = await client.GetDocumentAsync("/api/deals?sort=amount");
ascending.ShouldBeSortedBy("amount");
var descending = await client.GetDocumentAsync("/api/deals?sort=-amount");
descending.ShouldBeSortedBy("amount", descending: true);
Pagination link availability, per "keys MUST either be omitted or have a null value to indicate that a particular link is unavailable" — ShouldHaveUnavailableLink accepts both forms:
var first = await client.GetDocumentAsync("/api/deals?page[size]=1&page[number]=1");
first.ShouldHaveUnavailableLink(Prev); // omitted or explicit null both pass
first.ShouldHaveAvailableLink(Next);
first.ShouldHaveAvailableLink(Last);
And the write contract on deletions, per "the server MUST return either a 200 OK status code and response document or a 204 No Content status code":
var deleted = await client.DeleteAsync($"/api/deals/{id}");
deleted.ShouldBeSuccessfulWrite();
Exact matching with document builders
Exact matching pins the whole payload — an undeclared member fails the test. JsonApiDocuments and ResourceExpectation build the complete expected documents to pair with it:
ResourceExpectation Company(string id, string name) =>
new ResourceExpectation("companies", id, $"/api/companies/{id}")
.Attr("name", name)
.RelatedOnlyRel("contacts");
var document = await client.GetDocumentAsync($"/api/companies/{id}");
document.ShouldMatchExactly(JsonApiDocuments.Single(Company(id, "Acme Manufacturing")));
// Collection documents come with full pagination links and total/pageCount meta.
var page = await client.GetDocumentAsync("/api/companies?page[number]=1&page[size]=2");
page.ShouldMatchExactly(JsonApiDocuments.Page("/api/companies", query: null, number: 1, size: 2,
total: 7, [Company(id, "Acme Manufacturing"), Company(otherId, "Borealis Ltd")]));
In a real suite the per-resource builders (like Company above) take the entities the test arranged and live in test infrastructure — a golden model that re-encodes the wire contract independently of the production mapping code, so a mapping bug cannot hide inside the expectation.
A failing match reports where and what diverged:
JsonApiKit.Testing.JsonApiMatchException : JSON mismatch at $.attributes.name:
expected "Acme Manufacturing" but got "Acme Corp"
Expected (subset):
{ ... }
Actual:
{ ... }
The assertions throw their own JsonApiMatchException rather than depending on xunit/NUnit/MSTest — which is what keeps the package dependency-free and usable from any test framework.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.1.1 | 123 | 7/15/2026 |
| 0.1.1-preview.1.11 | 57 | 7/15/2026 |
| 0.1.1-preview.1.9 | 62 | 7/15/2026 |