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:
Writing Domain Model Tests
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:
Setting Up Seed Data
Create seed data classes that are applied when the InMemory database is created:
Writing Validation Tests
Testing SlugUnique Validation
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
Writing Query Tests
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.
Best Practices Summary
✅ Do
Use unique database names per test class:
Implement IDisposable to clean up resources:
Use xUnit collection fixtures for expensive setup:
Leverage seed data for realistic test scenarios:
Use FluentAssertions for readable assertions:
Test edge cases (null, empty, whitespace):
❌ Don't
Don't share mutable state between tests without isolation
Don't rely on test execution order
Don't use magic strings - define constants or use seed data
Don't test FlexBase internals - focus on your business logic
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
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>
{
}
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
}
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
}
}
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
}
);
}
}
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();
}
}
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();
}
}
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>
{
}
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");
}
}
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");
}
}
_databaseName = $"TestName_{Guid.NewGuid():N}";
public void Dispose() => _repoFactory?.Dispose();
[Collection("FlexHostMock")]
public class MyTests
var packet = CreateDataPacket("cat-electronics"); // Known seed data