Back to list
YosrBennagra

testing

by YosrBennagra

1🍴 0📅 Jan 21, 2026

SKILL.md


name: testing description: Testing strategies for the 3SC widget host. Covers unit tests, integration tests, ViewModel testing, mocking patterns, and test organization.

Testing

Overview

A comprehensive testing strategy ensures code quality, prevents regressions, and enables confident refactoring. This skill covers testing patterns for WPF desktop applications.

Definition of Done (DoD)

  • New features have unit tests for business logic
  • ViewModels have tests for command behavior
  • Repository operations have integration tests
  • Critical paths have edge case coverage
  • Tests are independent and repeatable
  • Test names clearly describe the scenario

Test Project Structure

3SC.Domain.Tests/
├── Entities/
│   ├── WidgetTests.cs
│   └── LayoutTests.cs
└── ValueObjects/
    ├── WidgetSizeTests.cs
    └── SemanticVersionTests.cs

3SC.Infrastructure.Tests/
├── Repositories/
│   ├── WidgetRepositoryTests.cs
│   └── LayoutRepositoryTests.cs
├── Security/
│   └── SecureStorageServiceTests.cs
└── DbTestFactory.cs  # Shared test database setup

3SC.UI.Tests/
├── ViewModels/
│   ├── ShellViewModelTests.cs
│   └── WidgetLibraryViewModelTests.cs
├── Services/
│   └── NavigationServiceTests.cs
└── Converters/
    └── BoolToVisibilityConverterTests.cs

Naming Conventions

Use the pattern: MethodName_Scenario_ExpectedResult

public class WidgetTests
{
    [Fact]
    public void Constructor_WithValidParameters_CreatesWidget() { }
    
    [Fact]
    public void Constructor_WithEmptyKey_ThrowsArgumentException() { }
    
    [Fact]
    public void UpdatePosition_WithNewPosition_RaisesPropertyChanged() { }
    
    [Theory]
    [InlineData("")]
    [InlineData("   ")]
    [InlineData(null)]
    public void Validate_WithInvalidName_ReturnsFalse(string? name) { }
}

Domain Layer Testing

Domain entities should be pure and testable without mocks:

public class WidgetSizeTests
{
    [Fact]
    public void Constructor_WithValidDimensions_CreateSize()
    {
        // Arrange & Act
        var size = new WidgetSize(300, 200);
        
        // Assert
        Assert.Equal(300, size.Width);
        Assert.Equal(200, size.Height);
    }
    
    [Theory]
    [InlineData(0, 100)]
    [InlineData(100, 0)]
    [InlineData(-1, 100)]
    public void Constructor_WithInvalidDimensions_ThrowsArgumentException(int width, int height)
    {
        // Act & Assert
        Assert.Throws<ArgumentException>(() => new WidgetSize(width, height));
    }
    
    [Fact]
    public void Equals_WithSameDimensions_ReturnsTrue()
    {
        // Arrange
        var size1 = new WidgetSize(300, 200);
        var size2 = new WidgetSize(300, 200);
        
        // Assert
        Assert.Equal(size1, size2);
        Assert.True(size1 == size2);
    }
}

ViewModel Testing

Setup with Mocks

public class WidgetLibraryViewModelTests : IDisposable
{
    private readonly Mock<IWidgetRepository> _widgetRepoMock;
    private readonly Mock<IWidgetPackageInstaller> _installerMock;
    private readonly Mock<IUserNotificationService> _notificationMock;
    private readonly WidgetLibraryViewModel _viewModel;
    
    public WidgetLibraryViewModelTests()
    {
        _widgetRepoMock = new Mock<IWidgetRepository>();
        _installerMock = new Mock<IWidgetPackageInstaller>();
        _notificationMock = new Mock<IUserNotificationService>();
        
        _viewModel = new WidgetLibraryViewModel(
            _widgetRepoMock.Object,
            _installerMock.Object,
            _notificationMock.Object);
    }
    
    public void Dispose()
    {
        // Cleanup if needed
    }
}

Testing Commands

[Fact]
public async Task LoadWidgetsCommand_OnSuccess_PopulatesWidgets()
{
    // Arrange
    var widgets = new List<Widget>
    {
        new Widget("clock", "Clock", "1.0.0"),
        new Widget("weather", "Weather", "2.0.0")
    };
    _widgetRepoMock
        .Setup(r => r.GetAllAsync(It.IsAny<CancellationToken>()))
        .ReturnsAsync(widgets);
    
    // Act
    await _viewModel.LoadWidgetsCommand.ExecuteAsync(null);
    
    // Assert
    Assert.Equal(2, _viewModel.Widgets.Count);
    Assert.False(_viewModel.IsLoading);
    Assert.Null(_viewModel.ErrorMessage);
}

[Fact]
public async Task LoadWidgetsCommand_OnError_SetsErrorMessage()
{
    // Arrange
    _widgetRepoMock
        .Setup(r => r.GetAllAsync(It.IsAny<CancellationToken>()))
        .ThrowsAsync(new InvalidOperationException("Database error"));
    
    // Act
    await _viewModel.LoadWidgetsCommand.ExecuteAsync(null);
    
    // Assert
    Assert.Empty(_viewModel.Widgets);
    Assert.False(_viewModel.IsLoading);
    Assert.NotNull(_viewModel.ErrorMessage);
}

[Fact]
public async Task LoadWidgetsCommand_WhenCancelled_DoesNotSetError()
{
    // Arrange
    var cts = new CancellationTokenSource();
    cts.Cancel();
    
    _widgetRepoMock
        .Setup(r => r.GetAllAsync(It.IsAny<CancellationToken>()))
        .ThrowsAsync(new OperationCanceledException());
    
    // Act
    await _viewModel.LoadWidgetsCommand.ExecuteAsync(cts.Token);
    
    // Assert
    Assert.Null(_viewModel.ErrorMessage);
}

Testing Property Changes

[Fact]
public void SelectedWidget_WhenChanged_NotifiesPropertyChanged()
{
    // Arrange
    var widget = new WidgetViewModel(new Widget("test", "Test", "1.0.0"));
    var changedProperties = new List<string>();
    _viewModel.PropertyChanged += (_, e) => changedProperties.Add(e.PropertyName!);
    
    // Act
    _viewModel.SelectedWidget = widget;
    
    // Assert
    Assert.Contains(nameof(WidgetLibraryViewModel.SelectedWidget), changedProperties);
    Assert.Contains(nameof(WidgetLibraryViewModel.HasSelection), changedProperties);
}

Integration Testing

In-Memory Database

public class DbTestFactory
{
    public static AppDbContext CreateInMemoryContext()
    {
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseSqlite("DataSource=:memory:")
            .Options;
        
        var context = new AppDbContext(options);
        context.Database.OpenConnection();
        context.Database.EnsureCreated();
        
        return context;
    }
    
    public static AppDbContext CreateTestContext(string testName)
    {
        var dbPath = Path.Combine(
            Path.GetTempPath(), 
            $"3sc_test_{testName}_{Guid.NewGuid():N}.db");
        
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseSqlite($"Data Source={dbPath}")
            .Options;
        
        var context = new AppDbContext(options);
        context.Database.EnsureCreated();
        
        return context;
    }
}

Repository Integration Tests

public class WidgetRepositoryTests : IDisposable
{
    private readonly AppDbContext _context;
    private readonly WidgetRepository _repository;
    
    public WidgetRepositoryTests()
    {
        _context = DbTestFactory.CreateInMemoryContext();
        _repository = new WidgetRepository(_context);
    }
    
    public void Dispose()
    {
        _context.Database.CloseConnection();
        _context.Dispose();
    }
    
    [Fact]
    public async Task AddAsync_WithValidWidget_PersistsToDatabase()
    {
        // Arrange
        var widget = new Widget("test-widget", "Test Widget", "1.0.0");
        
        // Act
        await _repository.AddAsync(widget);
        await _context.SaveChangesAsync();
        
        // Assert
        var saved = await _context.Widgets.FirstOrDefaultAsync(w => w.WidgetKey == "test-widget");
        Assert.NotNull(saved);
        Assert.Equal("Test Widget", saved.DisplayName);
    }
    
    [Fact]
    public async Task GetAllAsync_WithMultipleWidgets_ReturnsAll()
    {
        // Arrange
        _context.Widgets.AddRange(
            new Widget("widget1", "Widget 1", "1.0.0"),
            new Widget("widget2", "Widget 2", "1.0.0"));
        await _context.SaveChangesAsync();
        
        // Act
        var widgets = await _repository.GetAllAsync();
        
        // Assert
        Assert.Equal(2, widgets.Count);
    }
}

Mocking Guidelines

What to Mock

MockWhy
RepositoriesAvoid database in unit tests
External APIsAvoid network calls
File systemAvoid file dependencies
Time providersControl time in tests
User dialogsAvoid UI interactions

What NOT to Mock

Don't MockWhy
Domain entitiesPure, no dependencies
Value objectsPure, no dependencies
Simple utilitiesMore complex than needed
The thing being testedDefeats the purpose

Mock Verification

[Fact]
public async Task InstallCommand_OnSuccess_ShowsNotification()
{
    // Arrange
    var package = CreateTestPackage();
    _installerMock
        .Setup(i => i.InstallAsync(package, It.IsAny<CancellationToken>()))
        .ReturnsAsync(InstallResult.Success);
    
    // Act
    _viewModel.SelectedPackage = package;
    await _viewModel.InstallCommand.ExecuteAsync(null);
    
    // Assert
    _notificationMock.Verify(
        n => n.ShowSuccess(It.Is<string>(s => s.Contains("installed"))),
        Times.Once);
}

Test Data Builders

For complex object creation:

public class WidgetBuilder
{
    private string _key = "test-widget";
    private string _displayName = "Test Widget";
    private string _version = "1.0.0";
    private bool _hasSettings = false;
    
    public WidgetBuilder WithKey(string key)
    {
        _key = key;
        return this;
    }
    
    public WidgetBuilder WithDisplayName(string name)
    {
        _displayName = name;
        return this;
    }
    
    public WidgetBuilder WithSettings()
    {
        _hasSettings = true;
        return this;
    }
    
    public Widget Build() => new(_key, _displayName, _version) { HasSettings = _hasSettings };
}

// Usage
var widget = new WidgetBuilder()
    .WithKey("clock")
    .WithDisplayName("Clock Widget")
    .WithSettings()
    .Build();

Async Testing

[Fact]
public async Task LongRunningOperation_WithCancellation_ThrowsOperationCancelledException()
{
    // Arrange
    using var cts = new CancellationTokenSource();
    cts.CancelAfter(TimeSpan.FromMilliseconds(100));
    
    // Act & Assert
    await Assert.ThrowsAsync<OperationCanceledException>(
        () => _service.LongRunningOperationAsync(cts.Token));
}

[Fact]
public async Task AsyncOperation_WithTimeout_CompletesInTime()
{
    // Arrange
    var timeout = TimeSpan.FromSeconds(5);
    
    // Act
    var task = _service.OperationAsync();
    var completedInTime = await Task.WhenAny(task, Task.Delay(timeout)) == task;
    
    // Assert
    Assert.True(completedInTime, $"Operation did not complete within {timeout}");
}

Test Organization

Arrange-Act-Assert

[Fact]
public void MethodName_Scenario_ExpectedResult()
{
    // Arrange - Set up test data and dependencies
    var input = CreateTestInput();
    
    // Act - Execute the method under test
    var result = _sut.MethodUnderTest(input);
    
    // Assert - Verify the outcome
    Assert.Equal(expected, result);
}

Theory for Parameterized Tests

[Theory]
[InlineData("1.0.0", "1.0.0", 0)]   // Equal
[InlineData("1.0.0", "1.0.1", -1)]  // Less than
[InlineData("2.0.0", "1.9.9", 1)]   // Greater than
public void CompareTo_VersionComparison_ReturnsExpected(
    string version1, 
    string version2, 
    int expected)
{
    var v1 = SemanticVersion.Parse(version1);
    var v2 = SemanticVersion.Parse(version2);
    
    var result = Math.Sign(v1.CompareTo(v2));
    
    Assert.Equal(expected, result);
}

Coverage Goals

LayerTargetFocus
Domain90%+Business rules, validation
ViewModels80%+Commands, state changes
Infrastructure70%+Happy paths, edge cases
UI/Converters60%+Edge cases, null handling

References

Score

Total Score

40/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

Reviews

💬

Reviews coming soon