This document defines the testing strategy for ExpressiveSharp.
The emitter maps each IOperation to the most direct System.Linq.Expressions equivalent.
For example, IForEachLoopOperation emits Expression.Loop() with the enumerator pattern —
not a direct LINQ rewrite. Transformers (like ConvertLoopsToLinq) handle the conversion to
LINQ for providers that can't translate LoopExpression.
This means:
- The emitter targets the full
System.Linq.ExpressionsAPI surface - Transformers adapt expression trees for specific providers (EF Core, etc.)
- The ExpressionCompile runner validates all expression types (compiles and executes in-process)
- The EntityFrameworkCore runner validates the EF Core-translatable subset
The source generator produces C# code that builds an Expression<TDelegate> using factory methods.
This generated code must compile without errors.
Test mechanism: GeneratorTestBase.RunExpressiveGenerator() compiles the generated output
via Roslyn and asserts zero diagnostics.
TreatWarningsAsErrors is enabled project-wide. The generator reports diagnostics (EXP0001–EXP0012)
for unsupported constructs, and these are verified by tests.
The generated expression tree must be functionally equivalent to the original method body. Integration tests seed canonical data, execute queries via expanded expression trees, and assert results match expected values.
| Project | Purpose | Level |
|---|---|---|
ExpressiveSharp.Generator.Tests |
Snapshot tests (Verify.MSTest) — validates generated C# output | 1, 2 |
ExpressiveSharp.Tests |
Unit tests for runtime services, transformers, extensions | 3 |
ExpressiveSharp.IntegrationTests |
Shared abstract test classes, Store scenario models, seed data | — |
ExpressiveSharp.IntegrationTests.ExpressionCompile |
Compiles expression trees to delegates, executes in-memory | 3 |
ExpressiveSharp.IntegrationTests.EntityFrameworkCore |
EF Core + SQLite query translation and execution | 3 |
ExpressiveSharp.EntityFrameworkCore.Tests |
EF Core-specific hooks (conventions, ExpressiveDbSet) | 3 |
Three-project structure:
-
ExpressiveSharp.IntegrationTests — shared class library (not MSTest.Sdk) with Store scenario models (
Order,Customer,LineItem,Address), seed data, and abstract test classes. The generator runs here to produce expression registries for[Expressive]models. -
ExpressiveSharp.IntegrationTests.ExpressionCompile — compiles expression trees to delegates and executes against in-memory
List<T>. Validates all expression types includingLoopExpression. -
ExpressiveSharp.IntegrationTests.EntityFrameworkCore — executes against SQLite via EF Core with
UseExpressives(). Validates the LINQ-translatable subset. Transformers (ConvertLoopsToLinq,RemoveNullConditionalPatterns,FlattenBlockExpressions) run before queries.
Concrete test classes are one-liners that inherit abstract tests and override CreateRunner():
[TestClass]
public class ArithmeticTests : Scenarios.Common.Tests.ArithmeticTests
{
protected override IIntegrationTestRunner CreateRunner() => new ExpressionCompileTestRunner();
}To add a new integration (e.g., NHibernate): implement IIntegrationTestRunner, create concrete subclasses.
When implementing a new EmitOperation case:
-
Snapshot test — add a test in
ExpressiveSharp.Generator.Teststhat feeds source code through the generator and verifies the output compiles and matches a snapshot. -
Integration test — add an
[Expressive]member to the Store model or useExpressionPolyfill.Create(...), then add assertions in an abstract test class. Both ExpressionCompile and EntityFrameworkCore runners pick it up automatically. -
Transformer unit test — if the feature requires a transformer for LINQ provider compatibility, add tests in
ExpressiveSharp.Tests/Transformers/that build expression trees manually and verify the transformer rewrites them correctly.
Beyond correctness testing, the project includes BenchmarkDotNet benchmarks in
benchmarks/ExpressiveSharp.Benchmarks/ that track performance across key hot paths:
| Benchmark class | What it measures |
|---|---|
GeneratorBenchmarks |
Cold and incremental ExpressiveGenerator runs (parameterized by member count) |
PolyfillGeneratorBenchmarks |
Cold and incremental PolyfillInterceptorGenerator runs |
ExpressionResolverBenchmarks |
Registry vs. reflection lookup for properties, methods, constructors |
ExpressionReplacerBenchmarks |
ExpressiveReplacer.Replace on various expression tree shapes |
TransformerBenchmarks |
Each transformer in isolation + full ExpandExpressives pipeline |
EFCoreQueryOverheadBenchmarks |
End-to-end EF Core ToQueryString() overhead + cold-start cost |
A separate GitHub Actions workflow (.github/workflows/benchmarks.yml) runs benchmarks on every
push to main and on pull requests. Results are stored on the gh-pages branch and compared
against the last main baseline using benchmark-action/github-action-benchmark. PRs that
regress beyond 20% receive an automated comment.
# Run all benchmarks (full BenchmarkDotNet defaults)
dotnet run -c Release --project benchmarks/ExpressiveSharp.Benchmarks/ExpressiveSharp.Benchmarks.csproj -- --filter "*"
# Run a specific class
dotnet run -c Release --project benchmarks/ExpressiveSharp.Benchmarks/ExpressiveSharp.Benchmarks.csproj -- --filter "*GeneratorBenchmarks*"
# Quick run (CI-style, fewer iterations)
dotnet run -c Release --project benchmarks/ExpressiveSharp.Benchmarks/ExpressiveSharp.Benchmarks.csproj -- --filter "*" --job short --iterationCount 3 --warmupCount 1# All tests
dotnet test
# Single project
dotnet test tests/ExpressiveSharp.Generator.Tests
# Accept new snapshots
VERIFY_AUTO_APPROVE=true dotnet test tests/ExpressiveSharp.Generator.Tests
# Single test by name
dotnet test --filter "FullyQualifiedName~MyTestName"