Testa utan ditt produktionsdatabassystem
På den här sidan går vi igenom tekniker för att skriva automatiserade test som inte använder databassystemet mot vilket applikationen körs i produktion, genom att ersätta din databas med en testdubbel. Det finns olika typer av testdubblar och metoder för att göra detta, och vi rekommenderar att du noggrant läser Välja en teststrategi för att helt förstå de olika alternativen. Slutligen är det också möjligt att testa mot produktionsdatabassystemet. Detta beskrivs i Testning mot produktionsdatabassystemet.
Tips
Den här sidan visar xUnit- tekniker, men liknande begrepp finns i andra testramverk, inklusive NUnit.
Databasmönster
Om du har valt att skriva tester utan att involvera ditt produktionsdatabassystem är den rekommenderade metoden att använda förvarsmönstret. Mer bakgrund om detta finns i det här avsnittet. Det första steget för att implementera förrådsmönstret är att extrahera dina EF Core LINQ-frågor till ett separat lager, som vi senare ska stubba eller mocka. Här är ett exempel på ett databasgränssnitt för vårt bloggsystem:
public interface IBloggingRepository
{
Task<Blog> GetBlogByNameAsync(string name);
IAsyncEnumerable<Blog> GetAllBlogsAsync();
void AddBlog(Blog blog);
Task SaveChangesAsync();
}
... och här är en partiell exempelimplementering för produktionsanvändning:
public class BloggingRepository : IBloggingRepository
{
private readonly BloggingContext _context;
public BloggingRepository(BloggingContext context)
=> _context = context;
public async Task<Blog> GetBlogByNameAsync(string name)
=> await _context.Blogs.FirstOrDefaultAsync(b => b.Name == name);
// Other code...
}
Det är inte mycket mer än så: lagringsplatsen omsluter helt enkelt en EF Core-context och exponerar metoder som kör databasfrågor och uppdateringar med den. En viktig punkt att notera är att vår GetAllBlogs
-metod returnerar IAsyncEnumerable<Blog>
(eller IEnumerable<Blog>
) och inte IQueryable<Blog>
. Om du returnerar det senare alternativet skulle det innebära att frågeoperatorer fortfarande kan kombineras med resultatet, vilket kräver att EF Core fortfarande är involverat i att översätta frågan. Detta skulle motverka syftet med att ha en lagringsplats från första början.
IAsyncEnumerable<Blog>
gör att vi enkelt kan använda stubbar eller simulera det som lagringsplatsen returnerar.
För ett ASP.NET Core-program måste vi registrera lagringsplatsen som en tjänst i beroendeinmatningen genom att lägga till följande i programmets ConfigureServices
:
services.AddScoped<IBloggingRepository, BloggingRepository>();
Slutligen injiceras våra kontroller med lagringsplatstjänsten i stället för EF Core-kontexten och kör metoder på tjänsten.
private readonly IBloggingRepository _repository;
public BloggingControllerWithRepository(IBloggingRepository repository)
=> _repository = repository;
[HttpGet]
public async Task<Blog> GetBlog(string name)
=> await _repository.GetBlogByNameAsync(name);
I det här läget är ditt program konstruerat enligt lagringsplatsens mönster: den enda kontaktpunkten med dataåtkomstskiktet – EF Core – är nu via lagringsplatslagret, som fungerar som medlare mellan programkod och faktiska databasfrågor. Tester kan nu skrivas helt enkelt genom att stubba ut lagringsplatsen eller genom att håna den med ditt favoritbibliotek. Här är ett exempel på ett modellbaserat test med det populära Moq-biblioteket:
[Fact]
public async Task GetBlog()
{
// Arrange
var repositoryMock = new Mock<IBloggingRepository>();
repositoryMock
.Setup(r => r.GetBlogByNameAsync("Blog2"))
.Returns(Task.FromResult(new Blog { Name = "Blog2", Url = "http://blog2.com" }));
var controller = new BloggingControllerWithRepository(repositoryMock.Object);
// Act
var blog = await controller.GetBlog("Blog2");
// Assert
repositoryMock.Verify(r => r.GetBlogByNameAsync("Blog2"));
Assert.Equal("http://blog2.com", blog.Url);
}
Den fullständiga exempelkoden kan visas här.
SQLite i minnet
SQLite kan enkelt konfigureras som EF Core-provider för din testsvit i stället för ditt produktionsdatabassystem (e.g. SQL Server); Mer information finns i dokumentationen om SQLite-providern. Det är dock vanligtvis en bra idé att använda SQLite:s minnesintern databas funktion vid testning, eftersom det ger enkel isolering mellan tester och inte kräver att du hanterar faktiska SQLite-filer.
Om du vill använda minnesintern SQLite är det viktigt att förstå att en ny databas skapas när en lågnivåanslutning öppnas och att den tas bort när anslutningen stängs. Vid normal användning öppnas EF Cores DbContext
och stänger databasanslutningar efter behov – varje gång en fråga körs – för att undvika att hålla anslutningen i onödan långa tider. Men med minnesintern SQLite skulle detta leda till att databasen återställs varje gång. Så som en lösning öppnar vi anslutningen innan den skickas till EF Core och ser till att den stängs endast när testet är klart:
public SqliteInMemoryBloggingControllerTest()
{
// Create and open a connection. This creates the SQLite in-memory database, which will persist until the connection is closed
// at the end of the test (see Dispose below).
_connection = new SqliteConnection("Filename=:memory:");
_connection.Open();
// These options will be used by the context instances in this test suite, including the connection opened above.
_contextOptions = new DbContextOptionsBuilder<BloggingContext>()
.UseSqlite(_connection)
.Options;
// Create the schema and seed some data
using var context = new BloggingContext(_contextOptions);
if (context.Database.EnsureCreated())
{
using var viewCommand = context.Database.GetDbConnection().CreateCommand();
viewCommand.CommandText = @"
CREATE VIEW AllResources AS
SELECT Url
FROM Blogs;";
viewCommand.ExecuteNonQuery();
}
context.AddRange(
new Blog { Name = "Blog1", Url = "http://blog1.com" },
new Blog { Name = "Blog2", Url = "http://blog2.com" });
context.SaveChanges();
}
BloggingContext CreateContext() => new BloggingContext(_contextOptions);
public void Dispose() => _connection.Dispose();
Tester kan nu anropa CreateContext
, som returnerar en kontext med hjälp av den anslutning som vi konfigurerade i konstruktorn, vilket säkerställer att vi har en ren databas med de data som har hämtats.
Den fullständiga exempelkoden för SQLite-minnesintern testning kan visas här.
Minnesleverantör
Enligt beskrivningen på testöversiktssidanavråder vi starkt från att använda in-memory-providern för testning; överväg att använda SQLite i ställeteller implementera repositoriemönstret. Om du har valt att använda minnesinternt är här en typisk testklasskonstruktor som konfigurerar och skapar en ny minnesintern databas före varje test:
public InMemoryBloggingControllerTest()
{
_contextOptions = new DbContextOptionsBuilder<BloggingContext>()
.UseInMemoryDatabase("BloggingControllerTest")
.ConfigureWarnings(b => b.Ignore(InMemoryEventId.TransactionIgnoredWarning))
.Options;
using var context = new BloggingContext(_contextOptions);
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
context.AddRange(
new Blog { Name = "Blog1", Url = "http://blog1.com" },
new Blog { Name = "Blog2", Url = "http://blog2.com" });
context.SaveChanges();
}
Den fullständiga exempelkoden för minnesintern testning kan visas här.
Namn på minnesintern databas
Minnesinterna databaser identifieras med ett enkelt strängnamn och det är möjligt att ansluta till samma databas flera gånger genom att ange samma namn (det är därför exemplet ovan måste anropa EnsureDeleted
före varje test). Observera dock att minnesinterna databaser är rotade i kontextens interna tjänstleverantör. medan kontexter i de flesta fall delar samma tjänstleverantör, kan konfigurering av kontexter med olika alternativ utlösa användningen av en ny intern tjänstleverantör. När så är fallet skickar du uttryckligen samma instans av InMemoryDatabaseRoot till UseInMemoryDatabase
för alla kontexter som ska dela minnesinterna databaser (detta görs vanligtvis genom att ha ett statiskt InMemoryDatabaseRoot
fält).
Transaktioner
Observera att om en transaktion startas som standard utlöser den minnesinterna providern ett undantag eftersom transaktioner inte stöds. Du kanske vill att transaktionerna ska ignoreras tyst i stället genom att konfigurera EF Core för att ignorera InMemoryEventId.TransactionIgnoredWarning
som i exemplet ovan. Men om koden faktiskt förlitar sig på transaktionssemantik – t.ex. beroende av återställning som faktiskt återställer ändringar – fungerar inte testet.
Visningar
Den minnesinterna leverantören tillåter att vyer definieras genom LINQ-frågor, med användning av ToInMemoryQuery:
modelBuilder.Entity<UrlResource>()
.ToInMemoryQuery(() => context.Blogs.Select(b => new UrlResource { Url = b.Url }));