Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions Src/FluentAssertions/AssertionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,40 @@ public static GenericAsyncFunctionAssertions<T> Should<T>([NotNull] this Func<Ta
return new GenericAsyncFunctionAssertions<T>(action, Extractor, AssertionChain.GetOrCreate());
}

/// <summary>
/// Returns a <see cref="NonGenericAsyncFunctionAssertions"/> object that can be used to assert the
/// current <see><cref>System.Func{ValueTask}</cref></see>.
/// </summary>
/// <remarks>
/// Because the assertions operate on tasks, the delegate is wrapped in a <see cref="Func{Task}"/> that converts the
/// value task returned by every invocation into a task. The <c>Subject</c> property therefore exposes that wrapper
/// instead of the original delegate.
/// </remarks>
[Pure]
public static NonGenericAsyncFunctionAssertions Should([NotNull] this Func<ValueTask> action)
{
Func<Task> taskFunc = action is null ? null : () => action().AsTask();

return new NonGenericAsyncFunctionAssertions(taskFunc, Extractor, AssertionChain.GetOrCreate());
}

/// <summary>
/// Returns a <see cref="GenericAsyncFunctionAssertions{T}"/> object that can be used to assert the
/// current <see><cref>System.Func{ValueTask{T}}</cref></see>.
/// </summary>
/// <remarks>
/// Because the assertions operate on tasks, the delegate is wrapped in a <see cref="Func{Task}"/> that converts the
/// value task returned by every invocation into a task. The <c>Subject</c> property therefore exposes that wrapper
/// instead of the original delegate.
/// </remarks>
[Pure]
public static GenericAsyncFunctionAssertions<T> Should<T>([NotNull] this Func<ValueTask<T>> action)
{
Func<Task<T>> taskFunc = action is null ? null : () => action().AsTask();

return new GenericAsyncFunctionAssertions<T>(taskFunc, Extractor, AssertionChain.GetOrCreate());
}

/// <summary>
/// Returns a <see cref="FunctionAssertions{T}"/> object that can be used to assert the
/// current <see cref="Func{T}"/>.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ namespace FluentAssertions
public static FluentAssertions.Primitives.DateTimeOffsetAssertions Should(this System.DateTimeOffset actualValue) { }
public static FluentAssertions.Primitives.NullableDateTimeOffsetAssertions Should([System.Diagnostics.CodeAnalysis.NotNull] this System.DateTimeOffset? actualValue) { }
public static FluentAssertions.Specialized.NonGenericAsyncFunctionAssertions Should([System.Diagnostics.CodeAnalysis.NotNull] this System.Func<System.Threading.Tasks.Task> action) { }
public static FluentAssertions.Specialized.NonGenericAsyncFunctionAssertions Should([System.Diagnostics.CodeAnalysis.NotNull] this System.Func<System.Threading.Tasks.ValueTask> action) { }
public static FluentAssertions.Primitives.GuidAssertions Should(this System.Guid actualValue) { }
public static FluentAssertions.Primitives.NullableGuidAssertions Should([System.Diagnostics.CodeAnalysis.NotNull] this System.Guid? actualValue) { }
public static FluentAssertions.Streams.BufferedStreamAssertions Should([System.Diagnostics.CodeAnalysis.NotNull] this System.IO.BufferedStream actualValue) { }
Expand Down Expand Up @@ -148,6 +149,7 @@ namespace FluentAssertions
where TAssertions : FluentAssertions.Primitives.SimpleTimeSpanAssertions<TAssertions> { }
public static FluentAssertions.Collections.GenericCollectionAssertions<T> Should<T>([System.Diagnostics.CodeAnalysis.NotNull] this System.Collections.Generic.IEnumerable<T> actualValue) { }
public static FluentAssertions.Specialized.GenericAsyncFunctionAssertions<T> Should<T>([System.Diagnostics.CodeAnalysis.NotNull] this System.Func<System.Threading.Tasks.Task<T>> action) { }
public static FluentAssertions.Specialized.GenericAsyncFunctionAssertions<T> Should<T>([System.Diagnostics.CodeAnalysis.NotNull] this System.Func<System.Threading.Tasks.ValueTask<T>> action) { }
public static FluentAssertions.Specialized.FunctionAssertions<T> Should<T>([System.Diagnostics.CodeAnalysis.NotNull] this System.Func<T> func) { }
public static FluentAssertions.Numeric.ComparableTypeAssertions<T> Should<T>([System.Diagnostics.CodeAnalysis.NotNull] this System.IComparable<T> comparableValue) { }
public static FluentAssertions.Specialized.TaskCompletionSourceAssertions<T> Should<T>(this System.Threading.Tasks.TaskCompletionSource<T> tcs) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ namespace FluentAssertions
public static FluentAssertions.Primitives.DateTimeOffsetAssertions Should(this System.DateTimeOffset actualValue) { }
public static FluentAssertions.Primitives.NullableDateTimeOffsetAssertions Should([System.Diagnostics.CodeAnalysis.NotNull] this System.DateTimeOffset? actualValue) { }
public static FluentAssertions.Specialized.NonGenericAsyncFunctionAssertions Should([System.Diagnostics.CodeAnalysis.NotNull] this System.Func<System.Threading.Tasks.Task> action) { }
public static FluentAssertions.Specialized.NonGenericAsyncFunctionAssertions Should([System.Diagnostics.CodeAnalysis.NotNull] this System.Func<System.Threading.Tasks.ValueTask> action) { }
public static FluentAssertions.Primitives.GuidAssertions Should(this System.Guid actualValue) { }
public static FluentAssertions.Primitives.NullableGuidAssertions Should([System.Diagnostics.CodeAnalysis.NotNull] this System.Guid? actualValue) { }
public static FluentAssertions.Streams.BufferedStreamAssertions Should([System.Diagnostics.CodeAnalysis.NotNull] this System.IO.BufferedStream actualValue) { }
Expand Down Expand Up @@ -168,6 +169,7 @@ namespace FluentAssertions
where TAssertions : FluentAssertions.Primitives.TimeOnlyAssertions<TAssertions> { }
public static FluentAssertions.Collections.GenericCollectionAssertions<T> Should<T>([System.Diagnostics.CodeAnalysis.NotNull] this System.Collections.Generic.IEnumerable<T> actualValue) { }
public static FluentAssertions.Specialized.GenericAsyncFunctionAssertions<T> Should<T>([System.Diagnostics.CodeAnalysis.NotNull] this System.Func<System.Threading.Tasks.Task<T>> action) { }
public static FluentAssertions.Specialized.GenericAsyncFunctionAssertions<T> Should<T>([System.Diagnostics.CodeAnalysis.NotNull] this System.Func<System.Threading.Tasks.ValueTask<T>> action) { }
public static FluentAssertions.Specialized.FunctionAssertions<T> Should<T>([System.Diagnostics.CodeAnalysis.NotNull] this System.Func<T> func) { }
public static FluentAssertions.Numeric.ComparableTypeAssertions<T> Should<T>([System.Diagnostics.CodeAnalysis.NotNull] this System.IComparable<T> comparableValue) { }
public static FluentAssertions.Collections.GenericCollectionAssertions<T> Should<T>(this System.Memory<T> actualValue) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ namespace FluentAssertions
public static FluentAssertions.Primitives.DateTimeOffsetAssertions Should(this System.DateTimeOffset actualValue) { }
public static FluentAssertions.Primitives.NullableDateTimeOffsetAssertions Should([System.Diagnostics.CodeAnalysis.NotNull] this System.DateTimeOffset? actualValue) { }
public static FluentAssertions.Specialized.NonGenericAsyncFunctionAssertions Should([System.Diagnostics.CodeAnalysis.NotNull] this System.Func<System.Threading.Tasks.Task> action) { }
public static FluentAssertions.Specialized.NonGenericAsyncFunctionAssertions Should([System.Diagnostics.CodeAnalysis.NotNull] this System.Func<System.Threading.Tasks.ValueTask> action) { }
public static FluentAssertions.Primitives.GuidAssertions Should(this System.Guid actualValue) { }
public static FluentAssertions.Primitives.NullableGuidAssertions Should([System.Diagnostics.CodeAnalysis.NotNull] this System.Guid? actualValue) { }
public static FluentAssertions.Streams.BufferedStreamAssertions Should([System.Diagnostics.CodeAnalysis.NotNull] this System.IO.BufferedStream actualValue) { }
Expand Down Expand Up @@ -146,6 +147,7 @@ namespace FluentAssertions
where TAssertions : FluentAssertions.Primitives.SimpleTimeSpanAssertions<TAssertions> { }
public static FluentAssertions.Collections.GenericCollectionAssertions<T> Should<T>([System.Diagnostics.CodeAnalysis.NotNull] this System.Collections.Generic.IEnumerable<T> actualValue) { }
public static FluentAssertions.Specialized.GenericAsyncFunctionAssertions<T> Should<T>([System.Diagnostics.CodeAnalysis.NotNull] this System.Func<System.Threading.Tasks.Task<T>> action) { }
public static FluentAssertions.Specialized.GenericAsyncFunctionAssertions<T> Should<T>([System.Diagnostics.CodeAnalysis.NotNull] this System.Func<System.Threading.Tasks.ValueTask<T>> action) { }
public static FluentAssertions.Specialized.FunctionAssertions<T> Should<T>([System.Diagnostics.CodeAnalysis.NotNull] this System.Func<T> func) { }
public static FluentAssertions.Numeric.ComparableTypeAssertions<T> Should<T>([System.Diagnostics.CodeAnalysis.NotNull] this System.IComparable<T> comparableValue) { }
public static FluentAssertions.Specialized.TaskCompletionSourceAssertions<T> Should<T>(this System.Threading.Tasks.TaskCompletionSource<T> tcs) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ namespace FluentAssertions
public static FluentAssertions.Primitives.DateTimeOffsetAssertions Should(this System.DateTimeOffset actualValue) { }
public static FluentAssertions.Primitives.NullableDateTimeOffsetAssertions Should([System.Diagnostics.CodeAnalysis.NotNull] this System.DateTimeOffset? actualValue) { }
public static FluentAssertions.Specialized.NonGenericAsyncFunctionAssertions Should([System.Diagnostics.CodeAnalysis.NotNull] this System.Func<System.Threading.Tasks.Task> action) { }
public static FluentAssertions.Specialized.NonGenericAsyncFunctionAssertions Should([System.Diagnostics.CodeAnalysis.NotNull] this System.Func<System.Threading.Tasks.ValueTask> action) { }
public static FluentAssertions.Primitives.GuidAssertions Should(this System.Guid actualValue) { }
public static FluentAssertions.Primitives.NullableGuidAssertions Should([System.Diagnostics.CodeAnalysis.NotNull] this System.Guid? actualValue) { }
public static FluentAssertions.Streams.BufferedStreamAssertions Should([System.Diagnostics.CodeAnalysis.NotNull] this System.IO.BufferedStream actualValue) { }
Expand Down Expand Up @@ -150,6 +151,7 @@ namespace FluentAssertions
where TAssertions : FluentAssertions.Primitives.SimpleTimeSpanAssertions<TAssertions> { }
public static FluentAssertions.Collections.GenericCollectionAssertions<T> Should<T>([System.Diagnostics.CodeAnalysis.NotNull] this System.Collections.Generic.IEnumerable<T> actualValue) { }
public static FluentAssertions.Specialized.GenericAsyncFunctionAssertions<T> Should<T>([System.Diagnostics.CodeAnalysis.NotNull] this System.Func<System.Threading.Tasks.Task<T>> action) { }
public static FluentAssertions.Specialized.GenericAsyncFunctionAssertions<T> Should<T>([System.Diagnostics.CodeAnalysis.NotNull] this System.Func<System.Threading.Tasks.ValueTask<T>> action) { }
public static FluentAssertions.Specialized.FunctionAssertions<T> Should<T>([System.Diagnostics.CodeAnalysis.NotNull] this System.Func<T> func) { }
public static FluentAssertions.Numeric.ComparableTypeAssertions<T> Should<T>([System.Diagnostics.CodeAnalysis.NotNull] this System.IComparable<T> comparableValue) { }
public static FluentAssertions.Collections.GenericCollectionAssertions<T> Should<T>(this System.Memory<T> actualValue) { }
Expand Down
237 changes: 237 additions & 0 deletions Tests/FluentAssertions.Specs/Specialized/ValueTaskAssertionSpecs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions.Extensions;
using Xunit;
using Xunit.Sdk;

namespace FluentAssertions.Specs.Specialized;

public static class ValueTaskAssertionSpecs
{
public class ThrowAsync
{
[Fact]
public async Task Succeeds_for_a_value_task_that_throws_the_expected_exception()
{
// Arrange
Func<ValueTask> subject = async () =>
{
await Task.Yield();
throw new ArgumentException();
};

// Act / Assert
await subject.Should().ThrowAsync<ArgumentException>();
}

[Fact]
public async Task The_value_task_must_throw_the_expected_exception()
{
// Arrange
Func<ValueTask> subject = () => default;

// Act
Func<Task> act = () => subject.Should().ThrowAsync<ArgumentException>(
"because we want to test the failure {0}", "message");

// Assert
await act.Should().ThrowAsync<XunitException>()
.WithMessage("*because we want to test the failure message*");
}

[Fact]
public async Task Succeeds_for_a_generic_value_task_that_throws_the_expected_exception()
{
// Arrange
Func<ValueTask<int>> subject = async () =>
{
await Task.Yield();
throw new ArgumentException();
};

// Act / Assert
await subject.Should().ThrowAsync<ArgumentException>();
}

[Fact]
public async Task Fails_for_a_null_delegate()
{
// Arrange
Func<ValueTask> subject = null;

// Act
Func<Task> act = () => subject.Should().ThrowAsync<ArgumentException>();

// Assert
await act.Should().ThrowAsync<XunitException>()
.WithMessage("*found <null>*");
}
}

public class NotThrowAsync
{
[Fact]
public async Task Succeeds_for_a_value_task_that_does_not_throw()
{
// Arrange
Func<ValueTask> subject = () => default;

// Act / Assert
await subject.Should().NotThrowAsync();
}

[Fact]
public async Task Succeeds_for_a_generic_value_task_that_does_not_throw()
{
// Arrange
Func<ValueTask<int>> subject = () => new ValueTask<int>(42);

// Act / Assert
(await subject.Should().NotThrowAsync()).Which.Should().Be(42);
}

[Fact]
public async Task The_value_task_must_not_throw()
{
// Arrange
Func<ValueTask> subject = async () =>
{
await Task.Yield();
throw new ArgumentException("some message");
};

// Act
Func<Task> act = () => subject.Should().NotThrowAsync(
"because we want to test the failure {0}", "message");

// Assert
await act.Should().ThrowAsync<XunitException>()
.WithMessage("*because we want to test the failure message*some message*");
}

[Fact]
public async Task Fails_for_a_null_generic_delegate()
{
// Arrange
Func<ValueTask<int>> subject = null;

// Act
Func<Task> act = () => subject.Should().NotThrowAsync();

// Assert
await act.Should().ThrowAsync<XunitException>()
.WithMessage("*found <null>*");
}

[Fact]
public async Task The_value_task_is_invoked_only_once()
{
// Arrange
var invocations = 0;

Func<ValueTask> subject = () =>
{
invocations++;
return default;
};

// Act
await subject.Should().NotThrowAsync();

// Assert
invocations.Should().Be(1);
}
}

public class CompleteWithinAsync
{
[Fact]
public async Task Succeeds_for_a_value_task_that_completes_in_time()
{
// Arrange
Func<ValueTask> subject = () => default;

// Act / Assert
await subject.Should().CompleteWithinAsync(1.Minutes());
}

[Fact]
public async Task Succeeds_for_a_generic_value_task_that_completes_in_time()
{
// Arrange
Func<ValueTask<int>> subject = () => new ValueTask<int>(42);

// Act / Assert
(await subject.Should().CompleteWithinAsync(1.Minutes())).Which.Should().Be(42);
}

[Fact]
public async Task The_value_task_must_complete_within_the_time_limit()
{
// Arrange
using var cancellationTokenSource = new CancellationTokenSource();

// ReSharper disable once AccessToDisposedClosure
Func<ValueTask> subject = () => new ValueTask(Task.Delay(Timeout.Infinite, cancellationTokenSource.Token));

// Act
Func<Task> act = () => subject.Should().CompleteWithinAsync(
10.Milliseconds(), "because we want to test the failure {0}", "message");

// Assert
await act.Should().ThrowAsync<XunitException>()
.WithMessage("*because we want to test the failure message*");
}

[Fact]
public async Task Fails_for_a_null_delegate()
{
// Arrange
Func<ValueTask> subject = null;

// Act
Func<Task> act = () => subject.Should().CompleteWithinAsync(1.Minutes());

// Assert
await act.Should().ThrowAsync<XunitException>()
.WithMessage("*found <null>*");
}
}

public class Subject
{
[Fact]
public void Exposes_the_task_based_adapter_rather_than_the_original_delegate()
{
// Arrange
Func<ValueTask> subject = () => default;

// Act
Func<Task> exposedSubject = subject.Should().Subject;

// Assert
exposedSubject.Should().NotBeNull();
exposedSubject.As<object>().Should().NotBeSameAs(subject);
}

[Fact]
public async Task Invokes_the_original_delegate_when_the_adapter_is_invoked()
{
// Arrange
var invocations = 0;

Func<ValueTask> subject = () =>
{
invocations++;
return default;
};

// Act
await subject.Should().Subject();

// Assert
invocations.Should().Be(1);
}
}
}
7 changes: 7 additions & 0 deletions docs/_pages/exceptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,13 @@ await act.Should().ThrowAsync<ArgumentException>();

Both give you the same results, so it's just a matter of personal preference.

Delegates that return a `ValueTask` or `ValueTask<T>` are supported in the same way:

```csharp
Func<ValueTask> act = () => asyncObject.ThrowValueTaskAsync<ArgumentException>();
await act.Should().ThrowAsync<ArgumentException>();
```

As for synchronous methods, you can also check that an asynchronously executed method executes successfully after a given wait time using `NotThrowAfter`:

```csharp
Expand Down
Loading
Loading