Table of Contents

Testing Tools for DBTools Development

This guide covers the complete testing infrastructure for DBTools, including test categories, running tests, writing effective tests, and using the TestSupport library.


Overview: Testing Philosophy

DBTools follows a production-first testing philosophy:

  1. Fix production code before changing tests - When a test fails, investigate production code first
  2. Assert outcomes, not interactions - Tests must verify real results, not just that mocks were called
  3. Stub only true externals - Mock the Revit API, filesystem, and network; never mock your own services
  4. No fake tests - If a test passes regardless of production correctness, delete it

Source: .claude/skills/test-audit/README.md

What Makes a Test Legitimate

Legitimate (KEEP) Illegitimate (DELETE)
Revit API stubs Your own service mocks
File system stubs Orchestrator/handler mocks
Network stubs Test doubles with domain behavior
Time/Random stubs for determinism "Mock was called" assertions only

Test Categories

DBTools has two broad categories of tests, determined by execution environment.

1. Headless Tests (No Revit Required)

Tests that can run without Revit, via dotnet test:

  • Build Artifacts Tests - Verify DLL assembly structure and metadata
  • Unit Tests - Pure logic tests for services, algorithms, similarity detection
  • DA Tests - Design Automation-compatible tests
# Run headless tests
dotnet test testing/DBTools.BuildArtifacts.Tests/DBTools.BuildArtifacts.Tests.csproj -c Release

2. Revit Integration Tests

Tests that require a running Revit instance, executed via invoke-revit-tests.sh:

  • UI Tests - ViewModel integration tests
  • Document Tests - Tests requiring Revit documents with elements
  • Adapter Tests - Tests verifying Revit API interactions
# Run via test runner
bash invoke-revit-tests.sh --smart --tool GM

Test Category Markers

Use the TestCategories constants to mark tests appropriately:

Source: testing/TestSupport/TestCategories.cs:1-43

using DBTools.Tests.Shared;

[TestFixture]
public class MyTests
{
    [Test]
    [Category(TestCategories.RequiresRevitUI)]
    public void Test_RequiringUIApplication()
    {
        // Requires UIApplication, TaskDialog, or STA thread
    }

    [Test]
    [Category(TestCategories.RequiresActiveDocument)]
    public void Test_RequiringOpenDocument()
    {
        // Requires an open Revit document with elements
    }

    [Test]
    [Category(TestCategories.Slow)]
    public void Test_TakesLongTime()
    {
        // Tests > 5 seconds; may be excluded from quick runs
    }

    [Test]
    [Category(TestCategories.Integration)]
    public void Test_RequiringNetwork()
    {
        // Requires external dependencies (cloud, network)
    }
}

Category Effects:

  • RequiresRevitUI - Excluded from Design Automation runs
  • RequiresActiveDocument - Excluded from headless runs
  • Slow - May be filtered out in quick test cycles
  • Local - Runs only in local Revit environment

Test Project Structure

Tests live in two locations:

1. In-Tool Tests (src/Tools/<Tool>/Tests/)

Tool-specific tests embedded alongside the tool code:

src/Tools/
├── Common/GM/Tests/
│   ├── Duplicates/
│   │   ├── DuplicateDetectionServiceTests.cs
│   │   └── GmWindowViewModel_SpDuplicatesTests.cs
│   └── Reports/
│       └── GmWindowViewModel_SpPlanningTests.cs
└── Structural/SGT/Tests/
    ├── SgtAdapterTests.cs
    ├── SgtSegmentResolutionTests.cs
    ├── SgtUiServicesTests.cs
    └── ExtentResolverTests.cs

2. Standalone Test Projects (testing/)

Infrastructure and cross-cutting tests:

testing/
├── DBTools.BuildArtifacts.Tests/   # Assembly verification (headless)
├── DBTools.DA.Tests/               # Design Automation tests (headless)
├── TestSupport/                    # Shared test utilities
│   ├── CommonTestDoubles.cs
│   ├── TestCategories.cs
│   ├── TestPathResolver.cs
│   ├── GM/                         # GM-specific test utilities
│   └── SGT/                        # SGT-specific test utilities
└── RevitTestModels/                # Test Revit models by year
    ├── 2024/
    ├── 2025/
    └── 2026/

Setting Up a New Test Project

  1. Create the test class in the appropriate Tests/ directory
  2. Add NUnit attributes: [TestFixture] and [Test]
  3. Reference TestSupport: Add using DBTools.Tests.Shared
  4. Add categories if the test has special requirements

TestSupport Library

The TestSupport library provides shared utilities for all tests.

Core Test Doubles

Source: testing/TestSupport/CommonTestDoubles.cs:1-358

RecordingNotifier

Records error and success banner calls for verification:

var notifier = new RecordingNotifier();

// ... run code that triggers notifications ...

Assert.That(notifier.ErrorCount, Is.EqualTo(1));
Assert.That(notifier.LastTitle, Does.Contain("Error"));
Assert.That(notifier.AllBanners, Has.Count.EqualTo(2));

RecordingOverlay

Records progress overlay interactions:

var overlay = new RecordingOverlay();

// ... run code using progress overlay ...

Assert.That(overlay.ShowCount, Is.EqualTo(1));
Assert.That(overlay.CurrentTitle, Is.EqualTo("Processing..."));
Assert.That(overlay.StepUpdates, Has.Count.GreaterThan(0));

InlineExecutor

Runs ISafeExecutor actions synchronously (no async dispatch):

var executor = new InlineExecutor();

// ... inject into ViewModel ...

Assert.That(executor.RunCount, Is.EqualTo(1));

InlineTransactionRunner

Executes transactions inline without actual Revit transactions:

var runner = new InlineTransactionRunner();

// ... inject into service ...

Assert.That(runner.RunCount, Is.EqualTo(2));
Assert.That(runner.TransactionNames, Contains.Item("Apply Mappings"));

GM Test Utilities

Source: testing/TestSupport/GM/GmTestDataBuilder.cs:1-475

GmTestDataBuilder

Fluent builder for creating realistic GM test state:

var state = new GmTestDataBuilder()
    .AddFamilyWithIds(
        familyId: 100,
        familyName: "Detail Plate A",
        placement: "Face-Based",
        instanceCount: 10,
        (101, "Type A-1"),
        (102, "Type A-2"))
    .AddMaterialsWithIds(
        (1001, "Steel - Chrome"),
        (1002, "Steel - Brushed"))
    .LinkFamilyToMaterials(100, 1001, 1002)
    .Build();

Pre-built Test States

// Standard state with families, types, materials, styles
var standard = GmTestDataBuilder.CreateStandardTestState();

// Minimal state for simple tests
var minimal = GmTestDataBuilder.CreateMinimalTestState();

GmRealisticTestDoubles

Source: testing/TestSupport/GM/GmRealisticTestDoubles.cs:1-920

Realistic implementations of GM contracts:

// Project lifecycle that returns configured state
var lifecycle = RealisticProjectLifecycle.WithStandardTestState();

// Duplicate detection with pre-configured results
var duplicates = RealisticDuplicateDetection.WithStandardDuplicates();

// Element queries from state
var queries = new RealisticElementQuery(state);

// Configurable services for specific scenarios
var mapping = new ConfigurableMappingService()
    .WithApplyResult(new GmMappingApplyResult(...));

SGT Test Utilities

Source: testing/TestSupport/SGT/SgtRealisticTestDoubles.cs:1-318

ConfigurableOrchestratorFactory

var factory = new ConfigurableOrchestratorFactory()
    .Succeeds()  // or .FailsWithError("message")
    .SucceedsWithOutcome(new PlacementOutcome { ... });

// After test execution
Assert.That(factory.PlaceCallCount, Is.EqualTo(1));
Assert.That(factory.LastPlacedPlan, Is.Not.Null);

ConfigurablePlanBuilder

var builder = new ConfigurablePlanBuilder()
    .ReturnsNewPlan(expectedPlan);

// After test execution  
Assert.That(builder.BuildNewPlanCallCount, Is.EqualTo(1));
Assert.That(builder.LastTarget, Is.Not.Null);

Test Path Resolution

Source: testing/TestSupport/TestPathResolver.cs:1-66

Resolves test model paths from the test runner environment:

// Get base test models directory
var modelsDir = TestPathResolver.ResolveTestModelsDir();

// Get specific model path
var gmModel = TestPathResolver.ResolveTestModelPath("2026", "GM", "gm_test_model.rvt");

Writing Headless Tests

Headless tests run via dotnet test without Revit.

Example: Unit Test for Service Logic

Source: src/Tools/Common/GM/Tests/Duplicates/DuplicateDetectionServiceTests.cs:1-184

[TestFixture]
public class DuplicateDetectionServiceTests
{
    // Minimal stubs for true external dependencies only
    private sealed class StubNames : IElementQuery
    {
        private readonly Dictionary<int, string> _map;
        public StubNames(Dictionary<int, string> map) { _map = map; }
        
        public Task<string?> GetElementNameAsync(int elementId, CancellationToken ct = default)
            => Task.FromResult(_map.TryGetValue(elementId, out var n) ? n : null);
        
        public Task<IReadOnlyDictionary<int, string>> GetElementNamesAsync(
            IReadOnlyCollection<int> elementIds, CancellationToken ct = default)
            => Task.FromResult((IReadOnlyDictionary<int, string>)
                _map.Where(kv => elementIds.Contains(kv.Key))
                    .ToDictionary(kv => kv.Key, kv => kv.Value));
    }

    [Test]
    public async Task Families_With_SameStem_And_ParamOverlap_Generate_Pairs()
    {
        // Arrange: Build real kernel state
        var kernel = new GmProjectState
        {
            Families = new Dictionary<int, GmFamilyRecord>
            {
                [100] = new GmFamilyRecord { ElementId = 100, Name = "Plate" },
                [101] = new GmFamilyRecord { ElementId = 101, Name = "Plate1" }
            }
        };
        kernel.Families[100].Types[1000] = new GmTypeRecord 
        { 
            SymbolId = 1000, 
            FamilyId = 100, 
            DeepScanPayload = new GmTypeDeepScan { ParameterNames = new[] { "Width", "Height" } }
        };

        // Real service with minimal stubs
        var svc = new DuplicateDetectionService(
            new NamingSimilarityService(),      // Real service
            new ParameterSimilarityService(),   // Real service
            new StubNames(new Dictionary<int, string> { { 100, "Plate" }, { 101, "Plate1" } }),
            new StubMatPrev(new Dictionary<int, MaterialPreviewInfo?>()),
            new StubStylePrev(new Dictionary<int, StylePreviewInfo?>()),
            NullLogger.Instance);

        // Act
        var res = await svc.DetectAsync(kernel);

        // Assert: Real outcome
        var famGroups = res.Groups.Where(g => g.Kind == DuplicateKind.Families).ToList();
        Assert.That(famGroups.Count, Is.GreaterThanOrEqualTo(1));
        
        var anyPair = famGroups
            .SelectMany(g => g.Pairs)
            .Any(p => p.FromId == 100 && p.ToId == 101 && p.Score >= 90.0);
        Assert.That(anyPair, Is.True, "Expected high-score pair between 100 -> 101.");
    }
}

Example: Build Artifacts Test

Source: testing/DBTools.BuildArtifacts.Tests/AssemblyManifestTests.cs:1-341

[TestFixture]
[Category("BuildArtifacts")]
public sealed class AssemblyManifestTests
{
    private static readonly int[] SupportedYears = { 2024, 2025, 2026 };

    [TestCaseSource(nameof(SupportedYears))]
    public void DBTools_TargetFrameworkCorrect(int year)
    {
        if (!DistExists())
            Assert.Inconclusive("Build artifacts not found. Run build first.");

        var path = Path.Combine(GetYearPath(year), "DBTools.dll");
        var tfm = GetTargetFramework(path);

        if (year == 2024)
        {
            Assert.That(tfm, Does.Contain(".NETFramework").Or.Contain("net48"),
                $"Year {year} should target .NET Framework 4.8");
        }
        else
        {
            Assert.That(tfm, Does.Contain(".NETCoreApp").Or.Contain("net8.0"),
                $"Year {year} should target .NET 8.0");
        }
    }

    [TestCaseSource(nameof(SupportedYears))]
    public void DBTools_NoForbiddenReferences(int year)
    {
        var refs = GetReferencedAssemblies(path);

        foreach (var forbidden in ForbiddenReferences)
        {
            Assert.That(refs.Any(r => r.Equals(forbidden, StringComparison.OrdinalIgnoreCase)), 
                Is.False,
                $"Year {year}: Should not directly reference {forbidden}");
        }
    }
}

Writing Revit Tests

Revit integration tests use the ricaun.RevitTest framework.

Example: SGT UI Service Test

Source: src/Tools/Structural/SGT/Tests/SgtUiServicesTests.cs:1-150

[TestFixture]
public class SgtUiServicesTests
{
    private IUnitService _units = null!;

    [SetUp]
    public void SetUp()
    {
        _units = new UnitService();  // Real service, not a mock
    }

    private static ILogger<T> CreateLogger<T>() 
        => TestLoggingBridge.Initialize().CreateLogger<T>();

    private static WallPreviewData SampleWall(double length = 20.0)
    {
        return new WallPreviewData
        {
            WallId = 1,
            LengthFeet = length,
            BaseElevationFeet = 0.0,
            TopElevationFeet = 10.0,
            ThicknessFeet = 0.5,
            StartPoint = Vector3.Create(0, 0, 0).Value,
            EndPoint = Vector3.Create(length, 0, 0).Value,
            WallTypeId = 1,
            WallTypeName = "Type-A",
            Grids = new List<GridMarker>
            {
                new GridMarker { Name = "A", T = 0.25 },
                new GridMarker { Name = "B", T = 0.75 },
            },
            Openings = new List<ElevationOpeningRect>
            {
                new ElevationOpeningRect 
                { 
                    OpeningKey = "H:1:101", 
                    T0 = 0.40, 
                    T1 = 0.60, 
                    Y0 = 3.0, 
                    Y1 = 6.0 
                },
            }
        };
    }

    [Test]
    public void SgtGridManagementService_UpdateExtentEnables_RespectsGridCounts()
    {
        var svc = new SgtGridManagementService(CreateLogger<SgtGridManagementService>());
        var row = new SgtGirtRowItem { StartExtent = "A", EndExtent = "B" };
        var opts = new ObservableCollection<string> { "Full Length" };

        // 0 grids => both disabled and reset to Full Length
        SgtGridManagementService.UpdateExtentEnables(row, opts);
        
        Assert.That(row.StartExtentEnabled, Is.False);
        Assert.That(row.EndExtentEnabled, Is.False);
        Assert.That(row.StartExtent, Is.EqualTo("Full Length"));
        Assert.That(row.EndExtent, Is.EqualTo("Full Length"));

        // 1 grid => only one side enabled at a time
        opts.Add("A");
        SgtGridManagementService.UpdateExtentEnables(row, opts);
        Assert.That(row.StartExtentEnabled ^ row.EndExtentEnabled, Is.True);

        // 2+ grids => both enabled
        opts.Add("B");
        SgtGridManagementService.UpdateExtentEnables(row, opts);
        Assert.That(row.StartExtentEnabled && row.EndExtentEnabled, Is.True);
    }
}

Using RevitHost for Context

Source: testing/TestSupport/TestHost/RevitHost.cs:1-111

For tests requiring Revit context:

[Test]
[Category(TestCategories.RequiresRevitUI)]
public async Task MyTest_WithRevitContext(UIApplication app)
{
    await RevitHost.RunAsync(app, async () =>
    {
        // Code runs within Revit context
        var doc = app.ActiveUIDocument?.Document;
        Assert.That(doc, Is.Not.Null);
    });
}

Test Doubles: Guidelines

When to Use Test Doubles

Scenario Approach
Revit API dependency Stub it
File system access Stub it
Network/cloud services Stub it
Time-sensitive logic Stub with deterministic time
Your own services Use the real implementation
Orchestrators/handlers Use the real implementation

Anti-patterns to Avoid

// BAD: Mocking your own service to make test pass
var mockOrchestrator = new Mock<ISgtOrchestrator>();
mockOrchestrator.Setup(o => o.PlaceAsync(It.IsAny<SgtPlan>(), It.IsAny<CancellationToken>()))
    .ReturnsAsync(Result.Success(new PlacementOutcome()));

// BAD: Asserting only mock interactions
mockService.Verify(s => s.DoSomething(), Times.Once);
// Missing: actual outcome assertion!

// BAD: Test double that adds domain behavior
public class FakeOrchestrator : ISgtOrchestrator
{
    public Task<Result<PlacementOutcome, string>> PlaceAsync(...)
    {
        // WRONG: Implementing domain logic in test double
        var outcome = ComputePlacement(plan);  
        return Task.FromResult(Result.Success(outcome));
    }
}

Correct Pattern

// GOOD: Use real services, stub only true externals
var realService = new DuplicateDetectionService(
    new NamingSimilarityService(),      // Real
    new ParameterSimilarityService(),   // Real
    new StubElementQuery(testData),     // Stub: wraps Revit API
    new StubMaterialService(testData),  // Stub: wraps Revit API
    NullLogger.Instance);

// GOOD: Assert real outcomes
var result = await realService.DetectAsync(kernel);
Assert.That(result.Groups, Has.Count.GreaterThan(0));
Assert.That(result.Groups[0].Pairs, Contains.Item(
    Has.Property("FromId").EqualTo(100)
       .And.Property("ToId").EqualTo(101)));

Running Tests

Local: invoke-revit-tests.sh

Source: invoke-revit-tests.sh:1-1317

The primary test runner for Revit integration tests.

By default, the runner keeps terminal output compact and writes the full diagnostic payload into a per-run artifact bundle under .artifacts/revit-test-runs/. Use the generated failures.md report for bulk triage, and apply the test-audit rule when deciding whether a failure belongs in production code, test code, or test deletion.

Run Bundle Output

Each run produces:

  • console.log - full raw vstest console output
  • results.trx - structured test results
  • vstest.diag.log - test platform diagnostics
  • summary.json - aggregate counts and artifact paths
  • failures.md - aggregated failed-test report for bulk fix planning
  • failed/*.md - one file per failed test with captured details

Source: invoke-revit-tests.sh:97-117 Source: invoke-revit-tests.sh:960-969 Source: invoke-revit-tests.sh:1041-1283

Basic Usage

# Run all tests for a tool
bash invoke-revit-tests.sh --tool GM

# Smart mode (recommended) - auto-detects build changes
bash invoke-revit-tests.sh --smart --tool GM

# Specific Revit year
bash invoke-revit-tests.sh --smart --tool SGT -y 2025

# Custom filter
bash invoke-revit-tests.sh -f "FullyQualifiedName~DuplicateDetection"

# Specific fixture
bash invoke-revit-tests.sh --fixture SgtUiServicesTests

Revit Instance Modes

Mode Description
--smart Auto-detect build staleness; always uses --close
--reuse Rejected for isolated worktree runs
--persist Rejected for isolated worktree runs
--close Open a fresh isolated Revit instance and close it after tests (default)

The runner now stages a stable worktree-local payload under .artifacts/revit-test-host/<year>/payload and temporarily rewrites the AppLoader DBTools.Loader.dll entry to that stable path for the run. The stable target path keeps Revit trust decisions reusable, which eliminates the repeated "Always Load" prompt while preserving the existing RevitTest/AppLoader host contract.

Source: invoke-revit-tests.sh:153 Source: invoke-revit-tests.sh:190 Source: invoke-revit-tests.sh:1056

Test Discovery

# List all available tests
bash invoke-revit-tests.sh --discover

# List GM tests only
bash invoke-revit-tests.sh --discover --tool GM

# Show test count summary
bash invoke-revit-tests.sh --discover --summary

# Show DA-compatible tests only
bash invoke-revit-tests.sh --discover --da-only

Test History

# Show recent test history
bash invoke-revit-tests.sh --history

# Show failed tests
bash invoke-revit-tests.sh --show-failed

# Show tests not run in 7 days
bash invoke-revit-tests.sh --show-stale 7

# Show error details for specific tests
bash invoke-revit-tests.sh --get-errors "GM"

--get-errors replays the latest matching failures.md bundle, not just a short history snippet.

Source: invoke-revit-tests.sh:472-560 Source: invoke-revit-tests.sh:563-634

Parallel Agent Safety

Multiple agents are prevented from running tests for the same Revit year simultaneously via automatic per-year file locks. No explicit session flags are needed:

# Agent 1 (will lock Revit 2026 until done)
bash invoke-revit-tests.sh --smart --tool GM

# Agent 2 (will wait for lock if also targeting 2026, or run immediately if using -y 2025)
bash invoke-revit-tests.sh --smart --tool SGT

CI: dotnet test

For headless tests in CI pipelines:

# Build artifacts tests
dotnet test testing/DBTools.BuildArtifacts.Tests/DBTools.BuildArtifacts.Tests.csproj \
    -c Release

# Run with specific filter
dotnet test testing/DBTools.BuildArtifacts.Tests/DBTools.BuildArtifacts.Tests.csproj \
    -c Release \
    --filter "FullyQualifiedName~Metadata"

# Exclude slow tests
dotnet test . --filter "Category!=Slow"

Debugging Test Failures

Common Issues

1. "Missing required test params file"

Cause: Running Revit tests outside of invoke-revit-tests.sh, or the canonical dbtools.testparams.json was not published where RevitTest can find it.

Fix: Always run Revit tests via the runner:

bash invoke-revit-tests.sh --smart --tool GM

The runner writes .artifacts/dbtools.testparams.json and mirrors the same file to %APPDATA%/DBTools/dbtools.testparams.json for temp-hosted RevitTest runs.

Source: testing/TestSupport/TestPathResolver.cs:10-66 Source: invoke-revit-tests.sh:939-956

2. "Source files appear newer than build artifacts"

Cause: Source code was modified since the last build (advisory warning, not a hard failure)

Fix: Rebuild before testing:

bash build.sh BuildAll
bash invoke-revit-tests.sh --smart --tool GM

Note: The staleness check is advisory. The real integrity gate is per-DLL SHA256 verification, which will produce a hard failure if artifacts are actually corrupted.

3. "No running Revit.exe instance was found"

Cause: --reuse is not supported for isolated worktree runs.

Fix: Use --smart or the default --close mode instead:

bash invoke-revit-tests.sh --smart --tool GM

4. Test passes in isolation but fails in batch

Cause: Shared state pollution between tests

Fix: Ensure [SetUp] and [TearDown] properly isolate state:

[SetUp]
public void SetUp()
{
    _notifier = new RecordingNotifier();
    _overlay = new RecordingOverlay();
    // Reset any shared state
}

[TearDown]
public void TearDown()
{
    _notifier.Reset();
    _overlay.Reset();
}

5. Assert.Inconclusive: "Build artifacts not found"

Cause: Running build artifacts tests without building first

Fix: Build before testing:

bash build.sh BuildAll
dotnet test testing/DBTools.BuildArtifacts.Tests/DBTools.BuildArtifacts.Tests.csproj

Viewing Test Logs

The script displays the most recent log file path from %APPDATA%/DBTools/Logs/ after each test run.


Real Examples from the Codebase

GM: Duplicate Detection Tests

Source: src/Tools/Common/GM/Tests/Duplicates/DuplicateDetectionServiceTests.cs:58-87

Tests the real duplicate detection algorithm with minimal stubs:

[Test]
public async Task Families_With_SameStem_And_ParamOverlap_Generate_Pairs()
{
    var kernel = new GmProjectState { ... };
    var svc = new DuplicateDetectionService(
        new NamingSimilarityService(),      // Real
        new ParameterSimilarityService(),   // Real
        new StubNames(...),                 // Stub: Revit element names
        new StubMatPrev(...),               // Stub: Revit materials
        new StubStylePrev(...),             // Stub: Revit styles
        NullLogger.Instance);

    var res = await svc.DetectAsync(kernel);
    
    // Assert on actual detection results
    Assert.That(famGroups.Count, Is.GreaterThanOrEqualTo(1));
    Assert.That(anyPair, Is.True, "Expected high-score pair between 100 -> 101.");
}

SGT: UI Services Tests

Source: src/Tools/Structural/SGT/Tests/SgtUiServicesTests.cs:60-117

Tests grid management logic with real service and sample data:

[Test]
public void SgtGridManagementService_ResolveExtentTR_ComputesForGridAndOpening()
{
    var wall = SampleWall(length: 40.0);  // Real test data
    var gridT = new Dictionary<string, double> { ["A"] = 0.25, ["B"] = 0.75 };
    var openingT = new Dictionary<string, (double, double)> { ["H:1:101"] = (0.40, 0.60) };

    var row = new SgtGirtRowItem { ... };

    // Test real service logic
    SgtGridManagementService.ResolveExtentTR(wall, gridT, openingT, row, isStart: true, out tL, ref warnings);
    SgtGridManagementService.ResolveExtentTR(wall, gridT, openingT, row, isStart: false, out tR, ref warnings);
    
    Assert.That(tL, Is.EqualTo(0.25).Within(1e-9));
    Assert.That(tR, Is.EqualTo(0.60).Within(1e-9));
    Assert.That(string.IsNullOrWhiteSpace(warnings), Is.True);
}

Build Artifacts: Assembly Validation

Source: testing/DBTools.BuildArtifacts.Tests/AssemblyManifestTests.cs:86-106

Tests that built assemblies meet contract requirements:

[TestCaseSource(nameof(YearAssemblyPairs))]
public void Assembly_ExistsAndIsValid(int year, string assemblyName)
{
    if (!DistExists())
        Assert.Inconclusive("Build artifacts not found. Run build first.");

    var path = Path.Combine(GetYearPath(year), assemblyName);
    Assert.That(File.Exists(path), Is.True, $"Missing: {path}");

    var asmName = AssemblyName.GetAssemblyName(path);
    Assert.That(asmName, Is.Not.Null);
    Assert.That(asmName.Name, Does.StartWith("DBTools"));
}

Cross-References


Summary Checklist

When writing tests, ensure:

  • [ ] Tests use real service implementations where possible
  • [ ] Only true externals (Revit API, filesystem, network) are stubbed
  • [ ] Tests assert on outcomes, not just mock interactions
  • [ ] Tests are marked with appropriate categories (RequiresRevitUI, etc.)
  • [ ] [SetUp] and [TearDown] properly isolate test state
  • [ ] Test doubles don't add domain behavior
  • [ ] Failing tests trigger production code investigation first