Skip to content

Testing Strategies ​

Testing is essential for ensuring the correctness and reliability of applications using Raiqub Expressions. This guide covers strategies for testing specifications, query strategies, and database sessions.

Overview ​

Raiqub Expressions is designed to be testable through several key approaches:

  • Unit Testing - Test specifications and query strategies in isolation using in-memory collections
  • Integration Testing - Test database interactions with real or test databases
  • Query Translation Testing - Verify that specifications and query strategies translate correctly to SQL
  • Mocking - Use mocking frameworks to isolate components
  • Test Fixtures - Create reusable test data and setup

Testing Specifications ​

Specifications are highly testable because they encapsulate business rules in pure functions.

Unit Testing Specifications ​

Test specifications using the IsSatisfiedBy method with in-memory data:

csharp
public class ProductSpecificationTests
{
    [Fact]
    public void IsInStock_WithPositiveQuantity_ShouldReturnTrue()
    {
        // Arrange
        var product = new Product { AvailableQuantity = 10 };
        var specification = ProductSpecification.IsInStock;

        // Act
        var result = specification.IsSatisfiedBy(product);

        // Assert
        Assert.True(result);
    }

    [Theory]
    [InlineData(0, false)]
    [InlineData(-1, false)]
    [InlineData(1, true)]
    [InlineData(100, true)]
    public void IsInStock_VariousQuantities_ReturnsExpectedResult(
        int quantity, bool expected)
    {
        // Arrange
        var product = new Product { AvailableQuantity = quantity };
        var specification = ProductSpecification.IsInStock;

        // Act
        var result = specification.IsSatisfiedBy(product);

        // Assert
        Assert.Equal(expected, result);
    }
}
csharp
[TestFixture]
public class ProductSpecificationTests
{
    [Test]
    public void IsInStock_WithPositiveQuantity_ShouldReturnTrue()
    {
        // Arrange
        var product = new Product { AvailableQuantity = 10 };
        var specification = ProductSpecification.IsInStock;

        // Act
        var result = specification.IsSatisfiedBy(product);

        // Assert
        Assert.That(result, Is.True);
    }

    [TestCase(0, false)]
    [TestCase(-1, false)]
    [TestCase(1, true)]
    [TestCase(100, true)]
    public void IsInStock_VariousQuantities_ReturnsExpectedResult(
        int quantity, bool expected)
    {
        // Arrange
        var product = new Product { AvailableQuantity = quantity };
        var specification = ProductSpecification.IsInStock;

        // Act
        var result = specification.IsSatisfiedBy(product);

        // Assert
        Assert.That(result, Is.EqualTo(expected));
    }
}

Testing Combined Specifications ​

Test specification composition using logical operators:

csharp
public class CombinedSpecificationTests
{
    [Fact]
    public void AndSpecification_WhenBothConditionsMet_ShouldReturnTrue()
    {
        // Arrange
        var product = new Product
        {
            AvailableQuantity = 10,
            Price = 25.0m,
            Category = "Electronics"
        };

        var specification = ProductSpecification.IsInStock
            .And(ProductSpecification.InPriceRange(10.0m, 50.0m))
            .And(ProductSpecification.InCategory("Electronics"));

        // Act
        var result = specification.IsSatisfiedBy(product);

        // Assert
        Assert.True(result);
    }

    [Fact]
    public void OrSpecification_WhenEitherConditionMet_ShouldReturnTrue()
    {
        // Arrange
        var product = new Product
        {
            AvailableQuantity = 0,
            IsDiscounted = true
        };

        var specification = ProductSpecification.IsInStock
            .Or(ProductSpecification.IsDiscounted);

        // Act
        var result = specification.IsSatisfiedBy(product);

        // Assert
        Assert.True(result); // True because IsDiscounted is true
    }

    [Fact]
    public void NotSpecification_ShouldInvertResult()
    {
        // Arrange
        var product = new Product { AvailableQuantity = 0 };
        var specification = ProductSpecification.IsInStock.Not();

        // Act
        var result = specification.IsSatisfiedBy(product);

        // Assert
        Assert.True(result); // True because product is NOT in stock
    }
}

Testing Query Translation with Entity Framework ​

When using Entity Framework, you can use the ToQueryString() extension method to verify that specifications translate correctly to SQL:

csharp
public class SpecificationTranslationTests
{
    private readonly TestDbContext _context;

    public SpecificationTranslationTests()
    {
        var options = new DbContextOptionsBuilder<TestDbContext>()
            .UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=TestDb")
            .Options;
        _context = new TestDbContext(options);
    }

    [Fact]
    public void Specification_ShouldTranslateToValidSql()
    {
        // Arrange
        var specification = ProductSpecification.IsInStock;

        // Act
        var query = _context.Set<Product>().Where(specification.ToExpression());
        var sql = query.ToQueryString();

        // Assert
        Assert.NotNull(sql);
        Assert.Contains("WHERE", sql);
        Assert.Contains("AvailableQuantity", sql);
        _testOutputHelper.WriteLine(sql); // View generated SQL
    }

    [Fact]
    public void CombinedSpecification_ShouldTranslateToValidSql()
    {
        // Arrange
        var specification = ProductSpecification.IsInStock
            .And(ProductSpecification.InPriceRange(10.0m, 50.0m));

        // Act
        var query = _context.Set<Product>().Where(specification.ToExpression());
        var sql = query.ToQueryString();

        // Assert
        Assert.NotNull(sql);
        Assert.Contains("AND", sql);
        Assert.Contains("AvailableQuantity", sql);
        Assert.Contains("Price", sql);
        _testOutputHelper.WriteLine(sql);
    }

    [Fact]
    public void ComplexSpecification_ShouldNotThrowTranslationException()
    {
        // Arrange
        var specification = ProductSpecification.IsInStock
            .And(ProductSpecification.InCategory("Electronics"))
            .Or(ProductSpecification.IsDiscounted);

        // Act & Assert
        var query = _context.Set<Product>().Where(specification.ToExpression());

        // Should not throw InvalidOperationException
        var exception = Record.Exception(() => query.ToQueryString());
        Assert.Null(exception);
    }
}

Testing Query Strategies ​

Query strategies define query logic that can be tested independently of the database.

Unit Testing Query Strategies ​

Test query strategies with in-memory collections:

csharp
public class GetProductsQueryStrategyTests
{
    [Fact]
    public void Execute_ShouldFilterInStockProducts()
    {
        // Arrange
        var products = new List<Product>
        {
            new() { Id = Guid.NewGuid(), Name = "Product A", AvailableQuantity = 10 },
            new() { Id = Guid.NewGuid(), Name = "Product B", AvailableQuantity = 0 },
            new() { Id = Guid.NewGuid(), Name = "Product C", AvailableQuantity = 5 }
        }.AsQueryable();

        var strategy = new GetProductsQueryStrategy();

        // Act
        var result = strategy.Execute(products).ToList();

        // Assert
        Assert.Equal(2, result.Count);
        Assert.All(result, p => Assert.True(p.AvailableQuantity > 0));
    }

    [Fact]
    public void Execute_ShouldProjectToDto()
    {
        // Arrange
        var products = new List<Product>
        {
            new() { Id = Guid.NewGuid(), Name = "Product A", Price = 10.0m, AvailableQuantity = 5 }
        }.AsQueryable();

        var strategy = new GetProductDtoQueryStrategy();

        // Act
        var result = strategy.Execute(products).ToList();

        // Assert
        Assert.Single(result);
        Assert.IsType<ProductDto>(result[0]);
        Assert.Equal("Product A", result[0].Name);
    }

    [Fact]
    public void Execute_ShouldApplyOrderingCorrectly()
    {
        // Arrange
        var products = new List<Product>
        {
            new() { Name = "Zebra", AvailableQuantity = 10 },
            new() { Name = "Apple", AvailableQuantity = 5 },
            new() { Name = "Mango", AvailableQuantity = 3 }
        }.AsQueryable();

        var strategy = new GetProductsQueryStrategy();

        // Act
        var result = strategy.Execute(products).ToList();

        // Assert
        Assert.Equal("Apple", result[0].Name);
        Assert.Equal("Mango", result[1].Name);
        Assert.Equal("Zebra", result[2].Name);
    }
}

Testing Query Strategy Translation with Entity Framework ​

Verify that query strategies translate correctly to SQL:

csharp
public class QueryStrategyTranslationTests
{
    private readonly TestDbContext _context;
    private readonly ITestOutputHelper _testOutputHelper;

    public QueryStrategyTranslationTests(ITestOutputHelper testOutputHelper)
    {
        _testOutputHelper = testOutputHelper;
        var options = new DbContextOptionsBuilder<TestDbContext>()
            .UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=TestDb")
            .Options;
        _context = new TestDbContext(options);
    }

    [Fact]
    public void QueryStrategy_ShouldTranslateToValidSql()
    {
        // Arrange
        var strategy = new GetProductsQueryStrategy();
        var source = _context.Set<Product>().AsQueryable();

        // Act
        var query = strategy.Execute(source);
        var sql = query.ToQueryString();

        // Assert
        Assert.NotNull(sql);
        Assert.Contains("SELECT", sql);
        _testOutputHelper.WriteLine(sql);
    }

    [Fact]
    public void QueryStrategy_WithJoins_ShouldTranslateCorrectly()
    {
        // Arrange
        var strategy = new GetOrdersWithCustomerQueryStrategy();
        var querySource = new EfQuerySource(_context);

        // Act
        var query = strategy.Execute(querySource);
        var sql = query.ToQueryString();

        // Assert
        Assert.Contains("JOIN", sql);
        Assert.Contains("Customer", sql);
        _testOutputHelper.WriteLine(sql);
    }

    [Fact]
    public void QueryStrategy_WithComplexProjection_ShouldNotThrow()
    {
        // Arrange
        var strategy = new GetProductWithCategoryQueryStrategy();
        var source = _context.Set<Product>().AsQueryable();

        // Act & Assert
        var query = strategy.Execute(source);
        var exception = Record.Exception(() => query.ToQueryString());

        Assert.Null(exception);
    }

    [Fact]
    public void QueryStrategy_CheckGeneratedSqlForPerformance()
    {
        // Arrange
        var strategy = new GetProductsWithMultipleJoinsQueryStrategy();
        var source = _context.Set<Product>().AsQueryable();

        // Act
        var query = strategy.Execute(source);
        var sql = query.ToQueryString();

        // Assert - Verify no N+1 queries
        Assert.DoesNotContain("SELECT", sql.Substring(sql.IndexOf("FROM")));

        // Assert - Verify proper JOINs instead of subqueries
        var joinCount = Regex.Matches(sql, "JOIN").Count;
        Assert.InRange(joinCount, 1, 3);

        _testOutputHelper.WriteLine(sql);
    }
}

Testing Parameterized Query Strategies ​

Test query strategies with different parameters:

csharp
public class SearchProductsQueryStrategyTests
{
    [Theory]
    [InlineData("Apple", 1)]
    [InlineData("Banana", 1)]
    [InlineData("Orange", 0)]
    public void Execute_WithNameFilter_ShouldReturnMatchingProducts(
        string nameFilter, int expectedCount)
    {
        // Arrange
        var products = new List<Product>
        {
            new() { Name = "Apple" },
            new() { Name = "Banana" },
            new() { Name = "Apple Juice" }
        }.AsQueryable();

        var strategy = new SearchProductsQueryStrategy(nameFilter: nameFilter);

        // Act
        var result = strategy.Execute(products).ToList();

        // Assert
        Assert.Equal(expectedCount, result.Count);
    }

    [Fact]
    public void Execute_WithPriceRange_ShouldFilterCorrectly()
    {
        // Arrange
        var products = new List<Product>
        {
            new() { Price = 5.0m },
            new() { Price = 15.0m },
            new() { Price = 25.0m },
            new() { Price = 35.0m }
        }.AsQueryable();

        var strategy = new SearchProductsQueryStrategy(minPrice: 10.0m, maxPrice: 30.0m);

        // Act
        var result = strategy.Execute(products).ToList();

        // Assert
        Assert.Equal(2, result.Count);
        Assert.All(result, p => Assert.InRange(p.Price, 10.0m, 30.0m));
    }
}

Testing Database Sessions ​

Test database sessions using integration tests with real or in-memory databases.

Integration Testing with EF Core In-Memory ​

Use EF Core's in-memory provider for fast integration tests:

csharp
public class ProductServiceTests : IDisposable
{
    private readonly IDbSession _session;
    private readonly IServiceProvider _serviceProvider;

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

        // Configure in-memory database
        services.AddDbContextFactory<TestDbContext>(options =>
            options.UseInMemoryDatabase($"TestDb_{Guid.NewGuid()}"));

        services.AddEntityFrameworkExpressions()
            .AddSingleContext<TestDbContext>();

        _serviceProvider = services.BuildServiceProvider();
        _session = _serviceProvider.GetRequiredService<IDbSession>();
    }

    [Fact]
    public async Task CreateProduct_ShouldPersistToDatabase()
    {
        // Arrange
        var product = new Product
        {
            Id = Guid.NewGuid(),
            Name = "Test Product",
            Price = 10.0m,
            AvailableQuantity = 5
        };

        // Act
        _session.Add(product);
        await _session.SaveChangesAsync();

        // Assert
        var query = _session.Query<Product>().Where(p => p.Id == product.Id);
        var retrieved = await query.FirstAsync();

        Assert.NotNull(retrieved);
        Assert.Equal(product.Name, retrieved.Name);
    }

    [Fact]
    public async Task Query_WithSpecification_ShouldFilterCorrectly()
    {
        // Arrange
        _session.Add(new Product { Name = "Product 1", AvailableQuantity = 10 });
        _session.Add(new Product { Name = "Product 2", AvailableQuantity = 0 });
        await _session.SaveChangesAsync();

        // Act
        var query = _session.Query(ProductSpecification.IsInStock);
        var results = await query.ToListAsync();

        // Assert
        Assert.Single(results);
        Assert.Equal("Product 1", results[0].Name);
    }

    public void Dispose()
    {
        (_serviceProvider as IDisposable)?.Dispose();
    }
}

Integration Testing with SQLite ​

Use SQLite for more realistic integration tests:

csharp
public class ProductServiceIntegrationTests : IAsyncLifetime
{
    private readonly IDbSession _session;
    private readonly IServiceProvider _serviceProvider;
    private readonly SqliteConnection _connection;

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

        // Create in-memory SQLite connection
        _connection = new SqliteConnection("DataSource=:memory:");
        _connection.Open();

        services.AddDbContextFactory<TestDbContext>(options =>
            options.UseSqlite(_connection));

        services.AddEntityFrameworkExpressions()
            .AddSingleContext<TestDbContext>();

        _serviceProvider = services.BuildServiceProvider();
        _session = _serviceProvider.GetRequiredService<IDbSession>();
    }

    public async Task InitializeAsync()
    {
        // Create database schema
        var context = _serviceProvider.GetRequiredService<IDbContextFactory<TestDbContext>>()
            .CreateDbContext();
        await context.Database.EnsureCreatedAsync();
    }

    [Fact]
    public async Task Query_WithComplexSpecification_ShouldWork()
    {
        // Arrange
        var product = new Product
        {
            Name = "Test Product",
            Price = 25.0m,
            AvailableQuantity = 10,
            Category = "Electronics"
        };

        _session.Add(product);
        await _session.SaveChangesAsync();

        // Act
        var specification = ProductSpecification.IsInStock
            .And(ProductSpecification.InPriceRange(20.0m, 30.0m))
            .And(ProductSpecification.InCategory("Electronics"));

        var query = _session.Query(specification);
        var results = await query.ToListAsync();

        // Assert
        Assert.Single(results);
    }

    public async Task DisposeAsync()
    {
        await _connection.DisposeAsync();
        if (_serviceProvider is IAsyncDisposable asyncDisposable)
        {
            await asyncDisposable.DisposeAsync();
        }
    }
}

Integration Testing with Testcontainers ​

Use Testcontainers for testing with real databases:

csharp
public class ProductServiceDockerTests : IAsyncLifetime
{
    private readonly PostgreSqlContainer _postgres;
    private IServiceProvider _serviceProvider;
    private IDbSession _session;

    public ProductServiceDockerTests()
    {
        _postgres = new PostgreSqlBuilder()
            .WithImage("postgres:15-alpine")
            .Build();
    }

    public async Task InitializeAsync()
    {
        await _postgres.StartAsync();

        var services = new ServiceCollection();

        services.AddDbContextFactory<TestDbContext>(options =>
            options.UseNpgsql(_postgres.GetConnectionString()));

        services.AddEntityFrameworkExpressions()
            .AddSingleContext<TestDbContext>();

        _serviceProvider = services.BuildServiceProvider();
        _session = _serviceProvider.GetRequiredService<IDbSession>();

        // Create schema
        var context = _serviceProvider.GetRequiredService<IDbContextFactory<TestDbContext>>()
            .CreateDbContext();
        await context.Database.EnsureCreatedAsync();
    }

    [Fact]
    public async Task Query_AgainstRealPostgres_ShouldWork()
    {
        // Arrange
        _session.Add(new Product { Name = "Test Product", AvailableQuantity = 10 });
        await _session.SaveChangesAsync();

        // Act
        var query = _session.Query(ProductSpecification.IsInStock);
        var results = await query.ToListAsync();

        // Assert
        Assert.Single(results);
    }

    public async Task DisposeAsync()
    {
        await _postgres.DisposeAsync();
    }
}

Mocking Database Sessions ​

Use mocking frameworks to isolate components from database dependencies.

Mocking with Moq ​

csharp
public class ProductServiceTests
{
    [Fact]
    public async Task GetProducts_ShouldReturnProducts()
    {
        // Arrange
        var mockSession = new Mock<IDbQuerySession>();
        var mockQuery = new Mock<IDbQuery<Product>>();

        var expectedProducts = new List<Product>
        {
            new() { Id = Guid.NewGuid(), Name = "Product 1" },
            new() { Id = Guid.NewGuid(), Name = "Product 2" }
        };

        mockQuery
            .Setup(q => q.ToListAsync(It.IsAny<CancellationToken>()))
            .ReturnsAsync(expectedProducts);

        mockSession
            .Setup(s => s.Query<Product>())
            .Returns(mockQuery.Object);

        var service = new ProductService(mockSession.Object);

        // Act
        var result = await service.GetProductsAsync();

        // Assert
        Assert.Equal(2, result.Count);
        mockSession.Verify(s => s.Query<Product>(), Times.Once);
    }

    [Fact]
    public async Task CreateProduct_ShouldCallAddAndSaveChanges()
    {
        // Arrange
        var mockSession = new Mock<IDbSession>();
        var product = new Product { Name = "New Product" };

        mockSession
            .Setup(s => s.SaveChangesAsync(It.IsAny<CancellationToken>()))
            .ReturnsAsync(1);

        var service = new ProductService(mockSession.Object);

        // Act
        await service.CreateProductAsync(product);

        // Assert
        mockSession.Verify(s => s.Add(product), Times.Once);
        mockSession.Verify(s => s.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Once);
    }
}

Mocking with NSubstitute ​

csharp
public class OrderServiceTests
{
    [Fact]
    public async Task GetOrder_ShouldReturnOrder()
    {
        // Arrange
        var session = Substitute.For<IDbQuerySession>();
        var query = Substitute.For<IDbQuery<Order>>();

        var expectedOrder = new Order { Id = Guid.NewGuid() };

        query.FirstAsync(Arg.Any<CancellationToken>())
            .Returns(expectedOrder);

        session.Query<Order>()
            .Returns(query);

        var service = new OrderService(session);

        // Act
        var result = await service.GetOrderAsync(expectedOrder.Id);

        // Assert
        Assert.Equal(expectedOrder.Id, result.Id);
        await query.Received(1).FirstAsync(Arg.Any<CancellationToken>());
    }
}

Test Data Builders ​

Create test data builders for reusable test data:

csharp
public class ProductBuilder
{
    private Guid _id = Guid.NewGuid();
    private string _name = "Test Product";
    private decimal _price = 10.0m;
    private int _availableQuantity = 10;
    private string _category = "Test Category";

    public ProductBuilder WithId(Guid id)
    {
        _id = id;
        return this;
    }

    public ProductBuilder WithName(string name)
    {
        _name = name;
        return this;
    }

    public ProductBuilder WithPrice(decimal price)
    {
        _price = price;
        return this;
    }

    public ProductBuilder OutOfStock()
    {
        _availableQuantity = 0;
        return this;
    }

    public ProductBuilder InStock(int quantity = 10)
    {
        _availableQuantity = quantity;
        return this;
    }

    public ProductBuilder InCategory(string category)
    {
        _category = category;
        return this;
    }

    public Product Build()
    {
        return new Product
        {
            Id = _id,
            Name = _name,
            Price = _price,
            AvailableQuantity = _availableQuantity,
            Category = _category
        };
    }
}

// Usage
public class ProductTests
{
    [Fact]
    public void Specification_WithBuilder_ShouldWork()
    {
        // Arrange
        var product = new ProductBuilder()
            .WithName("Electronics Item")
            .InStock(5)
            .InCategory("Electronics")
            .Build();

        // Act
        var result = ProductSpecification.IsInStock.IsSatisfiedBy(product);

        // Assert
        Assert.True(result);
    }
}

Testing Best Practices ​

Do's ​

✅ Test business logic - Focus on testing specifications and query strategies
✅ Use in-memory tests - For fast unit tests of query logic
✅ Use real databases - For critical integration tests
✅ Test SQL translation - Use ToQueryString() to verify EF Core query generation
✅ Test edge cases - Null values, empty collections, boundary conditions
✅ Use test builders - For creating consistent test data
✅ Mock external dependencies - Isolate the code under test
✅ Test error handling - Verify exception handling and error scenarios
✅ Use descriptive test names - Make test intent clear
✅ Review generated SQL - Check for performance issues like N+1 queries

Don'ts ​

❌ Don't test framework code - Trust that EF Core/Marten work correctly
❌ Don't overuse mocks - Prefer integration tests for database operations
❌ Don't share state - Each test should be independent
❌ Don't test implementation details - Test behavior, not internal structure
❌ Don't skip cleanup - Always dispose resources properly
❌ Don't ignore async/await - Use proper async testing patterns
❌ Don't assume SQL correctness - Always verify with ToQueryString() in EF tests

Testing Patterns ​

Arrange-Act-Assert (AAA) ​

Structure tests using the AAA pattern:

csharp
[Fact]
public async Task GetProducts_ReturnsInStockProducts()
{
    // Arrange - Set up test data and dependencies
    var product = new ProductBuilder().InStock().Build();
    _session.Add(product);
    await _session.SaveChangesAsync();

    // Act - Execute the code under test
    var query = _session.Query(ProductSpecification.IsInStock);
    var results = await query.ToListAsync();

    // Assert - Verify the results
    Assert.Single(results);
    Assert.Equal(product.Id, results[0].Id);
}

Test Fixtures ​

Share setup across tests using fixtures:

csharp
public class DatabaseFixture : IAsyncLifetime
{
    private IServiceProvider _serviceProvider;
    private AsyncServiceScope _serviceScope;

    public IDbSession Session { get; private set; }

    public async Task InitializeAsync()
    {
        var services = new ServiceCollection();
        services.AddDbContextFactory<TestDbContext>(options =>
            options.UseInMemoryDatabase($"TestDb_{Guid.NewGuid()}"));
        services.AddEntityFrameworkExpressions().AddSingleContext<TestDbContext>();

        _serviceProvider = services.BuildServiceProvider();
        _serviceScope = _serviceProvider.CreateAsyncScope();
        Session = _serviceScope.ServiceProvider.GetRequiredService<IDbSession>();
    }

    public async Task DisposeAsync()
    {
        await _serviceScope.DisposeAsync();
        await _serviceProvider.DisposeAsync();
    }
}

public class ProductTests : IClassFixture<DatabaseFixture>
{
    private readonly DatabaseFixture _fixture;

    public ProductTests(DatabaseFixture fixture)
    {
        _fixture = fixture;
    }

    [Fact]
    public async Task Test_UsesSharedFixture()
    {
        var query = _fixture.Session.Query<Product>();
        var results = await query.ToListAsync();
        Assert.NotNull(results);
    }
}

See Also ​

Released under the MIT License.