# write-unit-tests ## When to Use Use this command when you need to: - Write unit tests for C# classes and methods using xUnit - Create comprehensive test coverage following best practices - Set up test projects with proper structure - Implement AAA (Arrange-Act-Assert) pattern tests - Handle mocking, stubbing, and test data management - Follow naming conventions and testing best practices ## Prerequisites - xUnit packages installed (`Xunit`, `Xunit.Runner.VisualStudio`, `Microsoft.NET.Test.Sdk`) - Test project exists or needs to be created (`.Tests` suffix convention) - Code to be tested is available and well-structured - Moq or similar mocking framework for dependencies - FluentAssertions for better assertion syntax (recommended) ## Execution Steps ### Step 1: Analyze Code to Test Examine the class/method that needs testing: **Identify:** - Class name and namespace - Public methods to test - Dependencies (interfaces, services) that need mocking - Constructor parameters - Expected behaviors and edge cases - Return types and exceptions **Check existing tests:** - Search for existing test files: `grep -r "ClassName" src/*/Tests/ --include="*.cs"` - Determine what tests are missing - Review test coverage gaps ### Step 2: Set Up Test Project Structure If test project doesn't exist, create it: **Create test project:** ```bash dotnet new xunit -n Managing.Application.Tests dotnet add Managing.Application.Tests/Managing.Application.Tests.csproj reference Managing.Application/Managing.Application.csproj ``` **Add required packages:** ```bash dotnet add Managing.Application.Tests package Xunit dotnet add Managing.Application.Tests package Xunit.Runner.VisualStudio dotnet add Managing.Application.Tests package Microsoft.NET.Test.Sdk dotnet add Managing.Application.Tests package Moq dotnet add Managing.Application.Tests package FluentAssertions dotnet add Managing.Application.Tests package AutoFixture ``` ### Step 3: Create Test Class Structure **Naming Convention:** - Test class: `[ClassName]Tests` (e.g., `TradingBotBaseTests`) - Test method: `[MethodName]_[Scenario]_[ExpectedResult]` (e.g., `Start_WithValidConfig_CallsLoadAccount`) **File Structure:** ``` src/ ├── Managing.Application.Tests/ │ ├── TradingBotBaseTests.cs │ ├── Services/ │ │ └── AccountServiceTests.cs │ └── Helpers/ │ └── TradingBoxTests.cs ``` ### Step 4: Implement Test Methods (AAA Pattern) **For each test method:** #### Arrange (Setup) - Create mock objects for dependencies - Set up test data and expected values - Configure mock behavior - Initialize system under test (SUT) #### Act (Execute) - Call the method being tested - Capture results or exceptions - Execute the behavior to test #### Assert (Verify) - Verify the expected outcome - Check return values, property changes, or exceptions - Verify interactions with mocks ### Step 5: Write Comprehensive Test Cases **Happy Path Tests:** - Test normal successful execution - Verify expected return values - Check side effects on dependencies **Edge Cases:** - Null/empty parameters - Boundary values - Invalid inputs **Error Scenarios:** - Expected exceptions - Error conditions - Failure paths **Integration Points:** - Verify correct interaction with dependencies - Test data flow through interfaces ### Step 6: Handle Mocking and Stubbing **Using Moq:** ```csharp // Arrange var mockLogger = new Mock>(); var mockScopeFactory = new Mock(); // Configure mock behavior mockLogger.Setup(x => x.LogInformation(It.IsAny())).Verifiable(); // Act var bot = new TradingBotBase(mockLogger.Object, mockScopeFactory.Object, config); // Assert mockLogger.Verify(x => x.LogInformation(It.IsAny()), Times.Once); ``` **Setup common mock configurations:** - Logger mocks (verify logging calls) - Service mocks (setup return values) - Repository mocks (setup data access) - External service mocks (simulate API responses) ### Step 7: Implement Test Data Management **Test Data Patterns:** - Inline test data for simple tests - Private methods for complex test data setup - Test data builders for reusable scenarios - Theory data for parameterized tests **Using AutoFixture:** ```csharp private readonly IFixture _fixture = new Fixture(); [Fact] public void Start_WithValidConfig_SetsPropertiesCorrectly() { // Arrange var config = _fixture.Create(); var bot = new TradingBotBase(_loggerMock.Object, _scopeFactoryMock.Object, config); // Act await bot.Start(BotStatus.Saved); // Assert bot.Config.Should().Be(config); } ``` ### Step 8: Add Proper Assertions **Using FluentAssertions:** ```csharp // Value assertions result.Should().Be(expectedValue); result.Should().BeGreaterThan(0); result.Should().NotBeNull(); // Collection assertions positions.Should().HaveCount(1); positions.Should().ContainSingle(); // Exception assertions await Assert.ThrowsAsync(() => method.CallAsync()); ``` **Common Assertion Types:** - Equality: `Should().Be()`, `Should().BeEquivalentTo()` - Null checks: `Should().NotBeNull()`, `Should().BeNull()` - Collections: `Should().HaveCount()`, `Should().Contain()` - Exceptions: `Should().Throw<>`, `Should().NotThrow()` - Types: `Should().BeOfType<>`, `Should().BeAssignableTo<>()` ### Step 9: Handle Async Testing **Async Test Methods:** ```csharp [Fact] public async Task LoadAccount_WhenCalled_LoadsAccountFromService() { // Arrange var expectedAccount = _fixture.Create(); _accountServiceMock.Setup(x => x.GetAccountByAccountNameAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(expectedAccount); // Act await _bot.LoadAccount(); // Assert _bot.Account.Should().Be(expectedAccount); } ``` **Async Exception Testing:** ```csharp [Fact] public async Task LoadAccount_WithInvalidAccountName_ThrowsArgumentException() { // Arrange _accountServiceMock.Setup(x => x.GetAccountByAccountNameAsync("InvalidName", It.IsAny(), It.IsAny())) .ThrowsAsync(new ArgumentException("Account not found")); // Act & Assert await Assert.ThrowsAsync(() => _bot.LoadAccount()); } ``` ### Step 10: Add Theory Tests for Multiple Scenarios **Parameterized Tests:** ```csharp [Theory] [InlineData(BotStatus.Saved, "🚀 Bot Started Successfully")] [InlineData(BotStatus.Stopped, "🔄 Bot Restarted")] public async Task Start_WithDifferentPreviousStatuses_LogsCorrectMessage(BotStatus previousStatus, string expectedMessage) { // Arrange _configMock.SetupGet(x => x.IsForBacktest).Returns(false); // Act await _bot.Start(previousStatus); // Assert _loggerMock.Verify(x => x.LogInformation(expectedMessage), Times.Once); } ``` ### Step 11: Implement Test Cleanup and Disposal **Test Cleanup:** ```csharp public class TradingBotBaseTests : IDisposable { private readonly MockRepository _mockRepository; public TradingBotBaseTests() { _mockRepository = new MockRepository(MockBehavior.Strict); // Setup mocks } public void Dispose() { _mockRepository.VerifyAll(); } } ``` **Reset State Between Tests:** - Clear static state - Reset mock configurations - Clean up test data ### Step 12: Run and Verify Tests **Run tests:** ```bash dotnet test src/Managing.Application.Tests/Managing.Application.Tests.csproj ``` **Check coverage:** ```bash dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura ``` **Verify test results:** - All tests pass - No unexpected exceptions - Coverage meets requirements (typically >80%) ### Step 13: Analyze Test Failures for Business Logic Issues **When tests fail unexpectedly, it may indicate business logic problems:** **Create TODO.md Analysis:** ```bash # Document test failures that reveal business logic issues # Analyze whether failures indicate bugs in implementation vs incorrect test assumptions ``` **Key Indicators of Business Logic Issues:** - Tests fail because actual behavior differs significantly from expected behavior - Core business calculations (P&L, fees, volumes) return incorrect values - Edge cases reveal fundamental logic flaws - Multiple related tests fail with similar patterns **Business Logic Failure Patterns:** - **Zero Returns**: Methods return 0 when they should return calculated values - **Null Returns**: Methods return null when valid data is provided - **Incorrect Calculations**: Mathematical results differ from expected formulas - **Validation Failures**: Valid inputs are rejected or invalid inputs are accepted **Create TODO.md when:** - ✅ Tests reveal potential bugs in business logic - ✅ Multiple tests fail with similar calculation errors - ✅ Core business metrics are not working correctly - ✅ Implementation behavior differs from business requirements **TODO.md Structure:** ```markdown # [Component] Unit Tests - Business Logic Issues Analysis ## Test Results Summary **Total Tests:** X - **Passed:** Y ✅ - **Failed:** Z ❌ ## Failed Test Categories & Potential Business Logic Issues [List specific failing tests and analyze root causes] ## Business Logic Issues Identified [Critical, Medium, Low priority issues] ## Recommended Actions [Immediate fixes, investigation steps, test updates needed] ``` ## Best Practices for Unit Testing ### Test Naming - ✅ `[MethodName]_[Scenario]_[ExpectedResult]` - ❌ `Test1`, `MethodTest`, `CheckIfWorks` ### Test Structure - ✅ One assertion per test (Single Responsibility) - ✅ Clear Arrange-Act-Assert sections - ✅ Descriptive variable names ### Mock Usage - ✅ Mock interfaces, not concrete classes - ✅ Verify important interactions - ✅ Avoid over-mocking (test behavior, not implementation) ### Test Data - ✅ Use realistic test data - ✅ Test boundary conditions - ✅ Use factories for complex objects ### Coverage Goals - ✅ Aim for >80% line coverage - ✅ Cover all public methods - ✅ Test error paths and edge cases ### Test Organization - ✅ Group related tests in classes - ✅ Use base classes for common setup - ✅ Separate integration tests from unit tests ## Common Testing Patterns ### Service Layer Testing ```csharp [Fact] public async Task GetAccountByName_WithValidName_ReturnsAccount() { // Arrange var accountName = "test-account"; var expectedAccount = new Account { Name = accountName }; _repositoryMock.Setup(x => x.GetByNameAsync(accountName)) .ReturnsAsync(expectedAccount); // Act var result = await _accountService.GetAccountByNameAsync(accountName); // Assert result.Should().Be(expectedAccount); } ``` ### Repository Testing ```csharp [Fact] public async Task SaveAsync_WithValidEntity_CallsSaveOnContext() { // Arrange var entity = _fixture.Create(); // Act await _repository.SaveAsync(entity); // Assert _contextMock.Verify(x => x.SaveChangesAsync(It.IsAny()), Times.Once); } ``` ### Validation Testing ```csharp [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] public async Task CreateAccount_WithInvalidName_ThrowsValidationException(string invalidName) { // Arrange var request = new CreateAccountRequest { Name = invalidName }; // Act & Assert await Assert.ThrowsAsync(() => _accountService.CreateAccountAsync(request)); } ``` ## Error Handling **If test project creation fails:** - Check .NET SDK installation - Verify project name follows conventions - Check for existing project conflicts **If package installation fails:** - Clear NuGet cache: `dotnet nuget locals all --clear` - Check network connectivity - Verify package names and versions **If tests fail:** - Debug individual test methods - Check mock configurations - Verify test data setup - Review assertion logic **If code changes break tests:** - Update test expectations - Modify test data if needed - Review if behavior changes are intentional ## Example Execution **User input:** Create unit tests for `TradingBotBase.Start()` method **AI execution:** 1. **Analyze code:** - `TradingBotBase` class with `Start(BotStatus)` method - Dependencies: `ILogger`, `IServiceScopeFactory` - Different behaviors based on `BotStatus` parameter 2. **Create test class:** ```csharp public class TradingBotBaseTests { private readonly Mock> _loggerMock; private readonly Mock _scopeFactoryMock; private readonly TradingBotConfig _config; public TradingBotBaseTests() { _loggerMock = new Mock>(); _scopeFactoryMock = new Mock(); _config = new TradingBotConfig { IsForBacktest = false }; } } ``` 3. **Write individual tests:** ```csharp [Fact] public async Task Start_WithSavedStatus_LoadsAccountAndLogsStartupMessage() { // Arrange var bot = new TradingBotBase(_loggerMock.Object, _scopeFactoryMock.Object, _config); // Act await bot.Start(BotStatus.Saved); // Assert _loggerMock.Verify(x => x.LogInformation(It.Is(s => s.Contains("🚀 Bot Started Successfully"))), Times.Once); } ``` 4. **Add edge cases:** ```csharp [Fact] public async Task Start_WithBacktestConfig_SkipsAccountLoading() { // Arrange _config.IsForBacktest = true; var bot = new TradingBotBase(_loggerMock.Object, _scopeFactoryMock.Object, _config); // Act await bot.Start(BotStatus.Saved); // Assert bot.Account.Should().BeNull(); } ``` 5. **Run tests and verify:** ```bash dotnet test --filter "TradingBotBaseTests" ``` ## Important Notes - ✅ **AAA Pattern**: Arrange-Act-Assert structure for clarity - ✅ **Single Responsibility**: One concept per test - ✅ **Descriptive Names**: Method_Scenario_Result naming convention - ✅ **Mock Dependencies**: Test in isolation - ✅ **Realistic Data**: Use meaningful test values - ✅ **Async Testing**: Use `async Task` for async methods - ✅ **Theory Tests**: Use `[Theory]` for multiple scenarios - ⚠️ **Avoid Over-Mocking**: Don't mock everything - ⚠️ **Integration Tests**: Separate from unit tests - 📦 **Test Packages**: Xunit, Moq - 🎯 **Coverage**: Aim for >80% coverage - 🔧 **Build Tests**: `dotnet test` command