> For the complete documentation index, see [llms.txt](https://docs.flexbase.in/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.flexbase.in/solution-structure/getting-started/unit-testing/xunit.md).

# 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             |
