Skip to content

improve multi mock behavior#979

Merged
pablo-msft merged 5 commits into
mainfrom
user/pablo/multimock
May 19, 2026
Merged

improve multi mock behavior#979
pablo-msft merged 5 commits into
mainfrom
user/pablo/multimock

Conversation

@pablo-msft
Copy link
Copy Markdown
Contributor

@pablo-msft pablo-msft commented May 15, 2026

Replicated #553

This PR updates the mocking behavior to match what's advertised by the doc header above HCMockAddMock:

/// You can set multiple active mock responses by calling HCMockAddMock() multiple 
/// times with a set of mock responses. If the HTTP call matches against a set mock responses, 
/// they will be executed in order for each subsequent call to HCHttpCallPerformAsync(). When the 
/// last matching mock response is hit, the last matching mock response will be repeated on 
/// each subsequent call to HCHttpCallPerformAsync().

Previously, the last-set mock for a particular URL was the only one that would ever return. Now the behavior matches the comment: Each matching mock is used and consumed in order they were created, with the last mock repeated forever.

@jasonsandlin
Copy link
Copy Markdown
Member

Concern: Auto-removing mocks changes the API contract

This PR changes mock behavior in a way that may surprise callers — mocks have always persisted until explicitly removed via HCMockRemoveMock() or HCMockClearMocks(). Silently removing them after use breaks that contract and means:

  • Tests can't replay the same sequence without re-registering mocks
  • Existing code relying on a mock being durable will break if a second matching mock happens to exist

Two changes in this PR

  1. Iteration order flip — from rbegin (stack/LIFO) to begin (FIFO). This alone is a reasonable fix if the intent is "use mocks in registration order."

  2. Auto-removal — after a mock is consumed, if more than one mock matched, it's removed from the list. The last one becomes a permanent fallback. This is the surprising part.

Suggested alternative: round-robin cycling

Instead of destroying mocks, keep them all and cycle through them with an index. This preserves the existing API contract while still supporting sequenced responses.

In global.h, add next to m_mocks:
cpp http_internal_map<http_internal_string, size_t> m_mockCycleIndex;

In lhc_mock.cpp, replace the matching + auto-removal logic with:
`cpp
// Collect all matching mocks in insertion order
http_internal_vector<HC_MOCK_CALL*> matchingMocks;
for (auto& m : mocks)
{
if (DoesMockCallMatch(m, originalCall))
{
matchingMocks.push_back(m);
}
}

if (matchingMocks.empty())
{
return false;
}

if (matchingMocks.size() == 1)
{
mock = matchingMocks[0];
}
else
{
// Build a key from method+url to track cycling position
http_internal_string key{ originalCall->method };
key += "|";
key += originalCall->url;

auto& idx = httpSingleton->m_mockCycleIndex[key];
mock = matchingMocks[idx % matchingMocks.size()];
++idx;

}
`

Clear m_mockCycleIndex in HCMockClearMocks() and HCMockRemoveMock().

Benefits of cycling over auto-removal

Auto-removal (this PR) Round-robin cycling
Mocks persist until explicitly removed ❌ No ✅ Yes
Supports sequenced responses ✅ Yes (one-shot) ✅ Yes (repeatable)
Test replay without re-registering ❌ No ✅ Yes
Single-mock behavior unchanged ✅ Yes ✅ Yes
API contract preserved ❌ No ✅ Yes

@pablo-msft
Copy link
Copy Markdown
Contributor Author

Suggested alternative: round-robin cycling

Instead of destroying mocks, keep them all and cycle through them with an index. This preserves the existing API contract while still supporting sequenced responses.

In global.h, add next to m_mocks:

http_internal_map<http_internal_string, size_t> m_mockCycleIndex;

In lhc_mock.cpp, replace the matching + auto-removal logic with:

 
 if (matchingMocks.empty()) { return false; }
 
 if (matchingMocks.size() == 1)
{
mock = matchingMocks[0];
}
else
{
// Build a key from method+url to track cycling position 
http_internal_string key{ originalCall->method };
key += "|";
key += originalCall->url; 

auto& idx = httpSingleton->m_mockCycleIndex[key];
mock = matchingMocks[idx % matchingMocks.size()];
++idx;
}

Am I correctly reading that m_mockCycleIndex is never pushed to? How do you anticipate it being used?

@jasonsandlin
Copy link
Copy Markdown
Member

Am I correctly reading that m_mockCycleIndex is never pushed to? How do you anticipate it being used?

Good question — m_mockCycleIndex is populated implicitly via operator[]. In C++ std::map::operator[] auto-inserts a default-constructed value (size_t(0)) on first access if the key doesn't exist. So this line:

auto& idx = httpSingleton->m_mockCycleIndex[key];

On the first call for a given method+url, it inserts 0, returns a reference to it, and we use it to index into the matching mocks. Then ++idx advances it for the next call. No explicit push/insert needed.

The broader suggestion is to use round-robin cycling instead of auto-removal, so mocks persist and are reusable across test replays without re-registration.

@pablo-msft
Copy link
Copy Markdown
Contributor Author

Thanks for the explanation. Pushed.

@jasonsandlin
Copy link
Copy Markdown
Member

The cycling logic looks great, but the old auto-removal code is still present (lines 149-165 in the new diff). The std::count_if + HCMockRemoveMock block needs to be removed — it conflicts with the cycling approach:

  1. The cycle index advances expecting N mocks, but auto-removal shrinks the list
  2. After mocks are removed, idx % matchingMocks.size() produces wrong results on subsequent calls
  3. It still breaks the API contract (mocks disappear without the caller removing them)

The fix is just deleting this block:

// If this is not the only mock that matches, remove it from the list of mocks so that multiple can be used
// in sequence
auto countMatching = std::count_if(
    mocks.begin(),
    mocks.end(),
    [originalCall](auto m)
    {
        return DoesMockCallMatch(m, originalCall);
    });

if (countMatching > 1)
{
    HCMockRemoveMock(mock);
}

The round-robin cycling already handles sequencing — no removal needed.

Copy link
Copy Markdown
Member

@jasonsandlin jasonsandlin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good — clean round-robin cycling, mocks persist as expected, cycle index properly cleared on removal.

@pablo-msft pablo-msft merged commit ab6a11b into main May 19, 2026
15 checks passed
@pablo-msft pablo-msft deleted the user/pablo/multimock branch May 19, 2026 21:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants