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

mock-overload-resolution

How to update Moq mock setups when a manager method gains new parameters — prevents silent overload mismatch bugs that cause MockException or wrong test results

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

설치 방법 보기

포함된 파일(1)

  • SKILL.md8.7 KB

SKILL.md(원문)

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

Context

When a manager interface method gains new parameters (e.g., ownerOid added as part of Epic #609's per-user ownership enforcement), every existing Moq .Setup() call that targets the old signature becomes a dead setup. Moq will either:

  1. Silently match nothing — the method is called but no setup matches, so Moq returns the default value (null for reference types, 0 for value types, false for bool). The test passes for the wrong reason, or fails with a NullReferenceException that is hard to diagnose.
  2. Throw MockException — when .Verifiable() is in play and the expected call never fired, the verify step throws.

This is a silent failure mode: the test compiles, may even pass, but is not testing what you think it's testing.

Project: JJGNET Broadcasting
Framework: xUnit + Moq 4.20.72 + FluentAssertions
Trigger: Any manager method signature change (new parameter added, parameter type changed, parameter removed)


The Problem

Scenario: Epic #609 — GetAllAsync gains ownerOid parameter

Before Epic #609, the manager interface was:

// IScheduledItemManager — BEFORE
Task<PagedResult<ScheduledItem>> GetAllAsync(int page, int pageSize);

After Epic #609, a new overload was added for owner-filtered access:

// IScheduledItemManager — AFTER (two overloads)
Task<PagedResult<ScheduledItem>> GetAllAsync(int page, int pageSize);                        // admin/unfiltered
Task<PagedResult<ScheduledItem>> GetAllAsync(string ownerOid, int page, int pageSize, ...);  // owner-filtered

The controller now calls the owner-filtered overload for non-admin users:

// SchedulesController (after Epic #609)
var result = await _scheduledItemManager.GetAllAsync(currentUserOid, page, pageSize);

Before (Broken)

The old mock setup targets the unfiltered overload (2 parameters). The controller now calls the owner-filtered overload (3+ parameters). Moq finds no matching setup.

// ❌ BROKEN — targets old 2-parameter overload; controller now calls 3-parameter overload
_scheduledItemManagerMock
    .Setup(m => m.GetAllAsync(It.IsAny<int>(), It.IsAny<int>()))
    .ReturnsAsync(new PagedResult<ScheduledItem> { Items = items, TotalCount = 1 });

var sut = CreateSut(Domain.Scopes.Schedules.All, ownerOid: "test-oid-12345");

var result = await sut.GetScheduledItemsAsync();

// result.Value is null — the controller called the 3-parameter overload, no setup matched.
// The test may fail with NullReferenceException or, worse, produce a false positive.
result.Value.Should().NotBeNull(); // ← throws NullReferenceException

Symptom: NullReferenceException, MockException, or (worst case) a test that "passes" because the null result flows through a code path that returns an empty 200 OK.


After (Fixed)

Match the overload that the controller actually calls. Use It.IsAny<T>() for the new parameter unless the test specifically needs to assert its value.

// ✅ FIXED — targets the 3-parameter owner-filtered overload that the controller now calls
_scheduledItemManagerMock
    .Setup(m => m.GetAllAsync(
        It.IsAny<string>(),   // ownerOid — new parameter; use IsAny<> unless test verifies it
        It.IsAny<int>(),      // page
        It.IsAny<int>()))     // pageSize
    .ReturnsAsync(new PagedResult<ScheduledItem> { Items = items, TotalCount = 1 });

var sut = CreateSut(Domain.Scopes.Schedules.All, ownerOid: "test-oid-12345");

var result = await sut.GetScheduledItemsAsync();

// Now resolves correctly — setup matches the actual call.
result.Value.Should().NotBeNull();
result.Value!.Items.Should().HaveCount(1);

When to Verify the Specific New Parameter

Use It.IsAny<string>() for the new parameter in most tests. Use an explicit value only when the test is specifically verifying that the controller passes the correct OID:

// ✅ Verifying that the controller passes the caller's OID to the manager
_scheduledItemManagerMock
    .Setup(m => m.GetAllAsync(
        "test-oid-12345",     // exact OID — this test verifies the controller uses the caller's OID
        It.IsAny<int>(),
        It.IsAny<int>()))
    .ReturnsAsync(new PagedResult<ScheduledItem> { Items = items, TotalCount = 1 });

// Then verify after Act:
_scheduledItemManagerMock.Verify(
    m => m.GetAllAsync("test-oid-12345", It.IsAny<int>(), It.IsAny<int>()),
    Times.Once);

Default rule: Use It.IsAny<T>() for new parameters unless there is an explicit test objective around that parameter's value.


Step-by-Step Fix Process

When a manager method signature changes:

  1. Find all affected .Setup() calls:

    Select-String -Path ".\src\**\*Tests.cs" -Pattern "\.Setup\(m => m\.GetAllAsync" -Recurse
    
  2. For each match, check the parameter count against the new interface.

    • If the count is wrong, it's a dead setup — update it.
  3. Add It.IsAny<T>() for each new parameter (unless the test needs to assert the specific value).

  4. Run the specific test class immediately to verify the fix before moving on:

    dotnet test .\src\ --filter "FullyQualifiedName~SchedulesControllerTests" --no-build --verbosity normal
    
  5. Run the full test suite before pushing:

    dotnet test .\src\ --no-build --verbosity normal --filter "FullyQualifiedName!~SyndicationFeedReader"
    

Admin Bypass Overload (Special Case)

When a controller has an IsSiteAdministrator() branch that calls the unfiltered overload, both setups must exist simultaneously in the same test class:

// Setup for non-admin tests (owner-filtered overload):
_scheduledItemManagerMock
    .Setup(m => m.GetAllAsync(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()))
    .ReturnsAsync(pagedResult);

// Setup for admin tests (unfiltered overload):
_scheduledItemManagerMock
    .Setup(m => m.GetAllAsync(It.IsAny<int>(), It.IsAny<int>()))
    .ReturnsAsync(pagedResult);

Then in admin-bypass tests, verify that the unfiltered overload fires and the owner-filtered one does not:

_scheduledItemManagerMock.Verify(
    m => m.GetAllAsync(It.IsAny<int>(), It.IsAny<int>()),
    Times.Once);
_scheduledItemManagerMock.Verify(
    m => m.GetAllAsync(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()),
    Times.Never);

This is the same pattern used in the security-test-checklist SKILL for admin bypass verification — the two skills are complementary.


Anti-Patterns

❌ Leaving old 2-parameter setup after controller switches to 3-parameter call

// WRONG — setup for old overload; controller calls new overload → null result
_managerMock.Setup(m => m.GetAllAsync(It.IsAny<int>(), It.IsAny<int>()))
            .ReturnsAsync(result);

❌ Using a fixed OID in setup for a general happy-path test

// WRONG — if the test OID ever changes, this setup silently stops matching
_managerMock.Setup(m => m.GetAllAsync("hardcoded-oid", It.IsAny<int>(), It.IsAny<int>()))
            .ReturnsAsync(result);

❌ Not running the test class after updating a setup

// WRONG workflow: update setup → push → find out CI failed
// RIGHT workflow: update setup → dotnet test --filter "FullyQualifiedName~XTests" → green → push

❌ Assuming the compiler catches overload mismatches

Moq setups are resolved at runtime, not compile time. A setup for the wrong overload compiles without warning. Always run the test after a setup change.


Related

  • security-test-checklist SKILL.md — the admin bypass pattern in Step 5 depends on having correct setups for both overloads
  • Anti-pattern in security-test-checklist: "Mock overload mismatch after controller signature change" (brief mention — this SKILL is the full treatment)
  • Epic #609 — the trigger: per-user content required GetAllAsync(ownerOid, page, size) alongside the existing GetAllAsync(page, size) on all four manager interfaces

References

  • Epic: #609 — Multi-tenancy: per-user content, publishers, and social tokens
  • Interfaces affected: IScheduledItemManager, IEngagementManager, ITalkManager, IMessageTemplateManager
  • Test files affected: All *ControllerTests.cs files in src/JosephGuadagno.Broadcasting.Api.Tests/Controllers/

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. 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의 스킬 모두 보기

이 스킬의 문제 신고