프로덕션 데이터베이스 시스템 없이 테스트
이 페이지에서는 애플리케이션이 프로덕션 환경에서 실행되는 데이터베이스 시스템을 포함하지 않고, 데이터베이스를 테스트 대역으로 교체하여 자동화된 테스트를 작성하는 기술에 대해 설명합니다. 이 작업을 수행하기 위한 다양한 유형의 테스트 더블과 접근 방식이 있으며, 다양한 옵션을 완전히 이해하려면 을(를) 읽고 테스트 전략을(를) 선택하는 것이 좋습니다. 마지막으로 프로덕션 데이터베이스 시스템에 대해 테스트할 수도 있습니다. 이는 프로덕션 데이터베이스 시스템대한
리포지토리 패턴
프로덕션 데이터베이스 시스템을 사용하지 않고 테스트를 작성하기로 결정한 경우 권장되는 방법은 리포지토리 패턴입니다. 이에 대한 자세한 배경 정보는 이 섹션
public interface IBloggingRepository
{
Task<Blog> GetBlogByNameAsync(string name);
IAsyncEnumerable<Blog> GetAllBlogsAsync();
void AddBlog(Blog blog);
Task SaveChangesAsync();
}
... 프로덕션 사용을 위한 부분 샘플 구현은 다음과 같습니다.
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...
}
전체적으로 복잡하지 않습니다: 리포지토리는 단순히 EF Core 컨텍스트를 래핑하고, 데이터베이스 쿼리 및 업데이트를 실행하는 메서드를 제공합니다. 핵심 사항은 GetAllBlogs
메서드가 IAsyncEnumerable<Blog>
(또는 IEnumerable<Blog>
)를 반환하며, IQueryable<Blog>
가 아니라는 것입니다. 후자를 반환하면 결과를 통해 쿼리 연산자를 계속 작성할 수 있으므로 EF Core가 쿼리 번역에 계속 관여해야 합니다. 이는 처음에 리포지토리를 갖는 목적을 무산시킬 수 있습니다.
IAsyncEnumerable<Blog>
리포지토리가 반환하는 내용을 쉽게 스텁하거나 조롱할 수 있습니다.
ASP.NET Core 애플리케이션의 경우, 종속성 주입에서 리포지토리를 서비스로 등록하려면 애플리케이션의 ConfigureServices
에 다음을 추가해야 합니다.
services.AddScoped<IBloggingRepository, BloggingRepository>();
마지막으로, 컨트롤러는 EF Core 컨텍스트 대신 리포지토리 서비스를 삽입하고 해당 컨텍스트에서 메서드를 실행합니다.
private readonly IBloggingRepository _repository;
public BloggingControllerWithRepository(IBloggingRepository repository)
=> _repository = repository;
[HttpGet]
public async Task<Blog> GetBlog(string name)
=> await _repository.GetBlogByNameAsync(name);
이 시점에서 애플리케이션은 리포지토리 패턴에 따라 설계됩니다. 데이터 액세스 계층과 접촉하는 유일한 지점인 EF Core는 이제 애플리케이션 코드와 실제 데이터베이스 쿼리 간의 중재자 역할을 하는 리포지토리 계층을 통해 구성됩니다. 이제 리포지토리를 스텁아웃하거나 즐겨 찾는 모의 라이브러리로 모의하여 테스트를 작성할 수 있습니다. 다음은 인기 있는 Moq 라이브러리를 사용하는 모의 기반 테스트의 예입니다.
[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);
}
전체 샘플 코드는 여기에서
메모리에서 SQLite
SQLite는 프로덕션 데이터베이스 시스템(e.g. SQL Server) 대신 테스트 도구 모음에 대한 EF Core 공급자로 쉽게 구성할 수 있습니다. 자세한 내용은 SQLite 공급자 문서 참조하세요. 그러나 테스트 간에 쉽게 격리되고 실제 SQLite 파일을 처리할 필요가 없으므로 테스트할 때 SQLite의 메모리 내 데이터베이스 기능을 사용하는 것이 좋습니다.
메모리 내 SQLite를 사용하려면 하위 수준 연결이 열릴 때마다 새 데이터베이스가 생성되고 해당 연결이 닫혀 있을 때 삭제된다는 것을 이해하는 것이 중요합니다. 일반적인 사용에서 EF Core의 DbContext
는 필요에 따라 데이터베이스 연결을 열고 닫는데, 이는 쿼리가 실행될 때마다 연결을 불필요하게 오랫동안 유지하지 않기 위함입니다. 그러나 메모리 내 SQLite를 사용하면 매번 데이터베이스가 다시 설정됩니다. 따라서 해결 방법으로 EF Core에 전달하기 전에 연결을 열고 테스트가 완료된 경우에만 연결을 닫도록 정렬합니다.
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();
이제 테스트는 CreateContext
호출할 수 있습니다. 그러면 생성자에서 설정한 연결을 사용하여 컨텍스트를 반환하여 시드된 데이터가 포함된 클린 데이터베이스가 있는지 확인할 수 있습니다.
SQLite 메모리 내 테스트에 대한 전체 샘플 코드는 여기
메모리 내 공급자
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();
}
메모리 내 테스트에 대한 전체 샘플 코드는 여기에서볼 수 있습니다.
메모리 내 데이터베이스 이름 지정
메모리 내 데이터베이스는 간단한 문자열 이름으로 식별되며 동일한 이름을 제공하여 동일한 데이터베이스에 여러 번 연결할 수 있습니다(위의 샘플이 각 테스트 전에 EnsureDeleted
호출해야 하는 이유입니다). 그러나 메모리 내 데이터베이스는 컨텍스트의 내부 서비스 공급자에 뿌리를 두고 있습니다. 대부분의 경우 컨텍스트는 동일한 서비스 공급자를 공유하지만 다른 옵션을 사용하여 컨텍스트를 구성하면 새 내부 서비스 공급자의 사용을 트리거할 수 있습니다. 이 경우 메모리 내 데이터베이스를 공유해야 하는 모든 컨텍스트에 대해 동일한 InMemoryDatabaseRoot 인스턴스를 UseInMemoryDatabase
명시적으로 전달합니다(일반적으로 정적 InMemoryDatabaseRoot
필드를 사용하여 수행됨).
거래
기본적으로 트랜잭션을 시작하면, 메모리 내 제공자가 예외를 throw하게 되는데, 이는 트랜잭션이 지원되지 않기 때문입니다. 위의 샘플과 같이 InMemoryEventId.TransactionIgnoredWarning
무시하도록 EF Core를 구성하여 트랜잭션을 자동으로 무시하려고 할 수 있습니다. 그러나 코드가 실제로 트랜잭션 기능에 의존하는 경우(예: 롤백이 실제로 변경 사항을 되돌려야 하는 경우) 테스트가 작동하지 않습니다.
보기
메모리 내 공급자는 LINQ 쿼리를 통해 ToInMemoryQuery사용하여 뷰의 정의를 허용합니다.
modelBuilder.Entity<UrlResource>()
.ToInMemoryQuery(() => context.Blogs.Select(b => new UrlResource { Url = b.Url }));
.NET