AspNetCore.Simple.MsTest.Sdk 5.1.0-alpha.8

This is a prerelease version of AspNetCore.Simple.MsTest.Sdk.
There is a newer version of this package available.
See the version list below for details.
dotnet add package AspNetCore.Simple.MsTest.Sdk --version 5.1.0-alpha.8                
NuGet\Install-Package AspNetCore.Simple.MsTest.Sdk -Version 5.1.0-alpha.8                
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="5.1.0-alpha.8" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add AspNetCore.Simple.MsTest.Sdk --version 5.1.0-alpha.8                
#r "nuget: AspNetCore.Simple.MsTest.Sdk, 5.1.0-alpha.8"                
#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.
// Install AspNetCore.Simple.MsTest.Sdk as a Cake Addin
#addin nuget:?package=AspNetCore.Simple.MsTest.Sdk&version=5.1.0-alpha.8&prerelease

// Install AspNetCore.Simple.MsTest.Sdk as a Cake Tool
#tool nuget:?package=AspNetCore.Simple.MsTest.Sdk&version=5.1.0-alpha.8&prerelease                

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 and AssertPostAsErrorAsync 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' \
    --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.

-----------------------------------------------------------
Http call as curl
-----------------------------------------------------------
curl \
--location \
--request GET 'https://localhost:5001/api/tests/v1/persons'
-----------------------------------------------------------
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. 
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
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.