Skip to content

Commit fd1d985

Browse files
CopilotMrKWatkins
andcommitted
Add AsyncActionAssertions with ThrowAsync/NotThrowAsync, Should(Func<Task>), and Awaiting extensions
Co-authored-by: MrKWatkins <345796+MrKWatkins@users.noreply.github.com>
1 parent 7b2c644 commit fd1d985

File tree

4 files changed

+236
-0
lines changed

4 files changed

+236
-0
lines changed
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
namespace MrKWatkins.Assertions.Tests;
2+
3+
public sealed class AsyncActionAssertionsTests
4+
{
5+
[Test]
6+
public async Task ThrowAsync()
7+
{
8+
Func<Task> doesNotThrow = () => Task.CompletedTask;
9+
10+
var exception = new InvalidOperationException("Test");
11+
Func<Task> throws = () => throw exception;
12+
13+
Func<Task> throwsWrongException = () => throw new NotSupportedException("Wrong");
14+
15+
await Assert.That(() => throws.Should().ThrowAsync<InvalidOperationException>()).ThrowsNothing();
16+
17+
await Assert.That(() => doesNotThrow.Should().ThrowAsync<InvalidOperationException>())
18+
.Throws<AssertionException>().WithMessage("Function should throw an InvalidOperationException.");
19+
20+
await Assert.That(() => throwsWrongException.Should().ThrowAsync<InvalidOperationException>())
21+
.Throws<AssertionException>().WithMessage("Function should throw an InvalidOperationException but threw a NotSupportedException with message \"Wrong\".");
22+
}
23+
24+
[Test]
25+
public async Task ThrowAsync_Chain()
26+
{
27+
var exception = new InvalidOperationException("Test");
28+
Func<Task> throws = () => throw exception;
29+
30+
var chain = await throws.Should().ThrowAsync<InvalidOperationException>().ConfigureAwait(false);
31+
32+
await Assert.That(chain.Exception).IsSameReferenceAs(exception);
33+
await Assert.That(chain.That).IsSameReferenceAs(exception);
34+
}
35+
36+
[Test]
37+
public async Task ThrowAsync_String()
38+
{
39+
Func<Task> doesNotThrow = () => Task.CompletedTask;
40+
41+
var exception = new InvalidOperationException("Test");
42+
Func<Task> throws = () => throw exception;
43+
44+
Func<Task> throwsWrongException = () => throw new NotSupportedException("Wrong");
45+
46+
await Assert.That(() => throws.Should().ThrowAsync<InvalidOperationException>("Test")).ThrowsNothing();
47+
48+
await Assert.That(() => throws.Should().ThrowAsync<InvalidOperationException>("Wrong Message"))
49+
.Throws<AssertionException>().WithMessage("Value should have Message \"Wrong Message\" but was \"Test\".");
50+
51+
await Assert.That(() => doesNotThrow.Should().ThrowAsync<InvalidOperationException>("Test"))
52+
.Throws<AssertionException>().WithMessage("Function should throw an InvalidOperationException.");
53+
54+
await Assert.That(() => throwsWrongException.Should().ThrowAsync<InvalidOperationException>("Test"))
55+
.Throws<AssertionException>().WithMessage("Function should throw an InvalidOperationException but threw a NotSupportedException with message \"Wrong\".");
56+
}
57+
58+
[Test]
59+
public async Task ThrowAsync_String_Chain()
60+
{
61+
var exception = new InvalidOperationException("Test");
62+
Func<Task> throws = () => throw exception;
63+
64+
var chain = await throws.Should().ThrowAsync<InvalidOperationException>("Test").ConfigureAwait(false);
65+
66+
await Assert.That(chain.Exception).IsSameReferenceAs(exception);
67+
await Assert.That(chain.That).IsSameReferenceAs(exception);
68+
}
69+
70+
[Test]
71+
public async Task NotThrowAsync()
72+
{
73+
Func<Task> doesNotThrow = () => Task.CompletedTask;
74+
75+
var exception = new InvalidOperationException("Test");
76+
Func<Task> throws = () => throw exception;
77+
78+
await Assert.That(() => doesNotThrow.Should().NotThrowAsync()).ThrowsNothing();
79+
80+
var actualException = await Assert.That(() => throws.Should().NotThrowAsync())
81+
.Throws<AssertionException>()
82+
.WithMessage("Function should not throw but threw an InvalidOperationException with message \"Test\".");
83+
await Assert.That(actualException!.InnerException).IsSameReferenceAs(exception);
84+
}
85+
86+
[Test]
87+
public async Task ThrowAsync_ActuallyAsync()
88+
{
89+
var exception = new InvalidOperationException("AsyncTest");
90+
Func<Task> throwsAsync = async () =>
91+
{
92+
await Task.Yield();
93+
throw exception;
94+
};
95+
96+
var chain = await throwsAsync.Should().ThrowAsync<InvalidOperationException>("AsyncTest").ConfigureAwait(false);
97+
await Assert.That(chain.Exception).IsSameReferenceAs(exception);
98+
}
99+
100+
[Test]
101+
public async Task NotThrowAsync_ActuallyAsync()
102+
{
103+
Func<Task> doesNotThrow = async () => await Task.Yield();
104+
105+
await Assert.That(() => doesNotThrow.Should().NotThrowAsync()).ThrowsNothing();
106+
}
107+
108+
[Test]
109+
public async Task Awaiting_InvokingExtensions()
110+
{
111+
var value = new TestClass();
112+
113+
await Assert.That(() => value.Awaiting(v => v.ThrowAsync()).Should().ThrowAsync<InvalidOperationException>())
114+
.ThrowsNothing();
115+
116+
await Assert.That(() => value.Awaiting(v => v.NotThrowAsync()).Should().NotThrowAsync())
117+
.ThrowsNothing();
118+
}
119+
120+
[Test]
121+
public async Task Awaiting_InvokingExtensions_WithReturn()
122+
{
123+
var value = new TestClass();
124+
125+
await Assert.That(() => value.Awaiting(v => v.ThrowWithReturnAsync()).Should().ThrowAsync<InvalidOperationException>())
126+
.ThrowsNothing();
127+
}
128+
129+
private sealed class TestClass
130+
{
131+
public Task ThrowAsync() => throw new InvalidOperationException();
132+
133+
public async Task NotThrowAsync() => await Task.Yield();
134+
135+
public Task<string> ThrowWithReturnAsync() => throw new InvalidOperationException();
136+
}
137+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
namespace MrKWatkins.Assertions;
2+
3+
/// <summary>
4+
/// Provides assertions for async actions (delegates returning <see cref="Task"/>), such as verifying that exceptions are thrown.
5+
/// </summary>
6+
/// <param name="action">The async action to assert on.</param>
7+
public sealed class AsyncActionAssertions(Func<Task> action)
8+
{
9+
/// <summary>
10+
/// Asserts that the async action throws an exception of the specified type.
11+
/// </summary>
12+
/// <typeparam name="TException">The expected exception type.</typeparam>
13+
/// <returns>A <see cref="Task{TResult}"/> that resolves to an <see cref="ActionAssertionsChain{TException}" /> containing the thrown exception.</returns>
14+
public async Task<ActionAssertionsChain<TException>> ThrowAsync<TException>()
15+
where TException : Exception
16+
{
17+
TException? thrown = null;
18+
try
19+
{
20+
await action().ConfigureAwait(false);
21+
}
22+
catch (Exception exception)
23+
{
24+
if (exception is not TException typedException)
25+
{
26+
throw Verify.CreateException($"Function should throw {Format.PrefixWithAOrAn(typeof(TException).Name)} but threw {Format.PrefixWithAOrAn(exception.GetType().Name)} with message {Format.Value(exception.Message)}.", exception);
27+
}
28+
thrown = typedException;
29+
}
30+
31+
if (thrown == null)
32+
{
33+
throw Verify.CreateException($"Function should throw {Format.PrefixWithAOrAn(typeof(TException).Name)}.");
34+
}
35+
36+
return new ActionAssertionsChain<TException>(thrown);
37+
}
38+
39+
/// <summary>
40+
/// Asserts that the async action throws an exception of the specified type with the specified message.
41+
/// </summary>
42+
/// <typeparam name="TException">The expected exception type.</typeparam>
43+
/// <param name="expectedMessage">The expected exception message.</param>
44+
/// <returns>A <see cref="Task{TResult}"/> that resolves to an <see cref="ActionAssertionsChain{TException}" /> containing the thrown exception.</returns>
45+
public async Task<ActionAssertionsChain<TException>> ThrowAsync<TException>(string expectedMessage)
46+
where TException : Exception
47+
{
48+
var chain = await ThrowAsync<TException>().ConfigureAwait(false);
49+
50+
chain.That.Should().HaveMessage(expectedMessage);
51+
52+
return chain;
53+
}
54+
55+
/// <summary>
56+
/// Asserts that the async action does not throw any exception.
57+
/// </summary>
58+
/// <returns>A <see cref="Task"/> that completes successfully if no exception is thrown.</returns>
59+
public async Task NotThrowAsync()
60+
{
61+
try
62+
{
63+
await action().ConfigureAwait(false);
64+
}
65+
catch (Exception exception)
66+
{
67+
throw Verify.CreateException($"Function should not throw but threw {Format.PrefixWithAOrAn(exception.GetType().Name)} with message {Format.Value(exception.Message)}.", exception);
68+
}
69+
}
70+
}

src/MrKWatkins.Assertions/InvokingExtensions.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,25 @@ public static class InvokingExtensions
2525
/// <returns>An action that invokes the specified function with the value, for use with <see cref="ShouldExtensions.Should(Action)" />.</returns>
2626
[Pure]
2727
public static Action Invoking<T, TResult>(this T value, Func<T, TResult> action) => () => action(value);
28+
29+
/// <summary>
30+
/// Wraps an async action on the specified value for assertion testing.
31+
/// </summary>
32+
/// <typeparam name="T">The type of the value.</typeparam>
33+
/// <param name="value">The value to pass to the async action.</param>
34+
/// <param name="action">The async action to test.</param>
35+
/// <returns>A <see cref="Func{Task}"/> that invokes the specified async action with the value, for use with <see cref="ShouldExtensions.Should(Func{Task})" />.</returns>
36+
[Pure]
37+
public static Func<Task> Awaiting<T>(this T value, Func<T, Task> action) => () => action(value);
38+
39+
/// <summary>
40+
/// Wraps an async function on the specified value for assertion testing, discarding the return value.
41+
/// </summary>
42+
/// <typeparam name="T">The type of the value.</typeparam>
43+
/// <typeparam name="TResult">The return type of the async function.</typeparam>
44+
/// <param name="value">The value to pass to the async function.</param>
45+
/// <param name="action">The async function to test.</param>
46+
/// <returns>A <see cref="Func{Task}"/> that invokes the specified async function with the value, for use with <see cref="ShouldExtensions.Should(Func{Task})" />.</returns>
47+
[Pure]
48+
public static Func<Task> Awaiting<T, TResult>(this T value, Func<T, Task<TResult>> action) => () => action(value);
2849
}

src/MrKWatkins.Assertions/ShouldExtensions.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,14 @@ public static ReadOnlyDictionaryAssertions<Dictionary<TKey, TValue>, TKey, TValu
112112
[Pure]
113113
public static ActionAssertions Should([InstantHandle] this Action value) => new(value);
114114

115+
/// <summary>
116+
/// Begins a fluent assertion on the specified async action.
117+
/// </summary>
118+
/// <param name="value">The async action to assert on.</param>
119+
/// <returns>An <see cref="AsyncActionAssertions" /> for the async action.</returns>
120+
[Pure]
121+
public static AsyncActionAssertions Should([InstantHandle] this Func<Task> value) => new(value);
122+
115123
/// <summary>
116124
/// Begins a fluent assertion on the specified byte value.
117125
/// </summary>

0 commit comments

Comments
 (0)