/Verify.EntityFramework

Extends Verify to allow verification of EntityFramework bits.

Primary LanguageC#MIT LicenseMIT

Verify.EntityFramework

Build status NuGet Status NuGet Status

Extends Verify to allow snapshot testing with EntityFramework.

NuGet packages

Enable

Enable VerifyEntityFramework once at assembly load time:

EF Core

VerifyEntityFramework.Enable();

snippet source | anchor

EF Classic

VerifyEntityFrameworkClassic.Enable();

snippet source | anchor

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);

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.

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());

snippet source | anchor

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]
    }
  ]
}

snippet source | anchor

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
});

snippet source | anchor

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(x => x.Content == "Title")
    .ToListAsync();

await Verify(data2.Companies.Count());

snippet source | anchor

{
  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]
    }
  ]
}

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.

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);
}

snippet source | anchor

Will result in the following verified file:

{
  Added: {
    Company: {
      Id: 0,
      Content: before
    }
  }
}

snippet source | anchor

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);
}

snippet source | anchor

Will result in the following verified file:

{
  Deleted: {
    Company: {
      Id: 0
    }
  }
}

snippet source | anchor

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);
}

snippet source | anchor

Will result in the following verified file:

{
  Modified: {
    Company: {
      Id: 0,
      Content: {
        Original: before,
        Current: after
      }
    }
  }
}

snippet source | anchor

Queryable

This test:

var queryable = data.Companies
    .Where(x => x.Content == "value");
await Verify(queryable);

snippet source | anchor

Will result in the following verified file:

EF Core

SELECT [c].[Id], [c].[Content]
FROM [Companies] AS [c]
WHERE [c].[Content] = N'value'

snippet source | anchor

EF Classic

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:

await Verify(data.AllData())
    .ModifySerialization(
        serialization =>
            serialization.AddExtraSettings(
                serializer =>
                    serializer.TypeNameHandling = TypeNameHandling.Objects));

snippet source | anchor

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
  }
]

snippet source | anchor

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)
        .ModifySerialization(
            x => x.IgnoreNavigationProperties(data));
}

snippet source | anchor

WebApplicationFactory

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);
});

snippet source | anchor

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);

snippet source | anchor

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
});

snippet source | anchor

Icon

Database designed by Creative Stall from The Noun Project.