# Verify.EntityFramework [![Discussions](https://img.shields.io/badge/Verify-Discussions-yellow?svg=true&label=)](https://github.com/orgs/VerifyTests/discussions) [![Build status](https://ci.appveyor.com/api/projects/status/g6njwv0aox62atu0?svg=true)](https://ci.appveyor.com/project/SimonCropp/verify-entityframework) [![NuGet Status](https://img.shields.io/nuget/v/Verify.EntityFramework.svg?label=Verify.EntityFramework)](https://www.nuget.org/packages/Verify.EntityFramework/) [![NuGet Status](https://img.shields.io/nuget/v/Verify.EntityFrameworkClassic.svg?label=Verify.EntityFrameworkClassic)](https://www.nuget.org/packages/Verify.EntityFrameworkClassic/) Extends [Verify](https://github.com/VerifyTests/Verify) to allow snapshot testing with EntityFramework. **See [Milestones](../../milestones?state=closed) for release notes.** ## NuGet packages * https://nuget.org/packages/Verify.EntityFramework/ * https://nuget.org/packages/Verify.EntityFrameworkClassic/ ## Enable Enable VerifyEntityFramework once at assembly load time: ### EF Core ```cs static IModel GetDbModel() { var options = new DbContextOptionsBuilder(); options.UseSqlServer("fake"); using var data = new SampleDbContext(options.Options); return data.Model; } [ModuleInitializer] public static void Init() { var model = GetDbModel(); VerifyEntityFramework.Initialize(model); } ``` snippet source | anchor The `GetDbModel` pattern allows an instance of the `IModel` to be stored for use when `IgnoreNavigationProperties` is called inside tests. This is optional, and instead can be passed explicitly to `IgnoreNavigationProperties`. ### EF Classic ```cs [ModuleInitializer] public static void Init() => VerifyEntityFrameworkClassic.Initialize(); ``` snippet source | anchor ## Recording Recording allows all commands executed by EF to be captured and then (optionally) verified. ### Enable Call `EfRecording.EnableRecording()` on `DbContextOptionsBuilder`. ```cs var builder = new DbContextOptionsBuilder(); builder.UseSqlServer(connection); builder.EnableRecording(); var data = new SampleDbContext(builder.Options); ``` snippet source | anchor `EnableRecording` should only be called in the test context. ### Usage To start recording call `EfRecording.StartRecording()`. The results will be automatically included in verified file. ```cs var company = new Company { Content = "Title" }; data.Add(company); await data.SaveChangesAsync(); EfRecording.StartRecording(); await data.Companies .Where(_ => _.Content == "Title") .ToListAsync(); await Verify(data.Companies.Count()); ``` snippet source | anchor Will result in the following verified file: ```txt { target: 5, sql: [ { Type: ReaderExecutedAsync, HasTransaction: false, Text: SELECT [c].[Id], [c].[Content] FROM [Companies] AS [c] WHERE [c].[Content] = N'Title' }, { Type: ReaderExecuted, HasTransaction: false, Text: SELECT COUNT(*) FROM [Companies] AS [c] } ] } ``` snippet source | anchor Sql entries can be explicitly read using `EfRecording.FinishRecording`, optionally filtered, and passed to Verify: ```cs var company = new Company { Content = "Title" }; data.Add(company); await data.SaveChangesAsync(); EfRecording.StartRecording(); await data.Companies .Where(_ => _.Content == "Title") .ToListAsync(); var entries = EfRecording.FinishRecording(); //TODO: optionally filter the results await Verify(new { target = data.Companies.Count(), sql = entries }); ``` snippet source | anchor ### DbContext spanning `StartRecording` can be called on different DbContext instances (built from the same options) and the results will be aggregated. ```cs var builder = new DbContextOptionsBuilder(); builder.UseSqlServer(connectionString); builder.EnableRecording(); await using var data1 = new SampleDbContext(builder.Options); EfRecording.StartRecording(); var company = new Company { Content = "Title" }; data1.Add(company); await data1.SaveChangesAsync(); await using var data2 = new SampleDbContext(builder.Options); await data2.Companies .Where(_ => _.Content == "Title") .ToListAsync(); await Verify(data2.Companies.Count()); ``` snippet source | anchor ```txt { target: 5, sql: [ { Type: ReaderExecutedAsync, HasTransaction: false, Parameters: { @p0 (Int32): 0, @p1 (String?): Title }, Text: SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; INSERT INTO [Companies] ([Id], [Content]) VALUES (@p0, @p1); }, { Type: ReaderExecutedAsync, HasTransaction: false, Text: SELECT [c].[Id], [c].[Content] FROM [Companies] AS [c] WHERE [c].[Content] = N'Title' }, { Type: ReaderExecuted, HasTransaction: false, Text: SELECT COUNT(*) FROM [Companies] AS [c] } ] } ``` snippet source | anchor ## ChangeTracking Added, deleted, and Modified entities can be verified by performing changes on a DbContext and then verifying the instance of ChangeTracking. This approach leverages the [EntityFramework ChangeTracker](https://docs.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.changetracking.changetracker). ### Added entity This test: ```cs [Test] public async Task Added() { var options = DbContextOptions(); await using var data = new SampleDbContext(options); var company = new Company { Content = "before" }; data.Add(company); await Verify(data.ChangeTracker); } ``` snippet source | anchor Will result in the following verified file: ```txt { Added: { Company: { Id: 0, Content: before } } } ``` snippet source | anchor ### Deleted entity This test: ```cs [Test] public async Task Deleted() { var options = DbContextOptions(); await using var data = new SampleDbContext(options); data.Add(new Company { Content = "before" }); await data.SaveChangesAsync(); var company = data.Companies.Single(); data.Companies.Remove(company); await Verify(data.ChangeTracker); } ``` snippet source | anchor Will result in the following verified file: ```txt { Deleted: { Company: { Id: 0 } } } ``` snippet source | anchor ### Modified entity This test: ```cs [Test] public async Task Modified() { var options = DbContextOptions(); await using var data = new SampleDbContext(options); var company = new Company { Content = "before" }; data.Add(company); await data.SaveChangesAsync(); data.Companies.Single().Content = "after"; await Verify(data.ChangeTracker); } ``` snippet source | anchor Will result in the following verified file: ```txt { Modified: { Company: { Id: 0, Content: { Original: before, Current: after } } } } ``` snippet source | anchor ## Queryable This test: ```cs var queryable = data.Companies .Where(_ => _.Content == "value"); await Verify(queryable); ``` snippet source | anchor Will result in the following verified file: ### EF Core ```txt [ { Content: value } ] ``` snippet source | anchor ```sql SELECT [c].[Id], [c].[Content] FROM [Companies] AS [c] WHERE [c].[Content] = N'value' ``` snippet source | anchor ### EF Classic ```txt SELECT [Extent1].[Id] AS [Id], [Extent1].[Content] AS [Content] FROM [dbo].[Companies] AS [Extent1] WHERE N'value' = [Extent1].[Content] ``` snippet source | anchor ## AllData This test: ```cs await Verify(data.AllData()) .AddExtraSettings( serializer => serializer.TypeNameHandling = TypeNameHandling.Objects); ``` snippet source | anchor Will result in the following verified file with all data in the database: ```txt [ { $type: Company, Id: 1, Content: Company1 }, { $type: Company, Id: 4, Content: Company2 }, { $type: Company, Id: 6, Content: Company3 }, { $type: Company, Id: 7, Content: Company4 }, { $type: Employee, Id: 2, CompanyId: 1, Content: Employee1, Age: 25 }, { $type: Employee, Id: 3, CompanyId: 1, Content: Employee2, Age: 31 }, { $type: Employee, Id: 5, CompanyId: 4, Content: Employee4, Age: 34 } ] ``` snippet source | anchor ## IgnoreNavigationProperties `IgnoreNavigationProperties` extends `SerializationSettings` to exclude all navigation properties from serialization: ```cs [Test] public async Task IgnoreNavigationProperties() { var options = DbContextOptions(); await using var data = new SampleDbContext(options); var company = new Company { Content = "company" }; var employee = new Employee { Content = "employee", Company = company }; await Verify(employee) .IgnoreNavigationProperties(); } ``` snippet source | anchor ### Ignore globally ```cs var options = DbContextOptions(); using var data = new SampleDbContext(options); VerifyEntityFramework.IgnoreNavigationProperties(); ``` snippet source | anchor ## WebApplicationFactory To be able to use [WebApplicationFactory](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1) for [integration testing](https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests) an identifier must be used to be able to retrieve the recorded commands. Start by enable recording with a unique identifier, for example the test name or a GUID: ```cs protected override void ConfigureWebHost(IWebHostBuilder webBuilder) { var dataBuilder = new DbContextOptionsBuilder() .EnableRecording(name) .UseSqlite($"Data Source={name};Mode=Memory;Cache=Shared"); webBuilder.ConfigureTestServices( _ => _.AddScoped(_ => dataBuilder.Options)); } ``` snippet source | anchor Then use the same identifier for recording: ```cs var httpClient = factory.CreateClient(); EfRecording.StartRecording(testName); var companies = await httpClient.GetFromJsonAsync("/companies"); var entries = EfRecording.FinishRecording(testName); ``` snippet source | anchor The results will not be automatically included in verified file so it will have to be verified manually: ```cs await Verify(new { target = companies!.Length, sql = entries }); ``` snippet source | anchor ## Icon [Database](https://thenounproject.com/term/database/310841/) designed by [Creative Stall](https://thenounproject.com/creativestall/) from [The Noun Project](https://thenounproject.com).