Python engineer comparing mutated unittest.mock call history with a deep-copied call-time argument snapshot on dual monitors.

Your Mock’s Call History Is Not a Snapshot of Its Arguments

Tue, Oct 20, 2026

The test passes. Its final assertion says the mocked dependency received a job requesting two units. Yet the caller supplied nine units and changed the same dictionary to two only after the call returned. The assertion executed, the call count was correct, and the expected dictionary looked sensible. The evidence answered a later-state question while the test claimed to verify an earlier value.

Reverse the mutation and the same design produces an obvious failure: a correct two-unit call appears to contain nine units when inspected afterward. Those two directions belong together. A repair that explains only the failing assertion leaves the more dangerous false pass unchallenged.

This playbook examines that boundary in one CPython process using standard-library unittest.mock, plain dictionaries and lists, and fixed synthetic identifiers. The operational contract concerns selected values when a synchronous mocked call happens. A separate control concerns whether the exact original object crossed the boundary. Those requirements need explicit, potentially separate evidence.

The complete harness produces nine diagnostic cases, including a reused dictionary passed twice, a shallow-copy failure, an owned deep-copy recorder, and a minimal immutable projection. It also checks that capture preserves the configured return value where specified. The resulting decision is whether to accept the captured values, repair their observation boundary, hold an unresolved identity contract, or qualify a different payload protocol. A passing harness establishes its diagnostic outcomes; each candidate still needs its own acceptance judgment.

State whether the assertion concerns a past value or current state

Write the contract with a time reference: when the dependency is called, job must be "J1" and units must contain the integer 2. If two calls are expected, specify their ordered values as well. This makes the assertion author responsible for identifying the event whose evidence matters.

A current-state contract is different. It might require the caller’s dictionary to contain nine units after processing finishes. That requirement can coexist with a correct earlier two-unit call. Inspecting only the caller’s final dictionary cannot establish both states when the same mutable object participates in both events.

For each disputed assertion, distinguish the time of the call from the time at which its recorded arguments are inspected.

Identity adds another dimension. Python’s data model distinguishes identity, type, and value; identity remains fixed for an object, and is compares identities. A dictionary can therefore retain its identity while its contents change.

For this laboratory, value acceptance means comparing independently captured selected fields against independently authored expectations. Identity acceptance means retaining the original reference and testing it against the required object. Equality between two dictionaries does not, by itself, establish that they are the same dictionary.

Before editing an assertion, complete this sentence: “This test must establish these fields, at this call, in this order, with this identity requirement.” If the interface owner cannot agree on that sentence, choosing a copying strategy is premature. A green assertion is useful only when its observation matches the agreed question.

Separate call recording from argument snapshotting

Python’s mock examples describe the mutable-argument history pitfall: call_args and call_args_list retain references to arguments. Later mutation can change the values reached through those references. Historical call records are therefore insufficient, on their own, to reconstruct earlier mutable contents.

The mock reference defines the recorded call structures. call_args describes the most recent call; call_args_list contains calls in sequence. Their positional and keyword components let a test inspect arguments, but that organization does not create independent versions of nested payloads.

In this experiment, the call record reaches an outer dictionary, which reaches a list under units. Updating one list element changes the value available along that path. No second call is required. The apparent discrepancy comes from reading the existing path later and treating its current endpoint as an earlier observation.

This remains separate from mock autospec and real call signatures. Signature checks concern whether a callable accepts the supplied argument arrangement. This article asks which values remain available for historical assertions after a correctly arranged call. M8 will keep that distinction visible without repeating a signature-validation matrix.

A correct call count does not freeze the payload

Count, order, object identity, and selected values are separate evidence dimensions. A single record can correctly establish one invocation while exposing a dictionary whose contents have since changed. Two ordered records can correctly establish two invocations while both reach the same final list value.

Inspect the relationships before blaming the recorder. M1 explicitly checks that the argument retained by the original mock is the caller’s dictionary. M6 keeps two records and reads both after a final mutation. These controls make the retained references visible; they do not suggest that the mock forgot an invocation.

The repair must therefore identify the observation that needs ownership. Installing a second recorder only helps when it receives independent values during the call. Merely assigning the original history to another variable preserves the same problem. Likewise, a later copy of that history begins its life with whatever values remain at the later copying moment. It cannot recover an earlier value that the test never retained.

Create a single-test, plain-data baseline

The supplied preflight reports execution of the complete fixture on October 20, 2026, using CPython 3.13.5 and its standard-library unittest.mock. An independent execution for this article on October 20, 2026, used CPython 3.12.14 and matched all nine diagnostic outcomes. The cited manuals use the Python 3.13 branch and identified themselves as 3.13.16 documentation when checked on October 20, 2026. That reference branch is distinct from both tested builds and is not a claim about the newest Python release.

All cases execute serially in one process. The payload factory returns a new dictionary containing job="J1" and a newly created one-element units list. Its default element is 2; passing 9 creates the deliberately incorrect call input. Repeated factory calls used for expectations create separate containers rather than borrowing the object under inspection.

The chosen contract allows the caller to edit its object after the synchronous mocked invocation. That permission is part of the laboratory design, not a rule imposed on every real interface. An interface that retains arguments for later consumption would require the owner to define a different observation event.

Keep the authored shape small enough to review directly. The job identifier stays fixed, so the changing unit value remains the selected variable. Values 2 and 9 reverse the pass/fail direction. Values 2, 3, and 4 distinguish the two call events from the final state in the reuse control. No random generation or private customer identifier contributes evidence.

Every independent scenario creates fresh payload state. M6 and M7 deliberately reuse an object within their own scenario because that reuse is the behavior being investigated. The problem therefore persists without mutable pytest inputs across test cases: no pytest installation, parameter ordering, or previous test is involved.

The laboratory also creates no network client or external resource. Its script reads its own source bytes for a fingerprint and writes a JSON report to standard output. Shell redirection creates the results file. There is no temporary service directory to discover, no production input path to configure, and no cleanup protocol hidden behind the example.

Keep that boundary when adapting the case. First reproduce the call, the explicit later mutation, and the intended assertion using these plain values. If the failure disappears only when an application-specific payload is replaced, record the changed assumption. Custom equality, copying, iteration, or external state needs its own analysis; it should not be smuggled into the plain-data conclusion. The baseline makes the timing defect inspectable before any broader test infrastructure becomes relevant.

Install the complete reproducible evidence harness

Save the following complete script as mock_probe.py in an owned scratch directory. It needs only the standard library. Run it from that directory with the same interpreter whose version you retain in the evidence record:

python mock_probe.py > mock-results.json

The supplied source fingerprint is 730f1fcf01f65a26d022438aa5cf51c31ff5f4d12e35317e3d7dda99962c4e86. The new execution matched that SHA-256 exactly. Retain the source alongside the JSON so that a later change to a helper, expected value, or mutation step is distinguishable from a runtime difference. Editing whitespace also changes this byte-level fingerprint; a changed hash requires explanation rather than an invented claim of identical source.

require raises AssertionError when a stated condition is false. These explicit checks establish the relationships the surrounding prose relies on. An unexpected failed check stops the script before its final success report. The script is a diagnostic driver, so some successful checks deliberately establish defective evidence behavior.

Inspect the process exit status as well as the file. Redirection can create an empty or partial file before a failed run finishes. The success object appears only after every case check completes; an existing filename alone cannot establish completion. Keep stderr when investigating a failed run so that the case-specific exception remains associated with its source and interpreter.

In particular, check_fails catches the expected assertion failure in M1. The subsequent require confirms that the failure occurred. It does not suppress failures generally, and a repaired historical-value assertion must not acquire that wrapper merely to make a suite pass. M2 has the opposite control: its historically misleading assertion must pass for the diagnostic to match the authored scenario.

from copy import copy, deepcopy
from pathlib import Path
from unittest.mock import DEFAULT, Mock, create_autospec
import hashlib
import json
import platform
def require(ok, message):
    if not ok:
        raise AssertionError(message)
def payload(units=2):
    return {"job": "J1", "units": [units]}
def check_fails(action):
    try:
        action()
    except AssertionError:
        return True
    return False
def snapshot_mock():
    recorder = Mock()
    def capture(*args, **kwargs):
        saved_args, saved_kwargs = deepcopy((args, kwargs))
        recorder(*saved_args, **saved_kwargs)
        return DEFAULT
    return Mock(side_effect=capture, return_value="ack"), recorder
rows = []
m = Mock()
p = payload()
m(p)
p["units"][0] = 9
old_failed = check_fails(lambda: m.assert_called_once_with(payload()))
require(old_failed, "M1: original-value assertion should now fail")
require(m.call_args.args[0] is p, "M1: original identity retained")
rows.append({"case": "M1", "at_call_units": 2, "later_history_units": m.call_args.args[0]["units"][0],
             "historically_correct_assertion_failed": old_failed})
m, p = Mock(), payload(9)
m(p)
p["units"][0] = 2
m.assert_called_once_with(payload())
rows.append({"case": "M2", "at_call_units": 9, "later_history_units": 2,
             "historically_wrong_assertion_passed": True})
saved = []
def shallow_capture(p):
    saved.append(copy(p))
    return DEFAULT
m, p = Mock(side_effect=shallow_capture, return_value="ack"), payload()
require(m(p) == "ack", "M3: DEFAULT preserves return value")
p["units"][0] = 9
require(saved[0] is not p and saved[0]["units"] is p["units"], "M3: nested alias")
require(saved[0] == payload(9), "M3: shallow copy is not nested snapshot")
rows.append({"case": "M3", "captured_later": deepcopy(saved), "return_value": "ack"})
m, recorder = snapshot_mock()
p = payload()
require(m(p) == "ack", "M4: return contract")
p["units"][0] = 9
recorder.assert_called_once_with(payload())
m.assert_called_once_with(payload(9))
rows.append({"case": "M4", "captured_units": recorder.call_args.args[0]["units"][0],
             "original_mock_later_units": m.call_args.args[0]["units"][0]})
facts = []
def project(p):
    # The fixture contract admits only a string job and a list of integers.
    require(type(p["job"]) is str and all(type(v) is int for v in p["units"]), "projection domain")
    facts.append((p["job"], tuple(p["units"])))
    return DEFAULT
m, p = Mock(side_effect=project, return_value="ack"), payload()
require(m(p) == "ack", "M5: return contract")
p["units"][0] = 9
require(facts == [("J1", (2,))], "M5: immutable selected facts")
rows.append({"case": "M5", "captured_facts": facts})
m, p = Mock(), payload()
m(p)
p["units"][0] = 3
m(p)
p["units"][0] = 4
seen = [call.args[0]["units"][0] for call in m.call_args_list]
require(seen == [4, 4], "M6: both records reference one reused object")
rows.append({"case": "M6", "at_call_sequence": [2, 3], "later_history": seen})
m, recorder = snapshot_mock()
p = payload()
m(p)
p["units"][0] = 3
m(p)
p["units"][0] = 4
captured = [call.args[0]["units"][0] for call in recorder.call_args_list]
require(captured == [2, 3], "M7: independent call-time values")
rows.append({"case": "M7", "captured_sequence": captured})
def deliver(p, *, request_id):
    return "real implementation is outside this lab"
m, p = create_autospec(deliver, return_value="ack"), payload()
m(p, request_id="R1")
p["units"][0] = 9
m.assert_called_once_with(payload(9), request_id="R1")
rows.append({"case": "M8", "autospec_history_units": m.call_args.args[0]["units"][0],
             "signature_validation_is_snapshotting": False})
token = object()
cloned_token = deepcopy(token)
require(cloned_token is not token, "M9: copied identity is different")
identity_recorder = Mock()
identity_recorder(token)
require(identity_recorder.call_args.args[0] is token, "M9: reference preserves identity")
rows.append({"case": "M9", "deepcopy_preserved_identity": False,
             "reference_preserved_identity": True})
print(json.dumps({"status": "PASS_9_CASES", "python": platform.python_version(),
                  "source_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
                  "rows": rows}, indent=2))

Record independent at-call expectations before mutation

The oracle comes from the authored operation sequence. M1 calls with 2 and changes to 9; M2 calls with 9 and changes to 2. M6 and M7 call first with 2, then with 3, before changing to 4. Their expected call-time sequence is therefore [2, 3], independently of whatever either mock reports later.

The JSON mixes scenario declarations with computed observations. For example, at_call_units and at_call_sequence are authored constants. They are not readings taken by a hidden automatic observer. M4’s two reported unit values are read from its different recorders, while M6 and M7 compute their sequences from the respective histories.

That distinction makes the controls reviewable. The source establishes the intended chronology; the assertions test the expected consequences; the report identifies the runtime and summarizes results. Neither the original mock nor the repaired recorder supplies its own expected answer.

On the new local run, the process exited successfully and reported PASS_9_CASES. The supplied 3.13.5 preflight reports the same case outcomes. That status includes M2’s deliberate false pass and M3’s insufficient shallow capture. It approves the diagnostic observations, not those candidates as evidence of past values.

Make a correct call look wrong after mutation

M1 begins with a fresh payload whose selected value is two units. The single invocation receives that dictionary before any mutation in the scenario. Only after m(p) returns does the driver replace the list’s first element with 9. The final assertion then compares the recorded argument with a fresh expected payload containing 2.

The chronology is the useful evidence:

Event

Caller’s units

What the test is doing

Before invocation

[2]

Establishing the authored input

During the single call

[2]

Passing the correct selected value

After return and mutation

[9]

Changing the existing nested list

During the late assertion

[9]

Reading the retained argument reference

The assertion fails, even though the selected fields were correct at the call. Calling this a false negative is relative to that historical-value contract. A different test requiring the final dictionary to contain 2 would be testing a different condition and could properly fail here.

M1 also checks m.call_args.args[0] is p. That identity result rules out an explanation based on the mock having recorded an unrelated dictionary. The retained argument is exactly the object the caller supplied. The independently created expectation differs because the original object has changed in the meantime.

The practical debugging sequence is to locate the invocation, locate each subsequent mutation of a selected field, and identify when the assertion dereferences the recorded argument. In this controlled program those steps are explicit adjacent operations. Reading only the assertion’s final failure message discards the temporal explanation that the source makes available.

Do not remove the mutation merely because doing so makes this demonstration pass. If post-call reuse belongs to the caller’s permitted behavior, suppressing it narrows the test until the evidence defect disappears. The assertion needs an observation made while the selected value is still present.

M4 will retain that value at the call boundary and leave the later mutation intact. This is a useful repair criterion: the test should preserve its ability to distinguish the earlier call from the later state. The original mock’s reference history can remain available for identity or current-state checks without being presented as the repaired historical record.

Make an incorrect call pass a later assertion

M2 keeps the required call-time value at 2 but constructs the actual payload with 9. The mock receives that incorrect selected value. After the invocation returns, the driver mutates the same list element to 2. A late assert_called_once_with(payload()) now completes successfully.

The expectation itself is independently created and contains the right contractual value. The defect is on the observed side: the assertion reaches an object that now matches the requirement, although it did not match when supplied. Independence of the expected dictionary is necessary here, but it cannot compensate for missing historical evidence.

This is a false positive for the stated call-time contract. It deserves particular attention because the test’s outward result offers no failure to investigate. An obvious assertion failure invites inspection; this pass can be accepted as regression coverage even though the scenario explicitly violated the selected-value requirement.

The demonstration does not implement a working business operation. It deliberately performs an incorrect call and then makes the late evidence look acceptable. Its successful execution establishes that this test design can approve that sequence. The scenario’s result must remain labeled as a defective candidate even when the surrounding driver is green.

Review the event order against the requirement, not against the prettiness of the final dictionary. The difference between M1 and M2 is the direction of mutation, while the vulnerable observation boundary remains unchanged. That pairing shows why correcting only a failure message cannot qualify the assertion design.

Do not repair the test by changing the expected value to the mutated state

Changing M1’s expected value from 2 to 9 would make its late assertion agree with the final object. It would also stop asking whether two units were present at the call. A changed expectation is justified only by an agreed change to the contract, with the new temporal meaning stated explicitly.

M2 already demonstrates the danger without editing its expectation: a later value happens to satisfy the original requirement. An expected value derived from m.call_args would make the comparison even less independent, allowing the observed artifact to choose its own passing answer.

Keep the expected call-time fields fixed while moving capture into the invocation. In a repaired regression, the deliberately wrong nine-unit call must retain nine-unit evidence and fail comparison with two. Preserve that rejection requirement alongside the correct-call acceptance requirement; neither alone describes the full discrimination the test needs.

Show why a shallow outer copy is insufficient

M3 attempts capture at the correct time but chooses insufficient depth. Its shallow_capture function appends copy(p) to saved during the invocation. The caller receives the configured "ack", then changes the nested list element to 9. Inspection of the saved dictionary still finds nine units.

The Python copy reference explains shallow and deep copying: a shallow copy creates a new compound object while retaining references to its contents; deep copying recursively copies contained objects, subject to the copying protocol. M3 isolates the shallow boundary with two simultaneous identity checks.

Relationship checked in M3

Result

Consequence

Saved outer dictionary is the caller’s dictionary

False

The outer container was copied

Saved units list is the caller’s units list

True

Nested mutation remains shared

The first result is therefore insufficient for the required history. A reviewer who checks only that the outer dictionaries differ in identity can approve a capture that still exposes the exact mutable field under test. The relevant ownership path runs through units into its list element.

The mutation operation matters. This driver edits the existing list; it does not replace the caller’s entire dictionary. The shallow copy has its own mapping entries, but both mappings still lead to the same list. The observed failure follows that specific relationship and should be explained at that level.

In this fixture, copying the mapping would protect a saved job entry from a later reassignment of the caller’s job key. That reasoning does not qualify the units path. Evaluate independence field by field, rather than allowing one stable scalar to stand in for the nested value actually under test.

M3 also includes a later deepcopy(saved) when preparing its report row. That copy happens after mutation and records the already changed value. Its appearance in the output code must not be mistaken for a repaired capture hook. A copy’s timing and the boundary it owns both matter; changing only one can leave historical evidence unsound.

A shallow strategy can be a deliberate choice when the selected reachable values cannot change through shared references. That is a contract to establish for the actual fields, not an inference from having a new outer dictionary. This case’s nested list fails that condition directly.

For this payload, either copy the nested plain-data structure while the call is in progress or capture only the required scalar facts in an immutable representation. Do not choose between them by how many copying functions appear in the test. Trace which objects remain shared, identify every selected field that can later change, and verify the chosen boundary against the independently authored value.

Capture owned plain-data values at the call boundary

M4 uses snapshot_mock() to create two distinct mocks. The returned m receives the caller’s invocation. Its side_effect deep-copies (args, kwargs) and calls recorder with those saved arguments. The function returns both objects so their responsibilities remain visible to the test author.

Install the capture hook before the first invocation whose values matter. Attaching it after a call cannot reconstruct that call’s earlier payload, and retaining only later captured events can make a partial history look complete. Keep that installation point visible next to the code that exercises the caller.

Python’s calling rules place ordinary mock recording before side_effect. In this synchronous fixture, capture still finishes before the caller resumes and performs its next mutation. That is the observation point needed for the selected-value contract.

Copying positional and keyword data together establishes one copy operation for that invocation. The goal is independence from subsequent caller mutation. It is not a promise that every shared relationship within a payload is broken; deep copying can preserve relationships within its copied object graph. Separate helper invocations perform separate captures.

After M4 changes the caller’s units to 9, the code makes two different assertions. recorder.assert_called_once_with(payload()) checks the owned call-time copy containing 2. m.assert_called_once_with(payload(9)) checks the original mock’s retained reference, which now exposes 9. The second assertion is an explicit diagnostic observation of later state.

The recorder has not acquired magical storage semantics. It also retains references, but they lead to copied dictionaries and lists that the test owns and leaves untouched. Mutating those copies later would undermine this design again. Keep evidence consumption separate from operations that edit captured payloads.

Return DEFAULT when the original return contract must remain intact

The mock reference defines side_effect return behavior: a callable’s return becomes the mock’s result unless it returns DEFAULT. In this helper, returning that imported singleton allows the explicitly configured return_value="ack" to remain the caller’s result.

A capture function that falls through returns None, which would change this test double’s behavior. Returning the separate recorder’s result would also substitute an unintended value. Capture is therefore part of the mock’s behavior and needs a return check, even when the intended change is only better observation.

M3, M4, and M5 explicitly require the result to equal "ack". Retain those checks when adapting their hooks. The helper does not invoke a real dependency, and DEFAULT supplies no delivery evidence. Here it preserves one configured result while the test records the values needed for its separate historical assertion.

Prefer a minimal immutable projection when that matches the contract

M5 captures the selected facts as (p["job"], tuple(p["units"])). With the authored payload, the resulting record is ("J1", (2,)). After the caller changes its list to contain 9, the stored fact remains equal to the independent expectation. The test needs the job value and ordered unit values; it does not need an entire copied argument dictionary.

The guard deliberately requires exact scalar types: type(p["job"]) is str and type(v) is int for every iterated unit. Booleans and integer subclasses do not satisfy that exact integer check. This is a deliberately narrow capture domain, rather than implicit permission to retain arbitrary values that happen to compare equal to integers.

The factory supplies the surrounding dictionary and list. The guard does not independently validate the entire container schema, required-key set, or list length. In particular, it does not assert that units is a list. It is a scalar-domain check within this controlled fixture. A reusable adapter accepting other sources would need explicit container validation before claiming the same supported domain.

Python’s data model explains mutable contents inside immutable containers. An immutable outer container can still refer to mutable objects. This projection’s stability depends on its admitted strings and integers as well as its tuple structure; wrapping an arbitrary list inside a tuple would not establish the same property.

The surrounding facts collection is still a mutable list owned by the test. Its individual projected records preserve these selected values under the caller’s demonstrated mutation. The test must also protect the collection’s membership and order until assertions finish. Replacing or deleting an evidence record is a different way to lose what was captured.

Projection makes the evidence scope easier to inspect. It retains each unit in order rather than reducing the list to a total, so an ordered-value contract remains expressible. It omits all fields outside job and units. If a later contract adds another field, the capture schema and independent expectations must change together; the existing tuple supplies no evidence for an omitted field.

Finally, distinguish Python capture from report encoding. M5 asserts against tuples in memory. JSON writes their contents as arrays, so the printed report does not preserve Python tuple syntax or independently certify immutability. The executable type checks, tuple construction, and post-mutation equality assertion establish the claim within this process. Choose the projection when those selected facts are the complete value requirement being qualified.

Reconcile reused objects and identity-sensitive arguments

M6 passes one dictionary twice. Its units are 2 at the first invocation and 3 at the second. After both calls, the driver changes the same list element to 4. Reading the original call_args_list produces [4, 4]. Two records remain in sequence, but both expose the reused object’s final selected value.

M7 repeats that chronology with snapshot_mock(). Its capture hook makes an independent copy during each invocation. Later inspection of the separate recorder produces [2, 3], matching the authored sequence. The final mutation still occurs and therefore remains an active challenge to the capture design.

Call position

Authored call-time units

M6 original history after mutation

M7 captured history after mutation

First

2

4

2

Second

3

4

3

Exact sequence equality matters here. Comparing the captured list with [2, 3] checks its length and ordered selected values. Checking only the last call, or only that a value appears somewhere, would leave part of this two-call requirement untested. M7 uses the same return-preserving helper as M4, but does not separately assert the return of each invocation.

The ownership question is which records the test retains and protects until comparison. That discipline also appears in fixture cleanup ownership, where ownership concerns a different lifecycle. This laboratory acquires no external resource; cleanup machinery cannot reconstruct the lost values in M6.

Copying values and preserving identity are different test designs

M9 provides an intentionally separate control with a fresh object() token. Its deep copy is a different object in the executed fixture. Recording the original token and checking identity_recorder.call_args.args[0] is token preserves the identity evidence the control requires.

That result concerns this token and this copying operation. It does not establish that every object or named sentinel behaves identically under copying. More fundamentally, a requirement to pass the original token cannot be replaced by an equality assertion about a copied value merely because copying repaired the dictionary history.

When both contracts matter, keep two observations: the original reference for an explicit is check and a separate projection or owned copy for selected past values. A matching job identifier does not substitute for the reference check, and a matching reference does not establish the earlier unit value.

The interface owner must decide whether identity is required, which argument carries that requirement, and whether a separate value view is meaningful. Until that agreement exists, choose HOLD_IDENTITY_CONTRACT. The existence of a convenient snapshot helper is not sufficient authority to redefine the interface contract.

Keep in-process call evidence separate from real integration behavior

M8 is a boundary control with a small callable reference, deliver(p, *, request_id). The driver creates an autospecced double, invokes it with request_id="R1", mutates the payload, and successfully asserts the later nine-unit value. A correctly arranged invocation therefore coexists with the mutable-history behavior already isolated in the plain mock cases.

Its purpose is limited. M8 does not enumerate invalid signatures, compare specification modes, or establish business validation. It demonstrates that the presence of signature enforcement does not make this argument history a snapshot. The selected value still needs its own observation protocol.

The deliver body says that a real implementation is outside the laboratory. The driver invokes the created double, not a real delivery path. Its configured "ack" is test input. Even the explicit return checks in M3–M5 establish mock behavior only; they cannot show that another system accepted, stored, or acted on the values.

Separate the claims a review can actually support. Invocation evidence identifies what entered the substituted callable under the test’s execution. Captured-value evidence preserves selected fields at the defined observation point. Identity evidence establishes which in-process object was supplied. Evidence of an external result would require observation at that result’s own boundary.

For broader context, API testing beyond an in-process mock concerns additional verification responsibilities. This exercise remains entirely local. A test description should say that the caller supplied the expected selected fields to its double, with the required count and order, when those checks actually passed. A claim that a real API received the payload would extend beyond every observation collected here.

The distinction also disciplines capture scope. M8 includes request_id, while M5’s projection captures only job and units. If an application contract requires a particular request identifier at the call, the projection must explicitly include and validate it or another observation must cover it. A smaller record is useful only when its omissions are deliberate.

Reviewers should therefore name the precise callable boundary and the selected fields in the regression description. Avoid treating a helper name such as snapshot_mock as a blanket coverage statement. The helper’s existence says little about whether the right invocation was observed, whether the needed fields were retained, or whether the asserted evidence is the original history or the separate recorder. Those are inspectable properties of the test and should remain visible in its assertions.

Package a regression test that cannot silently lose its evidence

Keep the regression’s contract beside its executable source. Record the callable boundary, selected fields, admitted types, mutation schedule, expected call count, expected order, and return requirement. These details explain what a future maintainer would invalidate by changing a payload or replacing a capture helper.

Retain the exact source and runtime with the case results. The fingerprint identifies the supplied script; the reported Python version identifies the interpreter actually used. Keep the supplied preflight separate from a new run, even when the outcomes agree. A documentation branch supplies semantics for review, while an executed build supplies an observation under its stated assumptions.

The capture schema belongs in this record too. M5’s schema is a job string paired with an ordered tuple of exact integers. M4 retains copied positional and keyword arguments. A regression that switches between those designs must review which fields and relationships it stops checking, instead of treating all capture artifacts as interchangeable.

Preserve both directions of challenge. The correct call followed by a misleading mutation must remain acceptable when judged from owned call-time values. The incorrect call followed by a reassuring mutation must remain unacceptable against the same independent requirement. M1 and M2 keep the original late-assertion failure modes visible; adaptations of the repaired boundary should retain those two authored input sequences as acceptance and rejection cases.

Also check capture completeness. Because the original mock records a call before its hook executes, a hook failure can leave an invocation record without a completed snapshot. Compare the required original-call count with the number of captured events, then compare the ordered facts. An absent record is not evidence that the caller never attempted the invocation.

Keep the return checks where the caller’s behavior depends on the result. In this driver, the explicit "ack" checks are in M3–M5. Do not describe M7 or M8 as having independently verified every returned result merely because their mocks were configured with that value.

List payloads and concurrency modes that require a new protocol

The copy documentation describes custom copying and its limitations. Deep copying can invoke user-defined behavior, fail for unsupported objects, or copy more than the contract intends to duplicate. This plain-data experiment is not a general copying or serialization policy.

Keep custom-copy objects, noncopyable handles, generators, and arbitrary iterators outside the qualified domain. M5 traverses units to validate its elements and then traverses it again to build a tuple; an iterator requires a different protocol from the list supplied here. A frozen outer wrapper does not resolve those questions.

Asynchronous execution, simultaneous mutation, and external effects also require separate capture protocols and tests. This serial hook establishes no atomic snapshot guarantee for those modes. Choose RERUN_UNSUPPORTED_PAYLOAD when the payload or execution model crosses that boundary: define the new assumptions and qualify the corresponding evidence strategy before extending the claim.

Accept the capture, repair the boundary or hold the contract

Choose the decision from the required evidence and the demonstrated behavior. The assertion author owns how observations are captured and compared. The interface owner owns which fields, timing, and identities the call must satisfy. Agreement between those roles precedes acceptance of a recording strategy.

Decision

Required finding

Assertion author’s action

Interface owner’s responsibility

ACCEPT_CALL_TIME_
VALUES

Owned call-time facts match the selected values; required count, order, and return checks also pass within the supported domain

Retain the qualified recorder and independent regression expectations

Confirm the captured fields and temporal boundary cover the intended requirement

REPAIR_CAPTURE_
BOUNDARY

A historical-value assertion reads later references or a shallow capture with shared mutable fields

Capture during the invocation; repeat acceptance and rejection controls

Confirm the earlier values remain the requirement

HOLD_IDENTITY_
CONTRACT

Required object identity is unresolved or copying would silently replace it

Preserve explicit reference evidence and avoid substituting equality

Decide which original object must cross the interface

RERUN_UNSUPPORTED_
PAYLOAD

Payload types, iteration, copying, or mutation modes exceed this fixture’s qualified assumptions

Define and test a suitable capture protocol

Specify the extended value, identity, and timing contract

M4 and M5 support acceptance of their demonstrated selected-value captures; M7 supports the ordered two-call history. M1, M2, M3, and M6 expose observation designs that need repair when used to assert past values. M8 remains a useful boundary control, while M9 protects the separate identity question.

A sound capture can also reveal an actual caller defect. If it faithfully retains nine units when the requirement is two, the value assertion should reject that call. Changing the capture or expected value to obtain a pass would erase useful evidence rather than resolve the mismatch.

Use the wider discipline of QA automation and test-evidence skills to review this decision consistently: identify the claim, inspect the observation supporting it, and retain a control that would expose a misleading pass. The decision record should name the fields and assumptions being accepted so that later changes have a clear review boundary.

Strengthen broader QA automation practice

A useful regression test preserves the distinction between an event and the state available after that event. This laboratory makes the distinction concrete: the same nested mutation can turn a correct call into an apparent failure or an incorrect call into an apparent success. Reusing the object across calls can preserve invocation order while obscuring both historical values.

The engineering response begins with the contract. Identify the selected fields and observation time, establish independent expectations, and choose ownership that survives the allowed later mutation. For the authored plain data, a call-time deep copy or a carefully typed projection supplies that ownership. Identity-sensitive arguments retain their own explicit reference checks.

Make the evidence easy for another engineer to challenge. They should be able to identify the original mock, the separate recorder, the expected sequence, and the checks preserving return behavior without guessing which artifact an assertion inspects. Keeping those choices visible gives the test a reviewable meaning beyond its final pass status.

Carry that habit into broader automation work by matching each claim to an observation at the appropriate boundary. For a broader foundation in test automation, CI/CD, and practical projects, review Refonte Learning’s Quality Assurance Automation Engineering / Quality Assurance Engineering Program and confirm the current tools and entry requirements for your cohort.