d8a1f2fe64 | ||
---|---|---|
.github | ||
src | ||
.gitignore | ||
code_of_conduct.md | ||
license.txt | ||
readme.md |
readme.md
Verify.EntityFramework
Extends Verify to allow snapshot testing with EntityFramework.
NuGet packages
- https://nuget.org/packages/Verify.EntityFramework/
- https://nuget.org/packages/Verify.EntityFrameworkClassic/
Enable
Enable VerifyEntityFramework once at assembly load time:
EF Core
static IModel GetDbModel()
{
var options = new DbContextOptionsBuilder<SampleDbContext>();
options.UseSqlServer("fake");
using var data = new SampleDbContext(options.Options);
return data.Model;
}
[ModuleInitializer]
public static void Init()
{
var model = GetDbModel();
VerifyEntityFramework.Enable(model);
}
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
VerifyEntityFrameworkClassic.Enable();
[ModuleInitializer]
public static void Init() =>
VerifyEntityFrameworkClassic.Enable();
Recording
Recording allows all commands executed by EF to be captured and then (optionally) verified.
Enable
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.
Usage
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(_ => _.Content == "Title")
.ToListAsync();
await Verify(data.Companies.Count());
Will result in the following verified file:
{
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]
}
]
}
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(_ => _.Content == "Title")
.ToListAsync();
var entries = EfRecording.FinishRecording();
//TODO: optionally filter the results
await Verify(new
{
target = data.Companies.Count(),
sql = entries
});
DbContext spanning
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(_ => _.Content == "Title")
.ToListAsync();
await Verify(data2.Companies.Count());
{
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]
}
]
}
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.
Added entity
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
}
}
}
Deleted entity
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
}
}
}
Modified entity
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
}
}
}
}
Queryable
This test:
var queryable = data.Companies
.Where(_ => _.Content == "value");
await Verify(queryable);
Will result in the following verified file:
EF Core
SELECT [c].[Id], [c].[Content]
FROM [Companies] AS [c]
WHERE [c].[Content] = N'value'
EF Classic
SELECT
[Extent1].[Id] AS [Id],
[Extent1].[Content] AS [Content]
FROM [dbo].[Companies] AS [Extent1]
WHERE N'value' = [Extent1].[Content]
AllData
This test:
await Verify(data.AllData())
.AddExtraSettings(
serializer =>
serializer.TypeNameHandling = TypeNameHandling.Objects);
Will result in the following verified file with all data in the database:
[
{
$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
}
]
IgnoreNavigationProperties
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)
.IgnoreNavigationProperties();
}
Ignore globally
var options = DbContextOptions();
using var data = new SampleDbContext(options);
VerifyEntityFramework.IgnoreNavigationProperties();
WebApplicationFactory
To be able to use WebApplicationFactory for integration testing 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:
.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
});
Icon
Database designed by Creative Stall from The Noun Project.