Skip to content

feat!: only expose property accessors the mock intercepts - #829

Merged
vbreuss merged 2 commits into
mainfrom
feat/suppress-absent-property-accessor-surface
Jul 29, 2026
Merged

feat!: only expose property accessors the mock intercepts#829
vbreuss merged 2 commits into
mainfrom
feat/suppress-absent-property-accessor-surface

Conversation

@vbreuss

@vbreuss vbreuss commented Jul 29, 2026

Copy link
Copy Markdown
Member

A generated mock advertised setup and verification for both accessors of every property, even when it intercepted only one. For a get-only property

    int RemainingBars { get; }

Setup.RemainingBars.OnSet and Verify.RemainingBars.Set(3) compiled and silently did nothing: no setter body is emitted, so a write can never be recorded and the verification could only ever report zero.

The generator now picks the facade from the accessors it actually emits a body for:

  • IPropertyGetterOnlySetup / IPropertySetterOnlySetup replace PropertySetup on the setup surface.
  • VerificationPropertyGetterResult and VerificationPropertySetterResult<TSubject, TParameter> replace VerificationPropertyResult<TSubject, TParameter> on the verify surface, each taking only the member id of the accessor it exposes.

GetInterceptedAccessors is the single classification behind all three decisions, keyed on the same Getter/Setter nullity that guards body emission, so the surface cannot drift from what the proxy intercepts.

This also completes the cross-assembly case from #828: a property such as { get; internal set; } whose setter is invisible to the mock's assembly now narrows to the getter instead of offering a write that never arrives.

BREAKING CHANGE: on a property the mock reads or writes but not both, Setup.X.OnSet/OnGet, Setup.X.Returns/Throws/InitializeWith and Verify.X.Set(...)/Got() no longer compile for the absent accessor. Each previously compiled and silently had no effect. The library API is additive; only generated mock surfaces narrow.

The restriction survives chaining: the narrowed setup facades have their own fluent builder hierarchies (IPropertyGetterOnlySetupReturnBuilder<T>, IPropertyGetterOnlySetupCallbackBuilder<T>, IPropertySetterOnlySetupCallbackBuilder<T> and their When/Parallel stages, including Forever/OnlyOnce), so chaining past Returns/Throws/Do/TransitionTo can never reach the accessor the mock does not intercept. The verify facades have no continuation and are likewise fully restricted. Indexers and init-only properties keep the full surface.

A generated mock advertised setup and verification for both accessors of
every property, even when it intercepted only one. For a get-only property

    int RemainingBars { get; }

`Setup.RemainingBars.OnSet` and `Verify.RemainingBars.Set(3)` compiled and
silently did nothing: no setter body is emitted, so a write can never be
recorded and the verification could only ever report zero.

The generator now picks the facade from the accessors it actually emits a
body for:

- IPropertyGetterOnlySetup<T> / IPropertySetterOnlySetup<T> replace
  PropertySetup<T> on the setup surface.
- VerificationPropertyGetterResult<TSubject> and
  VerificationPropertySetterResult<TSubject, TParameter> replace
  VerificationPropertyResult<TSubject, TParameter> on the verify surface,
  each taking only the member id of the accessor it exposes.

GetInterceptedAccessors is the single classification behind all three
decisions, keyed on the same Getter/Setter nullity that guards body
emission, so the surface cannot drift from what the proxy intercepts.

This also completes the cross-assembly case from #828: a property such as
`{ get; internal set; }` whose setter is invisible to the mock's assembly
now narrows to the getter instead of offering a write that never arrives.

BREAKING CHANGE: on a property the mock reads or writes but not both,
`Setup.X.OnSet`/`OnGet`, `Setup.X.Returns`/`Throws`/`InitializeWith` and
`Verify.X.Set(...)`/`Got()` no longer compile for the absent accessor.
Each previously compiled and silently had no effect. The library API is
additive; only generated mock surfaces narrow.

The restriction covers the property's own setup surface. The fluent
builders returned by Returns/Throws/Do/TransitionTo are shared with
read-write properties, so chaining past one of them reaches the full setup
again; the verify facades have no such continuation and are fully
restricted. Indexers and init-only properties keep the full surface.
@vbreuss vbreuss self-assigned this Jul 29, 2026
@vbreuss vbreuss added the enhancement New feature or request label Jul 29, 2026
@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown

Test Results

    24 files  ±  0      24 suites  ±0   10m 10s ⏱️ -1s
 4 263 tests + 25   4 261 ✅ + 25  2 💤 ±0  0 ❌ ±0 
27 209 runs  +163  27 205 ✅ +163  4 💤 ±0  0 ❌ ±0 

Results for commit 3cb6e46. ± Comparison against base commit 5a6b0ac.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown

🚀 Benchmark Results

Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.73GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.302
[Host] : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Event Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 302.7 ns 12.14 ns 11.36 ns 1.06 1.7 KB 1.00
Mockolate 286.0 ns 7.42 ns 6.94 ns 1.00 1.7 KB 1.00
Imposter 1,381.9 ns 35.51 ns 33.21 ns 4.83 8.8 KB 5.17
TUnitMocks 196.0 ns 2.29 ns 2.14 ns 0.69 1.34 KB 0.79
Moq 16,137.1 ns 151.64 ns 134.43 ns 56.46 12.51 KB 7.34
NSubstitute 5,574.7 ns 23.98 ns 21.25 ns 19.50 9.05 KB 5.31
FakeItEasy 214,799.1 ns 2,669.06 ns 2,496.64 ns 751.51 15.26 KB 8.96
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.302
[Host] : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Indexer N Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 1 952.9 ns 20.49 ns 18.17 ns 0.53 3.77 KB 1.00
Mockolate 1 1,786.2 ns 21.66 ns 19.20 ns 1.00 3.77 KB 1.00
Imposter 1 871.2 ns 6.38 ns 5.32 ns 0.49 5.16 KB 1.37
Moq 1 216,115.1 ns 776.78 ns 688.60 ns 121.00 20.37 KB 5.41
NSubstitute 1 9,277.4 ns 58.99 ns 55.18 ns 5.19 12.78 KB 3.39
FakeItEasy 1 11,694.8 ns 20.75 ns 17.33 ns 6.55 13.63 KB 3.62
baseline* 10 2,474.7 ns 28.21 ns 26.38 ns 0.98 4.82 KB 1.00
Mockolate 10 2,533.9 ns 8.06 ns 6.29 ns 1.00 4.82 KB 1.00
Imposter 10 2,063.4 ns 8.09 ns 7.57 ns 0.81 7.97 KB 1.65
Moq 10 227,668.5 ns 604.85 ns 565.78 ns 89.85 29.89 KB 6.20
NSubstitute 10 23,423.6 ns 273.28 ns 255.63 ns 9.24 25.58 KB 5.31
FakeItEasy 10 24,456.1 ns 138.50 ns 129.55 ns 9.65 32.97 KB 6.84
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 3.22GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.302
[Host] : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Property N Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 1 512.9 ns 16.08 ns 15.04 ns 0.77 2.41 KB 1.00
Mockolate 1 666.1 ns 23.94 ns 22.40 ns 1.00 2.41 KB 1.00
Imposter 1 516.0 ns 15.07 ns 14.10 ns 0.78 3.13 KB 1.29
TUnitMocks 1 493.1 ns 6.06 ns 5.38 ns 0.74 1.64 KB 0.68
Moq 1 12,122.5 ns 70.74 ns 62.71 ns 18.22 10.39 KB 4.30
NSubstitute 1 7,990.7 ns 48.50 ns 45.36 ns 12.01 11.45 KB 4.74
FakeItEasy 1 8,854.1 ns 70.53 ns 65.97 ns 13.31 11.24 KB 4.66
baseline* 10 979.6 ns 6.01 ns 5.63 ns 0.87 2.91 KB 1.00
Mockolate 10 1,127.5 ns 12.50 ns 10.44 ns 1.00 2.91 KB 1.00
Imposter 10 1,220.3 ns 35.05 ns 31.07 ns 1.08 4.67 KB 1.61
TUnitMocks 10 1,634.0 ns 20.02 ns 18.72 ns 1.45 3.94 KB 1.35
Moq 10 19,428.5 ns 77.05 ns 68.31 ns 17.23 18.28 KB 6.29
NSubstitute 10 17,738.1 ns 215.94 ns 201.99 ns 15.73 21.08 KB 7.25
FakeItEasy 10 20,701.3 ns 86.24 ns 76.45 ns 18.36 30.81 KB 10.60
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.64GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.302
[Host] : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

CreateMock Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 13.95 ns 0.095 ns 0.084 ns 0.78 160 B 1.00
Mockolate 17.91 ns 0.301 ns 0.281 ns 1.00 160 B 1.00
Imposter 296.07 ns 11.677 ns 10.351 ns 16.53 2248 B 14.05
TUnitMocks 35.78 ns 0.314 ns 0.294 ns 2.00 200 B 1.25
Moq 1,381.25 ns 8.826 ns 8.256 ns 77.12 2096 B 13.10
NSubstitute 1,945.61 ns 21.588 ns 20.194 ns 108.63 5048 B 31.55
FakeItEasy 1,807.91 ns 14.541 ns 13.602 ns 100.94 2763 B 17.27
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.302
[Host] : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Callback Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 338.5 ns 5.53 ns 4.90 ns 0.92 1.57 KB 1.00
Mockolate 369.0 ns 4.23 ns 3.53 ns 1.00 1.57 KB 1.00
Imposter 486.0 ns 7.48 ns 6.99 ns 1.32 2.38 KB 1.52
TUnitMocks 551.1 ns 5.80 ns 5.42 ns 1.49 1.99 KB 1.27
Moq 99,142.5 ns 324.82 ns 303.83 ns 268.72 8.88 KB 5.66
NSubstitute 4,626.0 ns 18.70 ns 14.60 ns 12.54 7.71 KB 4.91
FakeItEasy 5,138.3 ns 94.39 ns 88.29 ns 13.93 6.81 KB 4.33
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.302
[Host] : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Method N Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 1 355.5 ns 13.61 ns 12.73 ns 1.01 1.93 KB 1.00
Mockolate 1 353.4 ns 8.63 ns 8.07 ns 1.00 1.93 KB 1.00
Imposter 1 565.0 ns 7.31 ns 6.84 ns 1.60 4.04 KB 2.09
TUnitMocks 1 494.7 ns 3.65 ns 3.24 ns 1.40 2.02 KB 1.04
Moq 1 184,432.7 ns 682.74 ns 570.12 ns 522.16 14.58 KB 7.55
NSubstitute 1 5,774.1 ns 29.31 ns 27.42 ns 16.35 9.12 KB 4.72
FakeItEasy 1 6,233.8 ns 84.76 ns 75.14 ns 17.65 8.05 KB 4.17
baseline* 10 650.0 ns 11.00 ns 10.29 ns 0.99 2.14 KB 1.00
Mockolate 10 657.5 ns 4.79 ns 4.00 ns 1.00 2.14 KB 1.00
Imposter 10 1,114.2 ns 4.59 ns 4.29 ns 1.69 5.52 KB 2.58
TUnitMocks 10 1,383.0 ns 11.18 ns 9.91 ns 2.10 3.73 KB 1.74
Moq 10 189,001.9 ns 418.43 ns 349.41 ns 287.46 18.48 KB 8.63
NSubstitute 10 8,944.1 ns 73.97 ns 69.20 ns 13.60 12.07 KB 5.64
FakeItEasy 10 9,408.9 ns 56.04 ns 52.42 ns 14.31 15.42 KB 7.20

baseline* rows show the corresponding Mockolate benchmark from the most recent successful main branch build with results, for regression comparison.

The fluent builders returned by Returns, Throws, Do and TransitionTo on
IPropertyGetterOnlySetup<T>/IPropertySetterOnlySetup<T> previously
extended the full IPropertySetup<T>, so one chaining hop re-exposed the
accessor the mock never intercepts. Parallel getter-only and setter-only
builder hierarchies now keep the whole chain on the narrowed surface,
with Forever/OnlyOnce extensions for the narrowed when-builders.
@vbreuss
vbreuss force-pushed the feat/suppress-absent-property-accessor-surface branch from 1147269 to 3cb6e46 Compare July 29, 2026 09:09
@sonarqubecloud

Copy link
Copy Markdown

@vbreuss
vbreuss merged commit 01a814a into main Jul 29, 2026
17 checks passed
@vbreuss
vbreuss deleted the feat/suppress-absent-property-accessor-surface branch July 29, 2026 09:30
github-actions Bot added a commit that referenced this pull request Jul 29, 2026
github-actions Bot added a commit that referenced this pull request Jul 29, 2026
@github-actions

Copy link
Copy Markdown

This is addressed in release v3.4.0.

@github-actions github-actions Bot added the state: released The issue is released label Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request state: released The issue is released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant