# xUnit

This guide provides complete examples for testing FlexBase applications using xUnit, FluentAssertions, and InMemory EF Core.

## Prerequisites

### NuGet Packages

Add these packages to your test projects:

```xml
<ItemGroup>
  <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.x" />
  <PackageReference Include="xunit" Version="2.x" />
  <PackageReference Include="xunit.runner.visualstudio" Version="2.x" />
  <PackageReference Include="FluentAssertions" Version="6.x" />
  <PackageReference Include="Moq" Version="4.x" />
  <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.x" />
</ItemGroup>
```

***

## Part 1: Domain Model Tests

Domain model tests verify pure business logic without any database dependencies. They use `FlexHostMock` to create domain model instances with proper dependency injection.

### Setting Up FlexHostMockFixture

Create a shared fixture to configure `FlexHostMock` with AutoMapper and domain model registrations:

```csharp
using AutoMapper;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Sumeru.Flex;
using YourApp.ProductsModule;
using Xunit;

namespace YourApp.DomainModels.Tests.Fixtures;

/// <summary>
/// Shared fixture that configures FlexHostMock with AutoMapper and domain model registrations.
/// </summary>
public class FlexHostMockFixture : IDisposable
{
    public IFlexHost FlexHost { get; }
    public IServiceProvider ServiceProvider { get; }

    public FlexHostMockFixture()
    {
        var services = new ServiceCollection();

        // Register AutoMapper with all mapper profiles
        services.AddAutoMapper(cfg =>
        {
            cfg.AddProfile<CreateProductMapperConfiguration>();
            cfg.AddProfile<UpdateProductMapperConfiguration>();
            cfg.AddProfile<DeleteProductMapperConfiguration>();
        }, typeof(Product).Assembly, typeof(CreateProductMapperConfiguration).Assembly);

        // Register loggers using NullLoggerFactory for tests
        services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
        services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>));

        // Register domain models
        services.AddTransient<Product>(sp =>
        {
            var logger = sp.GetRequiredService<ILogger<Product>>();
            return new Product(logger);
        });

        // Build the service provider
        ServiceProvider = services.BuildServiceProvider();

        // Create FlexHostMock and configure it
        var flexHostMock = new FlexHostMock();
        flexHostMock.SetServiceCollection(services);
        flexHostMock.SetServiceProvider(ServiceProvider);
        FlexHost = flexHostMock;
    }

    public void Dispose()
    {
        if (ServiceProvider is IDisposable disposable)
        {
            disposable.Dispose();
        }
    }
}

/// <summary>
/// Collection definition for sharing FlexHostMockFixture across test classes.
/// </summary>
[CollectionDefinition("FlexHostMock")]
public class FlexHostMockCollection : ICollectionFixture<FlexHostMockFixture>
{
}
```

### Writing Domain Model Tests

```csharp
using FluentAssertions;
using Sumeru.Flex;
using YourApp.ProductsModule;
using YourApp.DomainModels.Tests.Fixtures;
using Xunit;

namespace YourApp.DomainModels.Tests.Products;

/// <summary>
/// Unit tests for the Product domain model.
/// Tests CreateProduct, UpdateProduct, and DeleteProduct operations.
/// </summary>
[Collection("FlexHostMock")]
public class ProductDomainModelTests
{
    private readonly FlexHostMockFixture _fixture;

    public ProductDomainModelTests(FlexHostMockFixture fixture)
    {
        _fixture = fixture;
    }

    #region CreateProduct Tests

    [Fact]
    public void CreateProduct_WithValidCommand_ShouldSetAllProperties()
    {
        // Arrange
        var product = _fixture.FlexHost.GetDomainModel<Product>();
        var dto = new CreateProductDto
        {
            Name = "Test Product",
            Slug = "test-product",
            ProductTypeId = "pt-001"
        };
        var command = new CreateProductCommand { Dto = dto };

        // Act
        var result = product.CreateProduct(command);

        // Assert
        result.Should().NotBeNull();
        result.Should().BeSameAs(product);
        result.Name.Should().Be(dto.Name);
        result.Slug.Should().Be(dto.Slug);
        result.ProductTypeId.Should().Be(dto.ProductTypeId);
    }

    [Fact]
    public void CreateProduct_WithOptionalFields_ShouldMapCorrectly()
    {
        // Arrange
        var product = _fixture.FlexHost.GetDomainModel<Product>();
        var dto = new CreateProductDto
        {
            Name = "Test Product",
            Slug = "test-product",
            ProductTypeId = "pt-001",
            Description = "Test description in EditorJS format",
            DescriptionPlaintext = "Test description plain text",
            CategoryId = "cat-123",
            Weight = 1.5m,
            ExternalReference = "ext-ref-001"
        };
        var command = new CreateProductCommand { Dto = dto };

        // Act
        var result = product.CreateProduct(command);

        // Assert
        result.Description.Should().Be(dto.Description);
        result.DescriptionPlaintext.Should().Be(dto.DescriptionPlaintext);
        result.CategoryId.Should().Be(dto.CategoryId);
        result.Weight.Should().Be(dto.Weight);
        result.ExternalReference.Should().Be(dto.ExternalReference);
    }

    [Fact]
    public void CreateProduct_WithNullCommand_ShouldThrowGuardException()
    {
        // Arrange
        var product = _fixture.FlexHost.GetDomainModel<Product>();

        // Act
        Action act = () => product.CreateProduct(null!);

        // Assert
        act.Should().Throw<Exception>()
           .WithMessage("*command cannot be empty*");
    }

    [Fact]
    public void CreateProduct_ShouldSetCreatedByFromAppContext()
    {
        // Arrange
        var product = _fixture.FlexHost.GetDomainModel<Product>();
        var dto = new CreateProductDto
        {
            Name = "Test Product",
            Slug = "test-product",
            ProductTypeId = "pt-001"
        };
        
        var userId = "user-123";
        var appContext = new FlexAppContextBridge { UserId = userId };
        dto.SetAppContext(appContext);
        
        var command = new CreateProductCommand { Dto = dto };

        // Act
        var result = product.CreateProduct(command);

        // Assert
        result.CreatedBy.Should().Be(userId);
        result.LastModifiedBy.Should().Be(userId);
    }

    #endregion

    #region UpdateProduct Tests

    [Fact]
    public void UpdateProduct_WithValidCommand_ShouldUpdateProperties()
    {
        // Arrange
        var product = _fixture.FlexHost.GetDomainModel<Product>();
        
        // First create the product
        var createDto = new CreateProductDto
        {
            Name = "Original Name",
            Slug = "original-slug",
            ProductTypeId = "pt-001"
        };
        createDto.SetAppContext(new FlexAppContextBridge { UserId = "creator" });
        product.CreateProduct(new CreateProductCommand { Dto = createDto });

        // Now update it
        var updateDto = new UpdateProductDto
        {
            Id = product.Id,
            Name = "Updated Name",
            Slug = "updated-slug"
        };
        updateDto.SetAppContext(new FlexAppContextBridge { UserId = "updater" });
        var updateCommand = new UpdateProductCommand { Dto = updateDto };

        // Act
        var result = product.UpdateProduct(updateCommand);

        // Assert
        result.Name.Should().Be("Updated Name");
        result.Slug.Should().Be("updated-slug");
        result.LastModifiedBy.Should().Be("updater");
    }

    #endregion
}
```

***

## Part 2: PreBus Validation Tests

PreBus validation tests verify your business rule plugins. These tests use `InMemoryRepoFactory` to provide a real (but in-memory) database with seed data.

### Setting Up InMemoryRepoFactory

Create an InMemory repository factory that uses EF Core's InMemory provider:

```csharp
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Sumeru.Flex;

namespace YourApp.Testing;

/// <summary>
/// InMemory RepoFactory for testing purposes.
/// Replaces the standard RepoFactory to use InMemory EF Core repositories.
/// </summary>
public class InMemoryRepoFactory : IRepoFactory, IDisposable
{
    private bool _disposed = false;
    private IFlexRepositoryBridge? _repo;
    private readonly string _databaseName;
    private readonly ILoggerFactory _loggerFactory;

    public InMemoryRepoFactory(string? databaseName = null, ILoggerFactory? loggerFactory = null)
    {
        // Use unique database name per test to ensure isolation
        _databaseName = databaseName ?? $"TestDb_{Guid.NewGuid():N}";
        _loggerFactory = loggerFactory ?? NullLoggerFactory.Instance;
    }

    public IRepoFactory Init(DtoBridge dto)
    {
        InitializeRepo(ConnectionType.WriteDb);
        return this;
    }

    public IRepoFactory Init(PagedQueryParamsDtoBridge dto)
    {
        InitializeRepo(ConnectionType.ReadDb);
        return this;
    }

    private void InitializeRepo(ConnectionType type)
    {
        var connectionProvider = new InMemoryConnectionProvider(_databaseName);

        if (type == ConnectionType.ReadDb)
        {
            var logger = _loggerFactory.CreateLogger<FlexReadDbRepositoryEFInMemory>();
            var repo = new FlexReadDbRepositoryEFInMemory(logger, _databaseName);
            repo.InitializeConnection(connectionProvider);
            _repo = (IFlexRepositoryBridge)repo;
        }
        else
        {
            var logger = _loggerFactory.CreateLogger<FlexWriteDbRepositoryEFInMemory>();
            var repo = new FlexWriteDbRepositoryEFInMemory(logger, _databaseName);
            repo.InitializeConnection(connectionProvider);
            _repo = repo;
        }
    }

    public IFlexRepositoryBridge GetRepo() => _repo!;

    public void Dispose()
    {
        _disposed = true;
        // InMemory database is automatically cleaned up when all references are released
    }
}
```

### Setting Up Seed Data

Create seed data classes that are applied when the InMemory database is created:

```csharp
using Microsoft.EntityFrameworkCore;

namespace YourApp.Testing.SeedData;

public static class Category_SeedData
{
    public static void Seed(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Category>().HasData(
            new Category
            {
                Id = "cat-electronics",
                Name = "Electronics",
                Slug = "electronics",
                Lft = 1,
                Rght = 6,
                TreeId = 1,
                Level = 0
            },
            new Category
            {
                Id = "cat-computers",
                Name = "Computers",
                Slug = "computers",
                ParentId = "cat-electronics",
                Lft = 2,
                Rght = 3,
                TreeId = 1,
                Level = 1
            }
        );
    }
}

public static class ProductType_SeedData
{
    public static void Seed(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<ProductType>().HasData(
            new ProductType
            {
                Id = "pt-physical",
                Name = "Physical Product",
                Slug = "physical-product",
                Kind = "NORMAL",
                HasVariants = true,
                IsShippingRequired = true,
                IsDigital = false
            },
            new ProductType
            {
                Id = "pt-electronics",
                Name = "Electronics",
                Slug = "electronics",
                Kind = "NORMAL"
            }
        );
    }
}

public static class Product_SeedData
{
    public static void Seed(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Product>().HasData(
            new Product
            {
                Id = "prod-001",
                Name = "MacBook Pro 16-inch M3 Max",
                Slug = "macbook-pro-16-m3-max",
                ProductTypeId = "pt-electronics",
                CategoryId = "cat-computers",
                Description = "{\"blocks\":[{\"type\":\"paragraph\",\"data\":{\"text\":\"Powerful laptop\"}}]}",
                DescriptionPlaintext = "Powerful laptop with M3 Max chip",
                Weight = 2.14m,
                Rating = 4.8m
            }
        );
    }
}
```

### Writing Validation Tests

```csharp
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Moq;
using Sumeru.Flex;
using YourApp.ProductsModule;
using YourApp.ProductsModule.CreateProductProductsPlugins;
using YourApp.Testing;
using Xunit;

namespace YourApp.PreBus.Tests.Products.Validations;

/// <summary>
/// Unit tests for CategoryExists validation plugin.
/// Tests that the CategoryId references an existing category.
/// Uses seed data which includes "cat-electronics".
/// </summary>
public class CategoryExistsValidationTests : IDisposable
{
    private readonly string _databaseName;
    private readonly Mock<ILoggerFactory> _mockLoggerFactory;
    private readonly Mock<ILogger<CategoryExists>> _mockLogger;
    private readonly InMemoryRepoFactory _repoFactory;

    public CategoryExistsValidationTests()
    {
        // Use unique database name per test class for isolation
        _databaseName = $"CategoryExistsTest_{Guid.NewGuid():N}";
        _mockLoggerFactory = new Mock<ILoggerFactory>();
        _mockLogger = new Mock<ILogger<CategoryExists>>();
        _mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny<string>()))
            .Returns(Mock.Of<ILogger>());
        _repoFactory = new InMemoryRepoFactory(_databaseName, _mockLoggerFactory.Object);
    }

    public void Dispose()
    {
        _repoFactory?.Dispose();
    }

    private CreateProductDataPacket CreateDataPacket(string? categoryId)
    {
        var dto = new CreateProductDto
        {
            Name = "Test Product",
            Slug = $"test-product-{Guid.NewGuid():N}",
            ProductTypeId = "pt-physical",
            CategoryId = categoryId
        };
        dto.SetAppContext(new FlexAppContextBridge { UserId = "test-user" });

        var mockPacketLogger = new Mock<ILogger<CreateProductDataPacket>>();
        var packet = new CreateProductDataPacket(mockPacketLogger.Object);
        packet.Dto = dto;
        return packet;
    }

    [Fact]
    public async Task Validate_WithExistingCategory_ShouldPass()
    {
        // Arrange - Use category from seed data
        var validation = new CategoryExists(_mockLogger.Object, _repoFactory);
        var packet = CreateDataPacket("cat-electronics");

        // Act
        await validation.Validate(packet);

        // Assert
        packet.HasError.Should().BeFalse();
    }

    [Fact]
    public async Task Validate_WithNonExistentCategory_ShouldAddError()
    {
        // Arrange
        var validation = new CategoryExists(_mockLogger.Object, _repoFactory);
        var packet = CreateDataPacket("non-existent-category-id");

        // Act
        await validation.Validate(packet);

        // Assert
        packet.HasError.Should().BeTrue();
        packet.Errors().Should().ContainKey("CategoryId");
    }

    [Fact]
    public async Task Validate_WithNullCategoryId_ShouldPass()
    {
        // Arrange - Category is optional
        var validation = new CategoryExists(_mockLogger.Object, _repoFactory);
        var packet = CreateDataPacket(null);

        // Act
        await validation.Validate(packet);

        // Assert - Null category is valid (it's optional)
        packet.HasError.Should().BeFalse();
    }

    [Fact]
    public async Task Validate_WithEmptyCategoryId_ShouldPass()
    {
        // Arrange - Empty string should be treated as no category
        var validation = new CategoryExists(_mockLogger.Object, _repoFactory);
        var packet = CreateDataPacket("");

        // Act
        await validation.Validate(packet);

        // Assert - Empty category is valid (it's optional)
        packet.HasError.Should().BeFalse();
    }

    [Fact]
    public async Task Validate_WithWhitespaceCategoryId_ShouldPass()
    {
        // Arrange
        var validation = new CategoryExists(_mockLogger.Object, _repoFactory);
        var packet = CreateDataPacket("   ");

        // Act
        await validation.Validate(packet);

        // Assert - Whitespace should be treated as empty
        packet.HasError.Should().BeFalse();
    }
}
```

### Testing SlugUnique Validation

```csharp
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Moq;
using Sumeru.Flex;
using YourApp.ProductsModule;
using YourApp.ProductsModule.CreateProductProductsPlugins;
using YourApp.Testing;
using Xunit;

namespace YourApp.PreBus.Tests.Products.Validations;

/// <summary>
/// Unit tests for SlugUnique validation plugin.
/// Ensures product slugs are unique across the catalog.
/// Uses seed data which includes product with slug "macbook-pro-16-m3-max".
/// </summary>
public class SlugUniqueValidationTests : IDisposable
{
    private readonly string _databaseName;
    private readonly Mock<ILoggerFactory> _mockLoggerFactory;
    private readonly Mock<ILogger<SlugUnique>> _mockLogger;
    private readonly InMemoryRepoFactory _repoFactory;

    public SlugUniqueValidationTests()
    {
        _databaseName = $"SlugUniqueTest_{Guid.NewGuid():N}";
        _mockLoggerFactory = new Mock<ILoggerFactory>();
        _mockLogger = new Mock<ILogger<SlugUnique>>();
        _mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny<string>()))
            .Returns(Mock.Of<ILogger>());
        _repoFactory = new InMemoryRepoFactory(_databaseName, _mockLoggerFactory.Object);
    }

    public void Dispose() => _repoFactory?.Dispose();

    private CreateProductDataPacket CreateDataPacket(string slug)
    {
        var dto = new CreateProductDto
        {
            Name = "Test Product",
            Slug = slug,
            ProductTypeId = "pt-physical"
        };
        dto.SetAppContext(new FlexAppContextBridge { UserId = "test-user" });

        var mockPacketLogger = new Mock<ILogger<CreateProductDataPacket>>();
        var packet = new CreateProductDataPacket(mockPacketLogger.Object);
        packet.Dto = dto;
        return packet;
    }

    [Fact]
    public async Task Validate_WithUniqueSlug_ShouldPass()
    {
        // Arrange
        var validation = new SlugUnique(_mockLogger.Object, _repoFactory);
        var packet = CreateDataPacket($"unique-slug-{Guid.NewGuid():N}");

        // Act
        await validation.Validate(packet);

        // Assert
        packet.HasError.Should().BeFalse();
    }

    [Fact]
    public async Task Validate_WithExistingSlug_ShouldAddError()
    {
        // Arrange - Use slug from seed data
        var validation = new SlugUnique(_mockLogger.Object, _repoFactory);
        var packet = CreateDataPacket("macbook-pro-16-m3-max");

        // Act
        await validation.Validate(packet);

        // Assert
        packet.HasError.Should().BeTrue();
        packet.Errors().Should().ContainKey("Slug");
    }

    [Fact]
    public async Task Validate_WithNullSlug_ShouldPass()
    {
        // Arrange - Null/empty slug will be auto-generated later
        var validation = new SlugUnique(_mockLogger.Object, _repoFactory);
        var packet = CreateDataPacket(null!);

        // Act
        await validation.Validate(packet);

        // Assert
        packet.HasError.Should().BeFalse();
    }
}
```

***

## Part 3: Query Tests

Query tests verify that your read operations correctly fetch data from the database. They use InMemory EF Core with seed data.

### Setting Up QueryTestFixture

```csharp
using AutoMapper;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Sumeru.Flex;
using YourApp.ProductsModule;
using YourApp.Testing;
using Xunit;

namespace YourApp.Queries.Tests.Fixtures;

/// <summary>
/// Shared fixture for Query tests with FlexHostMock and InMemory repository.
/// </summary>
public class QueryTestFixture : IDisposable
{
    public IFlexHost FlexHost { get; }
    public IServiceProvider ServiceProvider { get; }
    public string DatabaseName { get; }

    public QueryTestFixture()
    {
        // Use unique database name per fixture instance
        DatabaseName = $"QueryTestDb_{Guid.NewGuid():N}";
        
        var services = new ServiceCollection();

        // Register AutoMapper
        services.AddAutoMapper(cfg =>
        {
            cfg.AddProfile<CreateProductMapperConfiguration>();
            cfg.AddProfile<UpdateProductMapperConfiguration>();
        }, typeof(Product).Assembly);

        // Register logging
        services.AddLogging();

        // Register domain models
        services.AddTransient<Product>(sp =>
        {
            var logger = sp.GetRequiredService<ILogger<Product>>();
            return new Product(logger);
        });

        // Register InMemory RepoFactory
        services.AddScoped<IRepoFactory>(sp => 
            new InMemoryRepoFactory(DatabaseName, sp.GetRequiredService<ILoggerFactory>()));

        ServiceProvider = services.BuildServiceProvider();

        // Configure FlexContainer (required by some FlexBase internals)
        FlexContainer.ServiceCollection = services;
        FlexContainer.ServiceProvider = ServiceProvider;

        // Create FlexHostMock
        var flexHostMock = new FlexHostMock();
        flexHostMock.SetServiceCollection(services);
        flexHostMock.SetServiceProvider(ServiceProvider);
        FlexHost = flexHostMock;
    }

    /// <summary>
    /// Creates a new InMemoryRepoFactory with the same database name.
    /// </summary>
    public IRepoFactory CreateRepoFactory()
    {
        return new InMemoryRepoFactory(DatabaseName, ServiceProvider.GetRequiredService<ILoggerFactory>());
    }

    /// <summary>
    /// Creates a GetProductById query for testing.
    /// </summary>
    public GetProductById CreateGetProductByIdQuery()
    {
        var logger = ServiceProvider.GetRequiredService<ILogger<GetProductById>>();
        var repoFactory = CreateRepoFactory();
        return new GetProductById(logger, repoFactory);
    }

    /// <summary>
    /// Seeds a product into the database for testing.
    /// </summary>
    public async Task<Product> SeedProductAsync(string? id = null, string? name = null)
    {
        var product = FlexHost.GetDomainModel<Product>();
        var dto = new CreateProductDto
        {
            Name = name ?? $"Seeded Product {Guid.NewGuid():N}",
            Slug = $"seeded-product-{Guid.NewGuid():N}",
            ProductTypeId = "pt-001"
        };
        
        var appContext = new FlexAppContextBridge { UserId = "seed-user" };
        dto.SetAppContext(appContext);
        dto.SetGeneratedId(id ?? $"prod-{Guid.NewGuid():N}");
        
        var command = new CreateProductCommand { Dto = dto };
        product.CreateProduct(command);

        var repoFactory = CreateRepoFactory();
        repoFactory.Init(dto);
        repoFactory.GetRepo().InsertOrUpdate(product);
        await repoFactory.GetRepo().SaveAsync();

        return product;
    }

    public void Dispose()
    {
        if (ServiceProvider is IDisposable disposable)
        {
            disposable.Dispose();
        }
    }
}

[CollectionDefinition("QueryTests")]
public class QueryTestCollection : ICollectionFixture<QueryTestFixture>
{
}
```

### Writing Query Tests

```csharp
using FluentAssertions;
using Sumeru.Flex;
using YourApp.ProductsModule;
using YourApp.Queries.Tests.Fixtures;
using Xunit;

namespace YourApp.Queries.Tests.Products;

/// <summary>
/// Unit tests for GetProductById query.
/// Tests retrieval from InMemory EF Core database.
/// </summary>
[Collection("QueryTests")]
public class GetProductByIdTests
{
    private readonly QueryTestFixture _fixture;

    public GetProductByIdTests(QueryTestFixture fixture)
    {
        _fixture = fixture;
    }

    [Fact]
    public async Task Fetch_WithSeedDataProductId_ShouldReturnProductFromDatabase()
    {
        // Arrange - Use product from seed data
        // This proves data is being fetched from InMemory database
        var query = _fixture.CreateGetProductByIdQuery();
        var appContext = new FlexAppContextBridge { UserId = "test-user" };
        var @params = new GetProductByIdParams { Id = "prod-001" };
        @params.SetAppContext(appContext);
        
        query.AssignParameters(@params);

        // Act
        var result = await query.Fetch();

        // Assert - Verify actual seed data values
        result.Should().NotBeNull("product 'prod-001' should exist in seed data");
        result.Id.Should().Be("prod-001");
        result.Name.Should().Be("MacBook Pro 16-inch M3 Max");
        result.Slug.Should().Be("macbook-pro-16-m3-max");
        result.ProductTypeId.Should().Be("pt-electronics");
        result.CategoryId.Should().Be("cat-computers");
    }

    [Fact]
    public async Task Fetch_WithDynamicallySeedProduct_ShouldReturnProduct()
    {
        // Arrange
        var productId = $"prod-query-test-{Guid.NewGuid():N}";
        var productName = "Query Test Product";
        await _fixture.SeedProductAsync(productId, productName);
        
        var query = _fixture.CreateGetProductByIdQuery();
        var appContext = new FlexAppContextBridge { UserId = "test-user" };
        var @params = new GetProductByIdParams { Id = productId };
        @params.SetAppContext(appContext);
        
        query.AssignParameters(@params);

        // Act
        var result = await query.Fetch();

        // Assert
        result.Should().NotBeNull();
        result.Id.Should().Be(productId);
        result.Name.Should().Be(productName);
    }

    [Fact]
    public async Task Fetch_WithNonExistingProductId_ShouldReturnNull()
    {
        // Arrange
        var query = _fixture.CreateGetProductByIdQuery();
        var appContext = new FlexAppContextBridge { UserId = "test-user" };
        var @params = new GetProductByIdParams { Id = "non-existent-product-id" };
        @params.SetAppContext(appContext);
        
        query.AssignParameters(@params);

        // Act
        var result = await query.Fetch();

        // Assert
        result.Should().BeNull();
    }

    [Fact]
    public async Task Fetch_ShouldMapAllProductProperties()
    {
        // Arrange - Create product with all properties
        var productId = $"prod-full-props-{Guid.NewGuid():N}";
        
        var product = _fixture.FlexHost.GetDomainModel<Product>();
        var dto = new CreateProductDto
        {
            Name = "Full Properties Product",
            Slug = $"full-properties-{Guid.NewGuid():N}",
            ProductTypeId = "pt-001",
            CategoryId = "cat-electronics",
            Description = "Rich text description",
            DescriptionPlaintext = "Plain text description",
            Weight = 3.5m,
            ExternalReference = "ext-ref-test"
        };
        
        var appContext = new FlexAppContextBridge { UserId = "test-user" };
        dto.SetAppContext(appContext);
        dto.SetGeneratedId(productId);
        
        product.CreateProduct(new CreateProductCommand { Dto = dto });
        
        var repoFactory = _fixture.CreateRepoFactory();
        repoFactory.Init(dto);
        repoFactory.GetRepo().InsertOrUpdate(product);
        await repoFactory.GetRepo().SaveAsync();
        
        var query = _fixture.CreateGetProductByIdQuery();
        var queryParams = new GetProductByIdParams { Id = productId };
        queryParams.SetAppContext(appContext);
        query.AssignParameters(queryParams);

        // Act
        var result = await query.Fetch();

        // Assert
        result.Should().NotBeNull();
        result.Name.Should().Be("Full Properties Product");
        result.CategoryId.Should().Be("cat-electronics");
        result.Description.Should().Be("Rich text description");
        result.Weight.Should().Be(3.5m);
        result.ExternalReference.Should().Be("ext-ref-test");
    }
}
```

***

## Part 4: Handler Tests (Optional)

Handler tests verify the orchestration logic in command handlers. These are optional because domain model + validation tests often provide sufficient coverage.

```csharp
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Moq;
using Sumeru.Flex;
using YourApp.ProductsModule;
using YourApp.Testing;
using Xunit;

namespace YourApp.Handlers.Tests.Products;

/// <summary>
/// Unit tests for CreateProductHandler.
/// Tests command execution with mocked service bus context.
/// </summary>
public class CreateProductHandlerTests : IDisposable
{
    private readonly string _databaseName;
    private readonly Mock<ILogger<CreateProductHandler>> _mockLogger;
    private readonly Mock<ILoggerFactory> _mockLoggerFactory;
    private readonly InMemoryRepoFactory _repoFactory;
    private readonly IFlexHost _flexHost;

    public CreateProductHandlerTests()
    {
        _databaseName = $"HandlerTest_{Guid.NewGuid():N}";
        _mockLogger = new Mock<ILogger<CreateProductHandler>>();
        _mockLoggerFactory = new Mock<ILoggerFactory>();
        _mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny<string>()))
            .Returns(Mock.Of<ILogger>());
        _repoFactory = new InMemoryRepoFactory(_databaseName, _mockLoggerFactory.Object);

        // Setup FlexHost
        var services = new ServiceCollection();
        services.AddLogging();
        services.AddAutoMapper(cfg =>
        {
            cfg.AddProfile<CreateProductMapperConfiguration>();
        });
        services.AddTransient<Product>(sp =>
        {
            var logger = sp.GetRequiredService<ILogger<Product>>();
            return new Product(logger);
        });
        
        var serviceProvider = services.BuildServiceProvider();
        var flexHostMock = new FlexHostMock();
        flexHostMock.SetServiceCollection(services);
        flexHostMock.SetServiceProvider(serviceProvider);
        _flexHost = flexHostMock;
    }

    public void Dispose() => _repoFactory?.Dispose();

    [Fact]
    public async Task Execute_WithValidCommand_ShouldPersistProduct()
    {
        // Arrange
        var handler = new CreateProductHandler(_mockLogger.Object, _flexHost, _repoFactory);
        var mockContext = new Mock<IFlexServiceBusContext>();
        
        var dto = new CreateProductDto
        {
            Name = "Handler Test Product",
            Slug = $"handler-test-{Guid.NewGuid():N}",
            ProductTypeId = "pt-001"
        };
        var appContext = new FlexAppContextBridge { UserId = "test-user" };
        dto.SetAppContext(appContext);
        dto.SetGeneratedId($"prod-handler-{Guid.NewGuid():N}");
        
        var command = new CreateProductCommand { Dto = dto };

        // Act
        await handler.Execute(command, mockContext.Object);

        // Assert - Verify product was persisted
        var repoFactory = new InMemoryRepoFactory(_databaseName, _mockLoggerFactory.Object);
        repoFactory.Init(dto);
        var savedProduct = repoFactory.GetRepo()
            .FindAll<Product>()
            .FirstOrDefault(p => p.Id == dto.GetGeneratedId());
        
        savedProduct.Should().NotBeNull();
        savedProduct!.Name.Should().Be("Handler Test Product");
    }
}
```

***

## Best Practices Summary

### ✅ Do

1. **Use unique database names** per test class:

   ```csharp
   _databaseName = $"TestName_{Guid.NewGuid():N}";
   ```
2. **Implement IDisposable** to clean up resources:

   ```csharp
   public void Dispose() => _repoFactory?.Dispose();
   ```
3. **Use xUnit collection fixtures** for expensive setup:

   ```csharp
   [Collection("FlexHostMock")]
   public class MyTests
   ```
4. **Leverage seed data** for realistic test scenarios:

   ```csharp
   var packet = CreateDataPacket("cat-electronics"); // Known seed data
   ```
5. **Use FluentAssertions** for readable assertions:

   ```csharp
   result.Should().NotBeNull();
   result.Name.Should().Be("Expected Name");
   ```
6. **Test edge cases** (null, empty, whitespace):

   ```csharp
   [Fact] public async Task Validate_WithNullValue_ShouldPass() { }
   [Fact] public async Task Validate_WithEmptyValue_ShouldPass() { }
   ```

### ❌ Don't

1. **Don't share mutable state** between tests without isolation
2. **Don't rely on test execution order**
3. **Don't use magic strings** - define constants or use seed data
4. **Don't test FlexBase internals** - focus on your business logic
5. **Don't over-mock** - use real InMemory database when possible

***

## Troubleshooting

### Common Issues

**Issue:** Tests fail with "Entity type 'X' was not found" **Solution:** Ensure your `ApplicationEFInMemoryDbContext` includes all entity DbSets

**Issue:** Seed data not found during tests **Solution:** Verify `OnModelCreating` calls your seed data methods

**Issue:** Tests interfere with each other **Solution:** Use unique database names with `Guid.NewGuid()`

**Issue:** AutoMapper configuration errors **Solution:** Register all required mapper profiles in the fixture

***

## Reference: FlexBase Testing APIs

| API                            | Purpose                                  |
| ------------------------------ | ---------------------------------------- |
| `FlexHostMock`                 | Test-friendly `IFlexHost` implementation |
| `flexHost.GetDomainModel<T>()` | Gets domain model instance with DI       |
| `packet.Dto = dto`             | Assigns DTO to data packet               |
| `packet.HasError`              | Boolean - check if validation failed     |
| `packet.Errors()`              | Returns `FlexErrorDictionary`            |
| `packet.AddError(key, msg)`    | Adds validation error                    |
| `dto.SetAppContext(ctx)`       | Sets application context                 |
| `dto.SetGeneratedId(id)`       | Sets pre-generated entity ID             |
| `dto.GetAppContext()`          | Gets application context                 |
| `dto.GetGeneratedId()`         | Gets pre-generated entity ID             |


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.flexbase.in/solution-structure/getting-started/unit-testing/xunit.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
