AspNetCore.Simple.MsTest.Sdk 7.0.23

This package has a SemVer 2.0.0 package version: 7.0.23+1.
There is a newer version of this package available.
See the version list below for details.
dotnet add package AspNetCore.Simple.MsTest.Sdk --version 7.0.23
                    
NuGet\Install-Package AspNetCore.Simple.MsTest.Sdk -Version 7.0.23
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="AspNetCore.Simple.MsTest.Sdk" Version="7.0.23" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="AspNetCore.Simple.MsTest.Sdk" Version="7.0.23" />
                    
Directory.Packages.props
<PackageReference Include="AspNetCore.Simple.MsTest.Sdk" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add AspNetCore.Simple.MsTest.Sdk --version 7.0.23
                    
#r "nuget: AspNetCore.Simple.MsTest.Sdk, 7.0.23"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package AspNetCore.Simple.MsTest.Sdk@7.0.23
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=AspNetCore.Simple.MsTest.Sdk&version=7.0.23
                    
Install as a Cake Addin
#tool nuget:?package=AspNetCore.Simple.MsTest.Sdk&version=7.0.23
                    
Install as a Cake Tool

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 like Users.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}

๐Ÿ“ฆ Api
 โ”ฃ ๐Ÿ“‚ Users
 โ”ƒ โ”— ๐Ÿ“‚ V1
 โ”ƒ โ”ƒ โ”ฃ ๐Ÿ“‚ Create
 โ”ƒ โ”ƒ โ”ƒ โ”ฃ ๐Ÿ“‚ Status_200_Ok
 โ”ƒ โ”ƒ โ”ƒ โ”ƒ โ”ฃ ๐Ÿ“‚ Requests
 โ”ƒ โ”ƒ โ”ƒ โ”ƒ โ”ƒ โ”— ๐Ÿ“œ CreateUser.json
 โ”ƒ โ”ƒ โ”ƒ โ”ƒ โ”ฃ ๐Ÿ“‚ Responses
 โ”ƒ โ”ƒ โ”ƒ โ”ƒ โ”ƒ โ”— ๐Ÿ“œ CreateUser.json
 โ”ƒ โ”ƒ โ”ƒ โ”ƒ โ”— ๐Ÿ“œ CreateUser_Status_200_OK_Test.cs
  • Requests folder โ†’ input payload\
  • Responses folder โ†’ 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

  • ValueDifference
  • MissingInFirst
  • MissingInSecond

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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
Loading failed

Fix bugs with error output if call went wrong, like not found, method not allowed etc.