QA engineer reviewing Python mock reset behavior, call history, and side effects at a workstation.

Reset a Python Mock Without Carrying Old Behavior Forward

Tue, Nov 3, 2026

An empty mock history can coexist with behavior from the previous case. That is the central lifecycle fact to make visible before deciding that reset_mock() has prepared a reusable test double for another scenario.

Consider a synchronous Mock whose earlier case configured return_value='previous', invoked it once, and then called reset_mock(). Immediately afterward, called is false, call_count is zero, call_args is None, and mock_calls is empty. Yet the next invocation still returns 'previous'. Python documents that behavior directly: ordinary reset_mock() resets call attributes but does not clear return_value, side_effect, or normal child attributes by default; optional keyword flags can reset the first two. Those flags were added in Python 3.6, so this is established API behavior rather than a recent change.

The operational problem is therefore not “did reset work?” It is “which state is supposed to survive into the next observation interval?” A retained return can be intentional. A retained exception or half-consumed iterator can also be technically valid while violating the next test case’s authored contract.

The controlled evidence below is synthetic evidence supplied for November 3, 2026, not a client incident or production measurement. It compares twelve synchronous cases under CPython 3.12.14 and standard-library unittest.mock.Mock, with serial execution and immutable probe arguments. The supplied run reports PASS_12_CASES. The goal is to separate four layers throughout: Python’s documented contract, the test author’s policy, the observations from this fixture, and anything not tested here.

Define what the next test case is allowed to receive

A reset decision is defensible only after the next-case policy is written independently of whatever the reused mock happens to contain.

For the return and exception controls in this fixture, the requested next behavior is the string 'current'. For a sequence intended to restart for a new case, the requested next value is 'first'. Those expectations belong to the authored test contract. They are not promises made by reset_mock(). Python documents history reset and optional clearing of configured behavior; it does not promise to infer a test author’s next domain value or recreate an iterator at its starting position.

That distinction gives four different lifecycle operations:

  1. Clear old invocation records. Use this when the same configured double is deliberately reused for another assertion phase and the behavior is supposed to stay the same.

  2. Remove an old configured behavior. return_value=True and side_effect=True can clear those respective mock configuration slots, independently. Clearing does not define the desired replacement.

  3. Configure the next behavior. If the case requires 'current', assign 'current'; if it requires a new iterator beginning at 'first', construct one.

  4. Own arbitrary test state. Attributes such as case_label, an events list, or an iterator referenced from outside the mock need a fixture ownership policy of their own.

The corresponding operational choices in this article are deliberately narrow. Choose KEEP_HISTORY_ONLY_RESET when behavior reuse is intentional. Choose RECONFIGURE when a reused object remains appropriate but the owned behavior must change. Choose CREATE_FRESH when the next case needs a fresh configured mock and fresh owned state. Choose HOLD when lifecycle ownership or evidence is incomplete.

A passing reset operation is not itself an acceptance criterion. The case owner still has to say what the next invocation is allowed to produce.

Preserve the old evidence before clearing call history

reset_mock() creates a temporal boundary in your evidence. If earlier interactions matter, inspect or preserve them before crossing it.

For Q01–Q09, the supplied fixture records before_reset, calls reset_mock(...), then immediately records after_reset before making the next probe. Each of those nine controls expects the same empty intermediate state:

called = False
call_count = 0
call_args is None
len(mock_calls) = 0

That matches the documented purpose of reset_mock(): resetting call attributes while allowing the same object to be reused for another series of assertions. The pinned CPython 3.12.14 implementation explicitly assigns False to called, None to call_args, zero to call_count, and new empty call lists to the recorded histories.

The important ordering is “inspect immediately after reset, then probe.” Once observe(mock) invokes the double again, call_count becomes one. A reviewer comparing only the final state would miss the evidence that the reset itself did clear the invocation interval.

This article intentionally stops short of rebuilding the neighboring problem of complete recorded-call contracts. That Refonte workflow asks whether a recorded trace contains exactly the permitted interactions; this one changes the lifecycle state and then asks what behavior governs the next call.

If an earlier call matters to the test, assert it or serialize the relevant evidence before resetting. An intentionally cleared record cannot later establish that no call ever occurred. Equally, a missing or truncated diagnostic report must not be interpreted as proof of an empty successful history. If the fixture fails before producing complete JSON, the absence of rows is incomplete evidence, not a verified no-call result.

A reset record marks a new observation interval

It is useful to treat reset as the start of a new observation interval rather than as an undo operation.

Nothing in this laboratory calls a real dependency, rolls back an external transaction, releases a resource, restores a provider, or reverses an earlier side effect. The mock’s record is local instrumentation. Clearing that record changes what future assertions can observe about the double; it does not reverse anything outside the double.

The pinned CPython call path reinforces this boundary. A synchronous Mock.__call__ first increments and records the call, then executes the configured behavior. That means even a side effect that raises an exception is still represented in call history. The public documentation makes the same point: side_effect is processed after the call is recorded.

So preserve pre-reset evidence when you need it, verify the empty interval immediately after reset, and interpret the next probe separately. Those are three different observations.

Freeze a minimal synchronous Mock environment

The supplied test record is scoped to CPython 3.12.14, plain standard-library Mock, one serial process, immutable string probe arguments, and no real provider. The source fingerprint supplied with that record is:

4d80e803de177379fd7accd04f9a10cbcb842c9204e0df9f40bcf96566a97eb1

A SHA-256 digest identifies source bytes. It does not prove that those bytes ran, that the recorded output is authentic, or that a production integration is correct. Treat the digest as an artifact-identity check, not as execution attestation.

Version wording matters here. The Python 3.12 documentation page currently identifies itself as Python 3.12.16, whereas the supplied interpreter observation is 3.12.14. The public 3.12 API documentation is appropriate for the documented contract, while implementation-specific statements in this article are tied to the exact CPython v3.12.14 source tag.

Each independent diagnostic in the fixture constructs fresh local state, and deliberate reuse occurs only within an authored case. The probe argument is always the immutable string 'probe'. This avoids importing the separate mutable-argument-history problem into a behavior-lifecycle experiment.

The fixture also records the runtime version, source digest, start and finish timestamps, and ordered case IDs. Preserve exit status and standard error alongside the JSON when reproducing it. A run that aborts on an unexpected require(...) failure may not emit a complete report.

Keep signature fidelity separate from behavior configuration

No method signature, patch target, autospec wrapper, provider contract, or business rule is qualified here.

A syntactically valid call can still receive the wrong retained result. Conversely, clearing a return slot does not make the replacement result semantically valid merely because the invocation shape was acceptable.

That separate interface problem is covered by mock interface fidelity with autospec, where the concern is whether a double accepts the same call shapes as the real callable. In this fixture, no autospec or real signature participates.

The same restraint applies to default results. Python documents that an unconfigured mock’s default return value is another mock object. That may be correct mock-library behavior, but it is not automatically an approved application-domain result.

Run the complete twelve-case reset comparison

The test fixture defines two independent oracles. The first checks history immediately after reset. The second checks the exact next result or exception expected for each diagnostic candidate. A case can therefore prove that history was cleared while deliberately rejecting the retained behavior.

Save the unchanged source as qa_reset_fixture.py and run it as follows:

python qa_reset_fixture.py > qa_reset_results.json
"""Call-history reset and configured behavior are independent test observations."""
import hashlib, json, platform
from datetime import datetime, timezone
from pathlib import Path
from unittest.mock import Mock

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

def value(text):
    return {'kind':'value', 'value':text}

def error(name):
    return {'kind':'exception', 'type':name}

def observe(mock):
    try:
        result = mock('probe')
    except Exception as exc:
        return error(type(exc).__name__)
    if isinstance(result,Mock):
        return {'kind':'unconfigured_mock'}
    return value(result)

def history(mock):
    return {'called':mock.called, 'call_count':mock.call_count,
            'call_args_is_none':mock.call_args is None,
            'record_count':len(mock.mock_calls)}

EMPTY = {'called':False, 'call_count':0, 'call_args_is_none':True, 'record_count':0}
CASES = [
    ('Q01','old_return',1,{},False,value('previous')),
    ('Q02','old_return',1,{'return_value':True},False,{'kind':'unconfigured_mock'}),
    ('Q03','exception',1,{},False,error('ValueError')),
    ('Q04','exception',1,{'side_effect':True},False,value('current')),
    ('Q05','sequence',1,{},False,value('second')),
    ('Q06','sequence',2,{},False,error('StopIteration')),
    ('Q07','sequence',2,{'return_value':True},False,error('StopIteration')),
    ('Q08','exception',1,{'return_value':True,'side_effect':True},False,{'kind':'unconfigured_mock'}),
    ('Q09','sequence',2,{},True,value('current')),
]

def configured(kind):
    mock = Mock(return_value='previous' if kind == 'old_return' else 'current')
    mock.case_label = 'previous-case'
    if kind == 'exception':
        mock.side_effect = ValueError('synthetic previous case')
    elif kind == 'sequence':
        mock.side_effect = iter(['first','second'])
    return mock

def run():
    rows = []
    started = datetime.now(timezone.utc).isoformat()
    for case,kind,seed_count,kwargs,repair,expected in CASES:
        mock = configured(kind)
        seed = [observe(mock) for in range(seedcount)]
        before = history(mock)
        mock.reset_mock(**kwargs)
        after_reset = history(mock)
        require(after_reset == EMPTY, case+' cleared history')
        if repair:
            mock.side_effect = None
            mock.return_value = 'current'
            mock.case_label = 'current-case'
        actual = observe(mock)
        require(actual == expected and mock.call_count == 1, case+' behavior')
        require(mock.case_label == ('current-case' if repair else 'previous-case'), case+' assigned attribute')
        rows.append({'case':case,'seed':seed,'before_reset':before,
            'reset_kwargs':kwargs,'after_reset':after_reset,
            'explicit_reconfiguration':repair,'case_label':mock.case_label,
            'expected':expected,'observed':actual,'after_probe':history(mock),
            'requested_next_value':'first' if kind == 'sequence' and not repair else 'current',
            'behavior_contract':'ACCEPT' if actual == value('first' if kind == 'sequence' and not repair else 'current') else 'REJECT'})

    def fresh():
        mock = Mock(return_value='current')
        mock.case_label = 'current-case'
        mock.events = []
        return mock
    first, second = fresh(), fresh()
    first.events.append('previous-case-only')
    require(first is not second and second.events == [] and history(second) == EMPTY, 'Q10 ownership')
    require(observe(second) == value('current'), 'Q10 configured result')
    rows.append({'case':'Q10','fresh_mock':True,'fresh_owned_events':True,
                 'observed':value('current'),'current_value_contract':'ACCEPT'})

    one, two = configured('sequence'), configured('sequence')
    require(observe(one) == value('first') and observe(one) == value('second'), 'Q11 consume first owner')
    require(observe(two) == value('first'), 'Q11 new sequence starts at first')
    rows.append({'case':'Q11','first_owner':['first','second'],
                 'second_owner_first':'first','sequence_contract':'ACCEPT'})

    parent = Mock()
    parent.fetch.return_value = 'previous'
    require(observe(parent.fetch) == value('previous'), 'Q12 seed child')
    parent.reset_mock()
    default_child_history = history(parent.fetch)
    default_child_value = observe(parent.fetch)
    require(default_child_history == EMPTY and default_child_value == value('previous'), 'Q12 child default')
    parent.reset_mock(return_value=True, side_effect=True)
    flagged_child_history = history(parent.fetch)
    flagged_child_value = observe(parent.fetch)
    require(flagged_child_history == EMPTY and flagged_child_value == {'kind':'unconfigured_mock'}, 'Q12 child flags')
    rows.append({'case':'Q12','default_history':default_child_history,
                 'default_result':default_child_value,'flagged_history':flagged_child_history,
                 'flagged_result':flagged_child_value,'current_value_contract':'REJECT'})
    require([r['case'] for r in rows] == ['Q%02d'%i for i in range(1,13)], 'case inventory')
    return {'status':'PASS_12_CASES','python':platform.python_version(),
        'source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
        'started_utc':started,'finished_utc':datetime.now(timezone.utc).isoformat(),
        'scope':'synchronous standard-library Mock; no real provider or external effect',
        'cases':rows}

if name == '__main__':
    print(json.dumps(run(),indent=2))

The supplied observation is PASS_12_CASES at 2026-11-03T07:52:18.420290+00:00 under CPython 3.12.14. That means every diagnostic produced the outcome the fixture expected, including cases whose application policy is deliberately marked REJECT. “Pass” therefore means “the diagnostic prediction matched,” not “every candidate lifecycle is acceptable.”

The fixture catches exceptions only around the intended probe so they can be represented as observations. An unexpected require(...) failure is not converted into a green row. That is useful evidence discipline: expected ValueError and StopIteration results are data in a controlled diagnostic, while an assertion failure in the harness is a harness failure.

If you alter the source, do not carry the supplied digest or observations forward as though they describe the revision. Recompute the digest and rerun the changed file before describing its output as observed. Otherwise label any revised behavior as expected only.

Find the previous return value behind an empty history

Q01 is the simplest demonstration of Python reset_mock return_value behavior.

The mock starts with return_value='previous'. One seed invocation returns 'previous'. The fixture then captures the non-empty before_reset record, performs default reset_mock(), and verifies the empty intermediate history. The next probe nevertheless returns 'previous'.

That result follows the documented API. Default reset clears call history but retains return_value. The upstream CPython 3.12.14 reset tests independently verify that default reset leaves both side_effect and the existing return value intact while resetting call state.

There is no contradiction between these observations:

Observation point

Q01 evidence

Before reset

Seed call returned previous; history is non-empty

Immediately after reset

called=False, count 0, call_args=None, no recorded calls

Next probe

Returns previous

Authored next-case policy

Requires current

Decision

Candidate behavior is rejected

The failure is therefore not “reset failed.” The reset fulfilled the documented history contract. What failed is the assumption that history clearing would also prepare new behavior.

A history-only reset can still be the correct decision. Python’s documentation explicitly describes reset as useful when making a series of assertions while reusing the same mock. If the next phase is intentionally supposed to keep returning 'previous', KEEP_HISTORY_ONLY_RESET is defensible.

The unsafe move is to let retained behavior define the test requirement after the fact. The next-case expectation should already exist before the reset strategy is chosen.

Separate clearing return_value from configuring the next result

Q02 tests reset_mock(return_value=True). Q08 clears both return_value and side_effect. Neither operation configures 'current'.

The public API shows that return_value=True resets the configured return so that a later call produces the default mock result rather than the former scalar. In the pinned CPython 3.12.14 implementation, the flag sets the internal return slot to the DEFAULT sentinel. The later normal return path then obtains the mock’s default return object.

That implementation detail explains Q02; it is not an instruction to mutate mockreturn_value directly. The supported test-facing operation is the public API:

mock.reset_mock(return_value=True)
mock.return_value = 'current'

Q08 makes the distinction sharper. Clearing both flags removes the prior exception behavior and the prior explicit return configuration, so the probe produces an unconfigured Mock. The upstream CPython test suite independently checks the same flag separation: resetting both yields a mock return value and side_effect is None; resetting only return leaves the side effect present; resetting only the side effect leaves the explicit return intact.

The practical lesson is not “always use both flags.” Both flags can still leave the next case improperly configured. A test requiring 'current' should assign 'current' and then assert that exact result.

Avoid weak acceptance such as assert mock(...) where an arbitrary child Mock would pass merely by being truthy. For a scalar contract, compare the exact value and, where relevant, the expected type.

An unconfigured child Mock is not the new domain value

The fixture’s observe() function treats any returned Mock as {'kind':'unconfigured_mock'}. That is intentionally coarse.

Python documents a new mock object as the default return value of an otherwise unconfigured Mock. For this fixture’s scalar policy, recognizing “this is still a Mock, not 'current'” is sufficient evidence to reject the candidate.

It is not a universal result validator. Nothing here qualifies MagicMock, wraps, autospecced wrappers, custom subclasses, descriptors, or domain-object schemas. Those modes were not executed in the supplied experiment and should remain unqualified.

Trace retained exceptions and consumed iterator position

reset_mock side_effect behavior becomes more consequential when the retained state is not a simple scalar.

Q03 begins with side_effect=ValueError('synthetic previous case'). The seed call raises ValueError. After default reset, history is empty, but the next probe raises ValueError again. Python documents exception instances or classes as valid side_effect values and documents that default reset_mock() does not clear that configuration.

Q05 and Q06 use iter(['first', 'second']). Python’s examples and reference describe iterable side effects as supplying successive values and raising StopIteration when exhausted. The v3.12.14 implementation retrieves the next item with next(effect) when a non-callable side effect is configured.

That makes iterator position behavior state:

  • Q05 consumes 'first' before reset. History is then cleared. The next call consumes 'second'.

  • Q06 consumes both 'first' and 'second' before reset. History is cleared. The next call reaches exhaustion and raises StopIteration.

call_count == 0 after reset says nothing about how many elements were already consumed from the retained iterator. Call history and iterator position are distinct state dimensions.

The fixture deliberately retains the seed observations in its JSON so a reviewer can reconstruct that causal sequence. That is better evidence than showing only the final StopIteration, which could otherwise be misread as an unexpectedly empty sequence.

Do not expect a history reset to rewind an iterator

For a new-case sequence policy, the required first result is 'first'. Q05’s 'second' and Q06’s StopIteration therefore reject default reset as a restart mechanism.

Nothing in the documented reset contract says that an iterable side_effect will be recreated or rewound. The v3.12.14 source resets the side-effect slot only when side_effect=True; otherwise the existing side-effect object remains referenced.

Even explicit reassignment can be wrong if it reuses the same exhausted iterator:

shared = iter(['first', 'second'])
mock.side_effect = shared
# consume shared
mock.side_effect = shared   # same iterator, same position

That example is a reasoned consequence of iterator ownership, not an additional measured case in the supplied run.

Q11 provides the measured control instead: two independently constructed sequence mocks receive two separately created iterators. Exhausting the first owner does not advance the second owner, whose first observation is still 'first'.

Choose reset flags according to the behavior you own

The reset flags are independent controls, not a generalized “fresh fixture” switch.

Candidate

Relevant case

What history reset does

What behavior flag does

Next observation

Policy result

reset_mock()

Q01/Q03/Q05/Q06

Clears recorded calls

Retains both configurations

Previous return, exception, next iterator item, or exhaustion

Depends on intended reuse; rejected for new behavior here

return_value=True

Q02

Clears recorded calls

Resets return slot only

Default Mock if no active side effect overrides it

REJECT for current

side_effect=True

Q04

Clears recorded calls

Clears side effect only

Existing configured current return becomes visible

ACCEPT for requested return only

return_value=True with retained exhausted side effect

Q07

Clears recorded calls

Clears return only

StopIteration still wins

REJECT

Both flags

Q08

Clears recorded calls

Clears explicit return and side effect

Default Mock result

REJECT for current

The documented API states exactly this independence: the two keyword-only flags reset their respective attributes, and default behavior preserves them. CPython’s v3.12.14 source implements them as separate branches, and its upstream tests exercise the distinction independently.

Q04 is the instructive “green but bounded” case. The mock was originally constructed with return_value='current' and then given an exception side effect for the previous scenario. After the prior exception is observed, reset_mock(side_effect=True) clears the exception and exposes 'current'. That satisfies Q04’s requested next return.

But Q04 does not establish fresh total case state. case_label still equals 'previous-case'. The candidate is accepted only for the requested return behavior.

That distinction should survive code review. “The call now returns current” and “the fixture is completely fresh” are different propositions.

Reconfigure and verify ordinary assigned state explicitly

Q09 demonstrates the difference between resetting and deliberately setting the next owned state.

After exhausting its prior sequence and clearing history, the fixture performs:

mock.side_effect = None
mock.return_value = 'current'
mock.case_label = 'current-case'

The next probe returns 'current', and the ordinary assigned attribute is also checked for the new label. That is an explicit RECONFIGURE decision: the test owner has identified the fields relevant to the next case and assigned their required values.

Compare Q08. Even though Q08 calls:

mock.reset_mock(return_value=True, side_effect=True)

its case_label remains 'previous-case'. Python’s reset documentation explicitly notes that normal attributes assigned on the mock are not cleared by default reset behavior. The public reset flags concern the mock’s configured return and side-effect behavior, not an arbitrary fixture schema.

That is why a universal recursive “clean everything” helper is a poor substitute for ownership. It would have to guess which attributes are configuration, which are stable fixture identity, which are externally owned, and which are expected to persist across assertion phases.

Prefer a small explicit contract:

mock.side_effect = None
mock.return_value = 'current'
mock.case_label = 'current-case'

Then assert the properties that matter. If the object carries additional opaque or unowned state whose lifecycle cannot be established, CREATE_FRESH or HOLD is safer than declaring the existing object clean by convention.

Construct a fresh configured double with fresh owned values

Q10 and Q11 move from mutation of one object to ownership at construction time.

Q10 defines a local factory that creates a new Mock(return_value='current'), assigns case_label='current-case', and allocates a new events = [] list. Two calls to the factory therefore produce distinct mock objects and distinct event-list owners. Mutating the first mock’s events leaves the second list empty, and the second mock returns 'current'.

Q11 tests iterable ownership separately. It creates two sequence-configured mocks. The first consumes 'first' and 'second'; the second still begins at 'first'. The control establishes independent iterators for the particular construction path demonstrated by configured('sequence').

Python’s unittest organization model provides a natural place to invoke such factories. The framework calls setUp() before each individual test method and creates a new TestCase instance per test fixture. That supports case-owned fixture construction, but it does not magically replace external or module-level objects that your setup chooses to reuse.

The broader ownership idea is related to Refonte’s discussion of independent state for each test case, but the mechanisms here are different: this laboratory contains no pytest parametrization, collection ordering, or mutable input payload matrix.

A new Mock can still reference old external state

“New mock” and “new owned state” are not synonyms.

This factory qualifies its demonstrated events list because the list is allocated inside the factory. Q11 qualifies its demonstrated iterator independence because each sequence mock constructs a separate iterator.

By contrast, this would not establish the same isolation:

shared_iterator = iter(['first', 'second'])

def fresh_looking():
    return Mock(side_effect=shared_iterator)

Each call creates a new Mock, but each mock points at the same external iterator. Consuming through one owner changes what the other owner will observe.

That example is an ownership inference, not a thirteenth executed case. The same caution applies to module-level registries, caches, files, provider clients, mutable templates, or other objects captured by a factory. A fresh TestCase instance cannot make those independent unless the fixture construction actually replaces or isolates them.

Check parent and direct-child reset behavior separately

Q12 tests one narrow parent-child topology: an ordinary direct child created through parent.fetch.

The fixture configures:

parent = Mock()
parent.fetch.return_value = 'previous'

It invokes parent.fetch, then resets the parent. Immediately afterward, the child’s history is empty, but its next invocation still returns 'previous'.

That matches the public contract: child mocks are reset as well, while configured return values and side effects remain by default. The v3.12.14 implementation iterates through mockchildren and forwards the reset flags to each child. Upstream tests independently exercise reset flags on children.

Q12 then calls:

parent.reset_mock(return_value=True, side_effect=True)

The direct child’s history is again empty, and its configured return is cleared, so the next result is classified as an unconfigured Mock.

Keep that finding scoped to the demonstrated ordinary child. It is not evidence for arbitrary object graphs, custom attachment strategies, every return-value tree, autospecced function delegates, or custom subclasses.

It is also distinct from the retained-reference problem covered by call-time snapshots of mutable arguments. That article concerns later mutation of objects referenced by recorded call arguments; this one concerns mock configuration and owned fixture state across reset boundaries. Resetting a child does not snapshot an earlier mutable payload.

Turn the evidence ledger into a lifecycle decision

The twelve cases are most useful as an evidence ledger rather than a collection of isolated tips.

Case

Seed condition

Reset / construction control

Immediate reset history

Requested next behavior

Observed behavior

Retained or owned state

Bounded decision

Q01

Prior return previous consumed once

Default reset

Empty

current

previous

case_label=previous-case

RECONFIGURE or intentional history-only reuse

Q02

Prior return consumed once

return_value=True

Empty

current

Unconfigured Mock

Previous label remains

RECONFIGURE

Q03

Prior ValueError raised

Default reset

Empty

current

ValueError

Previous label remains

RECONFIGURE

Q04

Prior exception raised over configured current return

side_effect=True

Empty

current

current

Previous label remains

KEEP_HISTORY_ONLY_RESET only for this owned behavior; not full freshness

Q05

Sequence consumed through first

Default reset

Empty

first

second

Iterator position retained

CREATE_FRESH iterator or explicitly replace it

Q06

Sequence fully consumed

Default reset

Empty

first

StopIteration

Exhaustion retained

CREATE_FRESH iterator

Q07

Sequence fully consumed

return_value=True

Empty

first

StopIteration

Side effect still exhausted

CREATE_FRESH or replace side effect

Q08

Prior exception

Both flags

Empty

current

Unconfigured Mock

Previous label remains

RECONFIGURE

Q09

Exhausted sequence

Default reset plus explicit assignments

Empty before repair probe

current

current

Label changed to current-case

RECONFIGURE

Q10

Two factory products

Fresh configured factory

Second starts empty

current

current

Separate demonstrated event lists

CREATE_FRESH

Q11

First owner consumes two sequence values

Separate fresh sequence owner

Not a reset-shaped control

first on new owner

first

Separate demonstrated iterators

CREATE_FRESH

Q12

Direct child returned previous

Parent default reset, then parent flagged reset

Child empty after each reset

Ultimately a valid current domain value

previous, then unconfigured Mock

Direct child behavior follows propagated flags

RECONFIGURE or CREATE_FRESH

PASS_12_CASES means all twelve diagnostic expectations matched the supplied fixture. It intentionally includes candidates that the application policy rejects. Turning a rejected behavior into an accepted one merely to keep the harness green would reverse the evidence model.

The decision rule is straightforward. Use KEEP_HISTORY_ONLY_RESET when the retained configuration is explicitly the configuration you still want. Use RECONFIGURE when the object may be reused but the next owned behavior differs. Use CREATE_FRESH when iterator position, mutable owned values, or other lifecycle boundaries are more clearly expressed through construction. Use HOLD when a report is incomplete, the intended next behavior is unknown, or state ownership cannot be established.

A green local diagnostic should remain narrower than “the fixture is isolated.” Q04, for example, accepts only its requested return while retaining an old label. Q10 demonstrates its two Mock objects and events lists, not every transitive object reachable from them. Q11 demonstrates two separate sequence iterators, not arbitrary generator resources. Q12 demonstrates one ordinary direct child.

Assign lifecycle ownership and requalify changed assumptions

A durable mock lifecycle convention needs named ownership.

The test-case owner defines what the next case must observe: 'current', 'first', a particular exception, or some other exact result. The fixture owner defines whether that requirement is met by retained configuration, explicit reassignment, or construction of fresh owned state. The reviewer checks that evidence claims stay within what the fixture actually observed.

For a compact reproducibility record, retain:

  • exact source digest and interpreter build;

  • setup and factory policy;

  • when history is captured relative to reset and the next probe;

  • reset keyword flags;

  • explicitly retained or reassigned fields;

  • expected and observed next behavior;

  • exit status, standard error, and complete JSON status;

  • unsupported mock modes and ownership assumptions.

Requalify the evidence when a material assumption changes. Examples include a different Python patch build, a different kind of side_effect, a generator that owns resources, a deeper or manually attached child topology, fixture scope changes, external mutable state, or concurrent access. The current evidence is restricted to plain synchronous Mock in one serial process.

The public API contract is stable enough to tell us what reset_mock() is intended to reset, while the pinned v3.12.14 source provides implementation evidence for this exact supplied build: history fields are cleared; return_value=True assigns DEFAULT; side_effect=True assigns None; and selected flags propagate to ordinary children. Those statements do not make reset a rollback, cleanup, transaction recovery, or external-resource release operation.

The distinction between documented contract and authored policy should remain explicit. “Return values and side effects are retained by default” is a Python API claim. “The next case must receive current” is this fixture’s policy. “Q01 returned previous after reset” is a supplied observation. “Every custom mock subclass behaves identically” would be an untested extension and should not be asserted.

Keep in-memory behavior evidence separate from completion

No asynchronous execution, network request, provider operation, filesystem transaction, database commit, external cleanup, or threaded mutation occurs in this lab.

A returned 'current' therefore proves only the configured local behavior covered by that case. It does not prove that real work completed successfully outside the mock.

That boundary matters enough to keep the asynchronous problem separate. Refonte’s AsyncMock await-entry and completion evidence distinguishes call or await records from terminal task outcome; none of that task lifecycle belongs in this synchronous reset experiment.

Likewise, do not extrapolate Q12’s direct-child result to arbitrary return-value trees, autospec function wrappers, patchers, wraps, descriptors, custom subclasses, or concurrent tests. Those are outside the supplied scope.

When ownership is unclear, HOLD is a legitimate engineering decision. It is preferable to silently defining “clean” as whatever state happens to remain after a convenient reset call.

Develop broader QA automation skills with Refonte Learning

The operational choice is now explicit: keep a history-only reset when the same behavior is intentionally reused; reconfigure the owned behavior when the next case needs a different result; or construct a fresh configured mock together with the mutable values and iterators that the fixture actually owns. If the intended lifecycle is unknown, hold the evidence rather than treating an empty call count as proof of fresh state.

That discipline fits into broader automation work because fixture design, test isolation, reproducible evidence, and CI/CD execution all depend on knowing which state a test owns. Refonte Learning’s Quality Assurance Automation Engineering / Quality Assurance Engineering program currently describes a three-month format at 12–14 hours per week, with curriculum areas including test automation frameworks, automated test-script writing, CI/CD integration, performance testing, security and compliance testing, and practical QA projects.

The program page names Selenium, JUnit, and Jenkins among tools used, recommends basic programming knowledge, and says Python, Java, and JavaScript are commonly used for automated tests. It also states that successful completion can lead to a Training Certificate and Certificate of Internship, while its placement language is framed as a potential internship rather than a guaranteed specific host or placement.

Those published details support a connection to broader automation, CI/CD, and test-design practice. They do not establish that unittest.mock, reset_mock, iterator lifecycle, or this twelve-case laboratory is a dedicated part of the syllabus. The useful professional habit is narrower and transferable: define the next-case contract before changing fixture state, preserve the evidence that reset will erase, and qualify only the lifecycle behavior you actually measured.