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:
- Fix production code before changing tests - When a test fails, investigate production code first
- Assert outcomes, not interactions - Tests must verify real results, not just that mocks were called
- Stub only true externals - Mock the Revit API, filesystem, and network; never mock your own services
- 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 runsRequiresActiveDocument- Excluded from headless runsSlow- May be filtered out in quick test cyclesLocal- 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
- Create the test class in the appropriate
Tests/directory - Add NUnit attributes:
[TestFixture]and[Test] - Reference TestSupport: Add using
DBTools.Tests.Shared - 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 rawvstestconsole outputresults.trx- structured test resultsvstest.diag.log- test platform diagnosticssummary.json- aggregate counts and artifact pathsfailures.md- aggregated failed-test report for bulk fix planningfailed/*.md- one file per failed test with captured details
Source:
invoke-revit-tests.sh:97-117Source:invoke-revit-tests.sh:960-969Source: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:153Source:invoke-revit-tests.sh:190Source: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-560Source: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-66Source: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
- Test Pipeline Architecture: ../architecture/test-pipeline.md
- New Tool Guide: new-tool-guide.md
- Build System: ../architecture/build-system.md
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