mstest-integration-test

star 0

適用於 MSTest 整合測試最佳實踐模式,裡面包含 MSTest 整合測試、NSubstitute Mock、Entity Framework Core InMemory 等

a35506322 By a35506322 schedule Updated 2/9/2026

name: mstest-integration-test description: 適用於 MSTest 整合測試最佳實踐模式,裡面包含 MSTest 整合測試、NSubstitute Mock、Entity Framework Core InMemory 等

MSTest 整合測試規範

適用於 MSTest 整合測試最佳實踐模式,裡面包含MSTest 整合測試NSubstitute MockEntity Framework Core InMemory

使用場景

  • 新增/修改 API 端點
  • 測試完整流程

測試工具

  • MSTest
  • NSubstitute (Mock 工具)
  • Entity Framework Core InMemory (In-Memory 資料庫)
  • UnitTesting.Assert (斷言工具)

MSTest 整合測試

主要測試引用專案的 API 端點

資料夾結構

Tests # 整合測試主目錄
├── Auth # 認證模組整合測試
│   └── LoginEndpointTests.cs # 登入端點整合測試
└── Todo # Todo 模組整合測試
    ├── DeleteTodoByIdEndpointTests.cs # 依 ID 刪除 Todo 端點整合測試
    ├── GetTodoByIdEndpointTests.cs # 依 ID 取得 Todo 端點整合測試
    ├── GetTodoByQueryStringEndpointTests.cs # 依查詢字串取得 Todo 端點整合測試
    ├── InsertTodoEndpointTests.cs # 新增 Todo 端點整合測試
    ├── TodoCrudFlowTests.cs # Todo CRUD 流程整合測試
    └── UpdateTodoByIdEndpointTests.cs # 依 ID 更新 Todo 端點整合測試

命名規範

類型 命名規範
測試單個端點檔名 [XXX]EndpointTests.cs
測試某個流程方法檔名 [XXX]FlowTests.cs

初始化設定

引用 CustomWebApplicationFactory

using TodoAPI.IntegrationTest.Helpers;

private static CustomWebApplicationFactory<Program> \_factory = null!;

[ClassInitialize]
public static void ClassInitialize(TestContext _)
{
    _factory = new CustomWebApplicationFactory<Program>();
}

Flow Test 範例

/// <summary>
/// Todo CRUD 正向流程整合測試
/// </summary>
[TestClass]
public class TodoCrudFlowTests
{
private static CustomWebApplicationFactory<Program> \_factory = null!;
private HttpClient \_client = null!;
private string \_token = null!;

    [ClassInitialize]
    public static void ClassInitialize(TestContext _)
    {
        _factory = new CustomWebApplicationFactory<Program>();
    }

    [ClassCleanup]
    public static void ClassCleanup()
    {
        _factory?.Dispose();
    }

    [TestInitialize]
    public async Task Setup()
    {
        _client = _factory.CreateClient();
        _token = await TestAuthHelper.GetTokenAsync(_client);
        _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
            "Bearer",
            _token
        );
    }

    [TestCleanup]
    public void Cleanup()
    {
        _client?.Dispose();
    }

    /// <summary>
    /// 測試完整 CRUD 流程:登入 → 新增 → 查詢 → 更新 → 刪除
    /// </summary>
    [TestMethod]
    public async Task TodoCrudFlow_CompleteFlow_ReturnsSuccess()
    {
        // Arrange
        var insertRequest = new InsertTodoRequest
        {
            TodoTitle = "整合測試待辦事項",
            TodoContent = "這是整合測試的待辦事項內容",
        };

        // Act 1: 新增待辦事項
        var todoId = await InsertTodoAsync(insertRequest);

        // Act 2: 查詢待辦事項
        await GetTodoByIdAsync(todoId, "整合測試待辦事項", "這是整合測試的待辦事項內容", "N");

        // Act 3: 更新待辦事項
        await UpdateTodoByIdAsync(todoId);

        // Act 4: 刪除待辦事項
        await DeleteTodoByIdAsync(todoId);

        // Act 5: 驗證刪除後查詢應返回 422
        await VerifyTodoDeletedAsync(todoId);
    }

    /// <summary>
    /// 新增待辦事項
    /// </summary>
    private async Task<long> InsertTodoAsync(InsertTodoRequest request)
    {
        var insertResponse = await _client.PostAsJsonAsync("/api/todo/todo", request);
        insertResponse.EnsureSuccessStatusCode();

        var insertApiResponse = await insertResponse.Content.ReadFromJsonAsync<
            APIResponse<InsertTodoResponse>
        >();

        Assert.IsNotNull(insertApiResponse);
        Assert.AreEqual(Code.成功, insertApiResponse.Code);
        Assert.IsNotNull(insertApiResponse.Data);
        Assert.IsTrue(insertApiResponse.Data.TodoId > 0);
        Assert.AreEqual(request.TodoTitle, insertApiResponse.Data.TodoTitle);
        Assert.AreEqual(request.TodoContent, insertApiResponse.Data.TodoContent);
        Assert.AreEqual("N", insertApiResponse.Data.IsComplete);

        return insertApiResponse.Data.TodoId;
    }

    /// <summary>
    /// 查詢待辦事項
    /// </summary>
    private async Task GetTodoByIdAsync(
        long todoId,
        string expectedTitle,
        string expectedContent,
        string expectedIsComplete
    )
    {
        var getResponse = await _client.GetAsync($"/api/todo/todo/{todoId}");
        getResponse.EnsureSuccessStatusCode();

        var getApiResponse = await getResponse.Content.ReadFromJsonAsync<
            APIResponse<GetTodoByIdResponse>
        >();

        Assert.IsNotNull(getApiResponse);
        Assert.AreEqual(Code.成功, getApiResponse.Code);
        Assert.IsNotNull(getApiResponse.Data);
        Assert.AreEqual(todoId, getApiResponse.Data.TodoId);
        Assert.AreEqual(expectedTitle, getApiResponse.Data.TodoTitle);
        Assert.AreEqual(expectedContent, getApiResponse.Data.TodoContent);
        Assert.AreEqual(expectedIsComplete, getApiResponse.Data.IsComplete);
    }

    /// <summary>
    /// 更新待辦事項
    /// </summary>
    private async Task UpdateTodoByIdAsync(long todoId)
    {
        var updateRequest = new UpdateTodoByIdRequest
        {
            TodoTitle = "更新後的待辦事項標題",
            TodoContent = "更新後的待辦事項內容",
            IsComplete = "Y",
        };

        var updateResponse = await _client.PutAsJsonAsync(
            $"/api/todo/todo/{todoId}",
            updateRequest
        );
        updateResponse.EnsureSuccessStatusCode();

        var updateApiResponse = await updateResponse.Content.ReadFromJsonAsync<
            APIResponse<UpdateTodoByIdResponse>
        >();

        Assert.IsNotNull(updateApiResponse);
        Assert.AreEqual(Code.成功, updateApiResponse.Code);
        Assert.IsNotNull(updateApiResponse.Data);
        Assert.AreEqual(todoId, updateApiResponse.Data.TodoId);
        Assert.AreEqual("更新後的待辦事項標題", updateApiResponse.Data.TodoTitle);
        Assert.AreEqual("更新後的待辦事項內容", updateApiResponse.Data.TodoContent);
        Assert.AreEqual("Y", updateApiResponse.Data.IsComplete);
        Assert.IsNotNull(updateApiResponse.Data.CompleteTime);
    }

    /// <summary>
    /// 刪除待辦事項
    /// </summary>
    private async Task DeleteTodoByIdAsync(long todoId)
    {
        var deleteResponse = await _client.DeleteAsync($"/api/todo/todo/{todoId}");
        deleteResponse.EnsureSuccessStatusCode();

        var deleteApiResponse = await deleteResponse.Content.ReadFromJsonAsync<
            APIResponse<DeleteTodoByIdResponse>
        >();

        Assert.IsNotNull(deleteApiResponse);
        Assert.AreEqual(Code.成功, deleteApiResponse.Code);
        Assert.IsNotNull(deleteApiResponse.Data);
        Assert.AreEqual(todoId, deleteApiResponse.Data.TodoId);
    }

    /// <summary>
    /// 驗證刪除後查詢應返回 422
    /// </summary>
    private async Task VerifyTodoDeletedAsync(long todoId)
    {
        var getAfterDeleteResponse = await _client.GetAsync($"/api/todo/todo/{todoId}");
        Assert.AreEqual(HttpStatusCode.UnprocessableEntity, getAfterDeleteResponse.StatusCode);

        var getAfterDeleteApiResponse = await getAfterDeleteResponse.Content.ReadFromJsonAsync<
            APIResponse<GetTodoByIdResponse>
        >();

        Assert.IsNotNull(getAfterDeleteApiResponse);
        Assert.AreEqual(Code.商業邏輯錯誤, getAfterDeleteApiResponse.Code);
    }

}

Endpoint Test 範例

/// <summary>
/// 依 ID 取得 Todo 端點整合測試
/// </summary>
[TestClass]
public class GetTodoByIdEndpointTests
{
    private static CustomWebApplicationFactory<Program> _factory = null!;
    private HttpClient _client = null!;
    private string _token = null!;

    [ClassInitialize]
    public static void ClassInitialize(TestContext _)
    {
        _factory = new CustomWebApplicationFactory<Program>();
    }

    [ClassCleanup]
    public static void ClassCleanup()
    {
        _factory?.Dispose();
    }

    [TestInitialize]
    public async Task Setup()
    {
        _client = _factory.CreateClient();
        _token = await TestAuthHelper.GetTokenAsync(_client);
        _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
            "Bearer",
            _token
        );
    }

    [TestCleanup]
    public void Cleanup()
    {
        _client?.Dispose();
    }

    /// <summary>
    /// 測試查詢有效的 Todo ID,應返回成功
    /// </summary>
    [TestMethod]
    public async Task GetTodoById_ValidId_ReturnsSuccess()
    {
        // Arrange: 先新增一個 Todo
        var insertRequest = new InsertTodoRequest
        {
            TodoTitle = "測試待辦",
            TodoContent = "測試內容",
        };

        var insertResponse = await _client.PostAsJsonAsync("/api/todo/todo", insertRequest);
        insertResponse.EnsureSuccessStatusCode();

        var insertApiResponse = await insertResponse.Content.ReadFromJsonAsync<
            APIResponse<InsertTodoResponse>
        >();
        var todoId = insertApiResponse!.Data!.TodoId;

        // Act: 查詢 Todo
        var getResponse = await _client.GetAsync($"/api/todo/todo/{todoId}");

        // Assert
        getResponse.EnsureSuccessStatusCode();

        var getApiResponse = await getResponse.Content.ReadFromJsonAsync<
            APIResponse<GetTodoByIdResponse>
        >();

        Assert.IsNotNull(getApiResponse);
        Assert.AreEqual(Code.成功, getApiResponse.Code);
        Assert.AreEqual("查詢成功", getApiResponse.Message);
        Assert.IsNotNull(getApiResponse.Data);
        Assert.AreEqual(todoId, getApiResponse.Data.TodoId);
        Assert.AreEqual(insertRequest.TodoTitle, getApiResponse.Data.TodoTitle);
        Assert.AreEqual(insertRequest.TodoContent, getApiResponse.Data.TodoContent);
        Assert.AreEqual("N", getApiResponse.Data.IsComplete);
    }

    /// <summary>
    /// 測試查詢無效的 Todo ID,應返回商業邏輯錯誤
    /// </summary>
    [TestMethod]
    public async Task GetTodoById_InvalidId_ReturnsBusinessLogicError()
    {
        // Arrange
        var invalidId = 999L;

        // Act
        var getResponse = await _client.GetAsync($"/api/todo/todo/{invalidId}");

        // Assert
        Assert.AreEqual(HttpStatusCode.UnprocessableEntity, getResponse.StatusCode);

        var getApiResponse = await getResponse.Content.ReadFromJsonAsync<
            APIResponse<GetTodoByIdResponse>
        >();

        Assert.IsNotNull(getApiResponse);
        Assert.AreEqual(Code.商業邏輯錯誤, getApiResponse.Code);
        Assert.AreEqual("找不到指定的待辦事項", getApiResponse.Message);
    }
}
Install via CLI
npx skills add https://github.com/a35506322/Lab.Todo --skill mstest-integration-test
Repository Details
star Stars 0
call_split Forks 1
navigation Branch main
article Path SKILL.md
More from Creator