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

5.0 KiB

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:

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:

// 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:

// 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:

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:

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
    }
}