跳到正文
cccskills
免费在 GitHub 公开

stateful-mocks

Testing patterns for simulating sequential calls and race conditions using Moq stateful callbacks

中文简介正在准备中。当前显示原文说明。

实测评分 未实测

查看安装方法

包含的文件(1)

  • SKILL.md7.5 KB

SKILL.md(原文)

安装前可以查看交给 AI 代理的指令内容。

Context

When testing scenarios where the same method is called multiple times with different outcomes (e.g., double-submit, race conditions, retry logic), you need to simulate state changes in your mocks. This is particularly useful for:

  • Double-submit bugs: First call succeeds, second call should fail
  • Race conditions: Multiple concurrent calls with different results
  • Retry logic: Failed calls followed by successful retry
  • State transitions: Operations that change system state between calls

Project: JJGNET Broadcasting
Framework: xUnit + Moq 4.20.72
Use Case: Issue #708 - Testing Web controller behavior when double-submit occurs

Patterns

Pattern 1: Counter-Based State Simulation

Use a local counter variable to track call count and return different results:

[Fact]
public async Task Method_DuplicateCall_HandlesSecondCallFailure()
{
    // Arrange
    var callCount = 0;
    _mockService
        .Setup(s => s.AddSomething(It.IsAny<int>(), It.IsAny<int>()))
        .ReturnsAsync(() =>
        {
            callCount++;
            if (callCount == 1)
            {
                return new SuccessResult();  // First call succeeds
            }
            throw new ConflictException("Duplicate");  // Second call fails
        });

    // Act
    var firstResult = await _controller.Action();   // Call 1: success
    var secondResult = await _controller.Action();  // Call 2: error

    // Assert
    Assert.IsType<RedirectResult>(firstResult);
    Assert.Contains("success", _controller.TempData["Message"]);
    Assert.IsType<RedirectResult>(secondResult);
    Assert.Contains("Duplicate", _controller.TempData["ErrorMessage"]);
}

Pattern 2: Queue-Based Sequence

For more complex sequences, use a queue of responses:

var responses = new Queue<Result>(new[] {
    new Result { Success = true },
    new Result { Success = false, Error = "Conflict" },
    new Result { Success = true }  // Retry succeeds
});

_mockService
    .Setup(s => s.Process())
    .ReturnsAsync(() => responses.Dequeue());

Pattern 3: Callback Capture + State Check

Capture the state passed to the mock to verify it changes between calls:

var capturedStates = new List<State>();
_mockService
    .Setup(s => s.Update(It.IsAny<State>()))
    .Callback<State>(state => capturedStates.Add(state))
    .ReturnsAsync(true);

// ... multiple calls ...

Assert.Equal(2, capturedStates.Count);
Assert.Equal("Initial", capturedStates[0].Status);
Assert.Equal("Updated", capturedStates[1].Status);

Examples

Real Example: Issue #708 AddPlatform Double-Submit Test

File: src/JosephGuadagno.Broadcasting.Web.Tests/Controllers/EngagementsControllerTests.cs

[Fact]
public async Task AddPlatform_Post_DuplicateAttempt_ShouldHandleHttpRequestException()
{
    // Arrange - Simulates the double-submit scenario from issue #708
    // First call succeeds, second call would fail with 409 Conflict
    var viewModel = new EngagementSocialMediaPlatformViewModel
    {
        EngagementId = 42,
        SocialMediaPlatformId = 1,
        Handle = "@TestHandle"
    };

    var firstCallSucceeds = new EngagementSocialMediaPlatform
    {
        EngagementId = 42,
        SocialMediaPlatformId = 1,
        Handle = "@TestHandle"
    };

    var callCount = 0;
    _engagementService
        .Setup(s => s.AddPlatformToEngagementAsync(42, 1, "@TestHandle"))
        .ReturnsAsync(() =>
        {
            callCount++;
            if (callCount == 1)
            {
                return firstCallSucceeds;
            }
            throw new HttpRequestException("API returned 409 Conflict");
        });

    // Act - First call (succeeds)
    var firstResult = await _controller.AddPlatform(viewModel);

    // Assert first call
    var firstRedirect = Assert.IsType<RedirectToActionResult>(firstResult);
    Assert.Equal("Edit", firstRedirect.ActionName);
    Assert.Equal("Platform added successfully.", _controller.TempData["SuccessMessage"]);

    // Act - Second call (simulates double-submit, should be caught)
    var secondResult = await _controller.AddPlatform(viewModel);

    // Assert second call - Error should be caught and user-friendly message shown
    var secondRedirect = Assert.IsType<RedirectToActionResult>(secondResult);
    Assert.Equal("Edit", secondRedirect.ActionName);
    Assert.Contains("409 Conflict", (string)_controller.TempData["ErrorMessage"]!);
}

Key Points:

  • Counter variable (callCount) tracks which call is executing
  • First call returns success result
  • Second call throws exception (simulating backend 409 Conflict)
  • Test verifies controller handles both outcomes correctly
  • Provides regression coverage for double-submit bug without requiring JavaScript tests

Anti-Patterns

❌ Using .SetupSequence() for Exception Cases

Don't:

_mock.SetupSequence(s => s.Method())
    .ReturnsAsync(new Result())
    .Throws(new Exception());  // Throws synchronously, not async!

Do:

_mock.Setup(s => s.Method())
    .ReturnsAsync(() => callCount++ == 0 ? new Result() : throw new Exception());

Reason: .Throws() in SetupSequence doesn't work with async methods. Use callback-based logic instead.

❌ Mutating Shared State Without Isolation

Don't:

private int _sharedCallCount = 0;  // Shared across tests!

[Fact]
public void Test1() { /* uses _sharedCallCount */ }

[Fact]
public void Test2() { /* uses _sharedCallCount */ }

Do:

[Fact]
public void Test1() 
{
    var callCount = 0;  // Local to this test
    // ...
}

Reason: xUnit doesn't guarantee test execution order. Shared state causes flaky tests.

❌ Complex State Logic in Mock Setup

Don't:

_mock.Setup(s => s.Method())
    .ReturnsAsync(() => {
        // 50 lines of complex business logic
        // ...
    });

Do:

  • Keep mock logic simple (counters, flags, queues)
  • If you need complex logic, extract it to a helper method or reconsider the test design
  • Complex mock logic often indicates the test is too broad

When to Use

Use stateful mocks when:

  • ✅ Testing sequential calls to the same method with different outcomes
  • ✅ Simulating state changes (e.g., resource created, then updated)
  • ✅ Testing error recovery or retry logic
  • ✅ Regression testing for race conditions or timing bugs
  • ✅ Verifying controller behavior across multiple requests (like double-submit)

Don't use stateful mocks when:

  • ❌ A single .Setup() with fixed return value suffices
  • ❌ You're testing business logic that should be in the service layer, not the controller
  • ❌ The state logic is so complex it becomes harder to understand than the code under test

Related Patterns

  • Callback Capture Pattern (Tank history #137) - Capture objects passed to mocks for assertion
  • Defense-in-Depth Testing (Tank history #263) - Layer validation at multiple levels (client, web, API, data)
  • Web.Tests TempData Pattern (Tank history #276) - Initialize TempData in Web controller tests

References

  • Issue: #708 - Double-submit bug on AddPlatform form
  • Files:
    • src/JosephGuadagno.Broadcasting.Web.Tests/Controllers/EngagementsControllerTests.cs (lines 505-560)
  • Decision: .squad/decisions/inbox/tank-708-web-tests.md
  • History: .squad/agents/tank/history.md (2026-04-13 session)

评价

还没有评价。欢迎分享你的使用感受。

评论

无需登录即可发表。欢迎分享使用感受、提问,或说明在哪些环境下无法运行。

还没有评论。来写第一条吧。

提交后会立即公开。请勿填写邮箱、电话等个人信息。URL 最多 2 个。

0 / 1000 字

此表单受 reCAPTCHA 保护,适用 Google 的隐私权政策和服务条款。

同一仓库的技能

Standard collaboration patterns for all squad agents — worktree awareness, decisions, cross-agent communication

中文简介正在准备中。当前显示原文说明。

jguadagno/jjgnet-broadcast182026年8月4日 更新

Shared hard rules enforced across all squad agents

中文简介正在准备中。当前显示原文说明。

jguadagno/jjgnet-broadcast182026年8月4日 更新

Comprehensive AI prompt engineering safety review and improvement prompt. Analyzes prompts for safety, bias, security vulnerabilities, and effectiveness while providing detailed improvement recommendations with extensive frameworks, testing methodologies, and educational content.

中文简介正在准备中。当前显示原文说明。

jguadagno/jjgnet-broadcast182026年8月4日 更新

Migrate API controller scope checks to framework-enforced authorization policies while preserving ownership checks.

中文简介正在准备中。当前显示原文说明。

jguadagno/jjgnet-broadcast182026年8月4日 更新

Review API DTO implementations for contract/domain separation, mapping patterns, and REST compliance

中文简介正在准备中。当前显示原文说明。

jguadagno/jjgnet-broadcast182026年8月4日 更新

Register shared claims transformation and hierarchical role policies in the API without breaking existing scope enforcement.

中文简介正在准备中。当前显示原文说明。

jguadagno/jjgnet-broadcast182026年8月4日 更新

查看 jguadagno 的全部技能

报告此技能的问题