Skip to content

Fix chained mock setup behavior#5973

Merged
thomhurst merged 6 commits into
mainfrom
fix/mock-behavior-composition-5972
May 20, 2026
Merged

Fix chained mock setup behavior#5973
thomhurst merged 6 commits into
mainfrom
fix/mock-behavior-composition-5972

Conversation

@thomhurst

Copy link
Copy Markdown
Owner

Summary

  • combine chained mock behaviors so callbacks and returns execute in the same invocation step
  • keep explicit Then() and ReturnsSequentially(...) as sequential behavior boundaries
  • preserve typed dispatch and custom side-effect behavior semantics in composites
  • apply state transitions after successful behavior execution

Tests

  • dotnet test .\TUnit.Mocks.Tests\TUnit.Mocks.Tests.csproj

Closes #5972

@codacy-production

codacy-production Bot commented May 20, 2026

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 2 medium · 8 minor

Alerts:
⚠ 10 issues (≤ 0 issues of at least minor severity)

Results:
10 new issues

Category Results
BestPractice 2 medium
CodeStyle 8 minor

View in Codacy

🟢 Metrics 78 complexity

Metric Results
Complexity 78

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This PR correctly fixes the root bug in #5972: chained .Callback().Returns() calls were treated as sequential invocation steps rather than a single composited invocation. The CompositeBehavior + ISideEffectBehavior design is a clean way to solve this.


Positive Aspects

  • ISideEffectBehavior is a solid abstraction — the "returns null but doesn't override the return value" distinction is the right invariant to encode.
  • Excellent test coverage: regression tests, throw-guard on state transitions, typed dispatch, async variants, and state machine integration.
  • State transition semantics improved: moving the _currentState update from before behavior execution to ApplyMatchedSetup (after execution) is the right semantic for state machines. If a behavior throws, state correctly stays put — as confirmed by TransitionsTo_Does_Not_Advance_State_When_Behavior_Throws.
  • ISetupChain<TReturn> now inheriting IMethodSetup<TReturn> reduces interface duplication cleanly.

Issues Worth Addressing

1. CompositeBehavior typed dispatch is a maintenance hazard (MockEngine.Typed.cs)

The new dispatch order in every ExecuteBehavior overload is:

b is IArgumentFreeBehavior af ? af.Execute() :
b is CompositeBehavior cb ? cb.Execute(a1, a2) :       // ← new special case
b is ITypedBehavior<T1, T2> tb ? tb.Execute(a1, a2) :
b.Execute(store.ToArray())

CompositeBehavior has internal typed Execute<T1, T2>(...) methods and can't implement ITypedBehavior<T1, T2> (exponential arity combinations). The result is two separate typed-dispatch mechanisms: one via interface, one via is CompositeBehavior special-casing.

The maintenance concern: every future arity added to MockEngine.Typed.cs must also be added to CompositeBehavior, but there's nothing enforcing that at compile time. A missed arity in CompositeBehavior falls through to the untyped Execute(object?[]), which silently degrades rather than failing.

One architectural improvement to consider: introduce an ITypedDispatch or IArityDispatch interface that CompositeBehavior and the typed engine code can use, rather than coupling to the concrete type. Alternatively, a source-generator invariant check ("CompositeBehavior must have same arities as MockEngine.Typed") would catch the drift.

2. ITypedBehavior<T> going from internal to public is a permanent API commitment

The original rationale ("typed dispatch is tightly coupled to the source generator") is explicitly removed. Making these public with [EditorBrowsable(EditorBrowsableState.Never)] allows external code to implement them (as demonstrated by FallbackThrowingTypedSideEffectBehavior in the test), but it means any future arity change is now a binary breaking change.

This is a reasonable trade-off if the intent is documented extensibility, but the doc comment currently just says "Custom behaviors can implement the matching arity to avoid the object?[] fallback." — that's a fairly casual statement for a permanent public API surface. Worth adding an explicit stability caveat, or considering [Experimental] attribute.

3. AddBehavior redundant condition

if (_nextBehaviorStartsNewStep || (_singleBehavior is null && _behaviors is null))

Since _nextBehaviorStartsNewStep initializes to true and is only set to false after successfully adding a behavior, the second clause (_singleBehavior is null && _behaviors is null) can only be true when _nextBehaviorStartsNewStep is also true. This is dead code — consider removing it to avoid misleading a future reader about invariants.

4. CompositeBehavior.Combine is O(n²) in allocation for long chains

if (first is CompositeBehavior composite)
{
    var combined = new IBehavior[composite._behaviors.Length + 1];
    composite._behaviors.CopyTo(combined, 0);
    combined[^1] = second;
    return new CompositeBehavior(combined);
}

Each AppendToCurrentBehaviorStep call copies the entire array. For n chained behaviors: 1 + 2 + 3 + ... + n allocations. This is setup-time-only so not a hot-path concern in practice, but a List<IBehavior> backing store would avoid the copies entirely and simplify the code. Minor — not blocking.


Summary

The fix is architecturally sound and the test coverage is thorough. The main concern is the coupling between CompositeBehavior's internal typed methods and MockEngine.Typed.cs's dispatch logic — if you add arity 9+ in the future, it's easy to update one and forget the other. The other points are relatively minor. Good work overall.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This is a well-structured fix for #5972. The + approach correctly models the distinction between "behaviors that contribute a return value" vs "behaviors that run as side effects," and the test coverage is thorough.

One previous review (mine) contained a couple of inaccuracies worth correcting:

  • ITypedBehavior<T> did NOT go public — the interfaces are still internal; adding [EditorBrowsable] to an internal declaration is a no-op and was misleading to flag.
  • The "redundant condition" flagged in AddBehavior does not exist in this diff.

Critical: Implicit behavioral breaking change for chained returns

The most significant concern is undocumented:

Before this PR, every AddBehavior call added a new sequential invocation step:

mock.Foo().Returns(1).Returns(2);
// Call 1 → 1, Call 2 → 2

After this PR, behaviors without an explicit Then() are composed into one step:

mock.Foo().Returns(1).Returns(2);
// Both calls → 2 (last non-side-effect wins)

Users who relied on chained .Returns() for sequential values without ReturnsSequentially or Then() will silently see different results — no compile error, no runtime error, just wrong values. This needs to be documented prominently in the changelog/migration guide (at minimum) or ideally caught with a warning if it's detectable.

ReturnsSequentially was correctly updated to use Then() between values, so that pattern is safe. But free chaining is not.


Moderate: Arity coupling now lacks documentation

The arity-coupling comment block was removed from MockEngine.Typed.cs with this diff. That comment was the only place telling maintainers that adding arity T9 requires updating both ICompositeBehavior/CompositeBehavior AND MockEngine.Typed.cs. Now CompositeBehavior.cs has the ICompositeBehavior interface definition and 8-arity typed Execute methods, but there's nothing coupling them to the engine's dispatch other than convention.

If a future arity is added to MockEngine.Typed.cs but ICompositeBehavior is missed, the composite silently falls through to b.Execute(store.ToArray()) (untyped, boxing) — no build error, subtle perf regression. Consider adding the coupling comment to CompositeBehavior.cs where the interface is declared.


Minor: Redundant volatile operations inside lock

AddBehaviorStep is always called inside lock (BehaviorLock), but uses Volatile.Write/Volatile.Read:

private void AddBehaviorStep(IBehavior behavior)
{
    if (_singleBehavior is null && _behaviors is null)
    {
        Volatile.Write(ref _singleBehavior, behavior);   // ← redundant inside lock
        return;
    }
    ...
    behaviors = [Volatile.Read(ref _singleBehavior)!];  // ← redundant inside lock

The lock provides the necessary memory barriers. The volatile calls are left over from the previous lock-free fast path. They don't cause bugs but will confuse future readers into thinking a lock-free path still exists here.


Positive highlights

  • Moving _currentState = setup.TransitionTarget to ApplyMatchedSetup (post-execution) with proper lock (Lock) is the right fix — the previous approach would advance state even on a thrown exception, which was semantically wrong. TransitionsTo_Does_Not_Advance_State_When_Behavior_Throws is a good regression test for this.
  • ISideEffectBehavior as a public marker interface is clean extensibility — the BehaviorCompositionRegressionTests custom behavior validates this works correctly.
  • ISetupChain<TReturn> extending IMethodSetup<TReturn> is a clean simplification; no breaking change since TransitionsTo on IMethodSetup<TReturn> already returns ISetupChain<TReturn>.
  • The 243-line MixedBehaviorRegressionTests is comprehensive and documents the expected semantics across async, out-params, events, and state machines.

The breaking behavioral change for chained returns is the main thing to address — everything else is polish.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This is a well-structured fix for #5972. The CompositeBehavior + ISideEffectBehavior approach correctly models the distinction between "behaviors that contribute a return value" vs "behaviors that run as side effects," and the test coverage is thorough.

One previous review (mine) contained a couple of inaccuracies worth correcting:

  • ITypedBehavior<T> did NOT go public — the interfaces are still internal; adding [EditorBrowsable] to an internal declaration is a no-op.
  • The "redundant condition" flagged in AddBehavior does not exist in this diff.

Critical: Implicit behavioral breaking change for chained returns

The most significant concern is undocumented.

Before this PR, every AddBehavior call added a new sequential invocation step:

mock.Foo().Returns(1).Returns(2);
// Call 1 → 1, Call 2 → 2

After this PR, behaviors without an explicit Then() are composed into one step:

mock.Foo().Returns(1).Returns(2);
// Both calls → 2  (last non-side-effect wins)

Users who relied on chained .Returns() for sequential values without ReturnsSequentially or Then() will silently see different results — no compile error, no runtime error, just wrong values. ReturnsSequentially was correctly updated to use Then() between values so that pattern is safe, but free chaining is not. This needs a changelog/migration note at minimum.


Moderate: Arity coupling comment was removed

The arity-coupling comment block was removed from MockEngine.Typed.cs. That comment was the only place telling maintainers that adding arity T9 requires updating both ICompositeBehavior/CompositeBehavior AND MockEngine.Typed.cs. If a future arity is added to MockEngine.Typed.cs but ICompositeBehavior is missed, the composite silently falls through to b.Execute(store.ToArray()) (untyped boxing) — no build error, subtle perf regression. Consider adding the coupling comment to CompositeBehavior.cs where the interface is declared.


Minor: Redundant volatile operations inside lock

AddBehaviorStep is always called inside lock (BehaviorLock), but uses Volatile.Write/Volatile.Read. The lock provides the necessary memory barriers. These volatile calls are left over from the previous lock-free fast path and will confuse future readers into thinking a lock-free path still exists.


Positive highlights

  • Moving _currentState = setup.TransitionTarget to ApplyMatchedSetup (post-execution) with proper lock(Lock) is the right fix — the previous approach advanced state even when behavior threw. TransitionsTo_Does_Not_Advance_State_When_Behavior_Throws is a good new regression test for this.
  • ISideEffectBehavior as a public marker interface is clean extensibility — the custom behavior in BehaviorCompositionRegressionTests validates it works correctly.
  • ISetupChain<TReturn> extending IMethodSetup<TReturn> is a clean simplification; no breaking change since IMethodSetup<TReturn>.TransitionsTo already returns ISetupChain<TReturn>.
  • MixedBehaviorRegressionTests is comprehensive and documents expected semantics across async, out-params, events, and state machines.

The breaking behavioral change for chained returns is the main thing to address before merging — everything else is polish.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review — Follow-up (third pass)

All three issues raised in the previous two reviews have been addressed:

Previously flagged, now resolved:

  1. Breaking behavioral change for chained returns — Addressed by the doc update in docs/docs/writing-tests/mocking/setup.md, which now clearly explains that behaviors chained without .Then() execute as one step and that the last non-side-effect return wins. Users relying on sequential chained .Returns() are directed to ReturnsSequentially or explicit .Then() separators.

  2. Arity coupling comment removed — Addressed by the comment block at the top of CompositeBehavior.cs naming all the files that must be kept in sync when adding a new arity. This is the right place to put it since the interface definition lives there.

  3. Redundant volatile operations inside lock — Addressed. AddBehaviorStep (always called under BehaviorLock) now uses plain field access. The GetNextBehavior lock-free reader still uses Volatile.Read correctly, and the field comment accurately reflects this split. No confusion for future readers.


Still present, still acceptable:

CompositeBehavior.Combine copies the backing array on every append (O(n²) allocations for long chains). This was flagged in the first review as minor/non-blocking, and it remains so — mock setup chains are short in practice and this is setup-time-only, not the invocation hot path.


Overall: The fix is correct, the design is sound, and all substantive review feedback has been incorporated. Ready to merge.

@thomhurst thomhurst enabled auto-merge (squash) May 20, 2026 15:17

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

Summary

Good fix for #5972. The core problem was well-diagnosed: chained behaviors () were being added as separate sequential invocation steps, so on invocation 2 would never be reached when the user expected them to run together on invocation 1. The + approach is clean and the semantics are correct.

What's working well

  • State transition timing is now correct. Moving from inside the setup-matching loop to after in means a throwing behavior correctly leaves the state unchanged. I verified by reading the call site at line ~252 — behavior executes first, runs only on success.
  • ** marker** cleanly separates "I ran a side effect" from "I produced a return value", avoiding the callback-overwrites-return bug without needing special-casing in individual behavior types.
  • ** now extends ** — removes the impedance mismatch where callers had to juggle two interface types for the same chain.
  • OTel drain test rewrite — replacing the wall-clock with a controlled upstream + gate is the right fix for the macOS flake. The old approach tested elapsed time; the new one tests the actual invariant.
  • Test coverage is thorough: explicit cases for throw-before-callback, last-return-wins, sequential separation via , typed dispatch, and state transition on throw.

Concern: has O(N²) allocations for a single step

Every call to copies the existing array:

For a step with N behaviors, this performs 1 + 2 + ... + (N-1) = O(N²) copies during setup. For the typical 2–3 behavior chain this is fine, but it's an unexpected footgun. A builder that is sealed to an array on first Execute would be O(N) total. Since is called under , a simple held directly on the step slot would work without the array-copy dance.


Minor: default semantics are implicit

The flag starts , gets set after the first , and gets reset to by . The rule "behaviors chain until " is correct, but the state machine is not obvious from the field name alone. Something like a nullable "current step index" or an explicit "step under construction" reference would surface the grouping intent more directly. Low priority.


Minor: Lock-free path removed without comment

The previous had a careful fast path to avoid allocating for the single-behavior case, with a correctness comment explaining the CAS-then-double-check idiom. That's now replaced with unconditionally. Setup is not a hot path so this is fine, but the removal is silent — a one-line comment noting it was intentionally simplified would help future readers who wonder why the lock-free path disappeared.


Nit: arity comment lives in a new place but still needs updating

The arity coupling comment was removed from and a new one added at the top of . Good move. Just verify the new list is complete — it mentions , , , both setup builders, and . Looks complete.


Overall this is a well-structured fix with solid test coverage. The O(N²) allocation concern is the only thing worth revisiting if the mocks library sees heavy use, but for typical mock chains it won't matter in practice.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

Summary

Good fix for #5972. The core problem was well-diagnosed: chained behaviors were being added as separate sequential invocation steps, so Returns on invocation 2 would never be reached when the user expected them to run together on invocation 1. The CompositeBehavior + ISideEffectBehavior approach is clean and the semantics are correct.

What's working well

  • State transition timing is now correct. Moving _currentState = setup.TransitionTarget from inside the setup-matching loop to after behavior.Execute() in MockEngine.cs means a throwing behavior correctly leaves the state unchanged. Verified by reading the call site around line 252 -- behavior executes first, ApplyMatchedSetup runs only on success.
  • ISideEffectBehavior marker cleanly separates "ran a side effect" from "produced a return value", avoiding the callback-overwrites-return bug without special-casing in individual behavior types.
  • ISetupChain<TReturn> now extends IMethodSetup<TReturn> -- removes the impedance mismatch where callers had to juggle two interface types for the same chain.
  • OTel drain test rewrite -- replacing the wall-clock Task.Delay(200ms) with a controlled upstream HttpListener + TaskCompletionSource gate is the right fix for the macOS flake. The old approach tested elapsed time; the new one tests the actual invariant.
  • Test coverage is thorough: explicit cases for throw-before-callback, last-return-wins, sequential separation via Then(), typed dispatch, and state transition on throw.

Concern: CompositeBehavior.Combine has O(N^2) allocations for a single step

Every call to AppendToCurrentBehaviorStep copies the existing _behaviors array:

var combined = new IBehavior[composite._behaviors.Length + 1];
composite._behaviors.CopyTo(combined, 0);
combined[^1] = second;
return new CompositeBehavior(combined);

For a step with N behaviors this performs 1 + 2 + ... + (N-1) = O(N^2) copies during setup. For a typical 2-3 behavior chain this is fine, but it is an unexpected footgun. A List<IBehavior> builder that seals to an array on first Execute would be O(N) total. Since AppendToCurrentBehaviorStep is already called under BehaviorLock, a simple list held directly on the step slot would work without the array-copy dance.


Minor: Lock-free path removed without comment

The previous AddBehavior had a careful Interlocked.CompareExchange fast path with an explicit correctness comment about the CAS-then-double-check idiom. That is now replaced with unconditional lock (BehaviorLock). Setup is not a hot path so this is fine, but a one-line note that it was intentionally simplified would help future readers who might wonder why the lock-free path disappeared.


Minor: _nextBehaviorStartsNewStep semantics are implicit

The flag starts true, becomes false after the first AddBehavior, and resets to true via Then(). The "behaviors chain unless separated by Then()" rule is correct, but it's not surfaced by the field name alone. Something like a nullable "step under construction" slot would make the grouping intent more direct. Low priority.


Overall this is a well-structured fix with solid test coverage. The O(N^2) allocation concern is the only thing worth revisiting if the mocks library sees heavy use, but for typical mock chains it will not matter in practice.

@thomhurst thomhurst disabled auto-merge May 20, 2026 15:33

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review — Final Pass

All previously raised concerns across the review history have been addressed. Here is a final verification:

Previously flagged, confirmed resolved:

  1. O(N²) allocations in CompositeBehavior — Resolved. CompositeBehavior now uses a List<IBehavior> internally with _snapshot caching and calls composite.Add(second) (O(1) append) when combining with an existing composite. O(N) total for a chain of N behaviors.

  2. Breaking behavioral change for chained .Returns() — Resolved. docs/docs/writing-tests/mocking/setup.md now clearly documents that chained behaviors without .Then() execute as a single invocation step and the last return wins, with explicit guidance to use ReturnsSequentially or .Then() when sequential steps are needed.

  3. Arity coupling comment removed — Resolved. The comment block at the top of CompositeBehavior.cs names all files that must be updated when adding a new arity, which is the right place since the interface lives there.

  4. Redundant volatile operations inside lock — Resolved. AddBehaviorStep (always called under BehaviorLock) uses plain field access. Volatile.Read in GetNextBehavior is still correctly used for the lock-free read path. The field comment accurately describes this split.

  5. Lock-free path removed without comment — Resolved. The lock (BehaviorLock) in AddBehavior now has an inline comment explaining the intentional removal of the CAS fast path.

  6. _hasOpenBehaviorStep semantics — Resolved. The field has a <summary> doc comment ("True while chained behaviors are being composed into the current invocation step") and a clear name. The state machine is obvious.


Design correctness verified:

  • State transition timing is correct: _currentState is set in ApplyMatchedSetup, which is called after behavior execution. If the behavior throws, the exception bypasses ApplyMatchedSetup and state stays unchanged. TransitionsTo_Does_Not_Advance_State_When_Behavior_Throws directly verifies this.

  • CompositeBehavior.Execute throw semantics are correct: if a ThrowBehavior is in the composite, behaviors before it (e.g. a callback) run, then the exception propagates out of Execute, skipping any behaviors after it and skipping ApplyMatchedSetup. The Throw_Before_Callback_Does_Not_Run_Later_Callback_In_Same_Behavior test confirms this.

  • ICompositeBehavior vs ISideEffectBehavior visibility split is correct: ISideEffectBehavior is public to allow user-defined behaviors (as demonstrated by CustomSideEffectBehavior in the tests), while ICompositeBehavior/CompositeBehavior remain internal.

  • ReturnsSequentially correctly inserts Then() between each value, ensuring each maps to a distinct invocation step.


The fix is correct, the design is sound, and all substantive feedback has been addressed. Ready to merge.

github-actions Bot pushed a commit to BenjaminMichaelis/DotnetTemplates that referenced this pull request Jun 8, 2026
Updated [TUnit](https://github.com/thomhurst/TUnit) from 1.45.0 to
1.51.0.

<details>
<summary>Release notes</summary>

_Sourced from [TUnit's
releases](https://github.com/thomhurst/TUnit/releases)._

## 1.51.0

<!-- Release notes generated using configuration in .github/release.yml
at v1.51.0 -->

## What's Changed
### Other Changes
* fix(mocks): WasCalled/WasNeverCalled assertions via
[GenerateAssertion] for all ICallVerification types by @​thomhurst in
thomhurst/TUnit#6176
* fix: create fresh non-shared instances per CombinedDataSources
combination by @​thomhurst in
thomhurst/TUnit#6179
* fix: assign TestDetails before TestContext is published to
ClassHookContext.Tests by @​thomhurst in
thomhurst/TUnit#6182
* fix: resolve inherited instance data source members for
MethodDataSource by @​thomhurst in
thomhurst/TUnit#6178
* feat(mocks): per-element matchers for params array parameters by
@​thomhurst in thomhurst/TUnit#6181
* fix: invoke inner Func for TestDataRow<Func<T>> data sources (#​6161)
by @​thomhurst in thomhurst/TUnit#6183
### Dependencies
* chore(deps): update _tunitpolyfillversion to 10.8.0 by @​thomhurst in
thomhurst/TUnit#6167
* chore(deps): update dependency azure.storage.blobs to 12.29.0 by
@​thomhurst in thomhurst/TUnit#6168
* chore(deps): update aspire by @​thomhurst in
thomhurst/TUnit#6165
* chore(deps): update dependency cliwrap to 3.10.2 by @​thomhurst in
thomhurst/TUnit#6166
* chore(deps): update dependency streamjsonrpc to 2.25.25 by @​thomhurst
in thomhurst/TUnit#6170
* chore(deps): update dependency polyfill to 10.8.0 by @​thomhurst in
thomhurst/TUnit#6169
* chore(deps): update tunit to 1.5* by @​thomhurst in
thomhurst/TUnit#6171
* chore(deps): update _tunitpolyfillversion to 10.8.1 by @​thomhurst in
thomhurst/TUnit#6174
* chore(deps): update dependency polyfill to 10.8.1 by @​thomhurst in
thomhurst/TUnit#6175


**Full Changelog**:
thomhurst/TUnit@v1.50.0...v1.51.0

## 1.50.0

<!-- Release notes generated using configuration in .github/release.yml
at v1.50.0 -->

## What's Changed
### Other Changes
* fix(analyzers): decouple code fixers from Rules to prevent
MissingFieldException in VS by @​thomhurst in
thomhurst/TUnit#6158
* Fix mock wrappers for indexers and generic methods by @​thomhurst in
thomhurst/TUnit#6163
* Add global mock default mode by @​thomhurst in
thomhurst/TUnit#6164


**Full Changelog**:
thomhurst/TUnit@v1.49.0...v1.50.0

## 1.49.0

<!-- Release notes generated using configuration in .github/release.yml
at v1.49.0 -->

## What's Changed
### Other Changes
* docs: benchmark page descriptions + promote Benchmarks in sidebar by
@​thomhurst in thomhurst/TUnit#6143
* feat(mocks): discriminate generic-method mocks by type argument by
@​thomhurst in thomhurst/TUnit#6153
* fix(source-gen): jagged array data fails to compile (#​6150) by
@​thomhurst in thomhurst/TUnit#6152
* fix: dispose shared fixtures when only a subset of consuming tests
runs by @​thomhurst in thomhurst/TUnit#6156
### Dependencies
* chore(deps): update tunit to 1.48.6 by @​thomhurst in
thomhurst/TUnit#6142
* chore(deps): update react to ^19.2.7 by @​thomhurst in
thomhurst/TUnit#6144
* chore(deps): update aspire to 13.4.0 by @​thomhurst in
thomhurst/TUnit#6145
* chore(deps): update dependency nunit.analyzers to 4.14.0 by
@​thomhurst in thomhurst/TUnit#6146
* chore(deps): update dependency polyfill to 10.7.2 by @​thomhurst in
thomhurst/TUnit#6148
* chore(deps): update dependency polyfill to 10.7.2 by @​thomhurst in
thomhurst/TUnit#6149
* chore(deps): update dependency dompurify to v3.4.8 by @​thomhurst in
thomhurst/TUnit#6155


**Full Changelog**:
thomhurst/TUnit@v1.48.6...v1.49.0

## 1.48.6

<!-- Release notes generated using configuration in .github/release.yml
at v1.48.6 -->

## What's Changed
### Other Changes
* fix(sourcegen): fully-qualify Linq calls in params array binding
(#​6140) by @​thomhurst in thomhurst/TUnit#6141
### Dependencies
* chore(deps): update tunit to 1.48.0 by @​thomhurst in
thomhurst/TUnit#6135
* chore(deps): update dependency polyfill to 10.7.1 by @​thomhurst in
thomhurst/TUnit#6137
* chore(deps): update dependency polyfill to 10.7.1 by @​thomhurst in
thomhurst/TUnit#6138
* chore(deps): update verify to 31.19.0 by @​thomhurst in
thomhurst/TUnit#6139


**Full Changelog**:
thomhurst/TUnit@v1.48.0...v1.48.6

## 1.48.0

<!-- Release notes generated using configuration in .github/release.yml
at v1.48.0 -->

## What's Changed
### Other Changes
* feat(html-report): baked-in C# syntax highlighting on Source tab by
@​slang25 in thomhurst/TUnit#6132
* feat(analyzers): suppress VSTHRD200 on test and hook methods by
@​thomhurst in thomhurst/TUnit#6123
* fix(source-gen): correct source location for cross-project inherited
tests by @​slang25 in thomhurst/TUnit#6133
* feat(assertions): add WasCalled to tunit mocks assertions by
@​robertcoltheart in thomhurst/TUnit#6126
* feat(arguments): bind array values to a single array test parameter by
@​thomhurst in thomhurst/TUnit#6122
* fix: populate retry/flaky attempt history in HTML report (#​6119) by
@​thomhurst in thomhurst/TUnit#6124
### Dependencies
* chore(deps): update tunit to 1.47.0 by @​thomhurst in
thomhurst/TUnit#6115
* chore(deps): update dependency
microsoft.visualstudio.threading.analyzers to 17.14.15 by @​thomhurst in
thomhurst/TUnit#6134


**Full Changelog**:
thomhurst/TUnit@v1.47.0...v1.48.0

## 1.47.0

<!-- Release notes generated using configuration in .github/release.yml
at v1.47.0 -->

## What's Changed
### Other Changes
* perf(engine): hoist GetParameters and dict-dedup AfterTestDiscovery
hooks by @​thomhurst in thomhurst/TUnit#6062
* perf(engine): hoist GetParameters and drop LINQ in reflection
discovery by @​thomhurst in thomhurst/TUnit#6063
* perf(engine): cache treenode filter path on TestMetadata by
@​thomhurst in thomhurst/TUnit#6064
* perf: use is T pattern in ReflectionExtensions.HasAttribute fallback
(#​6060) by @​thomhurst in thomhurst/TUnit#6066
* perf: replace OrderBy().ToArray() with Array.Sort in
ConstraintKeyScheduler by @​thomhurst in
thomhurst/TUnit#6067
* perf: pool HashSet in WaitingTestIndex.GetCandidatesForReleasedKeys by
@​thomhurst in thomhurst/TUnit#6069
* perf: collapse OfType chains in JUnitXmlWriter (#​6052) by @​thomhurst
in thomhurst/TUnit#6070
* perf(engine): avoid closure allocation in
AfterHookPairTracker.GetOrCreateAfterAssemblyTask (#​6041) by
@​thomhurst in thomhurst/TUnit#6071
* perf: avoid closure allocation in
BeforeHookTaskCache.GetOrCreateBeforeAssemblyTask (#​6040) by
@​thomhurst in thomhurst/TUnit#6073
* perf: use TryAdd in TestDependencyResolver dependency dedupe by
@​thomhurst in thomhurst/TUnit#6068
* perf: replace LINQ Any with foreach in TestGenericTypeResolver
(#​6044) by @​thomhurst in thomhurst/TUnit#6072
* perf: avoid Cast<object>().FirstOrDefault() iterator alloc in
CastHelper (#​6029) by @​thomhurst in
thomhurst/TUnit#6074
* perf(engine): avoid string round-trip when building nested type names
(#​6049) by @​thomhurst in thomhurst/TUnit#6075
* perf(engine): replace Select+ToArray with manual Type[] build (#​6043)
by @​thomhurst in thomhurst/TUnit#6076
* perf(core): replace OfType().FirstOrDefault()/.Any() with foreach in
ClassConstructorHelper by @​thomhurst in
thomhurst/TUnit#6078
* perf(engine): avoid FirstOrDefault iterator alloc in
TestGenericTypeResolver by @​thomhurst in
thomhurst/TUnit#6079
* perf(engine): use SearchValues<char> for reporter filename
sanitization by @​thomhurst in
thomhurst/TUnit#6090
* perf: dedupe TestDataFormatter.FormatArguments with pooled
StringBuilder by @​thomhurst in
thomhurst/TUnit#6088
* perf(engine): use MemoryExtensions.Split for path parsing in
MetadataFilterMatcher by @​thomhurst in
thomhurst/TUnit#6085
* perf(engine): use CollectionsMarshal.GetValueRefOrAddDefault for
dictionary index builds by @​thomhurst in
thomhurst/TUnit#6086
* perf(engine): replace LINQ Where closure with inline filter in
MetadataDependencyExpander BFS by @​thomhurst in
thomhurst/TUnit#6084
* perf(engine): pool StringBuilder in DisplayNameBuilder.FormatArguments
by @​thomhurst in thomhurst/TUnit#6082
* Preserve specialized chaining after null assertions by @​thomhurst in
thomhurst/TUnit#6008
* perf: use EnumerateLines for line splitting in HtmlReportGenerator by
@​thomhurst in thomhurst/TUnit#6089
* perf: collapse Replace chain in TestNameFormatter.BuildTestId by
@​thomhurst in thomhurst/TUnit#6083
* perf: use OrdinalIgnoreCase Contains in HtmlReportGenerator span
mapping by @​thomhurst in thomhurst/TUnit#6093
* perf(assertions): avoid eager interpolated-string alloc in assertion
source ctors by @​thomhurst in
thomhurst/TUnit#6091
* perf: optimize TestNameFormatter argument and bool formatting by
@​thomhurst in thomhurst/TUnit#6095
* perf: use FrozenSet/FrozenDictionary for read-only static lookups by
@​thomhurst in thomhurst/TUnit#6099
* perf: avoid GetCustomAttributes() + LINQ chain for per-property
attribute scans by @​thomhurst in
thomhurst/TUnit#6098
* perf(engine): replace magic-string RequiredAttribute match with type
check in ConstructorHelper by @​thomhurst in
thomhurst/TUnit#6087
* perf(core): replace Select+Func factory chain in DataSourceHelpers by
@​thomhurst in thomhurst/TUnit#6081
* perf: replace LINQ dependency extraction with manual loop by
@​thomhurst in thomhurst/TUnit#6096
* perf(core): avoid string[] alloc in ArgumentFormatter.FormatArguments
by @​thomhurst in thomhurst/TUnit#6080
* perf: use [GeneratedRegex] in MetadataFilterMatcher by @​thomhurst in
thomhurst/TUnit#6094
* perf: dedupe GetSimpleTypeName into shared TypeNameFormatter by
@​thomhurst in thomhurst/TUnit#6097
* fix: remove GitVersion MSBuild task, pin local builds to 99.99.99
(#​6077) by @​thomhurst in thomhurst/TUnit#6101
* HTML Report: source link + code snippet on Source tab (#​5993) by
@​thomhurst in thomhurst/TUnit#6100
* perf(sourcegen): Single-pass attribute classification by @​thomhurst
in thomhurst/TUnit#6111
* perf(core): eliminate per-test allocations in TestDetails/HookMethod
by @​thomhurst in thomhurst/TUnit#6109
* perf: hoist char[] alloc in FsCheckPropertyTestExecutor to static
SearchValues by @​thomhurst in
thomhurst/TUnit#6108
* perf(core): de-LINQ data-source expansion by @​thomhurst in
thomhurst/TUnit#6110
* perf: avoid LINQ chains in TestDependency equality and
MethodDataSourceAttribute method matching by @​thomhurst in
thomhurst/TUnit#6092
* perf(engine): reduce allocations in reflection-mode
discovery/execution by @​thomhurst in
thomhurst/TUnit#6113
* perf(assertions): allocation-free passing path (TUnit.Assertions) by
@​thomhurst in thomhurst/TUnit#6112
### Dependencies
 ... (truncated)

## 1.46.0

<!-- Release notes generated using configuration in .github/release.yml
at v1.46.0 -->

## What's Changed
### Other Changes
* docs: add Rider VSTest conflict troubleshooting by @​smolchanovsky in
thomhurst/TUnit#5989
* Populate generated test metadata with full source spans by @​Copilot
in thomhurst/TUnit#5991
* Add devcontainer configuration by @​Copilot in
thomhurst/TUnit#5995
* fix: treenode filter pre-filter rejects parenthesised segments
(#​6026) by @​thomhurst in thomhurst/TUnit#6027
* fix(engine): isolate per-session state under MTP server-mode
concurrency (#​6001) by @​thomhurst in
thomhurst/TUnit#6025
### Dependencies
* chore(deps): update dependency stackexchange.redis to 2.13.10 by
@​thomhurst in thomhurst/TUnit#5985
* chore(deps): update tunit to 1.45.29 by @​thomhurst in
thomhurst/TUnit#5986
* chore(deps): update dependency mockolate to 3.2.1 by @​thomhurst in
thomhurst/TUnit#5987
* chore(deps): update dependency microsoft.playwright to 1.60.0 by
@​thomhurst in thomhurst/TUnit#5988
* chore(deps): update dependency messagepack to 3.1.6 by @​thomhurst in
thomhurst/TUnit#5992
* chore(deps): update dependency polyfill to 10.7.0 by @​thomhurst in
thomhurst/TUnit#5998
* chore(deps): update dependency polyfill to 10.7.0 by @​thomhurst in
thomhurst/TUnit#5997
* chore(deps): update verify to 31.17.0 by @​thomhurst in
thomhurst/TUnit#6000
* chore(deps): update verify to 31.18.0 by @​thomhurst in
thomhurst/TUnit#6013
* chore(deps): update dependency microsoft.net.test.sdk to 18.6.0 by
@​thomhurst in thomhurst/TUnit#6016
* chore(deps): update dependency dompurify to v3.4.6 by @​thomhurst in
thomhurst/TUnit#6015
* chore(deps): update dependency dompurify to v3.4.7 by @​thomhurst in
thomhurst/TUnit#6019
* chore(deps): update dependency npgsql to 10.0.3 by @​thomhurst in
thomhurst/TUnit#6020
* chore(deps): update dependency stackexchange.redis to 2.13.17 by
@​thomhurst in thomhurst/TUnit#6021
* chore(deps): update dependency npgsql.entityframeworkcore.postgresql
to 10.0.2 by @​thomhurst in thomhurst/TUnit#6022

## New Contributors
* @​smolchanovsky made their first contribution in
thomhurst/TUnit#5989

**Full Changelog**:
thomhurst/TUnit@v1.45.29...v1.46.0

## 1.45.29

<!-- Release notes generated using configuration in .github/release.yml
at v1.45.29 -->

## What's Changed
### Other Changes
* Fix shared fixture lifetime for reused discovery instances by
@​thomhurst in thomhurst/TUnit#5983
* Preserve override accessibility in generated mocks by @​thomhurst in
thomhurst/TUnit#5984
### Dependencies
* chore(deps): update tunit to 1.45.22 by @​thomhurst in
thomhurst/TUnit#5974
* chore(deps): update dependency messagepack to 3.1.5 by @​thomhurst in
thomhurst/TUnit#5978
* chore(deps): update aspire to 13.3.5 by @​thomhurst in
thomhurst/TUnit#5980


**Full Changelog**:
thomhurst/TUnit@v1.45.22...v1.45.29

## 1.45.22

<!-- Release notes generated using configuration in .github/release.yml
at v1.45.22 -->

## What's Changed
### Other Changes
* Remove ".NET" from Aspire references by @​antmdvs in
thomhurst/TUnit#5968
* Fix chained mock setup behavior by @​thomhurst in
thomhurst/TUnit#5973
### Dependencies
* chore(deps): update tunit to 1.45.8 by @​thomhurst in
thomhurst/TUnit#5958
* chore(deps): update dependency nunit to 4.6.1 by @​thomhurst in
thomhurst/TUnit#5961
* chore(deps): update dependency testcontainers.postgresql to 4.12.0 by
@​thomhurst in thomhurst/TUnit#5963
* chore(deps): update dependency testcontainers.redis to 4.12.0 by
@​thomhurst in thomhurst/TUnit#5965
* chore(deps): update dependency testcontainers.kafka to 4.12.0 by
@​thomhurst in thomhurst/TUnit#5962
* chore(deps): update aspire to 13.3.4 by @​thomhurst in
thomhurst/TUnit#5966
* chore(deps): bump webpack-dev-server from 5.2.2 to 5.2.4 in /docs by
@​dependabot[bot] in thomhurst/TUnit#5964

## New Contributors
* @​antmdvs made their first contribution in
thomhurst/TUnit#5968

**Full Changelog**:
thomhurst/TUnit@v1.45.8...v1.45.22

## 1.45.8

<!-- Release notes generated using configuration in .github/release.yml
at v1.45.8 -->

## What's Changed
### Other Changes
* fix(aspire): route CreateHttpClient through IHttpClientFactory by
@​thomhurst in thomhurst/TUnit#5957
### Dependencies
* chore(deps): update tunit to 1.45.0 by @​thomhurst in
thomhurst/TUnit#5949
* chore(deps): update dependency dompurify to v3.4.5 by @​thomhurst in
thomhurst/TUnit#5951
* chore(deps): update dependency
microsoft.testing.extensions.codecoverage to 18.7.0 by @​thomhurst in
thomhurst/TUnit#5953
* chore(deps): update dependency coverlet.collector to 10.0.1 by
@​thomhurst in thomhurst/TUnit#5952
* chore(deps): update dependency polyfill to 10.6.0 by @​thomhurst in
thomhurst/TUnit#5955
* chore(deps): update dependency polyfill to 10.6.0 by @​thomhurst in
thomhurst/TUnit#5954


**Full Changelog**:
thomhurst/TUnit@v1.45.0...v1.45.8

Commits viewable in [compare
view](thomhurst/TUnit@v1.45.0...v1.51.0).
</details>

[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=TUnit&package-manager=nuget&previous-version=1.45.0&new-version=1.51.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This was referenced Jun 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Return values not being respected after Callback on mock

1 participant