Skip to content

Commit c9acf63

Browse files
refactor(tests): DRY up test infrastructure
- Extract TestListingSourceCodeServiceHelper with ResolveTestDataPath() and CreateService() so both ListingSourceCodeServiceTests and WebApplicationFactory share the same TestData-backed ListingSourceCodeService setup. Also aligns failure behavior: both now throw a clear InvalidOperationException if the TestData directory is missing. - Add SetupSearch() helper in AISearchServiceTests that returns the ISetup for SearchAsync, removing the repeated 5-line Moq matcher block from all 4 test bodies. Each test still specifies its own .Returns(...) inline so retry behavior stays visible.
1 parent 91aa472 commit c9acf63

File tree

4 files changed

+52
-76
lines changed

4 files changed

+52
-76
lines changed

EssentialCSharp.Chat.Tests/AISearchServiceTests.cs

Lines changed: 19 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Microsoft.Extensions.Logging;
55
using Microsoft.Extensions.VectorData;
66
using Moq;
7+
using Moq.Language.Flow;
78
using Npgsql;
89

910
namespace EssentialCSharp.Chat.Tests;
@@ -44,19 +45,21 @@ private static async IAsyncEnumerable<VectorSearchResult<BookContentChunk>> OneR
4445
await Task.CompletedTask;
4546
}
4647

48+
private static ISetup<VectorStoreCollection<string, BookContentChunk>, IAsyncEnumerable<VectorSearchResult<BookContentChunk>>>
49+
SetupSearch(Mock<VectorStoreCollection<string, BookContentChunk>> mock) =>
50+
mock.Setup(c => c.SearchAsync(
51+
It.IsAny<ReadOnlyMemory<float>>(),
52+
It.IsAny<int>(),
53+
It.IsAny<VectorSearchOptions<BookContentChunk>?>(),
54+
It.IsAny<CancellationToken>()));
55+
4756
[Test]
4857
public async Task ExecuteVectorSearch_HappyPath_ReturnsResultsWithoutRetry()
4958
{
5059
var (svc, collectionMock) = CreateService();
5160
int callCount = 0;
5261

53-
collectionMock
54-
.Setup(c => c.SearchAsync(
55-
It.IsAny<ReadOnlyMemory<float>>(),
56-
It.IsAny<int>(),
57-
It.IsAny<VectorSearchOptions<BookContentChunk>?>(),
58-
It.IsAny<CancellationToken>()))
59-
.Returns(() => { callCount++; return OneResultStream(); });
62+
SetupSearch(collectionMock).Returns(() => { callCount++; return OneResultStream(); });
6063

6164
var results = await svc.ExecuteVectorSearch("test query");
6265

@@ -70,19 +73,13 @@ public async Task ExecuteVectorSearch_RetriesOnce_WhenFirstAttemptThrows28000()
7073
var (svc, collectionMock) = CreateService();
7174
int callCount = 0;
7275

73-
collectionMock
74-
.Setup(c => c.SearchAsync(
75-
It.IsAny<ReadOnlyMemory<float>>(),
76-
It.IsAny<int>(),
77-
It.IsAny<VectorSearchOptions<BookContentChunk>?>(),
78-
It.IsAny<CancellationToken>()))
79-
.Returns(() =>
80-
{
81-
callCount++;
82-
if (callCount == 1)
83-
throw new PostgresException("auth token expired", "FATAL", "FATAL", "28000");
84-
return OneResultStream();
85-
});
76+
SetupSearch(collectionMock).Returns(() =>
77+
{
78+
callCount++;
79+
if (callCount == 1)
80+
throw new PostgresException("auth token expired", "FATAL", "FATAL", "28000");
81+
return OneResultStream();
82+
});
8683

8784
var results = await svc.ExecuteVectorSearch("test query");
8885

@@ -95,13 +92,7 @@ public async Task ExecuteVectorSearch_DoesNotRetry_WhenSqlStateIsNot28000()
9592
{
9693
var (svc, collectionMock) = CreateService();
9794

98-
collectionMock
99-
.Setup(c => c.SearchAsync(
100-
It.IsAny<ReadOnlyMemory<float>>(),
101-
It.IsAny<int>(),
102-
It.IsAny<VectorSearchOptions<BookContentChunk>?>(),
103-
It.IsAny<CancellationToken>()))
104-
.Returns(() => throw new PostgresException("table not found", "ERROR", "ERROR", "42P01"));
95+
SetupSearch(collectionMock).Returns(() => throw new PostgresException("table not found", "ERROR", "ERROR", "42P01"));
10596

10697
await Assert.ThrowsAsync<PostgresException>(() => svc.ExecuteVectorSearch("test query"));
10798
}
@@ -111,13 +102,7 @@ public async Task ExecuteVectorSearch_PropagatesException_WhenBothAttemptsFail28
111102
{
112103
var (svc, collectionMock) = CreateService();
113104

114-
collectionMock
115-
.Setup(c => c.SearchAsync(
116-
It.IsAny<ReadOnlyMemory<float>>(),
117-
It.IsAny<int>(),
118-
It.IsAny<VectorSearchOptions<BookContentChunk>?>(),
119-
It.IsAny<CancellationToken>()))
120-
.Returns(() => throw new PostgresException("auth failed", "FATAL", "FATAL", "28000"));
105+
SetupSearch(collectionMock).Returns(() => throw new PostgresException("auth failed", "FATAL", "FATAL", "28000"));
121106

122107
await Assert.ThrowsAsync<PostgresException>(() => svc.ExecuteVectorSearch("test query"));
123108
}

EssentialCSharp.Web.Tests/ListingSourceCodeServiceTests.cs

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
using EssentialCSharp.Web.Models;
22
using EssentialCSharp.Web.Services;
3-
using Microsoft.AspNetCore.Hosting;
4-
using Microsoft.Extensions.FileProviders;
5-
using Moq;
6-
using Moq.AutoMock;
73

84
namespace EssentialCSharp.Web.Tests;
95

@@ -130,30 +126,6 @@ public async Task GetListingsByChapterAsync_WithInvalidChapter_ReturnsEmptyList(
130126
await Assert.That(results).IsEmpty();
131127
}
132128

133-
private static ListingSourceCodeService CreateService()
134-
{
135-
DirectoryInfo testDataRoot = GetTestDataPath();
136-
137-
AutoMocker mocker = new();
138-
Mock<IWebHostEnvironment> mockWebHostEnvironment = mocker.GetMock<IWebHostEnvironment>();
139-
mockWebHostEnvironment.Setup(m => m.ContentRootPath).Returns(testDataRoot.FullName);
140-
mockWebHostEnvironment.Setup(m => m.ContentRootFileProvider).Returns(new PhysicalFileProvider(testDataRoot.FullName));
141-
142-
return mocker.CreateInstance<ListingSourceCodeService>();
143-
}
144-
145-
private static DirectoryInfo GetTestDataPath()
146-
{
147-
string baseDirectory = AppContext.BaseDirectory;
148-
string testDataPath = Path.Join(baseDirectory, "TestData");
149-
150-
DirectoryInfo testDataDirectory = new(testDataPath);
151-
152-
if (!testDataDirectory.Exists)
153-
{
154-
throw new InvalidOperationException($"TestData directory not found at: {testDataDirectory.FullName}");
155-
}
156-
157-
return testDataDirectory;
158-
}
129+
private static ListingSourceCodeService CreateService() =>
130+
TestListingSourceCodeServiceHelper.CreateService();
159131
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using EssentialCSharp.Web.Services;
2+
using Microsoft.AspNetCore.Hosting;
3+
using Microsoft.Extensions.FileProviders;
4+
using Moq.AutoMock;
5+
6+
namespace EssentialCSharp.Web.Tests;
7+
8+
internal static class TestListingSourceCodeServiceHelper
9+
{
10+
internal static string ResolveTestDataPath()
11+
{
12+
string testDataPath = Path.Join(AppContext.BaseDirectory, "TestData");
13+
if (!Directory.Exists(testDataPath))
14+
throw new InvalidOperationException($"TestData directory not found at: {testDataPath}");
15+
return testDataPath;
16+
}
17+
18+
internal static ListingSourceCodeService CreateService()
19+
{
20+
string testDataPath = ResolveTestDataPath();
21+
22+
AutoMocker mocker = new();
23+
mocker.Setup<IWebHostEnvironment, string>(m => m.ContentRootPath).Returns(testDataPath);
24+
mocker.Setup<IWebHostEnvironment, IFileProvider>(m => m.ContentRootFileProvider)
25+
.Returns(new PhysicalFileProvider(testDataPath));
26+
27+
return mocker.CreateInstance<ListingSourceCodeService>();
28+
}
29+
}

EssentialCSharp.Web.Tests/WebApplicationFactory.cs

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99
using Microsoft.EntityFrameworkCore.Infrastructure;
1010
using Microsoft.Extensions.DependencyInjection;
1111
using Microsoft.Extensions.DependencyInjection.Extensions;
12-
using Microsoft.Extensions.FileProviders;
13-
using Moq.AutoMock;
1412

1513
namespace EssentialCSharp.Web.Tests;
1614

@@ -77,16 +75,8 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)
7775

7876
// Replace IListingSourceCodeService with one backed by TestData
7977
services.RemoveAll<IListingSourceCodeService>();
80-
81-
string testDataPath = Path.Join(AppContext.BaseDirectory, "TestData");
82-
var fileProvider = new PhysicalFileProvider(testDataPath);
83-
services.AddSingleton<IListingSourceCodeService>(sp =>
84-
{
85-
var mocker = new AutoMocker();
86-
mocker.Setup<IWebHostEnvironment, string>(m => m.ContentRootPath).Returns(testDataPath);
87-
mocker.Setup<IWebHostEnvironment, IFileProvider>(m => m.ContentRootFileProvider).Returns(fileProvider);
88-
return mocker.CreateInstance<ListingSourceCodeService>();
89-
});
78+
services.AddSingleton<IListingSourceCodeService>(
79+
_ => TestListingSourceCodeServiceHelper.CreateService());
9080
});
9181
}
9282

0 commit comments

Comments
 (0)