AspNetCore.Simple.MsTest.Sdk
7.0.18
See the version list below for details.
dotnet add package AspNetCore.Simple.MsTest.Sdk --version 7.0.18
NuGet\Install-Package AspNetCore.Simple.MsTest.Sdk -Version 7.0.18
<PackageReference Include="AspNetCore.Simple.MsTest.Sdk" Version="7.0.18" />
<PackageVersion Include="AspNetCore.Simple.MsTest.Sdk" Version="7.0.18" />
<PackageReference Include="AspNetCore.Simple.MsTest.Sdk" />
paket add AspNetCore.Simple.MsTest.Sdk --version 7.0.18
#r "nuget: AspNetCore.Simple.MsTest.Sdk, 7.0.18"
#:package AspNetCore.Simple.MsTest.Sdk@7.0.18
#addin nuget:?package=AspNetCore.Simple.MsTest.Sdk&version=7.0.18
#tool nuget:?package=AspNetCore.Simple.MsTest.Sdk&version=7.0.18
AspNetCore.Simple.MsTest.Sdk
๐ Snapshot-based API testing for ASP.NET Core
Clean. Deterministic. Productive.
This package enables efficient and structured testing of your ASP.NET Core APIs using full-response snapshot comparison.
๐ฆ Installation
Prerequisites
- .NET 9
Install
dotnet add package AspNetCore.Simple.MsTest.Sdk
๐ฏ Why This SDK?
Traditional Testing | This SDK
-------------------------------------------|-----------------------------------------
Many asserts | One snapshot
Manual header checks | Automatic
Manual JSON comparison | Deep diff engine
Hard to debug | Structured diff table
No reproduction | Auto-generated curl
Boilerplate code | Minimal setup
Developer focus on asserts | Productivity focus on behavior
New properties not tested automatically | Full response snapshot coverage
High maintenance effort | Snapshot-driven maintenance
Error-prone manual comparisons | Deterministic recursive comparison
Unstructured test failures | Structured schema mismatch output
Difficult refactoring | Refactoring-safe snapshot validation
Inconsistent test styles | Standardized test architecture
Hidden breaking API changes | Immediate snapshot mismatch detection
Manual diff analysis | Explicit MemberPath-based diff
Low scalability for large APIs | Designed for large API landscapes
Poor test readability | Behavior-driven snapshot clarity
Manual reproduction of failing calls | Built-in curl reproduction
Manual ignore handling | Global and local ignore strategies
Duplicated comparison logic | Centralized comparison engine
Implicit test coverage | Explicit full-response validation
๐ง How to write tests
await Client.AssertPostAsync<AddUserResponse>("api/v1/users",
"NewUser.json",
"NewUser.json");
Payload: "NewUser.json"
{
"Id": 1,
"Name": "Son",
"FirstName": "Goku",
"Age": 99,
"Emails": [
{
"EmailAddress": "alf@gmx.de",
"Type": "GMX"
},
{
"EmailAddress": "abc@hotmail.de",
"Type": "Microsoft"
}
]
}
Response: "NewUser.json"
{
"Content": {
"Headers": [
{
"Key": "Content-Type",
"Value": [ "application/json; charset=utf-8" ]
}
],
"Value": {
"Id": 1,
"Name": "Son",
"FirstName": "Goku",
"Age": 99,
"Emails": []
}
},
"StatusCode": "OK",
"Headers": [],
"TrailingHeaders": [],
"IsSuccessStatusCode": true
}
Outcome
โ Full response comparison
โ Automatic difference table
โ Curl output on failure
โ Snapshot-based testing
Http call infos:
-----------------------------------------------------------------------------
| HttpMethod | Url | HttpStatusCode |
-----------------------------------------------------------------------------
| POST | https://localhost:5001/api/tests/v1/users | OK |
-----------------------------------------------------------------------------
Detected differences: 3
---------------------------------------------------------------------------------------------------------------------
| MemberPath | "NewUser.json" | CurrentResult |
---------------------------------------------------------------------------------------------------------------------
| Content.Headers["Content-Type"].Value[0] | application/octet; charset=utf-8 | application/json; charset=utf-8 |
---------------------------------------------------------------------------------------------------------------------
| Content.Value.FirstName | Goku Failed | Goku |
---------------------------------------------------------------------------------------------------------------------
| StatusCode | NotFound | OK |
---------------------------------------------------------------------------------------------------------------------
Expected result:
{"Version":"1.1","Content":{"Headers":[{"Key":"Content-Type","Value":["application/octet; charset=utf-8"]}],"Value":{"Id":1,"Name":"Son","FirstName":"Goku Failed","Age":42,"Emails":[]}},"StatusCode":"NotFound","ReasonPhrase":"OK","Headers":[],"TrailingHeaders":[],"IsSuccessStatusCode":true}
Current result:
{"Version":"1.1","Content":{"Headers":[{"Key":"Content-Type","Value":["application/json; charset=utf-8"]}],"Value":{"Id":1,"Name":"Son","FirstName":"Goku","Age":42,"Emails":[]}},"StatusCode":"OK","ReasonPhrase":"OK","Headers":[],"TrailingHeaders":[],"IsSuccessStatusCode":true}
--------------------------------------------------------------
Http call as curl
--------------------------------------------------------------
curl \
--location \
--request POST 'https://localhost:5001/api/tests/v1/users' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer Sorry i am secret :)'
--data-raw '{
"Id": 1,
"Name": "Son",
"FirstName": "Goku Failed",
"Age": 99,
"Emails": []
}'
--------------------------------------------------------------
๐ JSON File Convention (IMPORTANT)
From now on:
โ Only use the file name
โ Never use full paths likeUsers.V1.Payloads.NewUser.json
โ Correct
"NewUser.json"
๐ Simple object comparison
[TestMethod]
public void Simple_Object_Comparison()
{
var person1 = new Person("Son", "Goku", 29);
var person2 = new Person("Muten", "Roshi", 63);
Assert.That.ObjectsAreEqual(person1, person2, title: "Persons are not equal");
}
Persons are not equal
----------------------------------
| MemberPath | person1 | person2 |
----------------------------------
| Name | Son | Muten |
----------------------------------
| FamilyName | Goku | Roshi |
----------------------------------
| Age | 29 | 63 |
----------------------------------
Count: 3
Current result:
{"Name":"Muten","FamilyName":"Roshi","Age":63}
Expected result:
{"Name":"Son","FamilyName":"Goku","Age":29}
๐ฆ Recommended Test Folder Structure
๐ฆ Api
โฃ ๐ Users
โ โ ๐ V1
โ โ โฃ ๐ Create
โ โ โ โฃ ๐ Status_200_Ok
โ โ โ โ โฃ ๐ Requests
โ โ โ โ โ โ ๐ CreateUser.json
โ โ โ โ โฃ ๐ Responses
โ โ โ โ โ โ ๐ CreateUser.json
โ โ โ โ โ ๐ CreateUser_Status_200_OK_Test.cs
Requestsfolder โ input payload\Responsesfolder โ expected snapshot\- Test class sits in same logical folder\
- Only file name is required in your test
๐งช Setup Test Base
Just a sample, you can also use your own custom setup. This is just out of the box provided with this sdk.
namespace AspNetCore.Simple.MsTest.Sdk.Test
{
[TestClass]
public abstract class ApiTestBase
{
private static ApiTestBase<Startup> _apiTestBase = null!;
[AssemblyInitialize]
public static void AssemblyInitialize(TestContext _)
{
// 1. Super simple just use the provided API test base class and you are ready to go
_apiTestBase = new ApiTestBase<Startup>("Development", // The environment name
(_, _) => { }, // The register services action
[]); // Configure environment variables
// 2. We need once the http client to communicate with the started api
Client = _apiTestBase.CreateClient();
}
protected static HttpClient Client { get; private set; } = null!;
[AssemblyCleanup]
public static void AssemblyCleanup()
{
_apiTestBase.Dispose();
Client.Dispose();
}
}
}
๐งช Example Test
[TestClass]
public class Persons : ApiTestBase
{
[TestMethod]
public Task Should_Be_Able_To_Post_A_Person_By_Json()
{
return Client.AssertPostAsync<Person>("api/tests/v1/persons",
"SonGoku.json", // This json file must be an embedded file in your solution or native json string
"SonGoku.json"); // This json file must be an embedded file in your solution or native json string
}
}
// Possible but not recommended:
// You can use also raw json strings instead of files, but this is not recommended for large payloads
[TestMethod]
public Task Should_Return_No_Users_If_No_One_Was_Added()
{
return Client.AssertGetAsync<GetAllUsersResponse>("v1/users", """{ "Users": [] }""");
}
๐งฉ Ignore Generated Values
private static IEnumerable<Difference> IgnoreId(IImmutableList<Difference> differences)
{
foreach (var difference in differences)
{
if (difference.MemberPath == "Content.Value.Id")
{
continue;
}
yield return difference;
}
}
๐ Replacements
await Client.AssertGetAsync<GetUserByIdResponse>($"api/v1/users/{user.Id}",
"GetUser.json",
[
("$Id$", user.Id)
]);
Mismatch Types
ValueDifferenceMissingInFirstMissingInSecond
Scope ignore
[TestMethod]
public Task Should_Return_Expected_Result_For_Given_Payload_But_Ignore_Id()
{
await Client.AssertPostAsync<AddUserReponse>($"api/v1/users/",
"Users.V1.Payloads.NewUser.json",
"Users.V1.Results.NewUser.json",
differenceFunc: DifferenceFunc);
}
// Difference func can be used to intercept the object comparison in the background
private IEnumerable<Difference> DifferenceFunc(IImmutableList<Difference> differences)
{
foreach (var difference in differences)
{
// Here we ignore the Id property. Real world scenario generated id by database as an example
if (difference.MemberPath == nameof(User.Id))
{
continue;
}
yield return difference;
}
}
๐ Global Ignore
AssertObjectExtensions.DifferenceFunc = differences =>
{
foreach (var difference in differences)
{
if (difference.MemberPath.Contains("x-amzn-trace-id"))
{
continue;
}
yield return difference;
}
};
๐งช Enum Test Cases
Instead of this:
[DataTestMethod]
[DataRow(MyEnum.Feature)]
[DataRow(MyEnum.Component)]
[DataRow(MyEnum.System)]
[DataRow(MyEnum.Feature)]
[DataRow(MyEnum.Component)]
[DataRow(MyEnum.System)]
public async Task Should_Be_Able_To_Create_A_CapabilityType_If_Status_Is_Correct(MyEnum useCase)
{
// Your test logic here
}
Just use:
[DataTestMethod]
// Generates one test case for each enum value in CapabilityTypeEnum with status "Active"
// You can even add multiple sets
[EnumTestCase<CapabilityTypeEnum>()]
public async Task Should_Be_Able_To_Create_A_CapabilityType_If_Status_Is_Correct(MyEnum useCase)
{
// Your test logic here
}
๐ Test Response Writer
await Client.AssertPostAsync<CreateUserResponse>("api/v1/users",
"CreateUser.json",
"CreateUser.json",
writeResponse: true);
Global flag:
AssertObjectExtensions.WriteResponse = true;
Environment variable:
AspNetCoreSimpleMsTestSdk__WriteResponse=true
โ Use carefully --- this overwrites snapshots.
๐ฅ Curl Output
Each test used by the provided exetensions tracks the request and response.
curl \
--request POST 'https://localhost:5001/api/v1/users' \
--header 'Content-Type: application/json' \
--data-raw '{ ... }'
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. 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. |
-
net9.0
- ConsoleTables (>= 2.6.2)
- Extensions.Pack (>= 6.0.16)
- Microsoft.AspNetCore.Mvc.Testing (>= 9.0.10)
- Microsoft.AspNetCore.TestHost (>= 9.0.10)
- MSTest.TestFramework (>= 4.0.2)
- System.CodeDom (>= 9.0.10)
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 |
|---|---|---|
| 7.0.28 | 614 | 2/17/2026 |
| 7.0.27 | 263 | 2/16/2026 |
| 7.0.26 | 46 | 2/16/2026 |
| 7.0.25 | 40 | 2/16/2026 |
| 7.0.24 | 180 | 2/16/2026 |
| 7.0.23 | 28 | 2/16/2026 |
| 7.0.22 | 29 | 2/16/2026 |
| 7.0.21 | 464 | 2/14/2026 |
| 7.0.20 | 28 | 2/14/2026 |
| 7.0.19 | 76 | 2/13/2026 |
| 7.0.18 | 33 | 2/13/2026 |
| 7.0.17 | 29 | 2/13/2026 |
| 7.0.16 | 33 | 2/12/2026 |
| 7.0.15 | 41 | 2/11/2026 |
| 7.0.14 | 33 | 2/11/2026 |
| 7.0.13 | 431 | 2/10/2026 |
| 7.0.12 | 40 | 2/10/2026 |
| 7.0.11 | 151 | 2/9/2026 |
| 7.0.10 | 32 | 2/9/2026 |
| 7.0.9 | 63 | 2/9/2026 |
Fix bugs with error output if call went wrong, like not found, method not allowed etc.