Skip to content

Commit ba99b4a

Browse files
Make test project AOT-compatible: migrate to TUnit.Mocks and TUnit built-in assertions (#42)
* Initial plan * Initial plan for AOT compatibility and test framework migration Co-authored-by: Kumara-Krishnan <5687650+Kumara-Krishnan@users.noreply.github.com> * Migrate from NSubstitute to TUnit.Mocks and from AwesomeAssertions to TUnit built-in Assert; enable AOT in tests Co-authored-by: Kumara-Krishnan <5687650+Kumara-Krishnan@users.noreply.github.com> * Update test instructions to reflect TUnit.Mocks and TUnit built-in Assert migration Co-authored-by: Kumara-Krishnan <5687650+Kumara-Krishnan@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Kumara-Krishnan <5687650+Kumara-Krishnan@users.noreply.github.com>
1 parent e83bb95 commit ba99b4a

File tree

10 files changed

+584
-586
lines changed

10 files changed

+584
-586
lines changed

.github/instructions/tests.instructions.md

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ For the canonical, authoritative test guidance see:
1717
| Library | Purpose |
1818
|---|---|
1919
| **TUnit** | Test framework — use `[Test]`, `[Before(Test)]`, `[After(Test)]`, `[Category("…")]` |
20-
| **AwesomeAssertions** | Assertions `using AwesomeAssertions;` |
21-
| **NSubstitute** | Mocking `Substitute.For<T>()`, `.Returns()`, `.Received()` |
20+
| **TUnit Assertions** | Built-in assertions `await Assert.That(value).IsEqualTo(expected)` |
21+
| **TUnit.Mocks** | AOT-compatible source-generated mocking `Mock.Of<T>()`, `.Returns()`, `.WasCalled()` |
2222

23-
**Never** use xUnit, NUnit, MSTest, FluentAssertions, Moq, or raw `Assert.*`.
23+
**Never** use xUnit, NUnit, MSTest, FluentAssertions, AwesomeAssertions, Moq, NSubstitute, or raw `Assert.*`.
2424

2525
---
2626

@@ -65,12 +65,12 @@ public class FileSystemAdapterTests { … }
6565
### Unit tests — mock in `[Before(Test)]`, use `null!` fields
6666

6767
```csharp
68-
private ISomeDependency _dep = null!;
68+
private Mock<ISomeDependency> _dep = null!;
6969

7070
[Before(Test)]
7171
public void Setup()
7272
{
73-
_dep = Substitute.For<ISomeDependency>();
73+
_dep = Mock.Of<ISomeDependency>();
7474
}
7575
```
7676

@@ -101,22 +101,39 @@ Never share a `ServiceProvider` between tests.
101101

102102
### Assertion style
103103

104-
- `.Should().BeSameAs()` — reference equality (mock instance)
105-
- `.Should().Be()` — value equality
106-
- `await act.Should().ThrowAsync<TException>()` — for async exception assertions
104+
All assertions are async and must be awaited:
105+
106+
- `await Assert.That(actual).IsEqualTo(expected)` — value equality
107+
- `await Assert.That(actual).IsSameReferenceAs(expected)` — reference equality (mock instance)
108+
- `await Assert.That(actual).IsNotNull()` — null checks
109+
- `await Assert.That(actual).IsTrue()` / `.IsFalse()` — boolean assertions
110+
- `await Assert.That(act).Throws<TException>()` — exception assertions
111+
- `await Assert.That(act).ThrowsNothing()` — no-throw assertions
107112
- Assign the `act` lambda before asserting: `var act = async () => await …;`
108113

109-
### Exception propagation
114+
### Mock setup and verification
110115

111116
```csharp
112-
_dep.SomeMethod(Arg.Any<string>())
113-
.Returns<T>(x => throw new InvalidOperationException(""));
117+
// Setup return values
118+
_dep.SomeMethod(Any<string>()).Returns(expectedValue);
119+
120+
// Setup exceptions
121+
_dep.SomeMethod(Any<string>()).Throws(new InvalidOperationException(""));
122+
123+
// Setup callbacks
124+
_dep.SomeMethod(Any<string>()).Callback(() => { /* side effect */ });
125+
126+
// Verify calls
127+
_dep.SomeMethod("arg").WasCalled(Times.Once);
128+
_dep.SomeMethod(Any<string>()).WasNeverCalled();
114129
```
115130

116-
### Verify call counts
131+
### Argument matchers
117132

118-
- `_dep.Received(1).Method(…)` — expected exactly once
119-
- `_dep.DidNotReceive().Method(…)` — must not be called
133+
- `Any()` or `Any<T>()` — match any argument
134+
- `Is<T>(predicate)` — match with predicate
135+
- `IsNull<T>()` — match null values
136+
- Raw values — exact match
120137

121138
---
122139

Cyclotron.Tests/Cyclotron.Tests.csproj

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,11 @@
66
<Nullable>enable</Nullable>
77
<IsPackable>false</IsPackable>
88
<IsTestProject>true</IsTestProject>
9-
<IsAotCompatible>false</IsAotCompatible>
10-
<IsTrimmable>false</IsTrimmable>
11-
<EnableTrimAnalyzer>false</EnableTrimAnalyzer>
12-
<EnableSingleFileAnalyzer>false</EnableSingleFileAnalyzer>
139
</PropertyGroup>
1410

1511
<ItemGroup>
1612
<PackageReference Include="TUnit" />
17-
<PackageReference Include="NSubstitute" />
18-
<PackageReference Include="AwesomeAssertions" />
13+
<PackageReference Include="TUnit.Mocks" />
1914
<PackageReference Include="Microsoft.NET.Test.Sdk" />
2015
<PackageReference Include="Coverlet.Collector" />
2116
<PackageReference Include="Coverlet.MSBuild" />

Cyclotron.Tests/Integration/FileSystemAdapter/FileSystemAdapterIntegrationTests.cs

Lines changed: 44 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using Cyclotron.Tests.Integration.Fixtures;
22
using Cyclotron.Tests.TestHelpers;
3-
using AwesomeAssertions;
43

54
namespace Cyclotron.Tests.Integration.FileSystemAdapter;
65

@@ -33,7 +32,7 @@ public async Task WriteAllTextAsync_ThenReadAllTextAsync_ReturnsSameContent()
3332
await File.WriteAllTextAsync(filePath, content);
3433
var readContent = await File.ReadAllTextAsync(filePath);
3534

36-
readContent.Should().Be(content);
35+
await Assert.That(readContent).IsEqualTo(content);
3736
}
3837

3938
[Test]
@@ -45,7 +44,7 @@ public async Task WriteAllTextAsync_WithUtf8Encoding_PreservesContent()
4544
await File.WriteAllTextAsync(filePath, content, System.Text.Encoding.UTF8);
4645
var readContent = await File.ReadAllTextAsync(filePath, System.Text.Encoding.UTF8);
4746

48-
readContent.Should().Be(content);
47+
await Assert.That(readContent).IsEqualTo(content);
4948
}
5049

5150
[Test]
@@ -57,7 +56,7 @@ public async Task WriteAllBytesAsync_ThenReadAllBytesAsync_PreservesBinaryData()
5756
await File.WriteAllBytesAsync(filePath, bytes);
5857
var readBytes = await File.ReadAllBytesAsync(filePath);
5958

60-
readBytes.Should().BeEquivalentTo(bytes);
59+
await Assert.That(readBytes).IsEquivalentTo(bytes);
6160
}
6261

6362
[Test]
@@ -69,7 +68,7 @@ public async Task WriteAllBytesAsync_EmptyArray_CreatesEmptyFile()
6968
await File.WriteAllBytesAsync(filePath, bytes);
7069
var readBytes = await File.ReadAllBytesAsync(filePath);
7170

72-
readBytes.Should().BeEmpty();
71+
await Assert.That(readBytes).IsEmpty();
7372
}
7473

7574
#endregion
@@ -82,51 +81,51 @@ public async Task FileExists_ReturnsTrue_WhenFileExists()
8281
var filePath = _fixture.GetTempFilePath("exists.txt");
8382
await File.WriteAllTextAsync(filePath, "content");
8483

85-
File.Exists(filePath).Should().BeTrue();
84+
await Assert.That(File.Exists(filePath)).IsTrue();
8685
}
8786

8887
[Test]
89-
public void FileExists_ReturnsFalse_WhenFileDoesNotExist()
88+
public async Task FileExists_ReturnsFalse_WhenFileDoesNotExist()
9089
{
9190
var filePath = _fixture.GetTempFilePath("nonexistent.txt");
9291

93-
File.Exists(filePath).Should().BeFalse();
92+
await Assert.That(File.Exists(filePath)).IsFalse();
9493
}
9594

9695
#endregion
9796

9897
#region Directory Operations
9998

10099
[Test]
101-
public void CreateDirectory_CreatesNestedDirectories()
100+
public async Task CreateDirectory_CreatesNestedDirectories()
102101
{
103102
var dirPath = Path.Combine(_fixture.RootPath, "level1", "level2", "level3");
104103

105104
Directory.CreateDirectory(dirPath);
106105

107-
Directory.Exists(dirPath).Should().BeTrue();
106+
await Assert.That(Directory.Exists(dirPath)).IsTrue();
108107
}
109108

110109
[Test]
111-
public void CreateDirectory_IsIdempotent()
110+
public async Task CreateDirectory_IsIdempotent()
112111
{
113112
var dirPath = _fixture.GetTempDirectoryPath("idempotent");
114113

115114
Directory.CreateDirectory(dirPath);
116115
Directory.CreateDirectory(dirPath);
117116

118-
Directory.Exists(dirPath).Should().BeTrue();
117+
await Assert.That(Directory.Exists(dirPath)).IsTrue();
119118
}
120119

121120
[Test]
122-
public void DeleteDirectory_RemovesEmptyDirectory()
121+
public async Task DeleteDirectory_RemovesEmptyDirectory()
123122
{
124123
var dirPath = _fixture.GetTempDirectoryPath("todelete");
125124
Directory.CreateDirectory(dirPath);
126125

127126
Directory.Delete(dirPath);
128127

129-
Directory.Exists(dirPath).Should().BeFalse();
128+
await Assert.That(Directory.Exists(dirPath)).IsFalse();
130129
}
131130

132131
[Test]
@@ -139,7 +138,7 @@ public async Task DeleteDirectory_Recursive_RemovesNestedContent()
139138

140139
Directory.Delete(dirPath, recursive: true);
141140

142-
Directory.Exists(dirPath).Should().BeFalse();
141+
await Assert.That(Directory.Exists(dirPath)).IsFalse();
143142
}
144143

145144
[Test]
@@ -153,18 +152,18 @@ public async Task EnumerateFiles_ReturnsCorrectFiles()
153152

154153
var txtFiles = Directory.EnumerateFiles(dirPath, "*.txt").ToList();
155154

156-
txtFiles.Should().HaveCount(2);
155+
await Assert.That(txtFiles).Count().IsEqualTo(2);
157156
}
158157

159158
[Test]
160-
public void EnumerateFiles_EmptyDirectory_ReturnsEmpty()
159+
public async Task EnumerateFiles_EmptyDirectory_ReturnsEmpty()
161160
{
162161
var dirPath = _fixture.GetTempDirectoryPath("empty");
163162
Directory.CreateDirectory(dirPath);
164163

165164
var files = Directory.EnumerateFiles(dirPath).ToList();
166165

167-
files.Should().BeEmpty();
166+
await Assert.That(files).IsEmpty();
168167
}
169168

170169
[Test]
@@ -178,7 +177,7 @@ public async Task EnumerateFiles_Recursive_TraversesSubdirectories()
178177

179178
var files = Directory.EnumerateFiles(dirPath, "*.*", SearchOption.AllDirectories).ToList();
180179

181-
files.Should().HaveCount(2);
180+
await Assert.That(files).Count().IsEqualTo(2);
182181
}
183182

184183
#endregion
@@ -195,8 +194,8 @@ public async Task FileCopy_CreatesIdenticalCopy()
195194

196195
File.Copy(sourcePath, destPath);
197196

198-
File.Exists(destPath).Should().BeTrue();
199-
(await File.ReadAllTextAsync(destPath)).Should().Be(content);
197+
await Assert.That(File.Exists(destPath)).IsTrue();
198+
await Assert.That(await File.ReadAllTextAsync(destPath)).IsEqualTo(content);
200199
}
201200

202201
[Test]
@@ -208,8 +207,8 @@ public async Task FileMove_MovesFileToNewLocation()
208207

209208
File.Move(sourcePath, destPath);
210209

211-
File.Exists(sourcePath).Should().BeFalse();
212-
File.Exists(destPath).Should().BeTrue();
210+
await Assert.That(File.Exists(sourcePath)).IsFalse();
211+
await Assert.That(File.Exists(destPath)).IsTrue();
213212
}
214213

215214
[Test]
@@ -220,7 +219,7 @@ public async Task FileDelete_RemovesFileFromDisk()
220219

221220
File.Delete(filePath);
222221

223-
File.Exists(filePath).Should().BeFalse();
222+
await Assert.That(File.Exists(filePath)).IsFalse();
224223
}
225224

226225
[Test]
@@ -236,9 +235,9 @@ public async Task FileMove_AcrossDirectories_Works()
236235

237236
File.Move(sourcePath, destPath);
238237

239-
File.Exists(sourcePath).Should().BeFalse();
240-
File.Exists(destPath).Should().BeTrue();
241-
(await File.ReadAllTextAsync(destPath)).Should().Be("cross-dir move");
238+
await Assert.That(File.Exists(sourcePath)).IsFalse();
239+
await Assert.That(File.Exists(destPath)).IsTrue();
240+
await Assert.That(await File.ReadAllTextAsync(destPath)).IsEqualTo("cross-dir move");
242241
}
243242

244243
#endregion
@@ -251,8 +250,8 @@ public async Task FileOperations_WithSpacesInName_Work()
251250
var filePath = _fixture.GetTempFilePath("file with spaces.txt");
252251
await File.WriteAllTextAsync(filePath, "content");
253252

254-
File.Exists(filePath).Should().BeTrue();
255-
(await File.ReadAllTextAsync(filePath)).Should().Be("content");
253+
await Assert.That(File.Exists(filePath)).IsTrue();
254+
await Assert.That(await File.ReadAllTextAsync(filePath)).IsEqualTo("content");
256255
}
257256

258257
[Test]
@@ -261,32 +260,32 @@ public async Task FileOperations_WithUnicodeInName_Work()
261260
var filePath = _fixture.GetTempFilePath("файл_données_数据.txt");
262261
await File.WriteAllTextAsync(filePath, "unicode content");
263262

264-
File.Exists(filePath).Should().BeTrue();
265-
(await File.ReadAllTextAsync(filePath)).Should().Be("unicode content");
263+
await Assert.That(File.Exists(filePath)).IsTrue();
264+
await Assert.That(await File.ReadAllTextAsync(filePath)).IsEqualTo("unicode content");
266265
}
267266

268267
#endregion
269268

270269
#region Error Scenarios
271270

272271
[Test]
273-
public void ReadNonExistentFile_ThrowsFileNotFoundException()
272+
public async Task ReadNonExistentFile_ThrowsFileNotFoundException()
274273
{
275274
var filePath = _fixture.GetTempFilePath("nonexistent.txt");
276275

277276
var act = () => File.ReadAllText(filePath);
278277

279-
act.Should().Throw<FileNotFoundException>();
278+
await Assert.That(act).Throws<FileNotFoundException>();
280279
}
281280

282281
[Test]
283-
public void DeleteNonExistentFile_DoesNotThrow()
282+
public async Task DeleteNonExistentFile_DoesNotThrow()
284283
{
285284
var filePath = _fixture.GetTempFilePath("nonexistent.txt");
286285

287286
var act = () => File.Delete(filePath);
288287

289-
act.Should().NotThrow();
288+
await Assert.That(act).ThrowsNothing();
290289
}
291290

292291
#endregion
@@ -305,7 +304,7 @@ public async Task ParallelWrites_ToDifferentFiles_AllSucceed()
305304
await Task.WhenAll(tasks);
306305

307306
var files = Directory.EnumerateFiles(_fixture.RootPath, "parallel_*.txt").ToList();
308-
files.Should().HaveCount(50);
307+
await Assert.That(files).Count().IsEqualTo(50);
309308
}
310309

311310
[Test]
@@ -322,41 +321,41 @@ public async Task ParallelReads_FromSameFile_AllSucceed()
322321
});
323322

324323
var results = await Task.WhenAll(tasks);
325-
results.Should().AllBe(content);
324+
await Assert.That(results.All(r => r == content)).IsTrue();
326325
}
327326

328327
#endregion
329328

330329
#region TempFileSystemFixture Tests
331330

332331
[Test]
333-
public void Fixture_CreatesRootDirectory()
332+
public async Task Fixture_CreatesRootDirectory()
334333
{
335-
Directory.Exists(_fixture.RootPath).Should().BeTrue();
334+
await Assert.That(Directory.Exists(_fixture.RootPath)).IsTrue();
336335
}
337336

338337
[Test]
339-
public void Fixture_GetTempFilePath_ReturnsPathInRootDirectory()
338+
public async Task Fixture_GetTempFilePath_ReturnsPathInRootDirectory()
340339
{
341340
var path = _fixture.GetTempFilePath();
342341

343-
Path.GetDirectoryName(path).Should().Be(_fixture.RootPath);
342+
await Assert.That(Path.GetDirectoryName(path)).IsEqualTo(_fixture.RootPath);
344343
}
345344

346345
[Test]
347-
public void Fixture_GetTempFilePath_WithCustomName_UsesProvidedName()
346+
public async Task Fixture_GetTempFilePath_WithCustomName_UsesProvidedName()
348347
{
349348
var path = _fixture.GetTempFilePath("custom.txt");
350349

351-
Path.GetFileName(path).Should().Be("custom.txt");
350+
await Assert.That(Path.GetFileName(path)).IsEqualTo("custom.txt");
352351
}
353352

354353
[Test]
355-
public void Fixture_GetTempDirectoryPath_ReturnsPathInRootDirectory()
354+
public async Task Fixture_GetTempDirectoryPath_ReturnsPathInRootDirectory()
356355
{
357356
var path = _fixture.GetTempDirectoryPath();
358357

359-
Path.GetDirectoryName(path).Should().Be(_fixture.RootPath);
358+
await Assert.That(Path.GetDirectoryName(path)).IsEqualTo(_fixture.RootPath);
360359
}
361360

362361
#endregion

0 commit comments

Comments
 (0)