AspNetCore.Simple.MsTest.Sdk
5.1.0
See the version list below for details.
dotnet add package AspNetCore.Simple.MsTest.Sdk --version 5.1.0
NuGet\Install-Package AspNetCore.Simple.MsTest.Sdk -Version 5.1.0
<PackageReference Include="AspNetCore.Simple.MsTest.Sdk" Version="5.1.0" />
paket add AspNetCore.Simple.MsTest.Sdk --version 5.1.0
#r "nuget: AspNetCore.Simple.MsTest.Sdk, 5.1.0"
// Install AspNetCore.Simple.MsTest.Sdk as a Cake Addin #addin nuget:?package=AspNetCore.Simple.MsTest.Sdk&version=5.1.0 // Install AspNetCore.Simple.MsTest.Sdk as a Cake Tool #tool nuget:?package=AspNetCore.Simple.MsTest.Sdk&version=5.1.0
AspNetCore.Simple.MsTest.Sdk
This package is designed to enable efficient and clean testing against your ASP.NET Core APIs. It dramatically reduces the amount of required asserts, allowing for faster creation of more readable tests. It supports a Test-First approach, helping developers focus on testing earlier in the development cycle.
Getting started
Prerequisites
Install the package
dotnet add package AspNetCore.Simple.MsTest.Sdk
Basic concept
Our assert helpers are designed to streamline testing by doing the following:
- Asserting expected call outcomes (e.g.,
AssertPostAsync
for success andAssertPostAsErrorAsync
for errors). - Comparing the entire response structure for equality, not just the status code.
- Allowing for direct usage of JSON strings or files in tests.
- Directly indicating the route being tested.
- Enhancing productivity by comparing content headers, status codes, and more.
await Client.AssertPostAsync<AddUserReponse>($"api/v1/users/",
"Users.V1.Payloads.NewUser.json,
"Users.V1.Results.NewUser.json);
Setup your test environment
We provide you a simple ApiTestBase<Startup>
you can use it directly in your test
class. But we recommend that you setup a central base class for startup and tear down.
Sample:
using System;
using System.Net.Http;
using AspNetCore.Simple.MsTest.Sdk.Api;
using Microsoft.VisualStudio.TestTools.UnitTesting;
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();
}
}
}
Setup a test class
[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",
"Payloads.SonGoku.json", // This json file must be an embedded file in your solution or native json string
"Results.SonGoku.json"); // This json file must be an embedded file in your solution or native json string
}
}
Payload: "Payloads.SonGoku.json"
{
"Id": 1,
"Name": "Son",
"FirstName": "Goku",
"Age": 99,
"Emails": [
{
"EmailAddress": "alf@gmx.de",
"Type": "GMX"
},
{
"EmailAddress": "abc@hotmail.de",
"Type": "Microsoft"
}
]
}
Response: "Results.SonGoku.json"
{
"Version": "1.1",
"Content": {
"Headers": [
{
"Key": "Content-Type",
"Value": [ "application/json; charset=utf-8" ]
}
],
"Value": {
"Id": 1,
"Name": "Son",
"FirstName": "Goku",
"Age": 99,
"Emails": []
}
},
"StatusCode": "OK",
"ReasonPhrase": "OK",
"Headers": [],
"TrailingHeaders": [],
"IsSuccessStatusCode": true
}
Samples
Simple object comparisons
[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");
}
[TestMethod]
public void Simple_Object_Comparison()
{
var firstNumber = 1;
var secondNumber = 2;
Assert.That.ObjectsAreEqual(firstNumber,secondNumber, title: "Persons are not equal");
}
Assert.IsTrue failed.
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}
Embedded json file or native json string
[TestMethod]
public Task Should_Return_No_Users_If_No_One_Was_Added()
{
return Client.AssertGetAsync<GetAllUsersResponse>("v1/users", "EmptyUserResponse.json");
}
[TestMethod]
public Task Should_Return_No_Users_If_No_One_Was_Added()
{
return Client.AssertGetAsync<GetAllUsersResponse>("v1/users", """{ "Users": [] }""");
}
Assert that GET all Users will returned 401 Unauthorized
[TestMethod]
public Task Should_Not_Return_All_Users_Without_Authentication()
{
return Client.AssertGetAsUnauthorizedAsync("v1/users");
}
Assert that GET a user which not exists returns ProblemDetails
[TestMethod]
public Task Should_Return_Not_Found_Error_If_User_Does_Not_Exits()
{
return Client.AssertGetAsErrorAsync<ProblemDetails>($"v1/users/{1234}", "UserGetByIdErrorResponse.json");
}
Assert that GET all Users is successful and checks that response is empty
[TestMethod]
public Task Should_Return_No_Users_If_No_One_Was_Added()
{
return Client.AssertGetAsync<GetAllUsersResponse>("v1/users", "EmptyUserResponse.json");
}
Create a User and Ignore an Id which maybe is generated by the backend or database
[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;
}
}
Ignore functionality on error response
[TestMethod]
public Task Should_Handle_Error_Response_With_Filter_Func()
{
// 1. Call endpoint which will return an error response
return Client.AssertPostAsErrorAsync<ProblemDetails>("api/tests/v1/errors/not-implemented",
"ErrorResponse.json",
DifferenceFunc);
// 2. Intercept difference detection also for error response
static IEnumerable<Difference> DifferenceFunc(IImmutableList<Difference> differences)
{
foreach (var difference in differences)
{
yield return difference;
}
}
}
Fetch data from an API and do a post order to bring the items in the right order
[TestMethod]
public Task Should_Return_Expected_Result_For_Given_Payload_And_Sorted()
{
return Client.AssertGetAsync<IEnumerable<Person>>($"api/v1/users/",
"Users.V1.Results.AllUsers.json",
filterFunc: FilterFunc);
}
// The filter func can be used to sort or do some custom post filtering
// Sample: You get unsorted results from API so each call will provide
// the users in different order. Why a something like a DB query
// without sort action will not guarantee the order of the results.
// If results does not match the expected results (order as well),
// the test will fail
private IEnumerable<Person> FilterFunc(IEnumerable<Person> arg)
{
return arg.OrderBy(x => x.Id).ToImmutableList();
}
Whole create, get and delete scenario. Looks nice and clean
[TestMethod]
public Task Should_Return_The_User_Which_Was_Added()
{
// 1. Add an user
var addedUserResponse = await Client.AssertPostAsync<AddUserReponse>($"api/v1/users/",
"Users.V1.Payloads.NewUser.json",
"Users.V1.Results.NewUser.json");
// 2. Get the currently added user
await Client.AssertGetAsync<GetAllUserResponse>($"api/v1/users/{addedUserResponse.User.Id}"
"Users.V1.Results.AddedUser.json");
// 3. Delete the alrady added user -> Dependent on your test setup a test-tear down can also contain a cleanup step to remove all the created sources
await Client.AssertDeleteAsync($"api/v1/users/{addedUserResponse.User.Id}"
"Users.V1.Results.Deleteduser.json");
}
Replacements
[TestMethod]
public Task Should_Return_The_User_Which_Was_Added()
{
// 1. Add an user
var addedUserResponse = await Client.AssertPostAsync<AddUserReponse>($"api/v1/users/",
"Users.V1.Payloads.NewUser.json",
"Users.V1.Results.NewUser.json");
// 2. Get the currently added user
await Client.AssertGetAsync<GetUserByIdResponse>($"api/v1/users/{addedUserResponse.User.Id}"
"Users.V1.Results.AddedUser.json",
[("{Id}", addedUserResponse.User.Id)]); // New, will replace in the Users.V1.Results.AddedUser.json the {Id} with the value of addedUserResponse.User.Id
// 3. Delete the alrady added user -> Dependent on your test setup a test-tear down can also contain a cleanup step to remove all the created sources
await Client.AssertDeleteAsync($"api/v1/users/{addedUserResponse.User.Id}"
"Users.V1.Results.Deleteduser.json");
}
Header, Status codes and many more
For each test we are evaluating the whole response which is based on a "Snapshot" from your api response.
Assert.IsTrue failed.
Http call infos:
-----------------------------------------------------------------------------
| HttpMethod | Url | HttpStatusCode |
-----------------------------------------------------------------------------
| POST | https://localhost:5001/api/tests/v1/persons | OK |
-----------------------------------------------------------------------------
Detected differences: 3
-----------------------------------------------------------------------------------------------------
| MemberPath | "Results.NewPersonParameter.json" | CurrentResult |
-----------------------------------------------------------------------------------------------------
| Content.Headers[0].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/persons' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer Sorry i am secret :)'
--data-raw '{
"Id": 1,
"Name": "Son",
"FirstName": "Goku Failed",
"Age": 99,
"Emails": []
}'
--------------------------------------------------------------
Curl for each Asserted
call
How pratical can it be so share call scenarios with your consumers. For that reason you see in the test output the curl command for each asserted call.
This is like an aggreate function combined with possible context specific ignore functions. First this GlobalIgnore func will be executed pre filter the differences after this passed local ignore functions will be executed with the pre filtered differences.
```curl
-----------------------------------------------------------
Http call as curl
-----------------------------------------------------------
curl \
--location \
--request GET 'https://localhost:5001/api/tests/v1/persons'
--header 'Authorization: Bearer Sorry i am secret :)'
-----------------------------------------------------------
Global ignore func
We provide you a global ignore possibilty to ignore common values which contains random values
In this sample here, we ignore the x-amzn-trace-id
header if it is different for any assert in
your test assembly.
AssertObjectExtensions.DifferenceFunc = DifferenceFunc;
static IEnumerable<Difference> DifferenceFunc(IImmutableList<Difference> differences)
{
foreach (var difference in differences)
{
// 1. Response headers for x-amzn-trace-id are different any call
if (difference.MemberPath.Contains("x-amzn-trace-id"))
{
continue;
}
yield return difference;
}
}
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. 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. |
-
net8.0
- ConsoleTables (>= 2.6.2)
- Extensions.Pack (>= 5.0.5)
- Microsoft.AspNetCore.Mvc.Testing (>= 8.0.11)
- Microsoft.AspNetCore.TestHost (>= 8.0.11)
- MSTest.TestFramework (>= 3.6.3)
- System.CodeDom (>= 8.0.0)
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 |
---|---|---|
6.0.4 | 60 | 2/8/2025 |
6.0.3 | 241 | 2/2/2025 |
6.0.2 | 8,113 | 1/24/2025 |
6.0.1 | 46 | 1/24/2025 |
6.0.0 | 46 | 1/24/2025 |
5.1.4 | 112 | 1/19/2025 |
5.1.3 | 38 | 1/19/2025 |
5.1.2 | 49 | 1/18/2025 |
5.1.1 | 198 | 1/8/2025 |
5.1.0 | 81 | 1/5/2025 |
5.1.0-alpha.24 | 63 | 1/5/2025 |
5.1.0-alpha.21 | 43 | 1/5/2025 |
5.1.0-alpha.19 | 55 | 1/5/2025 |
5.1.0-alpha.17 | 61 | 1/4/2025 |
5.1.0-alpha.13 | 61 | 1/4/2025 |
5.1.0-alpha.8 | 77 | 1/3/2025 |
5.0.1 | 1,403 | 1/1/2025 |
5.0.0 | 17,234 | 11/20/2024 |
4.0.13 | 24,539 | 11/18/2024 |
4.0.12 | 2,686 | 11/13/2024 |
4.0.11 | 1,923 | 11/6/2024 |
4.0.10 | 15,046 | 9/6/2024 |
4.0.9 | 1,472 | 9/5/2024 |
4.0.8 | 1,727 | 9/3/2024 |
4.0.7 | 5,771 | 8/28/2024 |
4.0.6 | 1,795 | 8/27/2024 |
4.0.5 | 1,396 | 8/27/2024 |
4.0.4 | 1,391 | 8/27/2024 |
4.0.3 | 1,437 | 8/26/2024 |
4.0.2 | 1,380 | 8/26/2024 |
4.0.1 | 2,716 | 8/26/2024 |
4.0.0 | 1,405 | 8/26/2024 |
3.1.1 | 1,734 | 8/21/2024 |
3.1.0 | 1,466 | 8/20/2024 |
3.0.4 | 1,756 | 8/5/2024 |
3.0.3 | 5,022 | 6/20/2024 |
3.0.2 | 1,923 | 5/26/2024 |
3.0.1 | 2,691 | 4/29/2024 |
3.0.0 | 1,546 | 4/23/2024 |
2.0.2 | 1,838 | 4/7/2024 |
2.0.1 | 3,368 | 2/7/2024 |
2.0.0 | 5,045 | 11/28/2023 |
1.1.8 | 2,793 | 10/29/2023 |
1.1.7 | 1,424 | 10/29/2023 |
1.1.6 | 1,560 | 10/24/2023 |
1.1.5 | 6,241 | 7/20/2023 |
1.1.4 | 1,594 | 7/20/2023 |
1.1.3 | 3,094 | 5/31/2023 |
1.1.2 | 1,443 | 5/29/2023 |
1.1.1 | 2,105 | 5/20/2023 |
1.1.0 | 1,441 | 5/20/2023 |
1.0.9 | 2,919 | 4/30/2023 |
1.0.8 | 1,562 | 4/19/2023 |
1.0.7 | 1,449 | 4/18/2023 |
1.0.6 | 1,446 | 4/18/2023 |
1.0.5 | 2,534 | 3/23/2023 |
1.0.4 | 1,471 | 3/23/2023 |
1.0.3 | 5,088 | 1/1/2023 |
1.0.2 | 1,477 | 1/1/2023 |
1.0.1 | 1,458 | 1/1/2023 |
1.0.0 | 1,491 | 11/24/2022 |
0.7.0 | 3,853 | 11/21/2022 |
0.7.0-alpha.24 | 130 | 11/21/2022 |
0.7.0-alpha.23 | 2,191 | 10/23/2022 |
0.7.0-alpha.22 | 104 | 10/23/2022 |
0.7.0-alpha.20 | 176 | 10/20/2022 |
0.7.0-alpha.18 | 482 | 10/7/2022 |
0.7.0-alpha.16 | 123 | 10/3/2022 |
0.7.0-alpha.14 | 116 | 10/3/2022 |
0.7.0-alpha.13 | 122 | 10/3/2022 |
0.7.0-alpha.12 | 121 | 10/3/2022 |
0.7.0-alpha.11 | 107 | 10/3/2022 |
0.7.0-alpha.8 | 111 | 10/3/2022 |
0.7.0-alpha.7 | 117 | 10/3/2022 |
0.6.1 | 1,452 | 10/1/2022 |
0.6.0 | 1,426 | 10/1/2022 |
0.5.1 | 2,511 | 9/4/2022 |
0.5.0 | 1,433 | 9/4/2022 |
0.4.0 | 1,444 | 9/3/2022 |
0.3.1 | 1,475 | 8/24/2022 |
0.3.0 | 1,576 | 8/4/2022 |
0.2.0 | 1,564 | 7/25/2022 |
0.2.0-alpha.89 | 235 | 7/8/2022 |
0.2.0-alpha.87 | 127 | 7/8/2022 |
0.2.0-alpha.85 | 228 | 7/4/2022 |
0.2.0-alpha.83 | 183 | 6/30/2022 |
0.2.0-alpha.82 | 161 | 6/21/2022 |
0.2.0-alpha.81 | 273 | 6/19/2022 |
0.2.0-alpha.79 | 127 | 6/19/2022 |
0.2.0-alpha.77 | 241 | 5/25/2022 |
0.2.0-alpha.71 | 450 | 4/28/2022 |
0.2.0-alpha.70 | 310 | 3/11/2022 |
0.2.0-alpha.69 | 135 | 3/11/2022 |
0.2.0-alpha.68 | 983 | 7/2/2021 |
0.2.0-alpha.67 | 2,142 | 5/17/2021 |
0.2.0-alpha.66 | 706 | 4/26/2021 |
0.2.0-alpha.63 | 188 | 4/25/2021 |
0.2.0-alpha.54 | 322 | 4/22/2021 |
0.2.0-alpha.53 | 165 | 4/22/2021 |
0.2.0-alpha.51 | 192 | 4/20/2021 |
0.2.0-alpha.49 | 256 | 4/18/2021 |
0.2.0-alpha.48 | 208 | 4/18/2021 |
0.2.0-alpha.47 | 170 | 4/18/2021 |
0.2.0-alpha.46 | 158 | 4/17/2021 |
0.2.0-alpha.45 | 166 | 4/17/2021 |
0.2.0-alpha.44 | 165 | 4/17/2021 |
0.2.0-alpha.43 | 174 | 4/17/2021 |
0.1.0-alpha.39 | 171 | 4/17/2021 |
0.1.0-alpha.37 | 187 | 4/17/2021 |
0.1.0-alpha.35 | 185 | 4/17/2021 |
0.1.0-alpha.34 | 187 | 4/17/2021 |
0.1.0-alpha.31 | 180 | 4/16/2021 |
0.1.0-alpha.30 | 194 | 4/14/2021 |
0.1.0-alpha.29 | 163 | 4/14/2021 |
0.1.0-alpha.28 | 247 | 4/10/2021 |
0.1.0-alpha.20 | 173 | 4/9/2021 |
0.1.0-alpha.18 | 180 | 4/9/2021 |
0.1.0-alpha.17 | 199 | 4/9/2021 |
0.1.0-alpha.16 | 203 | 4/9/2021 |
0.1.0-alpha.14 | 185 | 4/9/2021 |
0.1.0-alpha.13 | 171 | 4/8/2021 |
0.1.0-alpha.12 | 360 | 4/5/2021 |
Fix bug with dev usage {} or [] to get the infos of the current result.