Files
Joseph Doherty c6aeb20d9c docs: update documentation for extraction functions migration
- Add ExtractionFunctions.md reference document
- Update database-schema spec with 11 extraction functions
- Update data-access spec to document extraction function approach
- Update search-processing spec with new query builder interface
- Add Database.Tests to Testing.md architecture doc
- Update DataFlow.md with extraction function flow
2026-01-06 14:54:10 -05:00

7.4 KiB

Testing Strategy

The test project uses xUnit for the framework, Shouldly for assertions, and NSubstitute for mocking.

Project Structure

tests/
├── 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
├── JdeScoping.Api.IntegrationTests/
│   └── ... (API integration tests)
└── JdeScoping.Database.Tests/
    ├── Infrastructure/
    │   └── DatabaseTestBase.cs
    ├── Functions/
    │   ├── ScalarFunctionTests.cs
    │   ├── SimpleTableFunctionTests.cs
    │   └── ComplexTableFunctionTests.cs
    └── Procedures/
        └── ValidateSearchCriteriaProcedureTests.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
    }
}

Database Tests

The JdeScoping.Database.Tests project tests SQL functions and stored procedures against a live SQL Server instance.

Test Infrastructure

Tests inherit from DatabaseTestBase which provides:

  • Connection management via DatabaseTestFixture
  • Automatic test data cleanup
  • Helper methods for inserting test searches
[Collection("DatabaseTests")]
public class ScalarFunctionTests : DatabaseTestBase
{
    [Fact]
    public async Task fn_GetSearchMinimumDt_ValidSearch_ReturnsDateTime()
    {
        // Arrange
        var criteria = new SearchCriteria { MinimumDt = new DateTime(2024, 6, 15) };
        var searchId = await InsertTestSearchAsync(criteria);

        // Act
        var result = await Connection.QuerySingleOrDefaultAsync<DateTime?>(
            "SELECT dbo.fn_GetSearchMinimumDt(@SearchId)",
            new { SearchId = searchId });

        // Assert
        result.Should().BeCloseTo(criteria.MinimumDt.Value, TimeSpan.FromSeconds(1));
    }
}

Test Categories

Category Tests Description
Scalar Functions 15 Test fn_GetSearchMinimumDt, fn_GetSearchMaximumDt, fn_GetSearchExtractMisData
Simple Table Functions 38 Test array extraction for work orders, items, profit centers, work centers, operators
Complex Table Functions 23 Test object extraction for component lots and part operations
Validation Procedure 6 Test usp_ValidateSearchCriteria error handling

Total: 82 database tests

Running Database Tests

# Run all database tests
dotnet test tests/JdeScoping.Database.Tests

# Run specific test class
dotnet test tests/JdeScoping.Database.Tests --filter "FullyQualifiedName~ScalarFunctionTests"

Prerequisites: SQL Server must be running on localhost:1434 with the ScopingTool database.