Verify.EntityFramework/readme.md

14 KiB

Verify.EntityFramework

Build status NuGet Status NuGet Status

Extends Verify to allow snapshot testing with EntityFramework.


Part of the .NET Foundation

NuGet package

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.

DbContextOptionsBuilder<SampleDbContext> builder = new();
builder.UseSqlServer(connection);
builder.EnableRecording();
SampleDbContext data = new(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 Verifier.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 Verifier.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 SampleDbContext data1 = new(builder.Options);
EfRecording.StartRecording();
var company = new Company
{
    Content = "Title"
};
data1.Add(company);
await data1.SaveChangesAsync();

await using SampleDbContext data2 = new(builder.Options);
await data2.Companies
    .Where(x => x.Content == "Title")
    .ToListAsync();

await Verifier.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 SampleDbContext data = new(options);
    var company = new Company
    {
        Content = "before"
    };
    data.Add(company);
    await Verifier.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 SampleDbContext data = new(options);
    data.Add(new Company {Content = "before"});
    await data.SaveChangesAsync();

    var company = data.Companies.Single();
    data.Companies.Remove(company);
    await Verifier.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 SampleDbContext data = new(options);
    Company company = new()
    {
        Content = "before"
    };
    data.Add(company);
    await data.SaveChangesAsync();

    data.Companies.Single().Content = "after";
    await Verifier.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 Verifier.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 Verifier.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 SampleDbContext data = new(options);

    var company = new Company
    {
        Content = "company"
    };
    var employee = new Employee
    {
        Content = "employee",
        Company = company
    };
    await Verifier.Verify(employee)
        .ModifySerialization(
            x => x.IgnoreNavigationProperties(data));
}

snippet source | anchor

Icon

Database designed by Creative Stall from The Noun Project.