Extends Verify to allow snapshot testing with EntityFramework.
- https://nuget.org/packages/Verify.EntityFramework/
- https://nuget.org/packages/Verify.EntityFrameworkClassic/
Enable VerifyEntityFramework once at assembly load time:
VerifyEntityFramework.Enable();
VerifyEntityFrameworkClassic.Enable();
Recording allows all commands executed by EF to be captured and then (optionally) verified.
Call EfRecording.EnableRecording()
on DbContextOptionsBuilder
.
var builder = new DbContextOptionsBuilder<SampleDbContext>();
builder.UseSqlServer(connection);
builder.EnableRecording();
var data = new SampleDbContext(builder.Options);
EnableRecording
should only be called in the test context.
To start recording call EfRecording.StartRecording()
. The results will be automatically included in verified file.
var company = new Company
{
Content = "Title"
};
data.Add(company);
await data.SaveChangesAsync();
EfRecording.StartRecording();
await data.Companies
.Where(x => x.Content == "Title")
.ToListAsync();
await Verify(data.Companies.Count());
Will result in the following verified file:
{
target: 5,
sql: [
{
Type: ReaderExecutedAsync,
Text:
SELECT [c].[Id], [c].[Content]
FROM [Companies] AS [c]
WHERE [c].[Content] = N'Title'
},
{
Type: ReaderExecuted,
Text:
SELECT COUNT(*)
FROM [Companies] AS [c]
}
]
}
Sql entries can be explicitly read using EfRecording.FinishRecording
, optionally filtered, and passed to Verify:
var company = new Company
{
Content = "Title"
};
data.Add(company);
await data.SaveChangesAsync();
EfRecording.StartRecording();
await data.Companies
.Where(x => x.Content == "Title")
.ToListAsync();
var entries = EfRecording.FinishRecording();
//TODO: optionally filter the results
await Verify(new
{
target = data.Companies.Count(),
sql = entries
});
StartRecording
can be called on different DbContext instances (built from the same options) and the results will be aggregated.
var builder = new DbContextOptionsBuilder<SampleDbContext>();
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(x => x.Content == "Title")
.ToListAsync();
await Verify(data2.Companies.Count());
{
target: 5,
sql: [
{
Type: ReaderExecutedAsync,
HasTransaction: true,
Parameters: {
@p0 (Int32): 0,
@p1 (String?): Title
},
Text:
SET NOCOUNT ON;
INSERT INTO [Companies] ([Id], [Content])
VALUES (@p0, @p1);
},
{
Type: ReaderExecutedAsync,
Text:
SELECT [c].[Id], [c].[Content]
FROM [Companies] AS [c]
WHERE [c].[Content] = N'Title'
},
{
Type: ReaderExecuted,
Text:
SELECT COUNT(*)
FROM [Companies] AS [c]
}
]
}
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.
This test:
[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);
}
Will result in the following verified file:
{
Added: {
Company: {
Id: 0,
Content: before
}
}
}
This test:
[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);
}
Will result in the following verified file:
{
Deleted: {
Company: {
Id: 0
}
}
}
This test:
[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);
}
Will result in the following verified file:
{
Modified: {
Company: {
Id: 0,
Content: {
Original: before,
Current: after
}
}
}
}
This test:
var queryable = data.Companies
.Where(x => x.Content == "value");
await Verify(queryable);
Will result in the following verified file:
SELECT [c].[Id], [c].[Content]
FROM [Companies] AS [c]
WHERE [c].[Content] = N'value'
SELECT
[Extent1].[Id] AS [Id],
[Extent1].[Content] AS [Content]
FROM [dbo].[Companies] AS [Extent1]
WHERE N'value' = [Extent1].[Content]
This test:
await Verify(data.AllData())
.ModifySerialization(
serialization =>
serialization.AddExtraSettings(
serializer =>
serializer.TypeNameHandling = TypeNameHandling.Objects));
Will result in the following verified file with all data in the database:
[
{
$type: Company,
Id: Id_1,
Content: Company1
},
{
$type: Company,
Id: Id_2,
Content: Company2
},
{
$type: Company,
Id: Id_3,
Content: Company3
},
{
$type: Company,
Id: Id_4,
Content: Company4
},
{
$type: Employee,
Id: Id_5,
CompanyId: Id_1,
Content: Employee1,
Age: 25
},
{
$type: Employee,
Id: Id_6,
CompanyId: Id_1,
Content: Employee2,
Age: 31
},
{
$type: Employee,
Id: Id_7,
CompanyId: Id_2,
Content: Employee4,
Age: 34
}
]
IgnoreNavigationProperties
extends SerializationSettings
to exclude all navigation properties from serialization:
[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)
.ModifySerialization(
x => x.IgnoreNavigationProperties(data));
}
To be able to use WebApplicationFactory for integration testingan 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:
.ConfigureTestServices(services =>
{
services.AddScoped(_ =>
new DbContextOptionsBuilder<SampleDbContext>()
.EnableRecording(testName)
.UseSqlite($"Data Source={testName};Mode=Memory;Cache=Shared")
.Options);
});
Then use the same identifier for recording:
var httpClient = factory.CreateClient();
EfRecording.StartRecording(testName);
var companies = await httpClient.GetFromJsonAsync<Company[]>("/companies");
var entries = EfRecording.FinishRecording(testName);
The results will not be automatically included in verified file so it will have to be verified manually:
await Verify(new
{
target = companies!.Length,
sql = entries
});
Database designed by Creative Stall from The Noun Project.