Files
jdescopingtool/DOCUMENTATION/Architecture/Testing.md
T
Joseph Doherty 26ff8d9b4f Initial commit: JDE Scoping Tool migration project
Set up repository with legacy .NET Framework 4.8 source (OLD/),
new .NET 10 Blazor solution (NEW/), OpenSpec specifications,
documentation, and project configuration.
2026-01-02 07:43:29 -05:00

182 lines
5.0 KiB
Markdown

# Testing Strategy
The test project uses xUnit for the framework, Shouldly for assertions, and NSubstitute for mocking.
## Project Structure
```
JdeScoping.Tests/
├── Unit/
│ ├── Services/
│ │ ├── SearchServiceTests.cs
│ │ ├── ExcelExportServiceTests.cs
│ │ └── DataSyncOrchestratorTests.cs
│ ├── Repositories/
│ │ └── SearchRepositoryTests.cs
│ └── Models/
│ └── SearchCriteriaTests.cs
└── Integration/
├── ApiTests/
│ ├── SearchControllerTests.cs
│ └── LookupControllerTests.cs
└── RepositoryTests/
└── JdeRepositoryTests.cs
```
## Unit Tests
Unit tests mock dependencies and test business logic in isolation:
```csharp
public class SearchServiceTests
{
[Fact]
public async Task ExecuteSearch_WithValidCriteria_ReturnsResults()
{
// Arrange
var mockRepo = Substitute.For<ISearchRepository>();
mockRepo.GetWorkOrdersAsync(Arg.Any<SearchCriteria>())
.Returns(new List<WorkOrder> { new WorkOrder { Number = "WO123" } });
var service = new SearchService(mockRepo);
var criteria = new SearchCriteria { ItemNumber = "ABC123" };
// Act
var results = await service.ExecuteAsync(criteria);
// Assert
results.Count.ShouldBeGreaterThan(0);
results.First().Number.ShouldBe("WO123");
}
[Fact]
public async Task ExecuteSearch_WithInvalidCriteria_ThrowsValidationException()
{
// Arrange
var mockRepo = Substitute.For<ISearchRepository>();
var service = new SearchService(mockRepo);
var criteria = new SearchCriteria(); // Empty criteria
// Act & Assert
await Should.ThrowAsync<ValidationException>(
() => service.ExecuteAsync(criteria));
}
}
```
## Shouldly Assertions
Shouldly provides readable assertion syntax without FluentAssertions licensing:
```csharp
// Value assertions
result.ShouldBe(expected);
result.ShouldNotBeNull();
result.ShouldBeGreaterThan(0);
// Collection assertions
list.ShouldContain(item);
list.ShouldBeEmpty();
list.Count.ShouldBe(5);
// String assertions
text.ShouldStartWith("Error");
text.ShouldContain("expected");
// Exception assertions
Should.Throw<ArgumentException>(() => service.Process(null));
await Should.ThrowAsync<InvalidOperationException>(() => service.ProcessAsync());
```
## NSubstitute Mocking
NSubstitute provides a simple API for creating test doubles:
```csharp
// Create substitute
var mockRepo = Substitute.For<ISearchRepository>();
// Configure returns
mockRepo.GetByIdAsync(123).Returns(new Search { Id = 123 });
mockRepo.GetByIdAsync(Arg.Any<int>()).Returns(x => new Search { Id = (int)x[0] });
// Verify calls
await mockRepo.Received().CreateAsync(Arg.Is<Search>(s => s.Status == "Queued"));
await mockRepo.DidNotReceive().DeleteAsync(Arg.Any<int>());
```
## Integration Tests
Integration tests use `WebApplicationFactory<Program>` for API tests:
```csharp
public class SearchControllerTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public SearchControllerTests(WebApplicationFactory<Program> factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task SubmitSearch_ReturnsSearchId()
{
// Arrange
var criteria = new SearchCriteria { ItemNumber = "TEST123" };
var content = new StringContent(
JsonSerializer.Serialize(criteria),
Encoding.UTF8,
"application/json");
// Act
var response = await _client.PostAsync("/api/search", content);
// Assert
response.StatusCode.ShouldBe(HttpStatusCode.OK);
var result = await response.Content.ReadFromJsonAsync<SearchResult>();
result.SearchId.ShouldBeGreaterThan(0);
}
}
```
## Database Integration Tests
Repository integration tests run against a local SQL Server instance:
```csharp
public class SearchRepositoryIntegrationTests : IDisposable
{
private readonly string _connectionString;
public SearchRepositoryIntegrationTests()
{
_connectionString = "Server=localhost;Database=LotFinder_Test;...";
// Setup test database
}
[Fact]
public async Task CreateAndRetrieve_RoundTrips()
{
var repo = new SearchRepository(_connectionString);
var search = new Search { UserId = "testuser", Status = "Queued" };
var id = await repo.CreateAsync(search);
var retrieved = await repo.GetByIdAsync(id);
retrieved.ShouldNotBeNull();
retrieved.UserId.ShouldBe("testuser");
}
public void Dispose()
{
// Cleanup test data
}
}
```
## Related Documentation
- [Core Project](./CoreProject.md)
- [Dependencies](./Dependencies.md)