ASP.NET Core의 Razor Pages 단위 테스트
ASP.NET Core는 Razor Pages 앱의 단위 테스트를 지원합니다. DAL(데이터 액세스 계층) 및 페이지 모델을 테스트하여 다음을 확인할 수 있습니다.
- Razor Pages 앱의 부분은 앱 생성 중에 별도로, 하나의 단위로 함께 작동합니다.
- 클래스 및 메서드의 책임 범위는 제한되어 있습니다.
- 앱이 동작하는 방식에 대한 추가 설명서가 있습니다.
- 코드 업데이트로 인해 오류가 발생하게 되는 회귀는 자동화된 빌드 및 배포 중에 발견됩니다.
이 항목에서는 Razor Pages 앱 및 단위 테스트에 대해 기본적으로 이해하고 있다고 가정합니다. Razor Pages 앱 또는 테스트 개념에 익숙하지 않은 경우 다음 항목을 참조하세요.
- ASP.NET Core의 Razor Pages 소개
- 자습서: ASP.NET Core에서 Razor Pages 시작
- dotnet 테스트 및 xUnit을 사용하여 .NET Core에서 C# 단위 테스트
샘플 프로젝트는 다음 두 앱으로 구성됩니다.
App | 프로젝트 폴더 | 설명 |
---|---|---|
메시지 앱 | src/RazorPagesTestSample | 사용자가 메시지를 추가하고, 메시지 하나를 삭제하고, 모든 메시지를 삭제하고, 메시지를 분석할 수 있도록 허용합니다(메시지당 평균 단어 수 확인). |
앱 테스트 | tests/RazorPagesTestSample.Tests | 메시지 앱의 DAL 및 인덱스 페이지 모델을 단위 테스트하는 데 사용됩니다. |
Visual Studio와 같은 IDE의 기본 제공 테스트 기능을 사용하여 테스트를 실행할 수 있습니다. Visual Studio Code 또는 명령줄을 사용하는 경우 tests/RazorPagesTestSample.Tests 폴더의 명령 프롬프트에서 다음 명령을 실행합니다.
dotnet test
메시지 앱 구성
메시지 앱은 다음과 같은 특징을 가진 Razor Pages 메시지 시스템입니다.
- 앱
Pages/Index.cshtml
(및Pages/Index.cshtml.cs
)의 인덱스 페이지는 메시지의 추가, 삭제 및 분석을 제어하는 UI 및 페이지 모델 메서드를 제공합니다(메시지당 평균 단어 수 찾기). - 메시지는 클래스()에서 두 가지
Id
속성(Data/Message.cs
키) 및Text
(메시지)로 설명Message
됩니다.Text
속성은 필수이며 200자로 제한됩니다. - 메시지는 Entity Framework의 메모리 내 데이터베이스를 사용하여 저장됩니다†.
- 앱은 데이터베이스 컨텍스트 클래스
AppDbContext
()에 DAL을Data/AppDbContext.cs
포함합니다. DAL 메서드는virtual
로 표시되므로 테스트에서 메서드를 모의로 사용해볼 수 있습니다. - 앱 시작 시 데이터베이스가 비어 있는 경우 메시지 저장소는 세 개의 메시지로 초기화됩니다. 이러한 시드된 메시지는 테스트에서도 사용됩니다.
†EF 항목 InMemory로 테스트에서는 MSTest를 사용하여 테스트에 메모리 내 데이터베이스를 사용하는 방법을 설명합니다. 이 항목에서는 xUnit 테스트 프레임워크를 사용합니다. 여러 테스트 프레임워크의 테스트 개념 및 테스트 구현은 비슷하지만 동일하지는 않습니다.
샘플 앱은 리포지토리 패턴을 사용하지 않고 UoW(작업 단위) 패턴의 효과적인 예가 아니지만 Razor Pages는 이 개발 패턴을 지원합니다. 자세한 내용은 인프라 지속성 계층 디자인 및 ASP.NET Core에서 컨트롤러 논리 테스트(샘플에서 리포지토리 패턴 구현)를 참조하세요.
테스트 앱 구성
테스트 앱은 tests/RazorPagesTestSample.Tests 폴더에 있는 콘솔 앱입니다.
테스트 앱 폴더 | 설명 |
---|---|
UnitTests |
|
유틸리티 | 데이터베이스가 각 테스트에 대한 기준 조건으로 다시 설정되도록 하기 위해 각 DAL 단위 테스트에 대한 새 데이터베이스 컨텍스트 옵션을 만드는 데 사용되는 TestDbContextOptions 메서드가 포함되어 있습니다. |
테스트 프레임워크는 xUnit입니다. 개체 모의 프레임워크는 Moq입니다.
DAL(데이터 액세스 계층)의 단위 테스트
메시지 앱의 AppDbContext
클래스(src/RazorPagesTestSample/Data/AppDbContext.cs
)에는 4가지 메서드를 포함하는 DAL이 있습니다. 테스트 앱의 각 메서드에는 하나 또는 두 개의 단위 테스트가 있습니다.
DAL 메서드 | 함수 |
---|---|
GetMessagesAsync |
Text 속성을 기준으로 정렬된 데이터베이스의 List<Message> 를 가져옵니다. |
AddMessageAsync |
데이터베이스에 Message 를 추가합니다. |
DeleteAllMessagesAsync |
데이터베이스에서 모든 Message 항목을 삭제합니다. |
DeleteMessageAsync |
데이터베이스에서 Id 별로 단일 Message 를 삭제합니다. |
각 테스트에 대해 새 AppDbContext
를 만들 때 DAL의 단위 테스트에는 DbContextOptions가 필요합니다. 각 테스트에 대해 DbContextOptions
를 만드는 한 가지 방법은 DbContextOptionsBuilder를 사용하는 것입니다.
var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase("InMemoryDb");
using (var db = new AppDbContext(optionsBuilder.Options))
{
// Use the db here in the unit test.
}
이 방법의 문제는 이전 테스트가 어떤 상태이든 관계없이 각 테스트가 데이터베이스를 수신한다는 것입니다. 서로 방해하지 않는 원자 단위 테스트를 작성하려고 할 때 문제가 될 수 있습니다. AppDbContext
에서 각 테스트에 대해 강제로 새 데이터베이스 컨텍스트를 사용하도록 하려면 새 서비스 공급자를 기준으로 하는 DbContextOptions
인스턴스를 제공합니다. 테스트 앱은 해당 Utilities
클래스 메서드 TestDbContextOptions
(tests/RazorPagesTestSample.Tests/Utilities/Utilities.cs
)를 사용하여 이 작업을 수행하는 방법을 보여 줍니다.
public static DbContextOptions<AppDbContext> TestDbContextOptions()
{
// Create a new service provider to create a new in-memory database.
var serviceProvider = new ServiceCollection()
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
// Create a new options instance using an in-memory database and
// IServiceProvider that the context should resolve all of its
// services from.
var builder = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase("InMemoryDb")
.UseInternalServiceProvider(serviceProvider);
return builder.Options;
}
DAL 단위 테스트에서 DbContextOptions
를 사용하면 각 테스트는 새 데이터베이스 인스턴스에서 원자 단위로 실행할 수 있습니다.
using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
{
// Use the db here in the unit test.
}
DataAccessLayerTest
클래스(UnitTests/DataAccessLayerTest.cs
)의 각 테스트 메서드는 비슷한 정렬-실행-어설션 패턴을 따릅니다.
- 정렬: 데이터베이스가 테스트에 대해 구성되었거나 예상 결과가 정의됩니다.
- 작업: 테스트가 실행됩니다.
- 어설션: 테스트 결과가 성공했는지 확인하기 위해 어설션이 만들어집니다.
예를 들어 DeleteMessageAsync
메서드는 해당 Id
(src/RazorPagesTestSample/Data/AppDbContext.cs
)로 식별되는 단일 메시지를 제거하는 일을 담당합니다.
public async virtual Task DeleteMessageAsync(int id)
{
var message = await Messages.FindAsync(id);
if (message != null)
{
Messages.Remove(message);
await SaveChangesAsync();
}
}
이 메서드에 대한 2가지 테스트가 있습니다. 한 가지 테스트는 데이터베이스에 메시지가 있는 경우 메서드가 메시지를 삭제하는지 확인합니다. 다른 메서드는 삭제를 위해 메시지 Id
가 존재하지 않는 경우 데이터베이스가 변경되지 않는지 테스트합니다. DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound
메서드는 다음과 같습니다.
[Fact]
public async Task DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound()
{
using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
{
// Arrange
var seedMessages = AppDbContext.GetSeedingMessages();
await db.AddRangeAsync(seedMessages);
await db.SaveChangesAsync();
var recId = 1;
var expectedMessages =
seedMessages.Where(message => message.Id != recId).ToList();
// Act
await db.DeleteMessageAsync(recId);
// Assert
var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
Assert.Equal(
expectedMessages.OrderBy(m => m.Id).Select(m => m.Text),
actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
}
}
첫째, 이 메서드는 실행 단계가 준비되는 정렬 단계를 수행합니다. 시드 메시지를 가져온 후 seedMessages
에 보관합니다. 시드 메시지는 데이터베이스에 저장됩니다. Id
가 1
인 메시지는 삭제용으로 설정됩니다. DeleteMessageAsync
메서드가 실행될 때 예상되는 메시지에는 Id
가 1
인 경우를 제외한 모든 메시지가 있어야 합니다. expectedMessages
변수는 이 예상 결과를 나타냅니다.
// Arrange
var seedMessages = AppDbContext.GetSeedingMessages();
await db.AddRangeAsync(seedMessages);
await db.SaveChangesAsync();
var recId = 1;
var expectedMessages =
seedMessages.Where(message => message.Id != recId).ToList();
메서드가 작동합니다. 메서드 DeleteMessageAsync
가 다음을 1
전달하여 recId
실행됩니다.
// Act
await db.DeleteMessageAsync(recId);
마지막으로, 이 메서드는 컨텍스트에서 Messages
를 가져온 다음, expectedMessages
와 비교하여 두 항목이 같다고 어설션합니다.
// Assert
var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
Assert.Equal(
expectedMessages.OrderBy(m => m.Id).Select(m => m.Text),
actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
두 List<Message>
가 동일한지 비교하기 위해 다음이 수행됩니다.
- 메시지는
Id
별로 정렬됩니다. - 메시지 쌍의
Text
속성이 비교됩니다.
유사한 테스트 메서드인 DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound
는 존재하지 않는 메시지를 삭제하려고 시도한 결과를 확인합니다. 이 경우 데이터베이스의 예상 메시지는 DeleteMessageAsync
메서드가 실행된 후의 실제 메시지와 같아야 합니다. 데이터베이스의 내용은 다음과 같이 변경되지 않아야 합니다.
[Fact]
public async Task DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound()
{
using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
{
// Arrange
var expectedMessages = AppDbContext.GetSeedingMessages();
await db.AddRangeAsync(expectedMessages);
await db.SaveChangesAsync();
var recId = 4;
// Act
try
{
await db.DeleteMessageAsync(recId);
}
catch
{
// recId doesn't exist
}
// Assert
var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
Assert.Equal(
expectedMessages.OrderBy(m => m.Id).Select(m => m.Text),
actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
}
}
페이지 모델 메서드의 단위 테스트
다른 단위 테스트 세트는 페이지 모델 메서드의 테스트를 담당합니다. 메시지 앱에서 인덱스 페이지 모델은 src/RazorPagesTestSample/Pages/Index.cshtml.cs
의 IndexModel
클래스에 있습니다.
페이지 모델 메서드 | 함수 |
---|---|
OnGetAsync |
GetMessagesAsync 메서드를 사용하여 UI에 대한 DAL에서 메시지를 가져옵니다. |
OnPostAddMessageAsync |
ModelState가 유효한 경우 AddMessageAsync 를 호출하여 데이터베이스에 메시지를 추가합니다. |
OnPostDeleteAllMessagesAsync |
DeleteAllMessagesAsync 를 호출하여 데이터베이스의 모든 메시지를 삭제합니다. |
OnPostDeleteMessageAsync |
DeleteMessageAsync 를 실행하여 지정된 Id 의 메시지를 삭제합니다. |
OnPostAnalyzeMessagesAsync |
하나 이상의 메시지가 데이터베이스에 있으면 메시지당 평균 단어 수를 계산합니다. |
페이지 모델 메서드는 IndexPageTests
클래스(tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs
)의 7가지 테스트를 사용하여 테스트됩니다. 테스트는 친숙한 정렬-어셜선-실행 패턴을 사용합니다. 이러한 테스트는 다음에 중점을 둡니다.
- ModelState가 잘못된 경우 메서드가 올바른 동작을 따르는지 확인합니다.
- 메서드를 확인하면 올바른 IActionResult가 생성됩니다.
- 해당 속성 값 할당에 대한 확인이 올바르게 수행됩니다.
이 테스트 그룹은 종종 DAL 메서드의 모의 버전을 만들어 페이지 모델 메서드가 실행되는 실행 단계를 위한 예상 데이터를 생성합니다. 예를 들어, AppDbContext
의 GetMessagesAsync
메서드의 모의 메서드를 만들어 출력을 생성합니다. 페이지 모델 메서드에서 이 메서드를 실행하면 모의 버전이 결과를 반환합니다. 이 데이터는 데이터베이스에서 제공되지 않습니다. 이렇게 하면 페이지 모델 테스트에서 DAL을 사용하기 위한 예측 가능하고 신뢰할 수 있는 테스트 조건이 생성됩니다.
OnGetAsync_PopulatesThePageModel_WithAListOfMessages
테스트는 페이지 모델에 대한 GetMessagesAsync
메서드의 모의 버전을 생성하는 방법을 보여 줍니다.
var mockAppDbContext = new Mock<AppDbContext>(optionsBuilder.Options);
var expectedMessages = AppDbContext.GetSeedingMessages();
mockAppDbContext.Setup(
db => db.GetMessagesAsync()).Returns(Task.FromResult(expectedMessages));
var pageModel = new IndexModel(mockAppDbContext.Object);
실행 단계에서 OnGetAsync
메서드를 수행하면 페이지 모델의 GetMessagesAsync
메서드가 호출됩니다.
단위 테스트 실행 단계(tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs
):
// Act
await pageModel.OnGetAsync();
IndexPage
페이지 모델의 OnGetAsync
메서드(src/RazorPagesTestSample/Pages/Index.cshtml.cs
):
public async Task OnGetAsync()
{
Messages = await _db.GetMessagesAsync();
}
DAL의 GetMessagesAsync
메서드는 이 메서드 호출에 대한 결과를 반환하지 않습니다. 이 메서드의 모의 버전이 결과를 반환합니다.
Assert
단계에서 실제 메시지(actualMessages
)가 페이지 모델의 Messages
속성에서 할당됩니다. 메시지가 할당될 때 형식 검사도 수행됩니다. 예상 및 실제 메시지는 해당 Text
속성과 비교됩니다. 이 테스트는 두 개의 List<Message>
인스턴스에 동일한 메시지가 포함되어 있음을 어설션합니다.
// Assert
var actualMessages = Assert.IsAssignableFrom<List<Message>>(pageModel.Messages);
Assert.Equal(
expectedMessages.OrderBy(m => m.Id).Select(m => m.Text),
actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
이 그룹의 다른 테스트에서는 DefaultHttpContext, the ModelStateDictionary, ActionContext를 포함하는 페이지 모델 개체를 만들어 PageContext
, ViewDataDictionary
및 PageContext
를 설정합니다. 이러한 기능은 테스트를 수행하는 데 유용합니다. 예를 들어, 메시지 앱은 AddModelError로 ModelState
오류를 설정하여 OnPostAddMessageAsync
가 실행될 때 유효한 PageResult가 반환되는지 확인합니다.
[Fact]
public async Task OnPostAddMessageAsync_ReturnsAPageResult_WhenModelStateIsInvalid()
{
// Arrange
var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase("InMemoryDb");
var mockAppDbContext = new Mock<AppDbContext>(optionsBuilder.Options);
var expectedMessages = AppDbContext.GetSeedingMessages();
mockAppDbContext.Setup(db => db.GetMessagesAsync()).Returns(Task.FromResult(expectedMessages));
var httpContext = new DefaultHttpContext();
var modelState = new ModelStateDictionary();
var actionContext = new ActionContext(httpContext, new RouteData(), new PageActionDescriptor(), modelState);
var modelMetadataProvider = new EmptyModelMetadataProvider();
var viewData = new ViewDataDictionary(modelMetadataProvider, modelState);
var tempData = new TempDataDictionary(httpContext, Mock.Of<ITempDataProvider>());
var pageContext = new PageContext(actionContext)
{
ViewData = viewData
};
var pageModel = new IndexModel(mockAppDbContext.Object)
{
PageContext = pageContext,
TempData = tempData,
Url = new UrlHelper(actionContext)
};
pageModel.ModelState.AddModelError("Message.Text", "The Text field is required.");
// Act
var result = await pageModel.OnPostAddMessageAsync();
// Assert
Assert.IsType<PageResult>(result);
}
추가 리소스
ASP.NET Core는 Razor Pages 앱의 단위 테스트를 지원합니다. DAL(데이터 액세스 계층) 및 페이지 모델을 테스트하여 다음을 확인할 수 있습니다.
- Razor Pages 앱의 부분은 앱 생성 중에 별도로, 하나의 단위로 함께 작동합니다.
- 클래스 및 메서드의 책임 범위는 제한되어 있습니다.
- 앱이 동작하는 방식에 대한 추가 설명서가 있습니다.
- 코드 업데이트로 인해 오류가 발생하게 되는 회귀는 자동화된 빌드 및 배포 중에 발견됩니다.
이 항목에서는 Razor Pages 앱 및 단위 테스트에 대해 기본적으로 이해하고 있다고 가정합니다. Razor Pages 앱 또는 테스트 개념에 익숙하지 않은 경우 다음 항목을 참조하세요.
- ASP.NET Core의 Razor Pages 소개
- 자습서: ASP.NET Core에서 Razor Pages 시작
- dotnet 테스트 및 xUnit을 사용하여 .NET Core에서 C# 단위 테스트
샘플 프로젝트는 다음 두 앱으로 구성됩니다.
App | 프로젝트 폴더 | 설명 |
---|---|---|
메시지 앱 | src/RazorPagesTestSample | 사용자가 메시지를 추가하고, 메시지 하나를 삭제하고, 모든 메시지를 삭제하고, 메시지를 분석할 수 있도록 허용합니다(메시지당 평균 단어 수 확인). |
앱 테스트 | tests/RazorPagesTestSample.Tests | 메시지 앱의 DAL 및 인덱스 페이지 모델을 단위 테스트하는 데 사용됩니다. |
Visual Studio와 같은 IDE의 기본 제공 테스트 기능을 사용하여 테스트를 실행할 수 있습니다. Visual Studio Code 또는 명령줄을 사용하는 경우 tests/RazorPagesTestSample.Tests 폴더의 명령 프롬프트에서 다음 명령을 실행합니다.
dotnet test
메시지 앱 구성
메시지 앱은 다음과 같은 특징을 가진 Razor Pages 메시지 시스템입니다.
- 앱
Pages/Index.cshtml
(및Pages/Index.cshtml.cs
)의 인덱스 페이지는 메시지의 추가, 삭제 및 분석을 제어하는 UI 및 페이지 모델 메서드를 제공합니다(메시지당 평균 단어 수 찾기). - 메시지는 클래스()에서 두 가지
Id
속성(Data/Message.cs
키) 및Text
(메시지)로 설명Message
됩니다.Text
속성은 필수이며 200자로 제한됩니다. - 메시지는 Entity Framework의 메모리 내 데이터베이스를 사용하여 저장됩니다†.
- 앱은 데이터베이스 컨텍스트 클래스
AppDbContext
()에 DAL을Data/AppDbContext.cs
포함합니다. DAL 메서드는virtual
로 표시되므로 테스트에서 메서드를 모의로 사용해볼 수 있습니다. - 앱 시작 시 데이터베이스가 비어 있는 경우 메시지 저장소는 세 개의 메시지로 초기화됩니다. 이러한 시드된 메시지는 테스트에서도 사용됩니다.
†EF 항목 InMemory로 테스트에서는 MSTest를 사용하여 테스트에 메모리 내 데이터베이스를 사용하는 방법을 설명합니다. 이 항목에서는 xUnit 테스트 프레임워크를 사용합니다. 여러 테스트 프레임워크의 테스트 개념 및 테스트 구현은 비슷하지만 동일하지는 않습니다.
샘플 앱은 리포지토리 패턴을 사용하지 않고 UoW(작업 단위) 패턴의 효과적인 예가 아니지만 Razor Pages는 이 개발 패턴을 지원합니다. 자세한 내용은 인프라 지속성 계층 디자인 및 ASP.NET Core에서 컨트롤러 논리 테스트(샘플에서 리포지토리 패턴 구현)를 참조하세요.
테스트 앱 구성
테스트 앱은 tests/RazorPagesTestSample.Tests 폴더에 있는 콘솔 앱입니다.
테스트 앱 폴더 | 설명 |
---|---|
UnitTests |
|
유틸리티 | 데이터베이스가 각 테스트에 대한 기준 조건으로 다시 설정되도록 하기 위해 각 DAL 단위 테스트에 대한 새 데이터베이스 컨텍스트 옵션을 만드는 데 사용되는 TestDbContextOptions 메서드가 포함되어 있습니다. |
테스트 프레임워크는 xUnit입니다. 개체 모의 프레임워크는 Moq입니다.
DAL(데이터 액세스 계층)의 단위 테스트
메시지 앱의 AppDbContext
클래스(src/RazorPagesTestSample/Data/AppDbContext.cs
)에는 4가지 메서드를 포함하는 DAL이 있습니다. 테스트 앱의 각 메서드에는 하나 또는 두 개의 단위 테스트가 있습니다.
DAL 메서드 | 함수 |
---|---|
GetMessagesAsync |
Text 속성을 기준으로 정렬된 데이터베이스의 List<Message> 를 가져옵니다. |
AddMessageAsync |
데이터베이스에 Message 를 추가합니다. |
DeleteAllMessagesAsync |
데이터베이스에서 모든 Message 항목을 삭제합니다. |
DeleteMessageAsync |
데이터베이스에서 Id 별로 단일 Message 를 삭제합니다. |
각 테스트에 대해 새 AppDbContext
를 만들 때 DAL의 단위 테스트에는 DbContextOptions가 필요합니다. 각 테스트에 대해 DbContextOptions
를 만드는 한 가지 방법은 DbContextOptionsBuilder를 사용하는 것입니다.
var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase("InMemoryDb");
using (var db = new AppDbContext(optionsBuilder.Options))
{
// Use the db here in the unit test.
}
이 방법의 문제는 이전 테스트가 어떤 상태이든 관계없이 각 테스트가 데이터베이스를 수신한다는 것입니다. 서로 방해하지 않는 원자 단위 테스트를 작성하려고 할 때 문제가 될 수 있습니다. AppDbContext
에서 각 테스트에 대해 강제로 새 데이터베이스 컨텍스트를 사용하도록 하려면 새 서비스 공급자를 기준으로 하는 DbContextOptions
인스턴스를 제공합니다. 테스트 앱은 해당 Utilities
클래스 메서드 TestDbContextOptions
(tests/RazorPagesTestSample.Tests/Utilities/Utilities.cs
)를 사용하여 이 작업을 수행하는 방법을 보여 줍니다.
public static DbContextOptions<AppDbContext> TestDbContextOptions()
{
// Create a new service provider to create a new in-memory database.
var serviceProvider = new ServiceCollection()
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
// Create a new options instance using an in-memory database and
// IServiceProvider that the context should resolve all of its
// services from.
var builder = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase("InMemoryDb")
.UseInternalServiceProvider(serviceProvider);
return builder.Options;
}
DAL 단위 테스트에서 DbContextOptions
를 사용하면 각 테스트는 새 데이터베이스 인스턴스에서 원자 단위로 실행할 수 있습니다.
using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
{
// Use the db here in the unit test.
}
DataAccessLayerTest
클래스(UnitTests/DataAccessLayerTest.cs
)의 각 테스트 메서드는 비슷한 정렬-실행-어설션 패턴을 따릅니다.
- 정렬: 데이터베이스가 테스트에 대해 구성되었거나 예상 결과가 정의됩니다.
- 작업: 테스트가 실행됩니다.
- 어설션: 테스트 결과가 성공했는지 확인하기 위해 어설션이 만들어집니다.
예를 들어 DeleteMessageAsync
메서드는 해당 Id
(src/RazorPagesTestSample/Data/AppDbContext.cs
)로 식별되는 단일 메시지를 제거하는 일을 담당합니다.
public async virtual Task DeleteMessageAsync(int id)
{
var message = await Messages.FindAsync(id);
if (message != null)
{
Messages.Remove(message);
await SaveChangesAsync();
}
}
이 메서드에 대한 2가지 테스트가 있습니다. 한 가지 테스트는 데이터베이스에 메시지가 있는 경우 메서드가 메시지를 삭제하는지 확인합니다. 다른 메서드는 삭제를 위해 메시지 Id
가 존재하지 않는 경우 데이터베이스가 변경되지 않는지 테스트합니다. DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound
메서드는 다음과 같습니다.
[Fact]
public async Task DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound()
{
using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
{
// Arrange
var seedMessages = AppDbContext.GetSeedingMessages();
await db.AddRangeAsync(seedMessages);
await db.SaveChangesAsync();
var recId = 1;
var expectedMessages =
seedMessages.Where(message => message.Id != recId).ToList();
// Act
await db.DeleteMessageAsync(recId);
// Assert
var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
Assert.Equal(
expectedMessages.OrderBy(m => m.Id).Select(m => m.Text),
actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
}
}
첫째, 이 메서드는 실행 단계가 준비되는 정렬 단계를 수행합니다. 시드 메시지를 가져온 후 seedMessages
에 보관합니다. 시드 메시지는 데이터베이스에 저장됩니다. Id
가 1
인 메시지는 삭제용으로 설정됩니다. DeleteMessageAsync
메서드가 실행될 때 예상되는 메시지에는 Id
가 1
인 경우를 제외한 모든 메시지가 있어야 합니다. expectedMessages
변수는 이 예상 결과를 나타냅니다.
// Arrange
var seedMessages = AppDbContext.GetSeedingMessages();
await db.AddRangeAsync(seedMessages);
await db.SaveChangesAsync();
var recId = 1;
var expectedMessages =
seedMessages.Where(message => message.Id != recId).ToList();
메서드가 작동합니다. 메서드 DeleteMessageAsync
가 다음을 1
전달하여 recId
실행됩니다.
// Act
await db.DeleteMessageAsync(recId);
마지막으로, 이 메서드는 컨텍스트에서 Messages
를 가져온 다음, expectedMessages
와 비교하여 두 항목이 같다고 어설션합니다.
// Assert
var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
Assert.Equal(
expectedMessages.OrderBy(m => m.Id).Select(m => m.Text),
actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
두 List<Message>
가 동일한지 비교하기 위해 다음이 수행됩니다.
- 메시지는
Id
별로 정렬됩니다. - 메시지 쌍의
Text
속성이 비교됩니다.
유사한 테스트 메서드인 DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound
는 존재하지 않는 메시지를 삭제하려고 시도한 결과를 확인합니다. 이 경우 데이터베이스의 예상 메시지는 DeleteMessageAsync
메서드가 실행된 후의 실제 메시지와 같아야 합니다. 데이터베이스의 내용은 다음과 같이 변경되지 않아야 합니다.
[Fact]
public async Task DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound()
{
using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
{
// Arrange
var expectedMessages = AppDbContext.GetSeedingMessages();
await db.AddRangeAsync(expectedMessages);
await db.SaveChangesAsync();
var recId = 4;
// Act
await db.DeleteMessageAsync(recId);
// Assert
var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
Assert.Equal(
expectedMessages.OrderBy(m => m.Id).Select(m => m.Text),
actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
}
}
페이지 모델 메서드의 단위 테스트
다른 단위 테스트 세트는 페이지 모델 메서드의 테스트를 담당합니다. 메시지 앱에서 인덱스 페이지 모델은 src/RazorPagesTestSample/Pages/Index.cshtml.cs
의 IndexModel
클래스에 있습니다.
페이지 모델 메서드 | 함수 |
---|---|
OnGetAsync |
GetMessagesAsync 메서드를 사용하여 UI에 대한 DAL에서 메시지를 가져옵니다. |
OnPostAddMessageAsync |
ModelState가 유효한 경우 AddMessageAsync 를 호출하여 데이터베이스에 메시지를 추가합니다. |
OnPostDeleteAllMessagesAsync |
DeleteAllMessagesAsync 를 호출하여 데이터베이스의 모든 메시지를 삭제합니다. |
OnPostDeleteMessageAsync |
DeleteMessageAsync 를 실행하여 지정된 Id 의 메시지를 삭제합니다. |
OnPostAnalyzeMessagesAsync |
하나 이상의 메시지가 데이터베이스에 있으면 메시지당 평균 단어 수를 계산합니다. |
페이지 모델 메서드는 IndexPageTests
클래스(tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs
)의 7가지 테스트를 사용하여 테스트됩니다. 테스트는 친숙한 정렬-어셜선-실행 패턴을 사용합니다. 이러한 테스트는 다음에 중점을 둡니다.
- ModelState가 잘못된 경우 메서드가 올바른 동작을 따르는지 확인합니다.
- 메서드를 확인하면 올바른 IActionResult가 생성됩니다.
- 해당 속성 값 할당에 대한 확인이 올바르게 수행됩니다.
이 테스트 그룹은 종종 DAL 메서드의 모의 버전을 만들어 페이지 모델 메서드가 실행되는 실행 단계를 위한 예상 데이터를 생성합니다. 예를 들어, AppDbContext
의 GetMessagesAsync
메서드의 모의 메서드를 만들어 출력을 생성합니다. 페이지 모델 메서드에서 이 메서드를 실행하면 모의 버전이 결과를 반환합니다. 이 데이터는 데이터베이스에서 제공되지 않습니다. 이렇게 하면 페이지 모델 테스트에서 DAL을 사용하기 위한 예측 가능하고 신뢰할 수 있는 테스트 조건이 생성됩니다.
OnGetAsync_PopulatesThePageModel_WithAListOfMessages
테스트는 페이지 모델에 대한 GetMessagesAsync
메서드의 모의 버전을 생성하는 방법을 보여 줍니다.
var mockAppDbContext = new Mock<AppDbContext>(optionsBuilder.Options);
var expectedMessages = AppDbContext.GetSeedingMessages();
mockAppDbContext.Setup(
db => db.GetMessagesAsync()).Returns(Task.FromResult(expectedMessages));
var pageModel = new IndexModel(mockAppDbContext.Object);
실행 단계에서 OnGetAsync
메서드를 수행하면 페이지 모델의 GetMessagesAsync
메서드가 호출됩니다.
단위 테스트 실행 단계(tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs
):
// Act
await pageModel.OnGetAsync();
IndexPage
페이지 모델의 OnGetAsync
메서드(src/RazorPagesTestSample/Pages/Index.cshtml.cs
):
public async Task OnGetAsync()
{
Messages = await _db.GetMessagesAsync();
}
DAL의 GetMessagesAsync
메서드는 이 메서드 호출에 대한 결과를 반환하지 않습니다. 이 메서드의 모의 버전이 결과를 반환합니다.
Assert
단계에서 실제 메시지(actualMessages
)가 페이지 모델의 Messages
속성에서 할당됩니다. 메시지가 할당될 때 형식 검사도 수행됩니다. 예상 및 실제 메시지는 해당 Text
속성과 비교됩니다. 이 테스트는 두 개의 List<Message>
인스턴스에 동일한 메시지가 포함되어 있음을 어설션합니다.
// Assert
var actualMessages = Assert.IsAssignableFrom<List<Message>>(pageModel.Messages);
Assert.Equal(
expectedMessages.OrderBy(m => m.Id).Select(m => m.Text),
actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
이 그룹의 다른 테스트에서는 DefaultHttpContext, the ModelStateDictionary, ActionContext를 포함하는 페이지 모델 개체를 만들어 PageContext
, ViewDataDictionary
및 PageContext
를 설정합니다. 이러한 기능은 테스트를 수행하는 데 유용합니다. 예를 들어, 메시지 앱은 AddModelError로 ModelState
오류를 설정하여 OnPostAddMessageAsync
가 실행될 때 유효한 PageResult가 반환되는지 확인합니다.
[Fact]
public async Task OnPostAddMessageAsync_ReturnsAPageResult_WhenModelStateIsInvalid()
{
// Arrange
var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase("InMemoryDb");
var mockAppDbContext = new Mock<AppDbContext>(optionsBuilder.Options);
var expectedMessages = AppDbContext.GetSeedingMessages();
mockAppDbContext.Setup(db => db.GetMessagesAsync()).Returns(Task.FromResult(expectedMessages));
var httpContext = new DefaultHttpContext();
var modelState = new ModelStateDictionary();
var actionContext = new ActionContext(httpContext, new RouteData(), new PageActionDescriptor(), modelState);
var modelMetadataProvider = new EmptyModelMetadataProvider();
var viewData = new ViewDataDictionary(modelMetadataProvider, modelState);
var tempData = new TempDataDictionary(httpContext, Mock.Of<ITempDataProvider>());
var pageContext = new PageContext(actionContext)
{
ViewData = viewData
};
var pageModel = new IndexModel(mockAppDbContext.Object)
{
PageContext = pageContext,
TempData = tempData,
Url = new UrlHelper(actionContext)
};
pageModel.ModelState.AddModelError("Message.Text", "The Text field is required.");
// Act
var result = await pageModel.OnPostAddMessageAsync();
// Assert
Assert.IsType<PageResult>(result);
}
추가 리소스
- dotnet 테스트 및 xUnit을 사용하여 .NET Core에서 C# 단위 테스트
- ASP.NET Core에서 컨트롤러 논리 테스트
- 코드 단위 테스트(Visual Studio)
- ASP.NET Core의 통합 테스트
- xUnit.net
- xUnit.net 시작: .NET SDK 명령줄에서 .NET Core 사용
- Moq
- Moq 빠른 시작
- JustMockLite: .NET 개발자를 위한 모의 프레임워크입니다. (Microsoft에서 유지 관리하거나 지원하지 않습니다.)
ASP.NET Core