Test Organization & Hierarchy Skill
When to Use This Skill
Use this skill when:
- Adding tests to a new component or service
- Refactoring existing tests that have become hard to navigate
- Setting up test data or fixtures across related tests
- Trying to decide where a test belongs in the file hierarchy
- Standardizing test naming across the team
- Managing test dependencies and shared test setup
- Scaling test suite as components grow in complexity
Philosophy: As test suites grow, organization becomes critical. Poorly organized tests are as problematic as untested code—hard to find, easy to duplicate, and increasingly expensive to maintain.
File Organization: Mirror Source Structure
The Pattern
Test files mirror the source code structure in WorkMood.MauiApp.Tests:
MauiApp/ WorkMood.MauiApp.Tests/
├── Services/ ├── Services/
│ ├── LineGraphService.cs │ ├── LineGraphServiceShould.cs
│ ├── MoodDataService.cs │ └── MoodDataServiceShould.cs
├── ViewModels/ ├── ViewModels/
│ └── DashboardViewModel.cs │ └── DashboardViewModelShould.cs
├── Models/ ├── Models/
│ ├── AxisRange.cs │ ├── AxisRangeShould.cs
│ └── GraphData.cs │ └── GraphDataShould.cs
└── Converters/ └── Converters/
└── MoodConverters.cs └── MoodConvertersShould.cs
Why This Works
- Intuitive navigation — Find test for component at predictable location
- Parallel mental models — Folder hierarchy in source matches test hierarchy
- Reduced cognitive load — No guessing where tests live
- Scales naturally — Adding new components adds new test files in expected places
- Easier onboarding — New team members immediately understand structure
Exception: Test Helpers & Shared Fixtures
Create TestHelpers/ directory for cross-cutting test utilities:
WorkMood.MauiApp.Tests/
├── Services/
├── ViewModels/
├── TestHelpers/ ← Shared across test suites
│ ├── MockFactory.cs ← Creates common mocks
│ ├── TestDataBuilder.cs ← Builds test data consistently
│ └── FixtureBase.cs ← Base class with common setup
What belongs in TestHelpers:
- Mock factories (e.g.,
MockMoodServiceFactory) - Common test data builders
- Shared fixture base classes
- Custom assertions
- Test constants (magic values used in multiple test files)
What does NOT:
- Tests that could logically belong in component-specific folder
- One-off test utilities for a single component
Test File Naming: [Component]Should
xUnit Convention in WorkMood
Format: [ComponentName]Should.cs
// ✅ Correct
LineGraphServiceShould.cs // Matches LineGraphService.cs
AxisRangeShould.cs // Matches AxisRange.cs
MoodConvertersShould.cs // Matches MoodConverters.cs
DashboardViewModelShould.cs // Matches DashboardViewModel.cs
// ❌ Avoid
LineGraphServiceTests.cs // "Tests" suffix is redundant, "Should" is clearer
LineGraphService_Tests.cs // Underscore unnecessarily complicates
TestLineGraphService.cs // Prefix is backwards from convention
LineGraphServiceTest.cs // Singular vs plural is inconsistent
Why [Component]Should?
- Reads naturally — File name becomes implicit English statement: "LineGraphService should..."
- Consistent with test method naming — File + method combined read as behavior statement
- Prevents "Test" overload — "Test" appears multiple times in naming; "Should" is distinctive
- Aligns with xUnit idiom — Fact-based testing uses "should" semantics
Test Class Naming: [Component]Should
Class Name Matches File Name
// File: LineGraphServiceShould.cs
namespace WorkMood.MauiApp.Tests.Services
{
public class LineGraphServiceShould
{
// All tests for LineGraphService go here
}
}
Summary Documentation
Every test class starts with a /// <summary> documenting purpose:
/// <summary>
/// Tests for LineGraphService - complex service handling graph generation
/// from mood data with GraphMode switching and background image support
/// </summary>
public class LineGraphServiceShould
{
// Tests here
}
What goes in summary:
- Component name and purpose
- Key responsibilities being tested
- Significant dependencies or complexity notes
- Reference to source file location (optional but helpful)
/// <summary>
/// Tests for AxisRange immutable record
/// Location: MauiApp/Models/AxisRange.cs
/// Purpose: Verify record behavior, factory methods, and range operations
/// </summary>
public class AxisRangeShould
Test Method Naming: Behavior as Specification
The Convention: [Method/Aspect]_[Condition]_Should[Behavior]
// Single concern
[Fact]
public void Constructor_WithValidDependencies_ShouldCreateInstance()
// Edge case
[Fact]
public void Constructor_WithNullTransformer_ShouldCreateInstanceWithoutThrowingException()
// Transformation behavior
[Fact]
public void TransformMoodData_WithEmptyList_ShouldReturnEmptyGraphData()
// Validation
[Fact]
public void ValidateDateRange_WithInvalidDates_ShouldThrowArgumentException()
// Property calculation
[Fact]
public void Average_WithMultipleMoods_ShouldCalculateCorrectAverage()
Breakdown of the Convention
| Part | Purpose | Example |
|---|---|---|
[Method] |
What code is being tested | Constructor, TransformMoodData, Average |
_With[Condition] |
Input state or scenario | WithValidDependencies, WithEmptyList, WithNullTransformer |
_Should[Behavior] |
Expected outcome | ShouldCreateInstance, ShouldReturnEmptyGraphData, ShouldThrowArgumentException |
Why This Works
- Self-documenting — Reading the test name tells you what it tests and why
- Searchable — Method names are easy to grep for related tests
- Encourages single behavior per test — Long names discourage testing multiple things
- Reduces need for comments — The name is the specification
When to Break the Convention (Rarely)
// ✅ Acceptable for very simple tests
[Fact]
public void BeCreatable()
// ✅ Acceptable for parameterized tests with single responsibility
[Theory]
[InlineData(1, 10, true)]
[InlineData(5, 5, false)]
public void BeValidWhen_MinAndMaxProvided(int min, int max, bool isValid)
Test Grouping with Regions: Organize for Navigation
Region Structure
Group related tests using #region sections within the class:
public class LineGraphServiceShould
{
#region Constructor Tests
[Fact]
public void Constructor_WithValidDependencies_ShouldCreateInstance() { }
[Fact]
public void Constructor_WithNullTransformer_ShouldCreateInstance() { }
#endregion
#region GenerateLineGraph Tests
[Fact]
public void GenerateLineGraph_WithValidData_ShouldReturnGraph() { }
[Fact]
public void GenerateLineGraph_WithEmptyData_ShouldReturnEmptyGraph() { }
#endregion
#region Mode Switching Tests
[Fact]
public void SwitchMode_FromCompactToDetailed_ShouldUpdateGraphSettings() { }
#endregion
}
Region Organization Strategy
- Constructors/Setup — Most critical, readers expect it first
- Main method/property — Primary responsibility of the class
- Secondary methods — Supporting functionality
- Edge cases — Boundary conditions, error handling
- Integration — Tests requiring multiple components working together
Why Regions Help
- Fast navigation — Fold regions to see structure at a glance
- Logical grouping — Related tests cluster together
- Prevents file sprawl — Even large 400+ line test files remain navigable
- Clear test boundaries — Shows where one behavior group ends and another begins
Test Data Management: Builders and Fixtures
The Problem: Test Data Duplication
// ❌ Bad: Duplicate setup scattered everywhere
[Fact]
public void CalculateAverage_WithThreeMoods_ShouldReturnAverage()
{
var moods = new List<Mood>
{
new Mood { Value = 5, RecordedAt = DateTime.Now },
new Mood { Value = 7, RecordedAt = DateTime.Now.AddDays(-1) },
new Mood { Value = 3, RecordedAt = DateTime.Now.AddDays(-2) }
};
// Test code...
}
[Fact]
public void FindTrend_WithThreeMoods_ShouldIdentifyDecline()
{
var moods = new List<Mood>
{
new Mood { Value = 5, RecordedAt = DateTime.Now },
new Mood { Value = 7, RecordedAt = DateTime.Now.AddDays(-1) },
new Mood { Value = 3, RecordedAt = DateTime.Now.AddDays(-2) }
};
// Different test code...
}
Problems:
- Duplicate setup code
- Changes to
Moodrequire updating in multiple places - Hard to understand which properties matter for the test
The Solution: Test Data Builders
// TestHelpers/MoodDataBuilder.cs
public class MoodDataBuilder
{
private List<Mood> _moods = new();
private int _defaultValue = 5;
private DateTime _defaultDate = DateTime.Now;
public static MoodDataBuilder CreateDefault() => new();
public MoodDataBuilder WithValue(int value)
{
_defaultValue = value;
return this;
}
public MoodDataBuilder WithDecline()
{
_moods.Add(new Mood { Value = 5, RecordedAt = _defaultDate });
_moods.Add(new Mood { Value = 7, RecordedAt = _defaultDate.AddDays(-1) });
_moods.Add(new Mood { Value = 3, RecordedAt = _defaultDate.AddDays(-2) });
return this;
}
public List<Mood> Build() => _moods;
}
Usage in Tests
// ✅ Good: Clear intent, reusable
[Fact]
public void CalculateAverage_WithThreeMoods_ShouldReturnAverage()
{
var moods = MoodDataBuilder.CreateDefault().WithDecline().Build();
var average = _service.CalculateAverage(moods);
Assert.Equal(5, average);
}
[Fact]
public void FindTrend_WithDecline_ShouldIdentifyDecline()
{
var moods = MoodDataBuilder.CreateDefault().WithDecline().Build();
var trend = _service.FindTrend(moods);
Assert.Equal(TrendType.Declining, trend);
}
When to Use Builders
- Complex test data — Multiple relationships, many properties
- Reused across tests — Pattern appears in 3+ test methods
- Frequently changing — Reduces refactoring burden
When Not to Bother
- Simple one-off data —
new Mood { Value = 5 }is clear enough - Test-specific data — Setup only makes sense for one test
- Primitive values — Simple integers/strings aren't worth abstraction
Test Fixtures and Shared Setup
Base Class Pattern for Common Setup
// TestHelpers/LineGraphServiceFixture.cs
public class LineGraphServiceFixture : IDisposable
{
public Mock<IGraphDataTransformer> MockTransformer { get; }
public Mock<ILineGraphGenerator> MockGenerator { get; }
public LineGraphService Service { get; }
public LineGraphServiceFixture()
{
MockTransformer = new Mock<IGraphDataTransformer>();
MockGenerator = new Mock<ILineGraphGenerator>();
Service = new LineGraphService(
MockTransformer.Object,
MockGenerator.Object
);
}
public void Dispose()
{
MockTransformer.Reset();
MockGenerator.Reset();
}
}
Using the Fixture in Tests
public class LineGraphServiceShould : IClassFixture<LineGraphServiceFixture>
{
private readonly LineGraphServiceFixture _fixture;
public LineGraphServiceShould(LineGraphServiceFixture fixture)
{
_fixture = fixture;
}
[Fact]
public void GenerateGraph_WithValidData_ShouldCallGenerator()
{
// Arrange - Use pre-configured mocks
_fixture.MockTransformer.Setup(t => t.Transform(It.IsAny<List<Mood>>()))
.Returns(new GraphData());
// Act
_fixture.Service.GenerateGraph(new List<Mood>());
// Assert
_fixture.MockGenerator.Verify(g => g.Generate(It.IsAny<GraphData>()), Times.Once);
}
}
When to Create a Fixture
- 5+ tests need identical setup
- Setup is complex (multiple mocks, intricate configuration)
- Teardown is required (cleanup resources, reset state)
- Component has many dependencies (reduces duplication)
When Not to Bother
- Setup is trivial —
new Mock<IService>()directly in test - Tests need different setup — Fixture forces common ground that doesn't exist
- Shared state causes test coupling — Tests become dependent on fixture state
Test Hierarchy: Organizing Large Test Suites
Example: Scaling LineGraphService Tests
As tests grow, organize into nested classes:
public class LineGraphServiceShould
{
#region Constructor Tests
[Fact]
public void Constructor_WithValidDependencies_ShouldCreateInstance() { }
#endregion
#region GenerateLineGraph Tests
public class GenerateLineGraphShould
{
private readonly LineGraphService _service;
private readonly Mock<IGraphDataTransformer> _mockTransformer;
public GenerateLineGraphShould()
{
_mockTransformer = new Mock<IGraphDataTransformer>();
var mockGenerator = new Mock<ILineGraphGenerator>();
_service = new LineGraphService(_mockTransformer.Object, mockGenerator.Object);
}
[Fact]
public void WithValidData_ShouldReturnGraphData() { }
[Fact]
public void WithEmptyData_ShouldReturnEmptyGraphData() { }
[Fact]
public void WithNullData_ShouldThrowArgumentNullException() { }
}
#endregion
#region GraphMode Switching Tests
public class SwitchModeShould
{
// Nested class for related tests
[Fact]
public void FromCompactToDetailed_ShouldUpdateSettings() { }
[Fact]
public void ToUnsupportedMode_ShouldThrowInvalidOperationException() { }
}
#endregion
}
Benefits of Nested Classes
- Hierarchical organization — Related tests group logically
- Shared setup per group — Each nested class has its own fixture
- Improved test discovery — Test runner shows nested structure
- Scoped mocks/fixtures — Nested class setup only affects its tests
- Natural hierarchy for complex components — Method + condition organization
When to Use Nested Classes
- 10+ tests for a single component
- Tests naturally divide into behavior groups
- Each group needs different setup (factories, fixtures, mocks)
- Component has many public methods (one nested class per method)
Test Constants and Magic Values
Centralize Magic Values in TestHelpers
// TestHelpers/TestConstants.cs
public static class MoodTestConstants
{
// Standard mood values
public const int NEUTRAL_MOOD = 5;
public const int HAPPY_MOOD = 8;
public const int SAD_MOOD = 2;
// Ranges
public const int MIN_VALID_MOOD = 1;
public const int MAX_VALID_MOOD = 10;
// Time constants
public static readonly DateTime STANDARD_TEST_DATE = new(2024, 01, 15);
public static readonly DateTime FUTURE_TEST_DATE = new(2040, 01, 15);
// Array/Collection sizes
public const int SMALL_DATASET_SIZE = 3;
public const int MEDIUM_DATASET_SIZE = 30;
public const int LARGE_DATASET_SIZE = 300;
}
Usage
[Fact]
public void CalculateAverage_WithMoodValues_ShouldCalculateCorrectly()
{
var moods = new List<int>
{
MoodTestConstants.HAPPY_MOOD,
MoodTestConstants.NEUTRAL_MOOD,
MoodTestConstants.SAD_MOOD
};
var average = _service.CalculateAverage(moods);
Assert.Equal(MoodTestConstants.NEUTRAL_MOOD, average);
}
Benefits
- Self-documenting — Constant names explain meaning
- Centralized changes — Update value once, affects all tests
- Prevents scattered magic — Clear where test values come from
- Easier maintenance — Business changes update constants, not individual tests
Anti-Patterns to Avoid
❌ God Test Files
// ❌ Bad: 1000+ lines, tests multiple components
public class AllServicesShould
{
// Tests for MoodService
// Tests for GraphService
// Tests for ScheduleService
// Tests for VisualizationService
// ... and 5 more services
}
Fix: Split into separate files matching component design
❌ Shared Mutable State
// ❌ Bad: Class-level state shared across tests
public class MoodServiceShould
{
private List<Mood> _testMoods = new(); // ❌ Shared state
[Fact]
public void FirstTest()
{
_testMoods.Add(new Mood { Value = 5 });
// Now state depends on test execution order!
}
[Fact]
public void SecondTest()
{
// _testMoods might already have items from FirstTest
_testMoods.Add(new Mood { Value = 7 });
}
}
Fix: Create fresh state per test
// ✅ Good: Fresh state per test
[Fact]
public void FirstTest()
{
var testMoods = new List<Mood> { new Mood { Value = 5 } };
// Fresh state, no coupling
}
[Fact]
public void SecondTest()
{
var testMoods = new List<Mood> { new Mood { Value = 7 } };
// Completely independent
}
❌ Test Organization by Test Type
// ❌ Bad: Organization by "type of test"
WorkMood.MauiApp.Tests/
├── UnitTests/
│ ├── LineGraphServiceTests.cs
│ ├── MoodDataServiceTests.cs
├── IntegrationTests/
│ ├── GraphVisualizationTests.cs
└── EdgeCaseTests/
├── NullHandlingTests.cs
Problem: Doesn't match source structure; hard to correlate tests with components
Fix: Mirror source structure
// ✅ Good: Organization mirrors source
WorkMood.MauiApp.Tests/
├── Services/
│ ├── LineGraphServiceShould.cs
│ ├── MoodDataServiceShould.cs
├── ViewModels/
│ └── DashboardViewModelShould.cs
❌ Over-Complex Test Data Builders
// ❌ Bad: Builders that do too much
var mood = MoodBuilder
.Create()
.WithValue(5)
.WithDate(DateTime.Now)
.WithNotes("test")
.AndColorCode(MoodColor.Neutral)
.AndBehaviorFlag(BehaviorFlag.Reflective)
.AndWeatherContext(Weather.Rainy)
.AndLocation("home")
.AndCompany(Company.Alone)
.WithPreviousMood(new Mood { Value = 7 })
.AndPredictedNextMood(new Mood { Value = 6 })
.Build();
Problem: Builder complexity obscures what the test actually needs
Fix: Keep builders focused; use targeted methods
// ✅ Better: Clear, focused builder
var mood = MoodBuilder.CreateWithValue(5);
// Or even simpler when not needed:
var mood = new Mood { Value = 5 };
Quick Checklist: Is My Test Suite Well-Organized?
- ✅ Test files mirror source structure (Services tests in Services/, Models tests in Models/)
- ✅ Test files named
[Component]Should.cs - ✅ Test methods named
[Method]_[Condition]_Should[Behavior] - ✅ Related tests grouped in regions (Constructor, main method, edge cases)
- ✅ Each test class has a summary comment explaining purpose
- ✅ Common setup extracted to TestHelpers/ or fixtures
- ✅ Magic values centralized in TestConstants.cs
- ✅ Test data builders for complex, reused scenarios
- ✅ No shared mutable state between tests
- ✅ Test file under 400 lines (consider nested classes if larger)
Next Steps with Organization Foundation
Once your tests are well-organized:
- Use
/tddskill for writing new tests with Red-Green-Refactor - Use
/refactoringskill to refactor production code with test safety - Use
/code-smellsskill to identify issues test structure reveals - Run test suite regularly — organized tests are easier to maintain and extend
Remember: Organization is maintenance. As your test suite grows, good organization prevents it from becoming a liability rather than an asset. 🤖
Converted and distributed by TomeVault — claim your Tome and manage your conversions.