Skip to content

Commit 354222c

Browse files
authored
Merge pull request #160 from feO2x/160-improve-test-quality
test: increase coverage and quality
2 parents cfa9ba1 + 88c4d94 commit 354222c

41 files changed

Lines changed: 1636 additions & 599 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

tests/AGENTS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@
1010
# How to run tests
1111

1212
- `dotnet test` for usual test runs.
13-
- `dotnet test -- --coverage --coverage-output-format cobertura` for test coverage metrics.
13+
- `dotnet test -- --coverage --coverage-output-format cobertura --coverage-settings CodeCoverage.config` for test coverage metrics (run from tests/Light.GuardClauses.Tests). The settings file excludes JetBrains.Annotations and Regex source generator output from the report.
14+
- Line coverage of 100% is not achievable: the closing brace after a call to a `Throw.*` helper is an unreachable sequence point because these helpers never return (285 such lines as of 2026-07-15).
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
Code coverage settings for Microsoft.Testing.Extensions.CodeCoverage.
4+
See tests/AGENTS.md for the command line that applies this file.
5+
6+
Excludes code that is not part of Light.GuardClauses itself:
7+
- JetBrains.Annotations attribute classes that are compiled into the assembly
8+
- the Regex source generator output for the email address pattern
9+
-->
10+
<Configuration>
11+
<CodeCoverage>
12+
<Functions>
13+
<Exclude>
14+
<Function>^JetBrains\.Annotations\..*</Function>
15+
<Function>^System\.Text\.RegularExpressions\.Generated\..*</Function>
16+
</Exclude>
17+
</Functions>
18+
</CodeCoverage>
19+
</Configuration>

tests/Light.GuardClauses.Tests/CollectionAssertions/AdditionalSpanAndMemoryGuardsTests.cs

Lines changed: 105 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using FluentAssertions;
3+
using Light.GuardClauses.ExceptionFactory;
34
using Light.GuardClauses.Exceptions;
45
using Xunit;
56

@@ -41,9 +42,9 @@ public static void EmptyGuardsCaptureCallerExpressions()
4142
spanAct.Should().Throw<EmptyCollectionException>().WithParameterName("emptySpan");
4243
readOnlySpanAct.Should().Throw<EmptyCollectionException>().WithParameterName("emptyReadOnlySpan");
4344
((Action) (() => emptyMemory.MustNotBeEmpty())).Should().Throw<EmptyCollectionException>()
44-
.WithParameterName(nameof(emptyMemory));
45+
.WithParameterName(nameof(emptyMemory));
4546
((Action) (() => emptyReadOnlyMemory.MustNotBeEmpty())).Should().Throw<EmptyCollectionException>()
46-
.WithParameterName(nameof(emptyReadOnlyMemory));
47+
.WithParameterName(nameof(emptyReadOnlyMemory));
4748
}
4849

4950
[Fact]
@@ -55,6 +56,22 @@ public static void EmptyGuardFactoriesReceiveEveryShape()
5556
Test.CustomMemoryException(ReadOnlyMemory<int>.Empty, (value, factory) => value.MustNotBeEmpty(factory));
5657
}
5758

59+
[Fact]
60+
public static void NonEmptyFactoriesDoNotThrowForEveryValidShape()
61+
{
62+
var array = new[] { 1, 2 };
63+
var span = array.AsSpan();
64+
ReadOnlySpan<int> readOnlySpan = array;
65+
var memory = array.AsMemory();
66+
ReadOnlyMemory<int> readOnlyMemory = array;
67+
ReadOnlySpanExceptionFactory<int> factory = _ => new ("The factory should not be invoked.");
68+
69+
(span.MustNotBeEmpty(factory) == span).Should().BeTrue();
70+
(readOnlySpan.MustNotBeEmpty(factory) == readOnlySpan).Should().BeTrue();
71+
memory.MustNotBeEmpty(factory).Should().Be(memory);
72+
readOnlyMemory.MustNotBeEmpty(factory).Should().Be(readOnlyMemory);
73+
}
74+
5875
[Fact]
5976
public static void RangedLengthGuardsPreserveBoundariesAndShapes()
6077
{
@@ -81,14 +98,44 @@ public static void RangedLengthGuardsPreserveBoundariesAndShapes()
8198
public static void RangedLengthFactoriesReceiveEveryShape()
8299
{
83100
var invalidRange = Range.InclusiveBetween(2, 3);
84-
Test.CustomSpanException(Span<int>.Empty, invalidRange,
85-
(value, range, factory) => value.MustHaveLengthIn(range, factory));
86-
Test.CustomSpanException(ReadOnlySpan<int>.Empty, invalidRange,
87-
(value, range, factory) => value.MustHaveLengthIn(range, factory));
88-
Test.CustomMemoryException(Memory<int>.Empty, invalidRange,
89-
(value, range, factory) => value.MustHaveLengthIn(range, factory));
90-
Test.CustomMemoryException(ReadOnlyMemory<int>.Empty, invalidRange,
91-
(value, range, factory) => value.MustHaveLengthIn(range, factory));
101+
Test.CustomSpanException(
102+
Span<int>.Empty,
103+
invalidRange,
104+
(value, range, factory) => value.MustHaveLengthIn(range, factory)
105+
);
106+
Test.CustomSpanException(
107+
ReadOnlySpan<int>.Empty,
108+
invalidRange,
109+
(value, range, factory) => value.MustHaveLengthIn(range, factory)
110+
);
111+
Test.CustomMemoryException(
112+
Memory<int>.Empty,
113+
invalidRange,
114+
(value, range, factory) => value.MustHaveLengthIn(range, factory)
115+
);
116+
Test.CustomMemoryException(
117+
ReadOnlyMemory<int>.Empty,
118+
invalidRange,
119+
(value, range, factory) => value.MustHaveLengthIn(range, factory)
120+
);
121+
}
122+
123+
[Fact]
124+
public static void RangedLengthFactoriesDoNotThrowForEveryValidShape()
125+
{
126+
var array = new[] { 1, 2, 3 };
127+
var validRange = Range.InclusiveBetween(3, 3);
128+
var span = array.AsSpan();
129+
ReadOnlySpan<int> readOnlySpan = array;
130+
var memory = array.AsMemory();
131+
ReadOnlyMemory<int> readOnlyMemory = array;
132+
ReadOnlySpanExceptionFactory<int, Range<int>> factory =
133+
(_, _) => new ("The factory should not be invoked.");
134+
135+
(span.MustHaveLengthIn(validRange, factory) == span).Should().BeTrue();
136+
(readOnlySpan.MustHaveLengthIn(validRange, factory) == readOnlySpan).Should().BeTrue();
137+
memory.MustHaveLengthIn(validRange, factory).Should().Be(memory);
138+
readOnlyMemory.MustHaveLengthIn(validRange, factory).Should().Be(readOnlyMemory);
92139
}
93140

94141
[Fact]
@@ -112,8 +159,22 @@ public static void MemoryExactLengthFailuresSupportMessagesExpressionsAndFactori
112159
.WithParameterName(nameof(memory))
113160
.WithMessage("*custom*");
114161
Test.CustomMemoryException(memory, 1, (value, length, factory) => value.MustHaveLength(length, factory));
115-
Test.CustomMemoryException(readOnlyMemory, 1,
116-
(value, length, factory) => value.MustHaveLength(length, factory));
162+
Test.CustomMemoryException(
163+
readOnlyMemory,
164+
1,
165+
(value, length, factory) => value.MustHaveLength(length, factory)
166+
);
167+
}
168+
169+
[Fact]
170+
public static void MemoryExactLengthFactoriesDoNotThrowForValidLengths()
171+
{
172+
var memory = new[] { 1, 2 }.AsMemory();
173+
ReadOnlyMemory<int> readOnlyMemory = memory;
174+
ReadOnlySpanExceptionFactory<int, int> factory = (_, _) => new ("The factory should not be invoked.");
175+
176+
memory.MustHaveLength(2, factory).Should().Be(memory);
177+
readOnlyMemory.MustHaveLength(2, factory).Should().Be(readOnlyMemory);
117178
}
118179

119180
[Fact]
@@ -152,16 +213,40 @@ public static void EmptyAndWhitespaceFailuresUseExistingStringExceptions()
152213
.WithMessage("*custom*");
153214
}
154215

216+
[Fact]
217+
public static void WhitespaceFactoriesDoNotThrowForEveryValidShape()
218+
{
219+
var characters = " a".ToCharArray();
220+
var span = characters.AsSpan();
221+
ReadOnlySpan<char> readOnlySpan = characters;
222+
var memory = characters.AsMemory();
223+
ReadOnlyMemory<char> readOnlyMemory = characters;
224+
ReadOnlySpanExceptionFactory<char> factory = _ => new ("The factory should not be invoked.");
225+
226+
(span.MustNotBeEmptyOrWhiteSpace(factory) == span).Should().BeTrue();
227+
(readOnlySpan.MustNotBeEmptyOrWhiteSpace(factory) == readOnlySpan).Should().BeTrue();
228+
memory.MustNotBeEmptyOrWhiteSpace(factory).Should().Be(memory);
229+
readOnlyMemory.MustNotBeEmptyOrWhiteSpace(factory).Should().Be(readOnlyMemory);
230+
}
231+
155232
[Fact]
156233
public static void WhitespaceFactoriesReceiveEveryShape()
157234
{
158-
Test.CustomSpanException(" ".ToCharArray().AsSpan(),
159-
(value, factory) => value.MustNotBeEmptyOrWhiteSpace(factory));
160-
Test.CustomSpanException((ReadOnlySpan<char>) " ".ToCharArray(),
161-
(value, factory) => value.MustNotBeEmptyOrWhiteSpace(factory));
162-
Test.CustomMemoryException(" ".ToCharArray().AsMemory(),
163-
(value, factory) => value.MustNotBeEmptyOrWhiteSpace(factory));
164-
Test.CustomMemoryException((ReadOnlyMemory<char>) " ".ToCharArray(),
165-
(value, factory) => value.MustNotBeEmptyOrWhiteSpace(factory));
235+
Test.CustomSpanException(
236+
" ".ToCharArray().AsSpan(),
237+
(value, factory) => value.MustNotBeEmptyOrWhiteSpace(factory)
238+
);
239+
Test.CustomSpanException(
240+
(ReadOnlySpan<char>) " ".ToCharArray(),
241+
(value, factory) => value.MustNotBeEmptyOrWhiteSpace(factory)
242+
);
243+
Test.CustomMemoryException(
244+
" ".ToCharArray().AsMemory(),
245+
(value, factory) => value.MustNotBeEmptyOrWhiteSpace(factory)
246+
);
247+
Test.CustomMemoryException(
248+
(ReadOnlyMemory<char>) " ".ToCharArray(),
249+
(value, factory) => value.MustNotBeEmptyOrWhiteSpace(factory)
250+
);
166251
}
167252
}

tests/Light.GuardClauses.Tests/CollectionAssertions/MustContainKeyTests.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public static void KeyNotPresent()
1919
{
2020
var dictionary = new Dictionary<string, int> { ["Foo"] = 1, ["Bar"] = 2 };
2121

22-
Action act = () => dictionary.MustContainKey("Baz", nameof(dictionary));
22+
Action act = () => dictionary.MustContainKey("Baz");
2323

2424
var assertion = act.Should().Throw<MissingKeyException>().Which;
2525
assertion.Message.Should()
@@ -41,7 +41,7 @@ public static void InterfaceKeyNotPresent()
4141
{
4242
IReadOnlyDictionary<string, object> dictionary = new Dictionary<string, object> { ["Foo"] = 1 };
4343

44-
Action act = () => dictionary.MustContainKey("Bar", nameof(dictionary));
44+
Action act = () => dictionary.MustContainKey("Bar");
4545

4646
act.Should().Throw<MissingKeyException>()
4747
.And.Message.Should()
@@ -127,7 +127,15 @@ public static void CustomExceptionNotThrown()
127127
{
128128
var dictionary = new Dictionary<string, int> { ["Foo"] = 1 };
129129

130-
dictionary.MustContainKey("Foo", (_, _) => new Exception()).Should().BeSameAs(dictionary);
130+
dictionary.MustContainKey("Foo", (_, _) => new ()).Should().BeSameAs(dictionary);
131+
}
132+
133+
[Fact]
134+
public static void InterfaceCustomExceptionNotThrown()
135+
{
136+
IReadOnlyDictionary<string, int> dictionary = new Dictionary<string, int> { ["Foo"] = 1 };
137+
138+
dictionary.MustContainKey("Foo", (_, _) => new ()).Should().BeSameAs(dictionary);
131139
}
132140

133141
[Fact]
@@ -158,7 +166,7 @@ public static void DictionaryShapeIsPreservedInFluentChains()
158166
{
159167
var map = new Dictionary<string, string> { ["endpoint"] = "https://example.com" };
160168

161-
Dictionary<string, string> result = map.MustNotBeNull().MustContainKey("endpoint");
169+
var result = map.MustNotBeNull().MustContainKey("endpoint");
162170

163171
result.Should().BeSameAs(map);
164172
}

tests/Light.GuardClauses.Tests/CollectionAssertions/MustContainTests.cs

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public static class MustContainTests
1515
[InlineData(new[] { -5491, 6199 }, 42)]
1616
public static void ItemNotPartOf(int[] collection, int item)
1717
{
18-
Action act = () => collection.MustContain(item, nameof(collection));
18+
Action act = () => collection.MustContain(item);
1919

2020
var assertion = act.Should().Throw<MissingItemException>().Which;
2121
assertion.Message.Should().Contain($"{nameof(collection)} must contain {item}, but it actually does not.");
@@ -41,29 +41,60 @@ public static void CollectionNull()
4141
[InlineData(new[] { long.MinValue, long.MaxValue }, 42L)]
4242
[InlineData(null, 42L)]
4343
public static void CustomException(long[] array, long item) =>
44-
Test.CustomException(array,
45-
item,
46-
(collection, i, exceptionFactory) => collection.MustContain(i, exceptionFactory));
44+
Test.CustomException(
45+
array,
46+
item,
47+
(collection, i, exceptionFactory) => collection.MustContain(i, exceptionFactory)
48+
);
4749

4850
[Fact]
4951
public static void CustomExceptionNotThrown()
5052
{
5153
var collection = new List<int> { 1, 2, 3 };
52-
collection.MustContain(2, (_, _) => new Exception()).Should().BeSameAs(collection);
54+
collection.MustContain(2, (_, _) => new ()).Should().BeSameAs(collection);
55+
}
56+
57+
[Fact]
58+
public static void LazyEnumerableItemPartOf()
59+
{
60+
var enumerable = new TrackingEnumerable<int>([1, 2, 3], false);
61+
62+
enumerable.MustContain(2).Should().BeSameAs(enumerable);
63+
}
64+
65+
[Fact]
66+
public static void LazyEnumerableItemNotPartOf()
67+
{
68+
var enumerable = new TrackingEnumerable<int>([1, 2, 3], false);
69+
70+
var act = () => enumerable.MustContain(42);
71+
72+
act.Should().Throw<MissingItemException>()
73+
.WithParameterName(nameof(enumerable));
74+
}
75+
76+
[Fact]
77+
public static void CustomExceptionNotThrownLazyEnumerable()
78+
{
79+
var enumerable = new TrackingEnumerable<int>([1, 2, 3], false);
80+
81+
enumerable.MustContain(3, (_, _) => new ()).Should().BeSameAs(enumerable);
5382
}
5483

5584
[Fact]
5685
public static void CustomMessage() =>
5786
Test.CustomMessage<MissingItemException>(message => new List<string>().MustContain("Foo", message: message));
5887

5988
[Fact]
60-
public static void CustomMessageCollectionNull() =>
61-
Test.CustomMessage<ArgumentNullException>(message => ((ObservableCollection<string>) null).MustContain("Foo", message: message));
89+
public static void CustomMessageCollectionNull() =>
90+
Test.CustomMessage<ArgumentNullException>(
91+
message => ((ObservableCollection<string>) null).MustContain("Foo", message: message)
92+
);
6293

6394
[Fact]
6495
public static void CallerArgumentExpression()
6596
{
66-
var array = new [] { "Foo", "Bar" };
97+
var array = new[] { "Foo", "Bar" };
6798

6899
var act = () => array.MustContain("Baz");
69100

@@ -77,7 +108,7 @@ public static void CallerArgumentExpression()
77108
public static void ImmutableArrayItemNotPartOf(int[] source, int item)
78109
{
79110
var immutableArray = source.ToImmutableArray();
80-
Action act = () => immutableArray.MustContain(item, nameof(immutableArray));
111+
Action act = () => immutableArray.MustContain(item);
81112

82113
var assertion = act.Should().Throw<MissingItemException>().Which;
83114
assertion.Message.Should().Contain($"{nameof(immutableArray)} must contain {item}, but it actually does not.");
@@ -119,7 +150,7 @@ public static void ImmutableArrayCustomException(long[] source, long item)
119150
public static void ImmutableArrayCustomExceptionNotThrown()
120151
{
121152
var immutableArray = ImmutableArray.Create(1, 2, 3);
122-
immutableArray.MustContain(2, (_, _) => new Exception()).Should().Equal(immutableArray);
153+
immutableArray.MustContain(2, (_, _) => new ()).Should().Equal(immutableArray);
123154
}
124155

125156
[Fact]
@@ -138,4 +169,4 @@ public static void ImmutableArrayCallerArgumentExpression()
138169
act.Should().Throw<MissingItemException>()
139170
.WithParameterName(nameof(immutableArray));
140171
}
141-
}
172+
}

tests/Light.GuardClauses.Tests/CollectionAssertions/MustHaveLengthInTests.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public static void LengthInRange(ImmutableArray<int> array, Range<int> range) =>
2525
[MemberData(nameof(LengthNotInRangeData))]
2626
public static void LengthNotInRange(ImmutableArray<int> array, Range<int> range)
2727
{
28-
var act = () => array.MustHaveLengthIn(range, nameof(array));
28+
var act = () => array.MustHaveLengthIn(range);
2929

3030
act.Should().Throw<ArgumentOutOfRangeException>()
3131
.And.Message.Should().Contain($"must have its length in between {range.CreateRangeDescriptionText("and")}")
@@ -49,6 +49,16 @@ public static void CustomException() =>
4949
(array, r, exceptionFactory) => array.MustHaveLengthIn(r, exceptionFactory)
5050
);
5151

52+
[Fact]
53+
public static void CustomExceptionNotThrown()
54+
{
55+
var array = ImmutableArray.Create(1, 2, 3);
56+
57+
var result = array.MustHaveLengthIn(Range.InclusiveBetween(1, 3), (_, _) => new ());
58+
59+
result.Should().Equal(array);
60+
}
61+
5262
[Fact]
5363
public static void CustomMessage() =>
5464
Test.CustomMessage<ArgumentOutOfRangeException>(
@@ -84,7 +94,7 @@ public static void DefaultImmutableArrayNotInRange()
8494
{
8595
var defaultArray = default(ImmutableArray<int>);
8696

87-
var act = () => defaultArray.MustHaveLengthIn(Range.FromInclusive(1).ToInclusive(5), nameof(defaultArray));
97+
var act = () => defaultArray.MustHaveLengthIn(Range.FromInclusive(1).ToInclusive(5));
8898

8999
act.Should().Throw<ArgumentOutOfRangeException>()
90100
.And.Message.Should().Contain("must have its length in between 1 (inclusive) and 5 (inclusive)")

0 commit comments

Comments
 (0)