Skip to content

feat: update Because method to handle null reason#973

Merged
vbreuss merged 2 commits into
mainfrom
topic/update-because-to-handle-null
May 31, 2026
Merged

feat: update Because method to handle null reason#973
vbreuss merged 2 commits into
mainfrom
topic/update-because-to-handle-null

Conversation

@vbreuss

@vbreuss vbreuss commented May 31, 2026

Copy link
Copy Markdown
Member

Updates the Because method to gracefully handle null and empty reasons instead of throwing.

Previously, calling .Because(null) would throw a NullReferenceException (the reason was passed straight into BecauseReason, which calls reason.Trim()), and an empty string produced a dangling ", because " fragment. Now both null and empty reasons are simply ignored.

Changes

  • ExpectationResult.Because and ExpectationResult<TType, TSelf>.Because now accept string? and skip adding the reason when it is null or empty.
  • Updated the XML docs with a <remarks> note describing the null/empty behavior.
  • Regenerated the public-API approval files (net8.0, net10.0, netstandard2.0) to reflect the string? signature.
  • Marked aweXpect.Core.Api.Tests' AcceptApiChanges as [Explicit], consistent with aweXpect.Api.Tests, so the API-regeneration helper no longer runs as part of the normal test suite (verification still happens in ApiApprovalTests).

Behavior

Call Before After
.Because("reason") adds reason adds reason (unchanged)
.Because("") ", because " ignored
.Because(null) NullReferenceException ignored

@vbreuss vbreuss self-assigned this May 31, 2026
@vbreuss vbreuss added the enhancement New feature or request label May 31, 2026
@vbreuss vbreuss enabled auto-merge (squash) May 31, 2026 06:31
@sonarqubecloud

Copy link
Copy Markdown

@github-actions

github-actions Bot commented May 31, 2026

Copy link
Copy Markdown
Contributor

🚀 Benchmark Results

Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.300
[Host] : .NET 8.0.27 (8.0.27, 8.0.2726.22922), X64 RyuJIT x86-64-v3

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

Method Mean Error StdDev Gen0 Gen1 Allocated
Bool_aweXpect 253.6 ns 1.79 ns 1.58 ns 0.0415 - 696 B
Bool_FluentAssertions 254.0 ns 2.04 ns 1.71 ns 0.0567 - 952 B
Equivalency_aweXpect 314,691.7 ns 2,387.76 ns 2,116.68 ns 20.0195 0.4883 335444 B
Equivalency_FluentAssertions 2,515,519.8 ns 28,095.68 ns 24,906.09 ns 289.0625 46.8750 4841651 B
Int_GreaterThan_aweXpect 279.0 ns 5.61 ns 4.98 ns 0.0515 - 864 B
Int_GreaterThan_FluentAssertions 246.2 ns 1.39 ns 1.24 ns 0.0730 - 1224 B
ItemsCount_AtLeast_aweXpect 502.8 ns 5.27 ns 4.93 ns 0.0811 - 1360 B
ItemsCount_AtLeast_FluentAssertions 490.9 ns 4.82 ns 4.50 ns 0.1192 - 2008 B
String_aweXpect 454.8 ns 3.23 ns 2.86 ns 0.0672 - 1128 B
String_FluentAssertions 1,171.9 ns 25.58 ns 23.93 ns 0.2346 - 3944 B
StringArray_aweXpect 1,934.0 ns 19.58 ns 18.32 ns 0.1564 - 2624 B
StringArray_FluentAssertions 1,385.8 ns 27.40 ns 25.63 ns 0.2480 - 4152 B
StringArrayInAnyOrder_aweXpect 2,512.8 ns 30.28 ns 25.29 ns 0.1678 - 2816 B
StringArrayInAnyOrder_FluentAssertions 20,836.6 ns 507.32 ns 474.55 ns 1.9836 0.0610 33471 B

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

     23 files  +    10       23 suites  +10   7m 43s ⏱️ + 3m 54s
 19 835 tests + 1 592   19 833 ✅ + 1 597  2 💤 +1  0 ❌  -  6 
102 542 runs  +50 336  102 540 ✅ +50 359  2 💤 +1  0 ❌  - 24 

Results for commit fbc2cb9. ± Comparison against base commit 01e9240.

This pull request removes 1582 and adds 3174 tests. Note that renamed tests count towards both.
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "message4e6366e2-7472-40f3-9593-c304b5d2119d")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "messagea96a5bac-aff4-4e64-b348-45102f2ced7d")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "messageea402a93-33a2-4a24-99d0-af698e0e3419")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "messagefead82a0-c3b1-4a4c-8137-581cf98055a9")
aweXpect.Core.Tests.Core.Exceptions.SkipExceptionTests ‑ Message_ShouldBeSet(message: "message29425bd5-87f4-4414-93ae-94ca124cc49d")
aweXpect.Core.Tests.Core.Exceptions.SkipExceptionTests ‑ Message_ShouldBeSet(message: "message365153e0-3431-47b2-9596-c2b1851b9749")
aweXpect.Core.Tests.Core.Exceptions.SkipExceptionTests ‑ Message_ShouldBeSet(message: "message41e91d95-189c-4b4b-9c7a-6baaf35d2104")
aweXpect.Core.Tests.Core.Exceptions.SkipExceptionTests ‑ Message_ShouldBeSet(message: "message5c8476d3-713e-4b29-9832-f704d6de5890")
aweXpect.Core.Tests.FailTests ‑ Test_ShouldThrowException(reason: "reason2d1f1233-5eb3-4bf9-88ed-19fe85ce5a1b")
aweXpect.Core.Tests.FailTests ‑ Test_ShouldThrowException(reason: "reason4f1ff746-183a-4eea-a35b-4b8857d391e5")
…
aweXpect.Core.Tests.Core.BecauseTests ‑ ActionDelegate_WhenReasonIsNullOrEmpty_ShouldNotIncludeBecause(because: "")
aweXpect.Core.Tests.Core.BecauseTests ‑ ActionDelegate_WhenReasonIsNullOrEmpty_ShouldNotIncludeBecause(because: null)
aweXpect.Core.Tests.Core.BecauseTests ‑ WhenReasonIsNullOrEmpty_ShouldNotIncludeBecause(because: "")
aweXpect.Core.Tests.Core.BecauseTests ‑ WhenReasonIsNullOrEmpty_ShouldNotIncludeBecause(because: null)
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "message213a4db3-5774-439f-9808-d17a65fd5aad")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "message24c3236e-d582-4a59-84c3-218ec5f3ccd6")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "message325674de-efec-4f45-8a23-5131cfc08343")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "message35f2f13e-1f38-4af6-85b1-931ab6e5e6de")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "messagec78d161f-12b9-4caa-8903-890d457f6982")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "messagee63cf545-b08c-495a-9763-bc271f554fba")
…
This pull request skips 1 test.
aweXpect.Core.Api.Tests.ApiAcceptance ‑ AcceptApiChanges()

@vbreuss vbreuss merged commit e8b1479 into main May 31, 2026
13 checks passed
@vbreuss vbreuss deleted the topic/update-because-to-handle-null branch May 31, 2026 06:37
@github-actions

Copy link
Copy Markdown
Contributor

👽 Mutation Results

Mutation testing badge

aweXpect

Details
File Score Killed Survived Timeout No Coverage Ignored Compile Errors Total Detected Total Undetected Total Mutants

The final mutation score is NaN%

Coverage Thresholds: high:80 low:60 break:0

aweXpect.Core

Details
File Score Killed Survived Timeout No Coverage Ignored Compile Errors Total Detected Total Undetected Total Mutants
Results/ExpectationResult.cs 97.67% 42 0 0 1 17 10 42 1 70

The final mutation score is 97.67%

Coverage Thresholds: high:80 low:60 break:0

github-actions Bot added a commit that referenced this pull request May 31, 2026
github-actions Bot added a commit that referenced this pull request May 31, 2026
vbreuss added a commit that referenced this pull request May 31, 2026
The synchronous `Because(string?)` overloads were already updated (#973) to ignore null or empty reasons. This applies the same behavior to the asynchronous `Because(Task<string>)` overloads.

The async overloads now accept `Task<string?>`, mirroring the nullable `string?` of the synchronous variants: the task reference is still required, but the value it resolves to may be null or empty. When the resolved value is null or empty, the reason is ignored (no `because` text is appended) instead of throwing a `NullReferenceException` (from `reason.Trim()`) or producing a dangling ", because " fragment.

Because the resolved string is only available after awaiting, the null/empty guard lives in `AsyncBecauseReason.ApplyTo`, where the result is left unchanged when the reason resolves to null or empty.

**Changes:**
- `ExpectationResult.Because` and `ExpectationResult<TType, TSelf>.Because` now take `Task<string?>`.
- `ExpectationBuilder.AddReason` and `AsyncBecauseReason` updated to `Task<string?>`; the resolved value is checked for null/empty.
- Regenerated the public-API approval files for all target frameworks.
- Added tests covering null and empty resolved reasons for both the non-generic and generic async overloads.
@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

This is addressed in release v2.35.0.

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