ASP.NET Core의 Razor Pages 단위 테스트

ASP.NET Core는 Razor Pages 앱의 단위 테스트를 지원합니다. DAL(데이터 액세스 계층) 및 페이지 모델을 테스트하여 다음을 확인할 수 있습니다.

  • Razor Pages 앱의 부분은 앱 생성 중에 별도로, 하나의 단위로 함께 작동합니다.
  • 클래스 및 메서드의 책임 범위는 제한되어 있습니다.
  • 앱이 동작하는 방식에 대한 추가 설명서가 있습니다.
  • 코드 업데이트로 인해 오류가 발생하게 되는 회귀는 자동화된 빌드 및 배포 중에 발견됩니다.

이 항목에서는 Razor Pages 앱 및 단위 테스트에 대해 기본적으로 이해하고 있다고 가정합니다. Razor Pages 앱 또는 테스트 개념에 익숙하지 않은 경우 다음 항목을 참조하세요.

예제 코드 살펴보기 및 다운로드 (다운로드 방법)

샘플 프로젝트는 다음 두 앱으로 구성됩니다.

프로젝트 폴더 설명
메시지 앱 src/RazorPagesTestSample 사용자가 메시지를 추가하고, 메시지 하나를 삭제하고, 모든 메시지를 삭제하고, 메시지를 분석할 수 있도록 허용합니다(메시지당 평균 단어 수 확인).
앱 테스트 tests/RazorPagesTestSample.Tests 메시지 앱의 DAL 및 인덱스 페이지 모델을 단위 테스트하는 데 사용됩니다.

Visual Studio 또는 Mac용 Visual Studio와 같은 IDE의 기본 제공 테스트 기능을 사용하여 테스트를 실행할 수 있습니다. Visual Studio Code 또는 명령줄을 사용하는 경우 tests/RazorPagesTestSample.Tests 폴더의 명령 프롬프트에서 다음 명령을 실행합니다.

dotnet test

메시지 앱 구성

메시지 앱은 다음과 같은 특징을 가진 Razor Pages 메시지 시스템입니다.

  • 앱의 인덱스 페이지(Pages/Index.cshtmlPages/Index.cshtml.cs)는 메시지의 추가, 삭제 및 분석을 제어하기 위한 UI 및 페이지 모델 메서드를 제공합니다(메시지당 평균 단어 수 확인).
  • 메시지는 Id(키) 및 Text(메시지)의 두 가지 속성을 사용하여 Message 클래스(Data/Message.cs)에서 설명됩니다. Text 속성은 필수이며 200자로 제한됩니다.
  • 메시지는 Entity Framework의 메모리 내 데이터베이스를 사용하여 저장됩니다†.
  • 앱은 데이터베이스 컨텍스트 클래스 AppDbContext(Data/AppDbContext.cs)에 DAL을 포함합니다. DAL 메서드는 virtual로 표시되므로 테스트에서 메서드를 모의로 사용해볼 수 있습니다.
  • 앱 시작 시 데이터베이스가 비어 있는 경우 메시지 저장소는 세 개의 메시지로 초기화됩니다. 이러한 시드된 메시지는 테스트에서도 사용됩니다.

†EF 항목 InMemory로 테스트에서는 MSTest를 사용하여 테스트에 메모리 내 데이터베이스를 사용하는 방법을 설명합니다. 이 항목에서는 xUnit 테스트 프레임워크를 사용합니다. 여러 테스트 프레임워크의 테스트 개념 및 테스트 구현은 비슷하지만 동일하지는 않습니다.

샘플 앱은 리포지토리 패턴을 사용하지 않고 UoW(작업 단위) 패턴의 효과적인 예가 아니지만 Razor Pages는 이 개발 패턴을 지원합니다. 자세한 내용은 인프라 지속성 계층 디자인ASP.NET Core에서 컨트롤러 논리 테스트(샘플에서 리포지토리 패턴 구현)를 참조하세요.

테스트 앱 구성

테스트 앱은 tests/RazorPagesTestSample.Tests 폴더에 있는 콘솔 앱입니다.

테스트 앱 폴더 설명
UnitTests
  • DataAccessLayerTest.cs에는 DAL에 대한 단위 테스트가 포함되어 있습니다.
  • IndexPageTests.cs에는 인덱스 페이지 모델에 대한 단위 테스트가 포함되어 있습니다.
유틸리티 데이터베이스가 각 테스트에 대한 기준 조건으로 다시 설정되도록 하기 위해 각 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)의 각 테스트 메서드는 비슷한 정렬-실행-어설션 패턴을 따릅니다.

  1. 정렬: 데이터베이스는 테스트를 위해 구성되고 예상된 결과가 정의됩니다.
  2. 실행: 테스트가 실행됩니다.
  3. 어설션: 테스트 결과가 성공인지 여부를 확인하기 위해 어설션이 수행됩니다.

예를 들어 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에 보관합니다. 시드 메시지는 데이터베이스에 저장됩니다. Id1인 메시지는 삭제용으로 설정됩니다. DeleteMessageAsync 메서드가 실행될 때 예상되는 메시지에는 Id1인 경우를 제외한 모든 메시지가 있어야 합니다. 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 메서드는 recId1를 전달하여 실행됩니다.

// 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.csIndexModel 클래스에 있습니다.

페이지 모델 메서드 기능
OnGetAsync GetMessagesAsync 메서드를 사용하여 UI에 대한 DAL에서 메시지를 가져옵니다.
OnPostAddMessageAsync ModelState가 유효한 경우 AddMessageAsync를 호출하여 데이터베이스에 메시지를 추가합니다.
OnPostDeleteAllMessagesAsync DeleteAllMessagesAsync를 호출하여 데이터베이스의 모든 메시지를 삭제합니다.
OnPostDeleteMessageAsync DeleteMessageAsync를 실행하여 지정된 Id의 메시지를 삭제합니다.
OnPostAnalyzeMessagesAsync 하나 이상의 메시지가 데이터베이스에 있으면 메시지당 평균 단어 수를 계산합니다.

페이지 모델 메서드는 IndexPageTests 클래스(tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs)의 7가지 테스트를 사용하여 테스트됩니다. 테스트는 친숙한 정렬-어셜선-실행 패턴을 사용합니다. 이러한 테스트는 다음에 중점을 둡니다.

  • ModelState가 잘못된 경우 메서드가 올바른 동작을 따르는지 확인합니다.
  • 메서드를 확인하면 올바른 IActionResult가 생성됩니다.
  • 해당 속성 값 할당에 대한 확인이 올바르게 수행됩니다.

이 테스트 그룹은 종종 DAL 메서드의 모의 버전을 만들어 페이지 모델 메서드가 실행되는 실행 단계를 위한 예상 데이터를 생성합니다. 예를 들어, AppDbContextGetMessagesAsync 메서드의 모의 메서드를 만들어 출력을 생성합니다. 페이지 모델 메서드에서 이 메서드를 실행하면 모의 버전이 결과를 반환합니다. 이 데이터는 데이터베이스에서 제공되지 않습니다. 이렇게 하면 페이지 모델 테스트에서 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, ViewDataDictionaryPageContext를 설정합니다. 이러한 기능은 테스트를 수행하는 데 유용합니다. 예를 들어, 메시지 앱은 AddModelErrorModelState 오류를 설정하여 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 앱 또는 테스트 개념에 익숙하지 않은 경우 다음 항목을 참조하세요.

예제 코드 살펴보기 및 다운로드 (다운로드 방법)

샘플 프로젝트는 다음 두 앱으로 구성됩니다.

프로젝트 폴더 설명
메시지 앱 src/RazorPagesTestSample 사용자가 메시지를 추가하고, 메시지 하나를 삭제하고, 모든 메시지를 삭제하고, 메시지를 분석할 수 있도록 허용합니다(메시지당 평균 단어 수 확인).
앱 테스트 tests/RazorPagesTestSample.Tests 메시지 앱의 DAL 및 인덱스 페이지 모델을 단위 테스트하는 데 사용됩니다.

Visual Studio 또는 Mac용 Visual Studio와 같은 IDE의 기본 제공 테스트 기능을 사용하여 테스트를 실행할 수 있습니다. Visual Studio Code 또는 명령줄을 사용하는 경우 tests/RazorPagesTestSample.Tests 폴더의 명령 프롬프트에서 다음 명령을 실행합니다.

dotnet test

메시지 앱 구성

메시지 앱은 다음과 같은 특징을 가진 Razor Pages 메시지 시스템입니다.

  • 앱의 인덱스 페이지(Pages/Index.cshtmlPages/Index.cshtml.cs)는 메시지의 추가, 삭제 및 분석을 제어하기 위한 UI 및 페이지 모델 메서드를 제공합니다(메시지당 평균 단어 수 확인).
  • 메시지는 Id(키) 및 Text(메시지)의 두 가지 속성을 사용하여 Message 클래스(Data/Message.cs)에서 설명됩니다. Text 속성은 필수이며 200자로 제한됩니다.
  • 메시지는 Entity Framework의 메모리 내 데이터베이스를 사용하여 저장됩니다†.
  • 앱은 데이터베이스 컨텍스트 클래스 AppDbContext(Data/AppDbContext.cs)에 DAL을 포함합니다. DAL 메서드는 virtual로 표시되므로 테스트에서 메서드를 모의로 사용해볼 수 있습니다.
  • 앱 시작 시 데이터베이스가 비어 있는 경우 메시지 저장소는 세 개의 메시지로 초기화됩니다. 이러한 시드된 메시지는 테스트에서도 사용됩니다.

†EF 항목 InMemory로 테스트에서는 MSTest를 사용하여 테스트에 메모리 내 데이터베이스를 사용하는 방법을 설명합니다. 이 항목에서는 xUnit 테스트 프레임워크를 사용합니다. 여러 테스트 프레임워크의 테스트 개념 및 테스트 구현은 비슷하지만 동일하지는 않습니다.

샘플 앱은 리포지토리 패턴을 사용하지 않고 UoW(작업 단위) 패턴의 효과적인 예가 아니지만 Razor Pages는 이 개발 패턴을 지원합니다. 자세한 내용은 인프라 지속성 계층 디자인ASP.NET Core에서 컨트롤러 논리 테스트(샘플에서 리포지토리 패턴 구현)를 참조하세요.

테스트 앱 구성

테스트 앱은 tests/RazorPagesTestSample.Tests 폴더에 있는 콘솔 앱입니다.

테스트 앱 폴더 설명
UnitTests
  • DataAccessLayerTest.cs에는 DAL에 대한 단위 테스트가 포함되어 있습니다.
  • IndexPageTests.cs에는 인덱스 페이지 모델에 대한 단위 테스트가 포함되어 있습니다.
유틸리티 데이터베이스가 각 테스트에 대한 기준 조건으로 다시 설정되도록 하기 위해 각 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)의 각 테스트 메서드는 비슷한 정렬-실행-어설션 패턴을 따릅니다.

  1. 정렬: 데이터베이스는 테스트를 위해 구성되고 예상된 결과가 정의됩니다.
  2. 실행: 테스트가 실행됩니다.
  3. 어설션: 테스트 결과가 성공인지 여부를 확인하기 위해 어설션이 수행됩니다.

예를 들어 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에 보관합니다. 시드 메시지는 데이터베이스에 저장됩니다. Id1인 메시지는 삭제용으로 설정됩니다. DeleteMessageAsync 메서드가 실행될 때 예상되는 메시지에는 Id1인 경우를 제외한 모든 메시지가 있어야 합니다. 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 메서드는 recId1를 전달하여 실행됩니다.

// 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.csIndexModel 클래스에 있습니다.

페이지 모델 메서드 기능
OnGetAsync GetMessagesAsync 메서드를 사용하여 UI에 대한 DAL에서 메시지를 가져옵니다.
OnPostAddMessageAsync ModelState가 유효한 경우 AddMessageAsync를 호출하여 데이터베이스에 메시지를 추가합니다.
OnPostDeleteAllMessagesAsync DeleteAllMessagesAsync를 호출하여 데이터베이스의 모든 메시지를 삭제합니다.
OnPostDeleteMessageAsync DeleteMessageAsync를 실행하여 지정된 Id의 메시지를 삭제합니다.
OnPostAnalyzeMessagesAsync 하나 이상의 메시지가 데이터베이스에 있으면 메시지당 평균 단어 수를 계산합니다.

페이지 모델 메서드는 IndexPageTests 클래스(tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs)의 7가지 테스트를 사용하여 테스트됩니다. 테스트는 친숙한 정렬-어셜선-실행 패턴을 사용합니다. 이러한 테스트는 다음에 중점을 둡니다.

  • ModelState가 잘못된 경우 메서드가 올바른 동작을 따르는지 확인합니다.
  • 메서드를 확인하면 올바른 IActionResult가 생성됩니다.
  • 해당 속성 값 할당에 대한 확인이 올바르게 수행됩니다.

이 테스트 그룹은 종종 DAL 메서드의 모의 버전을 만들어 페이지 모델 메서드가 실행되는 실행 단계를 위한 예상 데이터를 생성합니다. 예를 들어, AppDbContextGetMessagesAsync 메서드의 모의 메서드를 만들어 출력을 생성합니다. 페이지 모델 메서드에서 이 메서드를 실행하면 모의 버전이 결과를 반환합니다. 이 데이터는 데이터베이스에서 제공되지 않습니다. 이렇게 하면 페이지 모델 테스트에서 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, ViewDataDictionaryPageContext를 설정합니다. 이러한 기능은 테스트를 수행하는 데 유용합니다. 예를 들어, 메시지 앱은 AddModelErrorModelState 오류를 설정하여 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);
}

추가 자료