Python mock assertion comparison showing expected interactions, extra calls, and exact trace validation

Your Mock Found the Expected Calls. What Else Ran?

Sat, Oct 31, 2026

Start with Q03 because it captures the review mistake in one line of evidence: the recorded trace is reserve('order-17', units=2), then commit('order-17'), then cancel('order-17'). The two expected calls are present in the right order. assert_has_calls() passes. Yet the declared interaction contract says those two calls must be the whole owned trace. The cancel is therefore disallowed. Nothing went missing from the mock record; the assertion simply asked a narrower question than the release policy.

That distinction is the entire subject of this article. We are not testing whether the methods have realistic signatures, whether mutable arguments changed after recording, whether an AsyncMock was awaited to completion, or whether a provider performed a real transaction. The laboratory is synchronous, standard-library only, and deliberately multi-method. Each case creates one fresh parent Mock(spec_set=['reserve', 'commit', 'probe', 'cancel']), configures child return values to None, passes only immutable string and integer leaves, and compares four assertion policies against the same owned mock_calls history.

The ten controls answer one practical review question: when the contract is complete, does the assertion reject every extra, missing, reordered, duplicated, or wrong-valued recorded interaction that matters? The answer depends on which assertion you select. assert_has_calls() is useful when selected calls are the policy. Exact list equality is appropriate when the whole ordered trace is the policy. Counted unordered equality is a different policy again. And a no-work branch needs a truly empty owned history, not merely an empty expectation list.

Declare the complete interaction boundary

The contract comes before the assertion. In this lab, the owned dependency boundary exposes four known methods: reserve, commit, probe, and cancel. For the work path, the complete permitted interaction is exactly:

[
    call.reserve('order-17', units=2),
    call.commit('order-17'),
]

No prefix call is permitted. No suffix call is permitted. No interleaved call is permitted. No duplicate is permitted. Order matters. For the separate no-work path, the contract is exactly [] on this owned boundary.

That wording matters because an assertion such as assert_has_calls() does not infer completeness from business intent. It evaluates the condition it was designed to evaluate. The Python documentation says that assert_has_calls() checks mock_calls; in default mode the expected calls must be sequential, while extra calls may exist before or after them. With any_order=True, all expected calls must appear, but ordering is relaxed. If the reviewer wants “nothing else happened here,” that requirement must be represented by a full-history comparison rather than assumed from a selected-call assertion.

The boundary is also intentionally narrower than interface fidelity. spec_set restricts accessible attributes to the declared names, but this fixture is not teaching signature binding. Python documents spec_set as restricting get/set access to attributes in the specification; autospeccing is the mechanism that checks function or method signatures. For that separate problem, see mock signature fidelity with autospec. Here, every invoked method name is already admitted. The issue is whether additional valid-shaped interactions appear in the record.

The owner of this contract is whoever owns the semantic dependency boundary under test: for example, the component that is allowed to reserve and commit an order but not probe or cancel during this path. A test owner can encode that policy, but should not silently redefine it by choosing an easier assertion. The test is evidence about the recorded boundary, not a substitute for the boundary definition.

Read what each assertion actually selects

Four assertion styles appear in the matrix, and they answer different questions.

boundary.assert_has_calls(expected) asks whether the expected call list occurs as a sequential segment of boundary.mock_calls. The standard-library API explicitly allows extra calls before or after that segment. The official examples make the same distinction operationally: comparing mock_calls with a complete expected list checks order and rejects additional calls, while assert_has_calls() is offered when only a particular sequence among many calls matters.

boundary.assert_has_calls(expected, any_order=True) asks whether all expected calls can be found in the recorded calls without requiring the expected order. It is not a set comparison. In the pinned CPython 3.12.14 implementation, the method copies the actual call list and removes one matching actual occurrence for each expected occurrence; if an expected occurrence cannot be removed, the assertion fails. Any actual calls left over are not grounds for failure. That implementation detail is version-bound evidence for the executed 3.12.14 baseline, not a promise about every future implementation.

assertListEqual(list(boundary.mock_calls), list(expected)) asks a complete ordered-history question. Python documents assertListEqual as list equality with a useful difference report, and the mock examples show full mock_calls == expected comparison as the pattern that rejects unexpected methods as well as wrong order. In this fixture, that is the assertion aligned to the declared work-path contract.

assertCountEqual(normalized(boundary.mock_calls), normalized(expected)) asks a complete unordered but multiplicity-preserving question. Python documents assertCountEqual as requiring the same elements with the same counts regardless of order; duplicates are not ignored. It therefore represents a legitimate alternative policy only if order truly does not matter. It cannot be substituted for the ordered contract just because it makes a reversed trace pass.

The practical rule is simple: a selected expected sequence, a complete ordered trace, and a complete counted unordered trace are three distinct contracts. The assertion should follow the contract, not the other way around.

Prepare fresh mocks and immutable arguments

The commissioning baseline is CPython 3.12.14 on Linux, with the exact fixture source SHA-256 194d31f027c4534de8c10d20279cc820fd69f01519e4797bf6b5b6cf9340ebb0. The Python 3.12 documentation branch currently identifies itself as 3.12.16, so the documentation belongs to the same minor-version reference family but is not the same patch build as the measured interpreter. Where implementation behavior matters, this article therefore cites the exact CPython v3.12.14 source rather than pretending the two artifacts are identical.

Each diagnostic case creates a fresh parent mock. That prevents one case’s history from becoming another case’s input and makes every trace independently reviewable. Fresh state is a general testing discipline; independent test-case state covers a different pytest-specific state-isolation problem. This laboratory itself uses only unittest and unittest.mock.

The parent is created with:

Mock(spec_set=['reserve', 'commit', 'probe', 'cancel'])

and each child method’s return_value is set to None. The list passed to spec_set admits those attribute names and rejects unknown ones, but it does not define real callable signatures for the children. That is deliberate. We want every call in the matrix to remain inside one simple shape-controlled vocabulary so that the observed difference is inclusion, order, and multiplicity rather than interface binding.

Arguments are also deliberately stable: the key is the immutable string 'order-17', and unit values are integers. The fixture’s normalized() helper converts each call into a tuple of method name, positional tuple, and sorted keyword pairs. That representation is sufficient for this bounded data shape. It is not offered as a general serializer, a deep-copy mechanism, or a solution for custom equality semantics.

Finally, the child return values are configured even though no return value drives the matrix. This keeps the fixture from accidentally introducing chained return-value call records. The observation target remains the direct child-method history on the parent.

Run the ten-case comparison matrix

The complete fixture below is the commissioned source. The supplied run occurred on October 31, 2026 at 07:15 UTC under CPython 3.12.14 on Linux, produced PASS_10_CASES, exited with code 0, and had the SHA-256 stated above. Those are commissioning results supplied with the article brief; they are not presented as a production incident or as a personally witnessed CI run.

Save the source as qa_calls_fixture.py and invoke it with:

python qa_calls_fixture.py > qa_calls_results.json

"""Compare partial call assertions with an explicitly complete local trace."""
import hashlib, json, platform, unittest
from datetime import datetime, timezone
from pathlib import Path
from unittest.mock import Mock, call

KEY = 'order-17'
TARGET = [call.reserve(KEY, units=2), call.commit(KEY)]
RESERVE = ('reserve', (KEY,), {'units':2})
COMMIT = ('commit', (KEY,), {})
PROBE = ('probe', (), {})
CANCEL = ('cancel', (KEY,), {})
WRONG = ('reserve', (KEY,), {'units':9})
# Columns: ID, invoked actions, declared complete contract, expected assertion booleans.
CASES = [
    ('Q01', [RESERVE,COMMIT], TARGET, (True,True,True,True)),
    ('Q02', [PROBE,RESERVE,COMMIT], TARGET, (True,True,False,False)),
    ('Q03', [RESERVE,COMMIT,CANCEL], TARGET, (True,True,False,False)),
    ('Q04', [RESERVE,PROBE,COMMIT], TARGET, (False,True,False,False)),
    ('Q05', [COMMIT,RESERVE], TARGET, (False,True,False,True)),
    ('Q06', [RESERVE,RESERVE,COMMIT], TARGET, (True,True,False,False)),
    ('Q07', [RESERVE], TARGET, (False,False,False,False)),
    ('Q08', [WRONG,COMMIT], TARGET, (False,False,False,False)),
    ('Q09', [PROBE], [], (True,True,False,False)),
    ('Q10', [], [], (True,True,True,True)),
]

def require(ok, label):
    if not ok:
        raise AssertionError(label)

def passes(action):
    try:
        action()
        return True
    except AssertionError:
        return False

def normalized(records):
    # Only the fixture's plain, immutable argument leaves are admitted.
    return [(c[0], tuple(c[1]), tuple(sorted(c[2].items()))) for c in records]

def exercise(spec):
    identity, actions, expected, outcomes = spec
    boundary = Mock(spec_set=['reserve','commit','probe','cancel'])
    for method in ['reserve','commit','probe','cancel']:
        getattr(boundary,method).return_value = None
    for method,args,kwargs in actions:
        getattr(boundary,method)(*args, **kwargs)
    sequence = passes(lambda: boundary.assert_has_calls(expected))
    any_order = passes(lambda: boundary.assert_has_calls(expected, any_order=True))
    check = unittest.TestCase()
    exact_order = passes(lambda: check.assertListEqual(list(boundary.mock_calls), list(expected)))
    exact_unordered = passes(lambda: check.assertCountEqual(normalized(boundary.mock_calls), normalized(expected)))
    actual = (sequence, any_order, exact_order, exact_unordered)
    require(actual == outcomes, identity + ' assertion contract')
    # Calling children does not call the parent Mock itself.
    require(boundary.call_count == 0 and boundary.call_args_list == [], identity + ' observation scope')
    per_method = passes(lambda: boundary.reserve.assert_called_once_with(KEY,units=2)) and passes(lambda: boundary.commit.assert_called_once_with(KEY))
    if identity in ['Q02','Q03','Q04','Q05']:
        require(per_method, identity + ' isolated method assertions')
    return {'case':identity, 'expected_trace':[repr(c) for c in expected],
        'observed_trace':[repr(c) for c in boundary.mock_calls],
        'expected_assertions':outcomes, 'observed_assertions':actual,
        'has_calls_sequential':sequence, 'has_calls_any_order':any_order,
        'exact_order':exact_order, 'exact_unordered_multiplicity':exact_unordered,
        'parent_call_count':boundary.call_count,
        'per_method_once_assertions':per_method,
        'ordered_contract_decision':'ACCEPT_OWNED_TRACE' if exact_order else 'REJECT_OWNED_TRACE',
        'status':'PASS'}

if name == '__main__':
    started = datetime.now(timezone.utc).isoformat()
    rows = [exercise(case) for case in CASES]
    require([r['case'] for r in rows] == ['Q%02d' % i for i in range(1,11)], 'case inventory')
    print(json.dumps({'status':'PASS_10_CASES', 'python':platform.python_version(),
        'platform':platform.platform(), 'started_utc':started,
        'finished_utc':datetime.now(timezone.utc).isoformat(),
        'source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
        'scope':'synchronous owned mock boundary; no real service or completion claim',
        'cases':rows}, indent=2))

The four Boolean columns below describe assertion outcomes, not permission to accept the business operation. T means that assertion form passes for that diagnostic trace; F means it raises AssertionError inside the fixture’s passes() wrapper.

Case

Controlled trace

Default

Any order

Exact ordered

Exact counted

Declared ordered decision

Q01

reserve, commit

T

T

T

T

ACCEPT

Q02

probe, reserve, commit

T

T

F

F

REJECT

Q03

reserve, commit, cancel

T

T

F

F

REJECT

Q04

reserve, probe, commit

F

T

F

F

REJECT

Q05

commit, reserve

F

T

F

T

REJECT

Q06

reserve, reserve, commit

T

T

F

F

REJECT

Q07

reserve only

F

F

F

F

REJECT

Q08

reserve units=9, commit

F

F

F

F

REJECT

Q09

probe, expected empty

T

T

F

F

REJECT

Q10

no calls, expected empty

T

T

T

T

ACCEPT

Q01 is the positive work-path control. Q10 is the positive no-work control. Q07 and Q08 are negative controls proving that the selected-call assertions are not indiscriminately green: a missing commit and a wrong units value both fail. The interesting false-approval risk lies in Q02, Q03, Q06, and Q09, where assert_has_calls() can pass while the declared complete trace must still be rejected. Q04 and Q05 separate adjacency from order policy. Only Q01 and Q10 satisfy the actual ordered contract.

The fixture drives authored call sequences directly; it does not stand in for an application implementation. PASS_10_CASES means the predicted diagnostic behaviors matched the supplied measured results. It does not mean every case is acceptable. A deliberately failing complete-history assertion can be the correct evidence for a diagnostic case whose purpose is to demonstrate rejection.

Expose calls before and after the expected sequence

Q02 and Q03 are the clearest examples of Python assert_has_calls extra calls remaining compatible with a passing selected-sequence assertion.

Q02 records:

[call.probe(), call.reserve('order-17', units=2), call.commit('order-17')]

Q03 records:

[call.reserve('order-17', units=2), call.commit('order-17'), call.cancel('order-17')]

The expected sequence in both cases is:

[call.reserve('order-17', units=2), call.commit('order-17')]

Default assert_has_calls() passes both because the expected two-call sequence appears contiguously, with an extra permitted before it in Q02 or after it in Q03. That behavior is exactly what the API documents. any_order=True also passes because both expected calls are present. The complete ordered and complete counted comparisons fail because the actual inventory contains one additional method call.

The review language should be precise: the mock did not “miss” probe or cancel. The parent’s mock_calls record contains the extra interaction. The selected assertion simply did not make emptiness of the remainder part of its acceptance condition. That distinction matters when diagnosing a green test. If the record already contains the violating event, changing instrumentation is unnecessary; changing the assertion policy may be the required repair.

Inspect the recorded trace before changing the assertion

Before replacing assert_has_calls(), inspect the complete observed trace and compare it with an independently declared expected trace. Do not filter probe or cancel away merely because the expected methods are reserve and commit. Filtering can transform evidence of a contract violation into a green comparison by construction.

For Q03, the useful review artifact is the side-by-side mismatch:

Expected complete trace

Observed complete trace

reserve(order-17, units=2)

reserve(order-17, units=2)

commit(order-17)

commit(order-17)

No additional call

cancel(order-17)

Once the requirement says “exactly these two calls and no others,” the suffix is not noise. It is the defect signal for this boundary. Preserve it in the assertion failure so the reviewer can see what happened rather than only that “calls differ.”

Separate contiguity from order independence

Q04 and Q05 show why two common interpretations of assert_has_calls() need to be separated.

Q04 records reserve, then probe, then commit. The expected reserve/commit pair appears in the right relative order, but not as one contiguous segment. Default assert_has_calls() therefore fails. any_order=True passes because both expected calls are present somewhere in the actual history. Exact ordered and exact counted comparisons both reject the trace because probe is extra.

Q05 records only commit, then reserve. There are no extras, but the two expected calls are reversed. Default assert_has_calls() fails because the requested sequence is absent. any_order=True passes because order is deliberately ignored. Exact ordered equality fails. Exact counted equality passes because the two traces contain the same call elements with the same multiplicities.

That last result is important: Q05 is acceptable only under a different, explicitly unordered policy. The real contract in this article requires reserve before commit, so Q05 remains REJECT_OWNED_TRACE. A reviewer should never weaken the policy from ordered to unordered merely because unordered equality makes an inconvenient test green.

A sequence match is not an arbitrary ordered subsequence

The default behavior is more specific than “the calls appear in order somewhere.” In the pinned CPython 3.12.14 source, CallList._contains__ handles list membership by scanning contiguous slices of the actual call list and comparing each slice with the expected list. That source-level mechanism explains Q04: an unrelated call inserted between the two expected calls prevents the two-call list from appearing as a contiguous slice.

This is a version-bound implementation observation, not a universal specification for all Python versions. The public documentation describes the calls as sequential and allows extras before or after; the pinned source shows how that behavior is implemented in 3.12.14. Q04 demonstrates one controlled interleaving. It does not attempt to exhaust every matcher, nesting, or chained-call scenario.

The operational choice remains policy-first. If you care about a contiguous selected sequence within a larger trace, default assert_has_calls() is well matched. If you care only that selected calls occurred in any order, use any_order=True. If you care about the entire trace, neither mode is complete by itself.

Keep duplicate counts in the acceptance rule

Q06 is the multiplicity control. Its actual trace is:

reserve('order-17', units=2)
reserve('order-17', units=2)
commit('order-17')

Default assert_has_calls() passes because the final reserve/commit pair is a matching contiguous suffix. any_order=True also passes because it can consume one matching reserve and one matching commit while leaving the extra reserve in the actual list. The pinned implementation literally removes each expected match from a copy of the actual calls and does not require the remainder to be empty.

Both complete-history policies reject Q06. Ordered equality sees a three-call actual list versus a two-call expected list. Counted unordered equality sees two reserve occurrences where only one is permitted. Python’s assertCountEqual explicitly preserves duplicate counts, which is why it is suitable for a complete unordered multiplicity policy.

Representation

Preserves order?

Preserves multiplicity?

Rejects Q06 extra reserve?

Set of normalized calls

No

No

No

Counted unordered comparison

No

Yes

Yes

Ordered list comparison

Yes

Yes

Yes

A set is therefore the wrong replacement if duplicate calls matter. Converting each side to a set would collapse one reserve and two reserves into the same membership result. That discards evidence the declared policy needs.

Choose an unordered policy only when order is irrelevant

An exact unordered policy can be valid. For example, if a boundary permits exactly one reserve and one commit but their order is semantically irrelevant, assertCountEqual over a safe normalized representation can express that requirement. It preserves the inventory and multiplicities while ignoring sequence.

This article’s contract is not that policy. Here, Q05 demonstrates the counterfactual: exact counted equality accepts the reversed two-call trace, but the declared ordered interaction still rejects it. Q06 demonstrates an extra actual duplicate. The pinned implementation also shows that repeated expected calls under any_order=True require repeated matching actual calls because each expected match is removed separately. That duplicate-expected behavior is source-backed context, not an additional executed case in this ten-case matrix.

The broader review rule is to preserve counts whenever counts are part of permission. Do not deduplicate traces for convenience and then claim to have tested multiplicity.

Bound the meaning of argument equality

Q08 keeps argument correctness in the matrix without turning the article into a call-signature or mutable-snapshot tutorial. The actual trace is reserve('order-17', units=9) followed by commit('order-17'); the expected reserve requires units=2. All four assertion forms fail.

That negative control proves something useful about the matrix: selected-sequence assertions are permissive about extra calls, not indifferent to the equality of the calls they are asked to match. The wrong reserve call is not equal to the expected reserve call, so neither default nor unordered assert_has_calls() can satisfy the expectation. Exact ordered and exact counted comparisons fail for the same value mismatch.

The fixture’s normalized() helper preserves only what this fixture needs: the child method name, the positional arguments as a tuple, and the keyword items sorted into a tuple. Because the leaves are the immutable string 'order-17' and integers 2 or 9, this representation is stable for these diagnostics. It is not a general-purpose serialization contract.

That boundary should be stated explicitly because full-history equality does not solve every argument-evidence problem. If a mutable list or dictionary were passed and then changed, the recorded call can still hold a reference whose later contents differ from its call-time contents. That separate issue is covered in call-time snapshots for mutable arguments. The official mock documentation also warns about limitations involving nested chained calls and ancestor arguments; those are outside this fixture.

Nothing here claims that normalized() handles custom equality matchers, mutable object snapshots, chained return-value ancestry, or arbitrary objects. The lab intentionally excludes those cases so the acceptance decision stays focused on whether the complete owned synchronous trace contains exactly the permitted interactions.

Prove that a no-work branch made no recorded calls

Q09 and Q10 are the controls most likely to surprise a reviewer who builds expectations dynamically.

Both cases declare expected = []. Q09 nevertheless records probe(). Q10 records nothing. In both cases, default assert_has_calls([]) passes. So does assert_has_calls([], any_order=True). The exact complete comparisons reject Q09 and accept Q10.

The implementation explains why. For default mode, an empty expected list is considered present in CallList because a zero-length slice equals the empty list at the first scan position. For anyorder=True, the loop over expected calls executes zero iterations, so there is no missing expected call to trigger failure. The public API does not advertise “empty expectations prove no calls”; it defines assert_has_calls() in terms of finding the specified calls in mock_calls.

Operationally, that means an empty list of selected expectations is not a no-interaction assertion. If the branch contract says “this component must not touch the dependency at all,” the assertion must test the full owned history for emptiness, for example:

self.assertListEqual(list(boundary.mock_calls), [])

or an equivalent exact comparison on the same qualified observation boundary.

This distinction matters when expectation construction depends on branch logic. Suppose a test derives expected from input and a bug causes that builder to produce []. A passing assert_has_calls(expected) does not establish that zero calls occurred. It establishes only that the specified empty selection did not conflict with the recorded history. The expectation generator itself is part of the test design and needs review when emptiness carries semantic meaning.

Empty expectations need an explicit empty-history contract

For a true no-work branch, the policy should say “the complete owned mock_calls history equals [].” Q10 satisfies that proposition. Q09 does not.

Do not replace that check with boundary.call_count == 0 in this fixture. Every case has parent call_count == 0 because the parent object itself is never called as a callable. The interactions occur on child methods and are visible in the parent’s mock_calls. The standard-library documentation defines mock_calls as including calls to methods and child mocks, which is why it is the relevant multi-method observation record here.

The observation also needs qualification. A history that was reset before inspection, a child mock that was never attached to the manager being checked, or a separate dependency outside this parent’s record would require different evidence. This fixture creates fresh mocks and does not call reset_mock() to manufacture emptiness. Equality can only evaluate calls that reached the selected record.

Finally, missing output is not equivalent to an empty trace. If the diagnostic process crashes before producing its JSON, the correct release-review state is incomplete evidence, not “no calls happened.”

Reconcile parent history and per-method assertions

Every case in the fixture reports boundary.call_count == 0 and boundary.call_args_list == []. That result is correct because no case invokes boundary(...). Instead, the fixture invokes boundary.reserve(...), boundary.commit(...), boundary.probe(...), or boundary.cancel(...). Those child interactions roll up into boundary.mock_calls, which is the record this contract owns. Python documents mock_calls as recording calls to the mock, its methods, magic methods, and return-value mocks.

That difference is easy to miss when a review sees “call count zero” and treats it as “nothing happened.” The observation must be attached to the same object and record that the contract is about.

Question

Relevant observation

What it means here

Was the parent object itself called?

boundary.call_count, boundary.call_args_list

Always no in this fixture

Was one child called correctly once?

boundary.reserve.assert_called_once_with(...)

Local fact about that child only

What happened across all owned child methods, in order?

boundary.mock_calls

Complete multi-method record for this fixture

The per-method control is equally instructive. Separate reserve.assert_called_once_with(KEY, units=2) and commit.assert_called_once_with(KEY) checks pass in Q02, Q03, Q04, and Q05. Yet all four cases violate the declared complete ordered contract: Q02 has an extra prefix, Q03 an extra suffix, Q04 an interleaved extra, and Q05 reversed order. Each method-level assertion is true, but their conjunction still does not express the broader trace policy.

Attach the contract to the object that records it

The official examples show how calls to child mocks are recorded in a parent manager’s mock_calls, and how separately created mocks can be attached to a manager when cross-mock order is the thing being observed. This fixture needs no attachment tutorial because the children are created directly from one known parent.

The general evidence rule is narrower: certify only the observation boundary you actually recorded. A dependency that is not a child of this parent, is not attached, or is otherwise outside the record cannot be ruled in or ruled out by comparing this parent’s mock_calls. That is why the acceptance language in this article is ACCEPT_OWNED_TRACE, not “no unexpected call anywhere.”

The same restraint applies to parent call_count. It is useful when the parent callable itself is the policy object. It is not a substitute for a multi-method child history simply because both observations live on the same Mock instance.

Turn assertion results into a bounded decision

The matrix becomes useful only when assertion outcomes are translated back into the declared contract. For the ordered work path, exact list equality is the governing acceptance check. For the no-work path, exact emptiness of the same owned history is the governing check. Under those rules, Q01 and Q10 are accepted; every other trace is rejected.

Case

Main diagnostic meaning

Ordered-policy decision

Q01

exact reserve → commit

ACCEPT

Q02

extra probe prefix

REJECT

Q03

extra cancel suffix

REJECT

Q04

interleaved probe

REJECT

Q05

reversed expected calls

REJECT

Q06

duplicate reserve

REJECT

Q07

missing commit

REJECT

Q08

wrong units value

REJECT

Q09

no-work expectation but probe ran

REJECT

Q10

genuinely empty no-work trace

ACCEPT

Q05 is the deliberate policy fork. The counted unordered comparison accepts it because the inventory and multiplicities match. That is not an error in assertCountEqual; it is a faithful answer to a different question. If the product contract were changed so reserve and commit order no longer mattered, Q05 could become acceptable after an explicit policy review. Until then, accepting it would be a test weakening, not a repair.

The boundary of the decision also needs to remain synchronous. An exact mock_calls history does not prove that an asynchronous operation was awaited to successful completion, and it certainly does not prove that a remote provider committed external state. Those are separate evidence problems; AsyncMock completion evidence addresses the call/await/completion distinction directly. The neighboring article itself already includes an extra unawaited-call control, so this article’s contribution is not the generic discovery that extras can exist. It is the synchronous, multi-method completeness workflow over one owned trace.

Separate diagnostic success from system acceptance

PASS_10_CASES and ACCEPT_OWNED_TRACE are intentionally different statuses.

PASS_10_CASES means the fixture observed the expected assertion behavior for all ten diagnostic cases in the supplied CPython 3.12.14 commissioning run. A diagnostic case can therefore “pass” while demonstrating that a particular trace must be rejected. That is normal in a test harness whose purpose is to validate assertion semantics.

ACCEPT_OWNED_TRACE means the recorded history for one case satisfies this local interaction policy. It says nothing about a real reservation service, network request, database commit, payment, external side effect, or provider completion because no such system exists in the fixture.

A useful release-review vocabulary is therefore bounded: ACCEPT when the qualified evidence satisfies the local policy; REJECT when it contradicts the policy; REPAIR when the test assertion or implementation needs correction; HOLD when the observation is incomplete or its scope is uncertain. None of those labels should be stretched into a universal product claim.

Repair the test without overfitting implementation details

A repair should make the assertion match the semantic contract without turning the test into a transcript of incidental implementation steps.

Use a narrow workflow:

1.   Name the owned dependency boundary and state whether the contract is selected-call, complete ordered, complete counted unordered, or truly empty.

2.   Preserve the complete record that can reveal unapproved interactions. Do not pre-filter the history to expected method names.

3.   Choose exact ordered comparison when sequence is part of the policy; choose counted unordered equality only when order is explicitly irrelevant but multiplicity still matters.

4.   Add at least one extra-call control and one missing-call control so the test proves it can reject both over-interaction and under-interaction.

5.   Document what the record cannot observe, including unattached dependencies or external effects.

That workflow avoids two opposite mistakes. The first is under-specification: using assert_has_calls() when the real rule is “exactly these and no others.” The second is over-specification: asserting every private helper call simply because the implementation happens to make it today. Strict history tests are valuable only when the history itself is a meaningful contract at an owned boundary.

There are also cases where keeping assert_has_calls() is correct. If the contract genuinely says “these selected calls must occur, while other interactions on this boundary are permitted,” the method expresses that intent well. The test name should then say what is selected (for example, test_emits_required_reservation_sequence_among_other_boundary_calls) rather than implying exactness it does not check.

Repair is therefore not synonymous with “replace every assert_has_calls.” It means asking the policy question first, then using an assertion whose acceptance set matches that policy.

Assign ownership and requalify changed expectations

Complete interaction tests age well only when ownership is explicit. The interface or component owner should define which boundary interactions are permitted and whether order is semantic. The test owner should maintain an independently authored expected trace and should resist copying the implementation’s current call list into the test merely to restore green status.

Requalification is appropriate when any assumption that gives the assertion meaning changes: the boundary gains a permitted method; order becomes irrelevant or newly required; argument types move beyond the simple immutable leaves used here; custom equality enters the comparison; child mocks are attached or detached differently; or the Python runtime is upgraded and an implementation-sensitive behavior matters. The public API is the primary contract, while pinned source is useful when the decision depends on implementation details such as contiguous slice searching or per-occurrence removal.

Release review should treat an assertion-policy change as reviewable test logic, not clerical cleanup. The same general discipline (named ownership, explicit scope, and requalification before changing a gate) is useful in reviewable pytest expected-failure policies, though the mechanics there concern xfail/XPASS rather than mock traces.

A compact acceptance checklist is enough: confirm the owned observation record; confirm the expected trace was authored from the contract rather than copied from actual history; confirm order policy; confirm multiplicity policy; exercise one extra, one missing, and one empty-path control; and record unobserved dependencies. If any of those are unknown, HOLD is more accurate than declaring the interaction complete.

Nothing in this mock-only laboratory demonstrates production rollback, compensating action, or recovery of an external transaction. Those require their own system evidence and owners.

Build stronger QA evidence with Refonte Learning

The practical skill behind this playbook is not memorizing one mock assertion. It is learning to define an observation boundary, choose evidence that matches the policy, preserve negative controls, and review test logic as part of release quality.

Refonte Learning’s Quality Assurance Automation Engineering / Quality Assurance Engineering programme describes a three-month format at 12 to 14 hours per week and covers automated test-script writing, test automation frameworks, CI/CD integration, performance and security testing, and practical QA projects. Its FAQ lists tools such as Selenium, JUnit, and Jenkins, and notes Python, Java, and JavaScript as commonly used testing languages. The programme page also states that successful completion offers a Training Certificate and a Certificate of Internship.

Those published programme facts support a focused invitation: if you want to develop reviewable automated-testing and CI/CD skills, the programme provides a structured path for that broader QA practice. This article does not claim that the curriculum teaches unittest.mock, assert_has_calls(), or this exact laboratory. The specialist assertion workflow above remains a bounded technical exercise whose value comes from making the question, evidence, and acceptance decision explicit.