본문으로 건너뛰기
cccskills
무료GitHub에 공개

stateful-mocks

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

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

실행 점수 미검증

설치 방법 보기

포함된 파일(1)

  • SKILL.md7.5 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

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 / 1,000자

이 양식은 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의 스킬 모두 보기

이 스킬의 문제 신고