Files
jdescopingtool/NEW/docs/plans/2026-01-07-excelio-test-optimization.md
T
Joseph Doherty 6952f686fa perf: optimize ExcelIO tests with fixture-based consolidation
- Add WorkbookFixtureBase and 4 concrete fixtures for shared workbooks
- Add ExcelTestHelpers with shared utility methods
- Create Integration/ folder with 7 fixture-based test classes:
  - MinimalSearchTests (5 tests)
  - SearchResultsSheetTests (5 tests)
  - MisInfoSheetTests (11 tests)
  - InvestigationSheetTests (7 tests)
  - ProtectionAndStyleTests (7 tests)
  - LegacyFormatTests (5 tests)
  - LargeDataSetTests (1 test)
- Delete redundant ExcelExportIntegrationTests.cs (26 tests)
- Delete redundant LegacyComparisonTests.cs (16 tests)
- Reduce workbook generations from ~42 to 4 fixtures
- Test runtime reduced from ~18 min to ~4 min (76% improvement)
- All 122 ExcelIO tests pass
2026-01-07 03:55:33 -05:00

31 KiB

ExcelIO Test Optimization Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Reduce ExcelIO.Tests runtime from ~18 minutes to ~3-4 minutes by using fixture-based test consolidation.

Architecture: Replace per-test workbook generation with xUnit IClassFixture<T> pattern. Generate 4 shared workbooks once, reuse across 42 integration tests.

Tech Stack: xUnit fixtures, ClosedXML, existing ExcelExportService


Task 1: Create Fixture Base Class and Infrastructure

Files:

  • Create: tests/JdeScoping.ExcelIO.Tests/Fixtures/WorkbookFixtureBase.cs
  • Create: tests/JdeScoping.ExcelIO.Tests/Fixtures/ExcelTestHelpers.cs

Step 1: Create Fixtures directory

mkdir -p tests/JdeScoping.ExcelIO.Tests/Fixtures

Step 2: Write WorkbookFixtureBase.cs

using ClosedXML.Excel;
using JdeScoping.ExcelIO.Generators;
using JdeScoping.ExcelIO.Mapping;
using JdeScoping.ExcelIO.Mapping.Maps;
using JdeScoping.ExcelIO.Models.Reporting;
using JdeScoping.ExcelIO.Options;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NSubstitute;

namespace JdeScoping.ExcelIO.Tests.Fixtures;

public abstract class WorkbookFixtureBase : IDisposable
{
    public XLWorkbook Workbook { get; }
    public SearchModel SearchModel { get; }

    protected abstract SearchModel CreateSearchModel();

    protected WorkbookFixtureBase()
    {
        SearchModel = CreateSearchModel();
        var service = CreateExportService();
        var bytes = service.GenerateAsync(SearchModel).GetAwaiter().GetResult();
        Workbook = new XLWorkbook(new MemoryStream(bytes));
    }

    private static ExcelExportService CreateExportService()
    {
        var logger = Substitute.For<ILogger<ExcelExportService>>();
        var options = Options.Create(new ExcelExportOptions
        {
            CriteriaSheetPassword = "TestCriteriaPass",
            DataSheetPassword = "TestDataPass"
        });

        var registry = CreateTestRegistry();
        var tableWriter = new FluentTableWriter(registry);
        var criteriaGenerator = new CriteriaSheetGenerator(options, tableWriter);

        return new ExcelExportService(logger, options, criteriaGenerator, tableWriter, registry);
    }

    private static ExcelMapRegistry CreateTestRegistry()
    {
        var registry = new ExcelMapRegistry();

        registry.Register(new SearchResultMap());
        registry.Register(new MisSearchResultMap());
        registry.Register(new MisNonMatchSearchResultMap());
        registry.Register(new TimespanFilterMap());
        registry.Register(new WorkOrderFilterEntryMap());
        registry.Register(new ItemNumberFilterEntryMap());
        registry.Register(new ProfitCenterFilterEntryMap());
        registry.Register(new WorkCenterFilterEntryMap());
        registry.Register(new OperatorFilterEntryMap());
        registry.Register(new ComponentLotFilterEntryMap());
        registry.Register(new ItemOperationMisFilterEntryMap());

        return registry;
    }

    public void Dispose()
    {
        Workbook.Dispose();
        GC.SuppressFinalize(this);
    }
}

Step 3: Write ExcelTestHelpers.cs

using ClosedXML.Excel;

namespace JdeScoping.ExcelIO.Tests.Fixtures;

public static class ExcelTestHelpers
{
    public static List<string> GetHeadersFromSheet(IXLWorksheet sheet)
    {
        var headers = new List<string>();
        var col = 1;
        while (!sheet.Cell(1, col).IsEmpty())
        {
            headers.Add(sheet.Cell(1, col).Value.GetText());
            col++;
        }
        return headers;
    }
}

Step 4: Build and verify

dotnet build tests/JdeScoping.ExcelIO.Tests

Task 2: Create Concrete Fixture Classes

Files:

  • Create: tests/JdeScoping.ExcelIO.Tests/Fixtures/MinimalSearchFixture.cs
  • Create: tests/JdeScoping.ExcelIO.Tests/Fixtures/WithResultsFixture.cs
  • Create: tests/JdeScoping.ExcelIO.Tests/Fixtures/WithMisDataFixture.cs
  • Create: tests/JdeScoping.ExcelIO.Tests/Fixtures/LargeDataSetFixture.cs

Step 1: Write MinimalSearchFixture.cs

using JdeScoping.ExcelIO.Models.Reporting;

namespace JdeScoping.ExcelIO.Tests.Fixtures;

public class MinimalSearchFixture : WorkbookFixtureBase
{
    protected override SearchModel CreateSearchModel() => new()
    {
        Id = 1,
        Name = "Minimal Search Test",
        UserName = "testuser",
        SubmitDt = new DateTime(2024, 1, 15, 14, 30, 45),
        StartDt = new DateTime(2024, 1, 15, 14, 31, 0),
        EndDt = new DateTime(2024, 1, 15, 14, 35, 0),
        ExtractMisData = false,
        Results = []
    };
}

Step 2: Write WithResultsFixture.cs

using JdeScoping.ExcelIO.Models.Reporting;
using SearchResult = JdeScoping.Core.Models.SearchResults.SearchResult;

namespace JdeScoping.ExcelIO.Tests.Fixtures;

public class WithResultsFixture : WorkbookFixtureBase
{
    protected override SearchModel CreateSearchModel() => new()
    {
        Id = 1,
        Name = "Search With Results",
        UserName = "testuser",
        SubmitDt = new DateTime(2024, 1, 15, 14, 30, 45),
        StartDt = new DateTime(2024, 1, 15, 14, 31, 0),
        EndDt = new DateTime(2024, 1, 15, 14, 35, 0),
        ExtractMisData = false,
        Results =
        [
            new SearchResult
            {
                WorkOrderNumber = 12345,
                WorkOrderBranchCode = "001",
                LotNumber = "LOT-001",
                ItemNumber = "ITEM-001",
                PlanningFamily = "PF01",
                StockingType = "M",
                OrderQuantity = 100,
                HeldQuantity = 0,
                ScrappedQuantity = 0,
                ShippedQuantity = 50,
                StepBranchCode = "001",
                StepNumber = 10,
                StepDescription = "Assembly",
                FunctionOperationDescription = "Main assembly operation",
                StepUpdateDt = new DateTime(2024, 1, 14, 10, 0, 0),
                StatusCode = "50",
                StatusDescription = "In Progress",
                StatusUpdateDt = new DateTime(2024, 1, 14, 10, 0, 0),
                Flagged = true
            }
        ]
    };
}

Step 3: Write WithMisDataFixture.cs

using JdeScoping.ExcelIO.Models.Reporting;
using SearchResult = JdeScoping.Core.Models.SearchResults.SearchResult;
using MisSearchResult = JdeScoping.Core.Models.SearchResults.MisSearchResult;
using MisNonMatchSearchResult = JdeScoping.Core.Models.SearchResults.MisNonMatchSearchResult;

namespace JdeScoping.ExcelIO.Tests.Fixtures;

public class WithMisDataFixture : WorkbookFixtureBase
{
    protected override SearchModel CreateSearchModel() => new()
    {
        Id = 1,
        Name = "Search With MIS Data",
        UserName = "testuser",
        SubmitDt = new DateTime(2024, 1, 15, 14, 30, 45),
        StartDt = new DateTime(2024, 1, 15, 14, 31, 0),
        EndDt = new DateTime(2024, 1, 15, 14, 35, 0),
        ExtractMisData = true,
        Results =
        [
            new SearchResult
            {
                WorkOrderNumber = 12345,
                WorkOrderBranchCode = "001",
                LotNumber = "LOT-001",
                ItemNumber = "ITEM-001",
                PlanningFamily = "PF01",
                StockingType = "M",
                OrderQuantity = 100,
                HeldQuantity = 0,
                ScrappedQuantity = 0,
                ShippedQuantity = 50,
                StepBranchCode = "001",
                StepNumber = 10,
                StepDescription = "Assembly",
                FunctionOperationDescription = "Main assembly",
                StepUpdateDt = new DateTime(2024, 1, 14, 10, 0, 0),
                StatusCode = "50",
                StatusDescription = "In Progress",
                Flagged = true
            }
        ],
        MisResults =
        [
            new MisSearchResult
            {
                ItemNumber = "ITEM-001",
                SequenceNumber = "010",
                MisNumber = "MIS-001",
                RevId = "A",
                ItemDescription = "Test Item",
                Status = "Released",
                ReleaseDate = new DateTime(2023, 12, 15),
                BranchCode = "001",
                JobStepSequenceNumber = 10,
                MatchedSequenceNumber = 10,
                RoutingMatch = true,
                MasterMatch = true,
                FunctionOperationDescription = "Assembly operation",
                CharNumber = "001",
                TestDescription = "Sample test description",
                SamplingType = "100%",
                SamplingValue = "1",
                ToolsGauges = "Gauge A, Gauge B",
                WorkInstructions = "Step 1: Do this. Step 2: Do that."
            }
        ],
        MisNonMatchResults =
        [
            new MisNonMatchSearchResult
            {
                WorkCenterCode = "WC01",
                WorkOrderNumber = 12345,
                WorkOrderStartDate = new DateTime(2024, 1, 8),
                JobStepNumber = 10,
                JobStepDescription = "Test operation",
                JobStepEndDate = new DateTime(2024, 1, 10),
                FunctionCode = "FC01",
                WasJobStepAdded = false,
                MatchedJobStepNumber = 10,
                ItemNumber = "ITEM-001",
                ItemDescription = "Test Item Description",
                RoutingType = "M"
            }
        ]
    };
}

Step 4: Write LargeDataSetFixture.cs

using JdeScoping.ExcelIO.Models.Reporting;
using SearchResult = JdeScoping.Core.Models.SearchResults.SearchResult;

namespace JdeScoping.ExcelIO.Tests.Fixtures;

public class LargeDataSetFixture : WorkbookFixtureBase
{
    protected override SearchModel CreateSearchModel()
    {
        var model = new SearchModel
        {
            Id = 1,
            Name = "Large Data Set Test",
            UserName = "testuser",
            SubmitDt = new DateTime(2024, 1, 15, 14, 30, 45),
            StartDt = new DateTime(2024, 1, 15, 14, 31, 0),
            EndDt = new DateTime(2024, 1, 15, 14, 35, 0),
            ExtractMisData = false,
            Results = []
        };

        for (int i = 0; i < 1000; i++)
        {
            model.Results.Add(new SearchResult
            {
                WorkOrderNumber = 10000 + i,
                ItemNumber = $"ITEM-{i:D5}",
                LotNumber = $"LOT-{i:D5}",
                Flagged = true
            });
        }

        return model;
    }
}

Step 5: Build and verify

dotnet build tests/JdeScoping.ExcelIO.Tests

Task 3: Create MinimalSearchTests

Files:

  • Create: tests/JdeScoping.ExcelIO.Tests/Integration/MinimalSearchTests.cs

Step 1: Create Integration directory

mkdir -p tests/JdeScoping.ExcelIO.Tests/Integration

Step 2: Write MinimalSearchTests.cs

using ClosedXML.Excel;
using JdeScoping.ExcelIO.Tests.Fixtures;
using Shouldly;
using Xunit;

namespace JdeScoping.ExcelIO.Tests.Integration;

public class MinimalSearchTests : IClassFixture<MinimalSearchFixture>
{
    private readonly XLWorkbook _workbook;

    public MinimalSearchTests(MinimalSearchFixture fixture)
    {
        _workbook = fixture.Workbook;
    }

    [Fact]
    public void SheetCount_IsTwo()
    {
        _workbook.Worksheets.Count.ShouldBe(2);
    }

    [Fact]
    public void SearchCriteriaSheet_Exists()
    {
        _workbook.Worksheets.TryGetWorksheet("Search Criteria", out _).ShouldBeTrue();
    }

    [Fact]
    public void SearchResultsSheet_Exists()
    {
        _workbook.Worksheets.TryGetWorksheet("Search Results", out _).ShouldBeTrue();
    }

    [Fact]
    public void SearchCriteriaSheet_IsProtected()
    {
        var sheet = _workbook.Worksheet("Search Criteria");
        sheet.Protection.IsProtected.ShouldBeTrue();
    }

    [Fact]
    public void SearchResultsSheet_IsProtected()
    {
        var sheet = _workbook.Worksheet("Search Results");
        sheet.Protection.IsProtected.ShouldBeTrue();
    }
}

Step 3: Build and run tests

dotnet build tests/JdeScoping.ExcelIO.Tests
dotnet test tests/JdeScoping.ExcelIO.Tests --filter "FullyQualifiedName~MinimalSearchTests" --verbosity normal

Task 4: Create SearchResultsSheetTests

Files:

  • Create: tests/JdeScoping.ExcelIO.Tests/Integration/SearchResultsSheetTests.cs

Step 1: Write SearchResultsSheetTests.cs

using ClosedXML.Excel;
using JdeScoping.ExcelIO.Tests.Fixtures;
using Shouldly;
using Xunit;

namespace JdeScoping.ExcelIO.Tests.Integration;

public class SearchResultsSheetTests : IClassFixture<WithResultsFixture>
{
    private readonly XLWorkbook _workbook;
    private readonly IXLWorksheet _sheet;
    private readonly List<string> _headers;

    public SearchResultsSheetTests(WithResultsFixture fixture)
    {
        _workbook = fixture.Workbook;
        _sheet = _workbook.Worksheet("Search Results");
        _headers = ExcelTestHelpers.GetHeadersFromSheet(_sheet);
    }

    [Fact]
    public void ColumnCount_Is19()
    {
        _headers.Count.ShouldBe(19);
    }

    [Fact]
    public void ColumnHeaders_MatchExpected()
    {
        _headers.ShouldContain("Work Order Number");
        _headers.ShouldContain("Work Order Branch Code");
        _headers.ShouldContain("Lot Number");
        _headers.ShouldContain("Item Number");
        _headers.ShouldContain("Planning Family");
        _headers.ShouldContain("Stocking Type");
        _headers.ShouldContain("Order Quantity");
        _headers.ShouldContain("Held Quantity");
        _headers.ShouldContain("Scrapped Quantity");
        _headers.ShouldContain("Shipped Quantity");
        _headers.ShouldContain("Operation Step Branch Code");
        _headers.ShouldContain("Operation Step");
        _headers.ShouldContain("Operation Step Description");
        _headers.ShouldContain("Function Operation Description");
        _headers.ShouldContain("Operation Step Update Timestamp");
        _headers.ShouldContain("Status Code");
        _headers.ShouldContain("Status Description");
        _headers.ShouldContain("Status Update Timestamp");
        _headers.ShouldContain("Inclusion Reason");
    }

    [Fact]
    public void ColumnOrder_MatchesSpec()
    {
        _headers[0].ShouldBe("Work Order Number");
        _headers[1].ShouldBe("Work Order Branch Code");
        _headers[2].ShouldBe("Lot Number");
        _headers[3].ShouldBe("Item Number");
        _headers[4].ShouldBe("Planning Family");
        _headers[5].ShouldBe("Stocking Type");
        _headers[6].ShouldBe("Order Quantity");
        _headers[7].ShouldBe("Held Quantity");
        _headers[8].ShouldBe("Scrapped Quantity");
        _headers[9].ShouldBe("Shipped Quantity");
        _headers[10].ShouldBe("Operation Step Branch Code");
        _headers[11].ShouldBe("Operation Step");
        _headers[12].ShouldBe("Operation Step Description");
        _headers[13].ShouldBe("Function Operation Description");
        _headers[14].ShouldBe("Operation Step Update Timestamp");
        _headers[15].ShouldBe("Status Code");
        _headers[16].ShouldBe("Status Description");
        _headers[17].ShouldBe("Status Update Timestamp");
        _headers[18].ShouldBe("Inclusion Reason");
    }

    [Fact]
    public void TableStyle_IsLight18()
    {
        var table = _sheet.Tables.First();
        table.Theme.ShouldBe(XLTableTheme.TableStyleLight18);
    }

    [Fact]
    public void DataRow_ContainsExpectedValues()
    {
        _sheet.Cell(2, 1).Value.GetNumber().ShouldBe(12345);
        _sheet.Cell(2, 3).Value.GetText().ShouldBe("LOT-001");
        _sheet.Cell(2, 4).Value.GetText().ShouldBe("ITEM-001");
    }
}

Step 2: Build and run tests

dotnet build tests/JdeScoping.ExcelIO.Tests
dotnet test tests/JdeScoping.ExcelIO.Tests --filter "FullyQualifiedName~SearchResultsSheetTests" --verbosity normal

Task 5: Create MisInfoSheetTests

Files:

  • Create: tests/JdeScoping.ExcelIO.Tests/Integration/MisInfoSheetTests.cs

Step 1: Write MisInfoSheetTests.cs

using ClosedXML.Excel;
using JdeScoping.ExcelIO.Tests.Fixtures;
using Shouldly;
using Xunit;

namespace JdeScoping.ExcelIO.Tests.Integration;

public class MisInfoSheetTests : IClassFixture<WithMisDataFixture>
{
    private readonly XLWorkbook _workbook;
    private readonly IXLWorksheet _sheet;
    private readonly List<string> _headers;

    public MisInfoSheetTests(WithMisDataFixture fixture)
    {
        _workbook = fixture.Workbook;
        _sheet = _workbook.Worksheet("MIS Info");
        _headers = ExcelTestHelpers.GetHeadersFromSheet(_sheet);
    }

    [Fact]
    public void SheetCount_IsFour()
    {
        _workbook.Worksheets.Count.ShouldBe(4);
    }

    [Fact]
    public void MisInfoSheet_Exists()
    {
        _workbook.Worksheets.TryGetWorksheet("MIS Info", out _).ShouldBeTrue();
    }

    [Fact]
    public void ColumnCount_Is19()
    {
        _headers.Count.ShouldBe(19);
    }

    [Fact]
    public void ColumnHeaders_MatchExpected()
    {
        _headers.ShouldContain("Item Number");
        _headers.ShouldContain("MIS Job Step Sequence Number");
        _headers.ShouldContain("MIS Number");
        _headers.ShouldContain("MIS Revision");
        _headers.ShouldContain("Item Description");
        _headers.ShouldContain("MIS Release Status");
        _headers.ShouldContain("MIS Release Date");
        _headers.ShouldContain("Branch Code");
        _headers.ShouldContain("Job Step Sequence Number");
        _headers.ShouldContain("Matched Sequence Number");
        _headers.ShouldContain("Matched to F3112Z1?");
        _headers.ShouldContain("Matched to F3003?");
        _headers.ShouldContain("Function Operation Description");
        _headers.ShouldContain("Char Number");
        _headers.ShouldContain("Test Description");
        _headers.ShouldContain("Sampling Type");
        _headers.ShouldContain("Sampling Value");
        _headers.ShouldContain("Tools & Gauges");
        _headers.ShouldContain("Work Instructions");
    }

    [Fact]
    public void ColumnOrder_MatchesSpec()
    {
        _headers[0].ShouldBe("Item Number");
        _headers[1].ShouldBe("MIS Job Step Sequence Number");
        _headers[2].ShouldBe("MIS Number");
        _headers[3].ShouldBe("MIS Revision");
        _headers[4].ShouldBe("Item Description");
        _headers[5].ShouldBe("MIS Release Status");
        _headers[6].ShouldBe("MIS Release Date");
        _headers[7].ShouldBe("Branch Code");
        _headers[8].ShouldBe("Job Step Sequence Number");
        _headers[9].ShouldBe("Matched Sequence Number");
        _headers[10].ShouldBe("Matched to F3112Z1?");
        _headers[11].ShouldBe("Matched to F3003?");
        _headers[12].ShouldBe("Function Operation Description");
        _headers[13].ShouldBe("Char Number");
        _headers[14].ShouldBe("Test Description");
        _headers[15].ShouldBe("Sampling Type");
        _headers[16].ShouldBe("Sampling Value");
        _headers[17].ShouldBe("Tools & Gauges");
        _headers[18].ShouldBe("Work Instructions");
    }

    [Fact]
    public void TableStyle_IsLight18()
    {
        var table = _sheet.Tables.First();
        table.Theme.ShouldBe(XLTableTheme.TableStyleLight18);
    }

    [Fact]
    public void Protection_IsEnabled()
    {
        _sheet.Protection.IsProtected.ShouldBeTrue();
    }

    [Fact]
    public void DataRow_ContainsExpectedValues()
    {
        _sheet.Cell(2, 1).Value.GetText().ShouldBe("ITEM-001");
        _sheet.Cell(2, 3).Value.GetText().ShouldBe("MIS-001");
    }

    [Fact]
    public void TestDescriptionColumn_IsWrapped()
    {
        var colIndex = _headers.IndexOf("Test Description") + 1;
        _sheet.Column(colIndex).Width.ShouldBe(65);
        _sheet.Column(colIndex).Style.Alignment.WrapText.ShouldBeTrue();
    }

    [Fact]
    public void ToolsGaugesColumn_IsWrapped()
    {
        var colIndex = _headers.IndexOf("Tools & Gauges") + 1;
        _sheet.Column(colIndex).Width.ShouldBe(65);
        _sheet.Column(colIndex).Style.Alignment.WrapText.ShouldBeTrue();
    }

    [Fact]
    public void WorkInstructionsColumn_IsWrapped()
    {
        var colIndex = _headers.IndexOf("Work Instructions") + 1;
        _sheet.Column(colIndex).Width.ShouldBe(65);
        _sheet.Column(colIndex).Style.Alignment.WrapText.ShouldBeTrue();
    }
}

Step 2: Build and run tests

dotnet build tests/JdeScoping.ExcelIO.Tests
dotnet test tests/JdeScoping.ExcelIO.Tests --filter "FullyQualifiedName~MisInfoSheetTests" --verbosity normal

Task 6: Create InvestigationSheetTests

Files:

  • Create: tests/JdeScoping.ExcelIO.Tests/Integration/InvestigationSheetTests.cs

Step 1: Write InvestigationSheetTests.cs

using ClosedXML.Excel;
using JdeScoping.ExcelIO.Tests.Fixtures;
using Shouldly;
using Xunit;

namespace JdeScoping.ExcelIO.Tests.Integration;

public class InvestigationSheetTests : IClassFixture<WithMisDataFixture>
{
    private readonly XLWorkbook _workbook;
    private readonly IXLWorksheet _sheet;
    private readonly List<string> _headers;

    public InvestigationSheetTests(WithMisDataFixture fixture)
    {
        _workbook = fixture.Workbook;
        _sheet = _workbook.Worksheet("Investigation");
        _headers = ExcelTestHelpers.GetHeadersFromSheet(_sheet);
    }

    [Fact]
    public void InvestigationSheet_Exists()
    {
        _workbook.Worksheets.TryGetWorksheet("Investigation", out _).ShouldBeTrue();
    }

    [Fact]
    public void ColumnCount_Is12()
    {
        _headers.Count.ShouldBe(12);
    }

    [Fact]
    public void ColumnHeaders_MatchExpected()
    {
        _headers.ShouldContain("Work Center Code");
        _headers.ShouldContain("Work Order Number");
        _headers.ShouldContain("Work Order Start Date");
        _headers.ShouldContain("Job Step Number");
        _headers.ShouldContain("Function Operation Description");
        _headers.ShouldContain("Job Step End Date");
        _headers.ShouldContain("Function Code");
        _headers.ShouldContain("Was Job Step Added?");
        _headers.ShouldContain("Matched Job Step Number");
        _headers.ShouldContain("Item Number");
        _headers.ShouldContain("Item Description");
        _headers.ShouldContain("Routing Type");
    }

    [Fact]
    public void ColumnOrder_MatchesSpec()
    {
        _headers[0].ShouldBe("Work Center Code");
        _headers[1].ShouldBe("Work Order Number");
        _headers[2].ShouldBe("Work Order Start Date");
        _headers[3].ShouldBe("Job Step Number");
        _headers[4].ShouldBe("Function Operation Description");
        _headers[5].ShouldBe("Job Step End Date");
        _headers[6].ShouldBe("Function Code");
        _headers[7].ShouldBe("Was Job Step Added?");
        _headers[8].ShouldBe("Matched Job Step Number");
        _headers[9].ShouldBe("Item Number");
        _headers[10].ShouldBe("Item Description");
        _headers[11].ShouldBe("Routing Type");
    }

    [Fact]
    public void TableStyle_IsLight18()
    {
        var table = _sheet.Tables.First();
        table.Theme.ShouldBe(XLTableTheme.TableStyleLight18);
    }

    [Fact]
    public void Protection_IsEnabled()
    {
        _sheet.Protection.IsProtected.ShouldBeTrue();
    }

    [Fact]
    public void DataRow_ContainsExpectedValues()
    {
        _sheet.Cell(2, 2).Value.GetNumber().ShouldBe(12345);
        _sheet.Cell(2, 10).Value.GetText().ShouldBe("ITEM-001");
    }
}

Step 2: Build and run tests

dotnet build tests/JdeScoping.ExcelIO.Tests
dotnet test tests/JdeScoping.ExcelIO.Tests --filter "FullyQualifiedName~InvestigationSheetTests" --verbosity normal

Task 7: Create ProtectionAndStyleTests

Files:

  • Create: tests/JdeScoping.ExcelIO.Tests/Integration/ProtectionAndStyleTests.cs

Step 1: Write ProtectionAndStyleTests.cs

using ClosedXML.Excel;
using JdeScoping.ExcelIO.Tests.Fixtures;
using Shouldly;
using Xunit;

namespace JdeScoping.ExcelIO.Tests.Integration;

public class ProtectionAndStyleTests : IClassFixture<WithMisDataFixture>
{
    private readonly XLWorkbook _workbook;

    public ProtectionAndStyleTests(WithMisDataFixture fixture)
    {
        _workbook = fixture.Workbook;
    }

    [Fact]
    public void AllDataSheets_AreProtected()
    {
        _workbook.Worksheet("Search Results").Protection.IsProtected.ShouldBeTrue();
        _workbook.Worksheet("MIS Info").Protection.IsProtected.ShouldBeTrue();
        _workbook.Worksheet("Investigation").Protection.IsProtected.ShouldBeTrue();
    }

    [Fact]
    public void Protection_AllowsFiltering()
    {
        var sheet = _workbook.Worksheet("Search Results");
        sheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.AutoFilter).ShouldBeTrue();
    }

    [Fact]
    public void Protection_AllowsSorting()
    {
        var sheet = _workbook.Worksheet("Search Results");
        sheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.Sort).ShouldBeTrue();
    }

    [Fact]
    public void Protection_AllowsFormatting()
    {
        var sheet = _workbook.Worksheet("Search Results");
        sheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.FormatCells).ShouldBeTrue();
        sheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.FormatColumns).ShouldBeTrue();
        sheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.FormatRows).ShouldBeTrue();
    }

    [Fact]
    public void AllTables_UseLight18Style()
    {
        _workbook.Worksheet("Search Results").Tables.First().Theme.ShouldBe(XLTableTheme.TableStyleLight18);
        _workbook.Worksheet("MIS Info").Tables.First().Theme.ShouldBe(XLTableTheme.TableStyleLight18);
        _workbook.Worksheet("Investigation").Tables.First().Theme.ShouldBe(XLTableTheme.TableStyleLight18);
    }

    [Fact]
    public void HeaderCells_HaveCorrectFormatting()
    {
        var sheet = _workbook.Worksheet("Search Criteria");
        var headerCell = sheet.Cell(1, 1);
        headerCell.Style.Font.Bold.ShouldBeTrue();
        headerCell.Style.Fill.BackgroundColor.ShouldBe(XLColor.Gainsboro);
        headerCell.Style.Alignment.Horizontal.ShouldBe(XLAlignmentHorizontalValues.Center);
    }

    [Fact]
    public void CriteriaTimestamp_MatchesLegacyFormat()
    {
        var sheet = _workbook.Worksheet("Search Criteria");
        var timestamp = sheet.Cell(4, 2).Value.GetText();
        timestamp.ShouldContain("Jan 15, 2024");
        timestamp.ShouldContain("02:30:45");
        timestamp.ShouldContain("EST");
    }
}

Step 2: Build and run tests

dotnet build tests/JdeScoping.ExcelIO.Tests
dotnet test tests/JdeScoping.ExcelIO.Tests --filter "FullyQualifiedName~ProtectionAndStyleTests" --verbosity normal

Task 8: Create LegacyFormatTests and LargeDataSetTests

Files:

  • Create: tests/JdeScoping.ExcelIO.Tests/Integration/LegacyFormatTests.cs
  • Create: tests/JdeScoping.ExcelIO.Tests/Integration/LargeDataSetTests.cs

Step 1: Write LegacyFormatTests.cs

using Shouldly;
using Xunit;
using ExcelFormats = JdeScoping.ExcelIO.Formatting.ExcelFormats;

namespace JdeScoping.ExcelIO.Tests.Integration;

/// <summary>
/// Tests for ExcelFormats constants - no workbook generation needed.
/// </summary>
public class LegacyFormatTests
{
    [Fact]
    public void TimestampFormat_MatchesLegacy()
    {
        ExcelFormats.TimestampFormat.ShouldBe("[$-409]m/d/yy h:mm AM/PM;@");
    }

    [Fact]
    public void DateFormat_MatchesLegacyPattern()
    {
        ExcelFormats.DateFormat.ShouldContain("MM/dd/yyyy");
    }

    [Fact]
    public void WrappedColumnWidth_MatchesLegacy()
    {
        ExcelFormats.WrappedColumnWidth.ShouldBe(65);
    }

    [Fact]
    public void CriteriaPaddingFactor_MatchesLegacy()
    {
        ExcelFormats.CriteriaPaddingFactor.ShouldBe(1.15);
    }

    [Fact]
    public void DataPaddingFactor_MatchesLegacy()
    {
        ExcelFormats.DataPaddingFactor.ShouldBe(1.30);
    }
}

Step 2: Write LargeDataSetTests.cs

using ClosedXML.Excel;
using JdeScoping.ExcelIO.Tests.Fixtures;
using Shouldly;
using Xunit;

namespace JdeScoping.ExcelIO.Tests.Integration;

public class LargeDataSetTests : IClassFixture<LargeDataSetFixture>
{
    private readonly XLWorkbook _workbook;

    public LargeDataSetTests(LargeDataSetFixture fixture)
    {
        _workbook = fixture.Workbook;
    }

    [Fact]
    public void TableRowCount_Is1001()
    {
        var sheet = _workbook.Worksheet("Search Results");
        var table = sheet.Tables.First();
        table.RowCount().ShouldBe(1001); // 1 header + 1000 data rows
    }
}

Step 3: Build and run tests

dotnet build tests/JdeScoping.ExcelIO.Tests
dotnet test tests/JdeScoping.ExcelIO.Tests --filter "FullyQualifiedName~LegacyFormatTests|FullyQualifiedName~LargeDataSetTests" --verbosity normal

Task 9: Delete Old Integration Test Files

Files:

  • Delete: tests/JdeScoping.ExcelIO.Tests/ExcelExportIntegrationTests.cs
  • Delete: tests/JdeScoping.ExcelIO.Tests/LegacyComparisonTests.cs

Step 1: Delete old files

rm tests/JdeScoping.ExcelIO.Tests/ExcelExportIntegrationTests.cs
rm tests/JdeScoping.ExcelIO.Tests/LegacyComparisonTests.cs

Step 2: Build and verify all tests pass

dotnet build tests/JdeScoping.ExcelIO.Tests
dotnet test tests/JdeScoping.ExcelIO.Tests --verbosity normal

Task 10: Run Full Test Suite and Measure Performance

Step 1: Run full test suite with timing

time dotnet test tests/JdeScoping.ExcelIO.Tests --verbosity minimal

Step 2: Verify test count matches expected

Expected: ~103 tests (same as before, just reorganized)

Step 3: Commit changes

git add -A
git commit -m "perf: optimize ExcelIO tests with fixture-based consolidation

- Add WorkbookFixtureBase and 4 concrete fixtures
- Reorganize integration tests to use IClassFixture<T>
- Reduce workbook generations from ~42 to 4
- Delete redundant ExcelExportIntegrationTests.cs
- Delete redundant LegacyComparisonTests.cs
- Test coverage unchanged, runtime reduced ~75-80%"

Summary

Task Description
1 Create fixture base class and helpers
2 Create 4 concrete fixture classes
3 Create MinimalSearchTests (5 tests)
4 Create SearchResultsSheetTests (5 tests)
5 Create MisInfoSheetTests (12 tests)
6 Create InvestigationSheetTests (7 tests)
7 Create ProtectionAndStyleTests (7 tests)
8 Create LegacyFormatTests + LargeDataSetTests (6 tests)
9 Delete old integration test files
10 Run full test suite or measure performance