Skip to content

Commit 0de585a

Browse files
authored
feat: add async Member() overloads for Task-returning member selectors (#5475)
* feat: add async Member() overloads for Task-returning member selectors Support asserting on async members directly in the fluent chain, e.g.: await Assert.That(obj).Member(x => x.ReadStringAsync(), str => str.IsEqualTo("expected")); Previously, users had to break async member assertions into separate statements. The new overloads accept Expression<Func<TObject, Task<TMember>>> and automatically await the Task before passing the unwrapped value to the assertion lambda. Also hoists Expression.Compile() outside mapper lambdas in all existing sync Member overloads to avoid recompilation on every evaluation, and improves GetMemberPath to handle method call expressions (e.g. BarAsync()) including chained member access (e.g. Foo.BarAsync()). Closes #5470 * chore: minor cleanup from code review - Rename `compiledSelector` to `compiled` in async overloads for consistency with sync overloads - Remove section separator comments from test file * fix: update public API snapshot for async Member() overloads Add the new Member<TObject, TMember> and Member<TObject, TMember, TTransformed> overloads for Task-returning member selectors to the verified API snapshot. * fix: address code review items for async member assertions - Fix GetMemberPath bug for chained method calls by merging the MethodCallExpression handling into the while loop so expressions like x => x.GetFoo().ReadAsync() correctly produce "GetFoo().ReadAsync()" - Add matching ValueTask<TMember> overloads for all three Task<TMember> Member() overload variants (TTransformed, TMember, and object fallback) - Fix misleading "backward compatibility" doc comment on the catch-all object-returning overload since this is new API, not legacy - Update public API snapshot for the new ValueTask overloads * fix: extract BuildMemberResult helper and remove fragile message assertion Extract the duplicated combine-and-return block (12 occurrences across sync and async Member overloads) into a private static BuildMemberResult helper method. Replace the fragile error-message assertion in Async_Member_Or_Both_Fail test with a type-only check, since ThrowsAsync<AssertionException> already validates the exception type. * refactor: use BuildMemberResult helper in ValueTask overloads Replace the inlined combine-and-return pattern in the 3 ValueTask Member() overloads with calls to the existing BuildMemberResult helper, matching the sync and Task overloads. * fix: update public API snapshots for net8.0 and net9.0 The previous snapshot updates only covered net10.0. The net8.0 and net9.0 snapshots also need updating to include the new ValueTask Member() overloads. * fix: update public API snapshot for net472
1 parent 30416fb commit 0de585a

6 files changed

+543
-107
lines changed
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
namespace TUnit.Assertions.Tests;
2+
3+
public class AsyncMemberTests
4+
{
5+
private sealed class MyObject
6+
{
7+
public required string Name { get; init; }
8+
public required int Number { get; init; }
9+
10+
public Task<string> ReadStringAsync() => Task.FromResult(Name);
11+
12+
public Task<int> ReadNumberAsync() => Task.FromResult(Number);
13+
14+
public async Task<string> ReadStringWithDelayAsync()
15+
{
16+
await Task.Delay(10);
17+
return Name;
18+
}
19+
20+
public Task<string> ThrowingAsync() => Task.FromException<string>(new InvalidOperationException("Boom"));
21+
22+
public Task<string?> ReadNullableStringAsync() => Task.FromResult<string?>(null);
23+
24+
public Task<string?> ReadNullableStringWithValueAsync() => Task.FromResult<string?>(Name);
25+
}
26+
27+
[Test]
28+
public async Task Async_Member_String_Success()
29+
{
30+
var obj = new MyObject { Name = "hello", Number = 42 };
31+
await Assert.That(obj).Member(x => x.ReadStringAsync(), value => value.IsEqualTo("hello"));
32+
}
33+
34+
[Test]
35+
public async Task Async_Member_Int_Success()
36+
{
37+
var obj = new MyObject { Name = "hello", Number = 42 };
38+
await Assert.That(obj).Member(x => x.ReadNumberAsync(), value => value.IsEqualTo(42));
39+
}
40+
41+
[Test]
42+
public async Task Async_Member_With_Delay_Success()
43+
{
44+
var obj = new MyObject { Name = "delayed", Number = 1 };
45+
await Assert.That(obj).Member(x => x.ReadStringWithDelayAsync(), value => value.IsEqualTo("delayed"));
46+
}
47+
48+
[Test]
49+
public async Task Async_Member_String_Contains()
50+
{
51+
var obj = new MyObject { Name = "hello world", Number = 1 };
52+
await Assert.That(obj).Member(x => x.ReadStringAsync(), value => value.Contains("world"));
53+
}
54+
55+
[Test]
56+
public async Task Async_Member_Nullable_IsNull_Success()
57+
{
58+
var obj = new MyObject { Name = "test", Number = 1 };
59+
await Assert.That(obj).Member(x => x.ReadNullableStringAsync(), value => value.IsNull());
60+
}
61+
62+
[Test]
63+
public async Task Async_Member_Nullable_IsNotNull_Success()
64+
{
65+
var obj = new MyObject { Name = "test", Number = 1 };
66+
await Assert.That(obj).Member(x => x.ReadNullableStringWithValueAsync(), value => value.IsNotNull());
67+
}
68+
69+
[Test]
70+
public async Task Async_Member_Chained_With_And()
71+
{
72+
var obj = new MyObject { Name = "hello", Number = 42 };
73+
74+
await Assert.That(obj)
75+
.Member(x => x.ReadStringAsync(), value => value.IsEqualTo("hello"))
76+
.And.Member(x => x.ReadNumberAsync(), value => value.IsEqualTo(42));
77+
}
78+
79+
[Test]
80+
public async Task Async_Member_Chained_With_Sync_Member()
81+
{
82+
var obj = new MyObject { Name = "hello", Number = 42 };
83+
84+
await Assert.That(obj)
85+
.Member(x => x.Name, value => value.IsEqualTo("hello"))
86+
.And.Member(x => x.ReadNumberAsync(), value => value.IsEqualTo(42));
87+
}
88+
89+
[Test]
90+
public async Task Async_Member_Chained_Sync_After_Async()
91+
{
92+
var obj = new MyObject { Name = "hello", Number = 42 };
93+
94+
await Assert.That(obj)
95+
.Member(x => x.ReadStringAsync(), value => value.IsEqualTo("hello"))
96+
.And.Member(x => x.Number, value => value.IsEqualTo(42));
97+
}
98+
99+
[Test]
100+
public async Task Async_Member_Chained_With_IsNotNull()
101+
{
102+
var obj = new MyObject { Name = "hello", Number = 42 };
103+
104+
await Assert.That(obj)
105+
.IsNotNull()
106+
.And.Member(x => x.ReadStringAsync(), value => value.IsEqualTo("hello"));
107+
}
108+
109+
[Test]
110+
public async Task Async_Member_Chained_With_Or()
111+
{
112+
var obj = new MyObject { Name = "hello", Number = 42 };
113+
114+
await Assert.That(obj)
115+
.Member(x => x.ReadStringAsync(), value => value.IsEqualTo("wrong"))
116+
.Or.Member(x => x.ReadNumberAsync(), value => value.IsEqualTo(42));
117+
}
118+
119+
[Test]
120+
public async Task Async_Member_Multiple_Async_Chained()
121+
{
122+
var obj = new MyObject { Name = "hello", Number = 42 };
123+
124+
await Assert.That(obj)
125+
.Member(x => x.ReadStringAsync(), value => value.IsEqualTo("hello"))
126+
.And.Member(x => x.ReadNumberAsync(), value => value.IsEqualTo(42))
127+
.And.Member(x => x.ReadStringWithDelayAsync(), value => value.IsEqualTo("hello"));
128+
}
129+
130+
[Test]
131+
public async Task Async_Member_String_Failure()
132+
{
133+
var obj = new MyObject { Name = "hello", Number = 42 };
134+
135+
var exception = await Assert.ThrowsAsync<AssertionException>(async () =>
136+
await Assert.That(obj).Member(x => x.ReadStringAsync(), value => value.IsEqualTo("world")));
137+
138+
await Assert.That(exception!.Message).Contains("world");
139+
}
140+
141+
[Test]
142+
public async Task Async_Member_Int_Failure()
143+
{
144+
var obj = new MyObject { Name = "hello", Number = 42 };
145+
146+
var exception = await Assert.ThrowsAsync<AssertionException>(async () =>
147+
await Assert.That(obj).Member(x => x.ReadNumberAsync(), value => value.IsEqualTo(99)));
148+
149+
await Assert.That(exception!.Message).Contains("99");
150+
await Assert.That(exception.Message).Contains("42");
151+
}
152+
153+
[Test]
154+
public async Task Async_Member_Throwing_Method()
155+
{
156+
var obj = new MyObject { Name = "hello", Number = 42 };
157+
158+
await Assert.ThrowsAsync<AssertionException>(async () =>
159+
await Assert.That(obj).Member(x => x.ThrowingAsync(), value => value.IsEqualTo("anything")));
160+
}
161+
162+
[Test]
163+
public async Task Async_Member_Null_Object()
164+
{
165+
MyObject obj = null!;
166+
167+
await Assert.ThrowsAsync<AssertionException>(async () =>
168+
await Assert.That(obj).Member(x => x.ReadStringAsync(), value => value.IsEqualTo("hello")));
169+
}
170+
171+
[Test]
172+
public async Task Async_Member_Chained_First_Fails()
173+
{
174+
var obj = new MyObject { Name = "hello", Number = 42 };
175+
176+
var exception = await Assert.ThrowsAsync<AssertionException>(async () =>
177+
await Assert.That(obj)
178+
.Member(x => x.ReadStringAsync(), value => value.IsEqualTo("wrong"))
179+
.And.Member(x => x.ReadNumberAsync(), value => value.IsEqualTo(42)));
180+
181+
await Assert.That(exception!.Message).Contains("wrong");
182+
}
183+
184+
[Test]
185+
public async Task Async_Member_Chained_Second_Fails()
186+
{
187+
var obj = new MyObject { Name = "hello", Number = 42 };
188+
189+
var exception = await Assert.ThrowsAsync<AssertionException>(async () =>
190+
await Assert.That(obj)
191+
.Member(x => x.ReadStringAsync(), value => value.IsEqualTo("hello"))
192+
.And.Member(x => x.ReadNumberAsync(), value => value.IsEqualTo(99)));
193+
194+
await Assert.That(exception!.Message).Contains("99");
195+
}
196+
197+
[Test]
198+
public async Task Async_Member_Or_Both_Fail()
199+
{
200+
var obj = new MyObject { Name = "hello", Number = 42 };
201+
202+
var exception = await Assert.ThrowsAsync<AssertionException>(async () =>
203+
await Assert.That(obj)
204+
.Member(x => x.ReadStringAsync(), value => value.IsEqualTo("wrong"))
205+
.Or.Member(x => x.ReadNumberAsync(), value => value.IsEqualTo(99)));
206+
207+
await Assert.That(exception).IsNotNull();
208+
}
209+
210+
[Test]
211+
public async Task Async_Member_Nullable_NotNull_Fails_When_Null()
212+
{
213+
var obj = new MyObject { Name = "test", Number = 1 };
214+
215+
var exception = await Assert.ThrowsAsync<AssertionException>(async () =>
216+
await Assert.That(obj).Member(x => x.ReadNullableStringAsync(), value => value.IsNotNull()));
217+
218+
await Assert.That(exception!.Message).Contains("not be null");
219+
}
220+
}

0 commit comments

Comments
 (0)