Skip to content

Commit 53f9bea

Browse files
committed
Close nunit#759 Use Assert.ThatAsync
1 parent 6d42b13 commit 53f9bea

File tree

10 files changed

+559
-0
lines changed

10 files changed

+559
-0
lines changed

documentation/NUnit2055.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# NUnit2055
2+
3+
## Use Assert.ThatAsync
4+
5+
| Topic | Value
6+
| :-- | :--
7+
| Id | NUnit2055
8+
| Severity | Info
9+
| Enabled | True
10+
| Category | Assertion
11+
| Code | [UseAssertThatAsyncAnalyzer](https://github.com/nunit/nunit.analyzers/blob/master/src/nunit.analyzers/UseAssertThatAsync/UseAssertThatAsyncAnalyzer.cs)
12+
13+
## Description
14+
15+
You can use `Assert.ThatAsync` to assert asynchronously.
16+
17+
## Motivation
18+
19+
`Assert.That` runs synchronously, even if pass an asynchronous delegate. This "sync-over-async" pattern blocks
20+
the calling thread, preventing it from doing anything else in the meantime.
21+
22+
`Assert.ThatAsync` allows for a proper async/await. This allows for a better utilization of threads while waiting for the
23+
asynchronous operation to finish.
24+
25+
## How to fix violations
26+
27+
Convert the asynchronous method call with a lambda expression and `await` the `Assert.ThatAsync` instead of the
28+
asynchronous method call.
29+
30+
```csharp
31+
Assert.That(await DoAsync(), Is.EqualTo(expected)); // bad (sync-over-async)
32+
await Assert.ThatAsync(() => DoAsync(), Is.EqualTo(expected)); // good (proper async/await)
33+
```
34+
35+
<!-- start generated config severity -->
36+
## Configure severity
37+
38+
### Via ruleset file
39+
40+
Configure the severity per project, for more info see
41+
[MSDN](https://learn.microsoft.com/en-us/visualstudio/code-quality/using-rule-sets-to-group-code-analysis-rules?view=vs-2022).
42+
43+
### Via .editorconfig file
44+
45+
```ini
46+
# NUnit2055: Use Assert.ThatAsync
47+
dotnet_diagnostic.NUnit2055.severity = chosenSeverity
48+
```
49+
50+
where `chosenSeverity` can be one of `none`, `silent`, `suggestion`, `warning`, or `error`.
51+
52+
### Via #pragma directive
53+
54+
```csharp
55+
#pragma warning disable NUnit2055 // Use Assert.ThatAsync
56+
Code violating the rule here
57+
#pragma warning restore NUnit2055 // Use Assert.ThatAsync
58+
```
59+
60+
Or put this at the top of the file to disable all instances.
61+
62+
```csharp
63+
#pragma warning disable NUnit2055 // Use Assert.ThatAsync
64+
```
65+
66+
### Via attribute `[SuppressMessage]`
67+
68+
```csharp
69+
[System.Diagnostics.CodeAnalysis.SuppressMessage("Assertion",
70+
"NUnit2055:Use Assert.ThatAsync",
71+
Justification = "Reason...")]
72+
```
73+
<!-- end generated config severity -->

documentation/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ Rules which improve assertions in the test code.
113113
| [NUnit2052](https://github.com/nunit/nunit.analyzers/tree/master/documentation/NUnit2052.md) | Consider using Assert.That(expr, Is.Negative) instead of ClassicAssert.Negative(expr) | :white_check_mark: | :information_source: | :white_check_mark: |
114114
| [NUnit2053](https://github.com/nunit/nunit.analyzers/tree/master/documentation/NUnit2053.md) | Consider using Assert.That(actual, Is.AssignableFrom(expected)) instead of ClassicAssert.IsAssignableFrom(expected, actual) | :white_check_mark: | :information_source: | :white_check_mark: |
115115
| [NUnit2054](https://github.com/nunit/nunit.analyzers/tree/master/documentation/NUnit2054.md) | Consider using Assert.That(actual, Is.Not.AssignableFrom(expected)) instead of ClassicAssert.IsNotAssignableFrom(expected, actual) | :white_check_mark: | :information_source: | :white_check_mark: |
116+
| [NUnit2055](https://github.com/nunit/nunit.analyzers/tree/master/documentation/NUnit2055.md) | Use Assert.ThatAsync | :white_check_mark: | :information_source: | :white_check_mark: |
116117

117118
## Suppressor Rules (NUnit3001 - )
118119

src/nunit.analyzers.tests/Constants/NUnitFrameworkConstantsTests.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ public sealed class NUnitFrameworkConstantsTests
110110
(nameof(NUnitFrameworkConstants.NameOfAssertIsNotNull), nameof(ClassicAssert.IsNotNull)),
111111
(nameof(NUnitFrameworkConstants.NameOfAssertNotNull), nameof(ClassicAssert.NotNull)),
112112
(nameof(NUnitFrameworkConstants.NameOfAssertThat), nameof(ClassicAssert.That)),
113+
#if NUNIT4
114+
(nameof(NUnitFrameworkConstants.NameOfAssertThatAsync), nameof(ClassicAssert.ThatAsync)),
115+
#else
116+
(nameof(NUnitFrameworkConstants.NameOfAssertThatAsync), "ThatAsync"),
117+
#endif
113118
(nameof(NUnitFrameworkConstants.NameOfAssertGreater), nameof(ClassicAssert.Greater)),
114119
(nameof(NUnitFrameworkConstants.NameOfAssertGreaterOrEqual), nameof(ClassicAssert.GreaterOrEqual)),
115120
(nameof(NUnitFrameworkConstants.NameOfAssertLess), nameof(ClassicAssert.Less)),
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
#if NUNIT4
2+
using Gu.Roslyn.Asserts;
3+
using Microsoft.CodeAnalysis.Diagnostics;
4+
using NUnit.Analyzers.Constants;
5+
using NUnit.Analyzers.UseAssertThatAsync;
6+
using NUnit.Framework;
7+
8+
namespace NUnit.Analyzers.Tests.UseAssertThatAsync;
9+
10+
[TestFixture]
11+
public sealed class UseAssertThatAsyncAnalyzerTests
12+
{
13+
private static readonly DiagnosticAnalyzer analyzer = new UseAssertThatAsyncAnalyzer();
14+
private static readonly ExpectedDiagnostic diagnostic = ExpectedDiagnostic.Create(AnalyzerIdentifiers.UseAssertThatAsync);
15+
private static readonly string[] configureAwaitValues =
16+
{
17+
"",
18+
".ConfigureAwait(true)",
19+
".ConfigureAwait(false)",
20+
};
21+
22+
[Test]
23+
public void AnalyzeWhenIntResultIsUsed()
24+
{
25+
var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
26+
public void Test()
27+
{
28+
Assert.That(GetIntAsync().Result, Is.EqualTo(42));
29+
}
30+
31+
private static Task<int> GetIntAsync() => Task.FromResult(42);");
32+
RoslynAssert.Valid(analyzer, testCode);
33+
}
34+
35+
[Test]
36+
public void AnalyzeWhenBoolResultIsUsed()
37+
{
38+
var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
39+
public void Test()
40+
{
41+
Assert.That(GetBoolAsync().Result);
42+
}
43+
44+
private static Task<bool> GetBoolAsync() => Task.FromResult(true);");
45+
RoslynAssert.Valid(analyzer, testCode);
46+
}
47+
48+
[Test]
49+
public void AnalyzeWhenAwaitIsNotUsedInLineForInt()
50+
{
51+
var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
52+
public async Task Test()
53+
{
54+
var fourtyTwo = await GetIntAsync();
55+
Assert.That(fourtyTwo, Is.EqualTo(42));
56+
}
57+
58+
private static Task<int> GetIntAsync() => Task.FromResult(42);");
59+
RoslynAssert.Valid(analyzer, testCode);
60+
}
61+
62+
// do not touch because there is no ThatAsync equivalent
63+
[Test]
64+
public void AnalyzeWhenExceptionMessageIsFuncString()
65+
{
66+
var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
67+
public async Task Test()
68+
{
69+
Assert.That(await GetBoolAsync(), () => ""message"");
70+
}
71+
72+
private static Task<bool> GetBoolAsync() => Task.FromResult(true);");
73+
RoslynAssert.Valid(analyzer, testCode);
74+
}
75+
76+
[Test]
77+
public void AnalyzeWhenAwaitIsNotUsedInLineForBool()
78+
{
79+
var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
80+
public async Task Test()
81+
{
82+
var myBool = await GetBoolAsync();
83+
Assert.That(myBool, Is.True);
84+
}
85+
86+
private static Task<bool> GetBoolAsync() => Task.FromResult(true);");
87+
RoslynAssert.Valid(analyzer, testCode);
88+
}
89+
90+
[Test]
91+
public void AnalyzeWhenAwaitIsUsedInLineForInt([ValueSource(nameof(configureAwaitValues))] string configureAwait, [Values] bool hasMessage)
92+
{
93+
var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings($@"
94+
public async Task Test()
95+
{{
96+
Assert.That(await GetIntAsync(){configureAwait}, Is.EqualTo(42){(hasMessage ? @", ""message""" : "")});
97+
}}
98+
99+
private static Task<int> GetIntAsync() => Task.FromResult(42);");
100+
RoslynAssert.Diagnostics(analyzer, diagnostic, testCode);
101+
}
102+
103+
[Test]
104+
public void AnalyzeWhenAwaitIsUsedInLineForBool([ValueSource(nameof(configureAwaitValues))] string configureAwait, [Values] bool hasConstraint, [Values] bool hasMessage)
105+
{
106+
var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings($@"
107+
public async Task Test()
108+
{{
109+
Assert.That(await GetBoolAsync(){configureAwait}{(hasConstraint ? ", Is.True" : "")}{(hasMessage ? @", ""message""" : "")});
110+
}}
111+
112+
private static Task<bool> GetBoolAsync() => Task.FromResult(true);");
113+
RoslynAssert.Diagnostics(analyzer, diagnostic, testCode);
114+
}
115+
116+
[Test]
117+
public void AnalyzeWhenAwaitIsUsedAsSecondArgument([ValueSource(nameof(configureAwaitValues))] string configureAwait, [Values] bool hasMessage)
118+
{
119+
var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings($@"
120+
public async Task Test()
121+
{{
122+
↓Assert.That(expression: Is.EqualTo(42), actual: await GetIntAsync(){configureAwait}{(hasMessage ? @", message: ""message""" : "")});
123+
}}
124+
125+
private static Task<int> GetIntAsync() => Task.FromResult(42);");
126+
RoslynAssert.Diagnostics(analyzer, diagnostic, testCode);
127+
}
128+
}
129+
#endif
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
#if NUNIT4
2+
using Gu.Roslyn.Asserts;
3+
using Microsoft.CodeAnalysis.CodeFixes;
4+
using Microsoft.CodeAnalysis.Diagnostics;
5+
using NUnit.Analyzers.Constants;
6+
using NUnit.Analyzers.UseAssertThatAsync;
7+
using NUnit.Framework;
8+
9+
namespace NUnit.Analyzers.Tests.UseAssertThatAsync;
10+
11+
[TestFixture]
12+
public sealed class UseAssertThatAsyncCodeFixTests
13+
{
14+
private static readonly DiagnosticAnalyzer analyzer = new UseAssertThatAsyncAnalyzer();
15+
private static readonly CodeFixProvider fix = new UseAssertThatAsyncCodeFix();
16+
private static readonly ExpectedDiagnostic diagnostic = ExpectedDiagnostic.Create(AnalyzerIdentifiers.UseAssertThatAsync);
17+
private static readonly string[] configureAwaitValues =
18+
{
19+
"",
20+
".ConfigureAwait(true)",
21+
".ConfigureAwait(false)",
22+
};
23+
24+
[Test]
25+
public void VerifyGetFixableDiagnosticIds()
26+
{
27+
var fix = new UseAssertThatAsyncCodeFix();
28+
var ids = fix.FixableDiagnosticIds;
29+
30+
Assert.That(ids, Is.EquivalentTo(new[] { AnalyzerIdentifiers.UseAssertThatAsync }));
31+
}
32+
33+
[Test]
34+
public void VerifyIntAndConstraint([ValueSource(nameof(configureAwaitValues))] string configureAwait, [Values] bool hasMessage)
35+
{
36+
var code = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@$"
37+
public async Task Test()
38+
{{
39+
Assert.That(await GetIntAsync(){configureAwait}, Is.EqualTo(42){(hasMessage ? @", ""message""" : "")});
40+
}}
41+
42+
private static Task<int> GetIntAsync() => Task.FromResult(42);");
43+
var fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings($@"
44+
public async Task Test()
45+
{{
46+
await Assert.ThatAsync(() => GetIntAsync(), Is.EqualTo(42){(hasMessage ? @", ""message""" : "")});
47+
}}
48+
49+
private static Task<int> GetIntAsync() => Task.FromResult(42);");
50+
RoslynAssert.CodeFix(analyzer, fix, diagnostic, code, fixedCode);
51+
}
52+
53+
[Test]
54+
public void VerifyTaskIntReturningInstanceMethodAndConstraint([ValueSource(nameof(configureAwaitValues))] string configureAwait, [Values] bool hasMessage)
55+
{
56+
var code = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@$"
57+
public async Task Test()
58+
{{
59+
Assert.That(await this.GetIntAsync(){configureAwait}, Is.EqualTo(42){(hasMessage ? @", ""message""" : "")});
60+
}}
61+
62+
private Task<int> GetIntAsync() => Task.FromResult(42);");
63+
var fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings($@"
64+
public async Task Test()
65+
{{
66+
await Assert.ThatAsync(() => this.GetIntAsync(), Is.EqualTo(42){(hasMessage ? @", ""message""" : "")});
67+
}}
68+
69+
private Task<int> GetIntAsync() => Task.FromResult(42);");
70+
RoslynAssert.CodeFix(analyzer, fix, diagnostic, code, fixedCode);
71+
}
72+
73+
[Test]
74+
public void VerifyBoolAndConstraint([ValueSource(nameof(configureAwaitValues))] string configureAwait, [Values] bool hasMessage)
75+
{
76+
var code = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@$"
77+
public async Task Test()
78+
{{
79+
Assert.That(await GetBoolAsync(){configureAwait}, Is.EqualTo(true){(hasMessage ? @", ""message""" : "")});
80+
}}
81+
82+
private static Task<bool> GetBoolAsync() => Task.FromResult(true);");
83+
var fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings($@"
84+
public async Task Test()
85+
{{
86+
await Assert.ThatAsync(() => GetBoolAsync(), Is.EqualTo(true){(hasMessage ? @", ""message""" : "")});
87+
}}
88+
89+
private static Task<bool> GetBoolAsync() => Task.FromResult(true);");
90+
RoslynAssert.CodeFix(analyzer, fix, diagnostic, code, fixedCode);
91+
}
92+
93+
// Assert.That(bool) is supported, but there is no overload of Assert.ThatAsync that only takes a single bool.
94+
[Test]
95+
public void VerifyBoolOnly([ValueSource(nameof(configureAwaitValues))] string configureAwait, [Values] bool hasMessage)
96+
{
97+
var code = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@$"
98+
public async Task Test()
99+
{{
100+
Assert.That(await GetBoolAsync(){configureAwait}{(hasMessage ? @", ""message""" : "")});
101+
}}
102+
103+
private static Task<bool> GetBoolAsync() => Task.FromResult(true);");
104+
var fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings($@"
105+
public async Task Test()
106+
{{
107+
await Assert.ThatAsync(() => GetBoolAsync(), Is.True{(hasMessage ? @", ""message""" : "")});
108+
}}
109+
110+
private static Task<bool> GetBoolAsync() => Task.FromResult(true);");
111+
RoslynAssert.CodeFix(analyzer, fix, diagnostic, code, fixedCode);
112+
}
113+
114+
[Test]
115+
public void VerifyIntAsSecondArgumentAndConstraint([ValueSource(nameof(configureAwaitValues))] string configureAwait)
116+
{
117+
var code = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@$"
118+
public async Task Test()
119+
{{
120+
↓Assert.That(expression: Is.EqualTo(42), actual: await GetIntAsync(){configureAwait});
121+
}}
122+
123+
private static Task<int> GetIntAsync() => Task.FromResult(42);");
124+
var fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
125+
public async Task Test()
126+
{
127+
await Assert.ThatAsync(() => GetIntAsync(), Is.EqualTo(42));
128+
}
129+
130+
private static Task<int> GetIntAsync() => Task.FromResult(42);");
131+
RoslynAssert.CodeFix(analyzer, fix, diagnostic, code, fixedCode);
132+
}
133+
134+
[Test]
135+
public void VerifyBoolAsSecondArgumentAndConstraint([ValueSource(nameof(configureAwaitValues))] string configureAwait)
136+
{
137+
var code = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@$"
138+
public async Task Test()
139+
{{
140+
↓Assert.That(message: ""message"", condition: await GetBoolAsync(){configureAwait});
141+
}}
142+
143+
private static Task<bool> GetBoolAsync() => Task.FromResult(true);");
144+
var fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
145+
public async Task Test()
146+
{
147+
await Assert.ThatAsync(() => GetBoolAsync(), Is.True, ""message"");
148+
}
149+
150+
private static Task<bool> GetBoolAsync() => Task.FromResult(true);");
151+
RoslynAssert.CodeFix(analyzer, fix, diagnostic, code, fixedCode);
152+
}
153+
}
154+
#endif

src/nunit.analyzers/Constants/AnalyzerIdentifiers.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ internal static class AnalyzerIdentifiers
9696
internal const string NegativeUsage = "NUnit2052";
9797
internal const string IsAssignableFromUsage = "NUnit2053";
9898
internal const string IsNotAssignableFromUsage = "NUnit2054";
99+
internal const string UseAssertThatAsync = "NUnit2055";
99100

100101
#endregion Assertion
101102

src/nunit.analyzers/Constants/NUnitFrameworkConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ public static class NUnitFrameworkConstants
8686
public const string NameOfAssertNotNull = "NotNull";
8787
public const string NameOfAssertIsNotNull = "IsNotNull";
8888
public const string NameOfAssertThat = "That";
89+
public const string NameOfAssertThatAsync = "ThatAsync";
8990
public const string NameOfAssertGreater = "Greater";
9091
public const string NameOfAssertGreaterOrEqual = "GreaterOrEqual";
9192
public const string NameOfAssertLess = "Less";

0 commit comments

Comments
 (0)