ASP.NET Core의 통합 테스트
작성자: Jos van der Til, Martin Costello 및 Javier Calvarro Nelson.
통합 테스트는 앱의 구성 요소가 데이터베이스, 파일 시스템 및 네트워크를 비롯하여 앱의 지원 인프라를 포함하는 수준에서 올바르게 작동하는지 확인합니다. ASP.NET Core는 테스트 웹 호스트 및 메모리 내 테스트 서버에서 단위 테스트 프레임워크를 사용하는 통합 테스트를 지원합니다.
이 문서에서는 단위 테스트에 대한 기본적인 지식이 있다고 가정합니다. 테스트 개념을 잘 모르는 경우 .NET Core 및 .NET Standard의 단위 테스트 문서 및 연결된 콘텐츠를 참조하세요.
샘플 앱은 Razor Pages 앱이며 Razor Pages를 기본적으로 이해하고 있다고 가정합니다. Razor Pages에 익숙하지 않은 경우 다음 문서를 참조하세요.
SPA 테스트의 경우 브라우저를 자동화할 수 있는 Playwright for .NET과 같은 도구를 사용하는 것이 좋습니다.
통합 테스트 소개
통합 테스트는 단위 테스트보다 광범위한 수준에서 앱의 구성 요소를 평가합니다. 단위 테스트는 개별 클래스 메서드와 같은 격리된 소프트웨어 구성 요소를 테스트하는 데 사용됩니다. 통합 테스트는 두 개 이상의 앱 구성 요소가 함께 작동하여 요청을 완전히 처리하는 데 필요한 모든 구성 요소를 포함하여 예상되는 결과를 생성하는지 확인합니다.
이러한 광범위한 테스트는 다음과 같은 구성 요소를 포함하여 앱의 인프라 및 전체 프레임워크를 테스트하는 데 사용됩니다.
- 데이터베이스
- 파일 시스템
- 네트워크 어플라이언스
- 요청-응답 파이프라인
단위 테스트는 인프라 구성 요소 대신 가짜 또는 모의 개체로 알려져 있는 제작된 구성 요소를 사용합니다.
단위 테스트와 달리, 통합 테스트는 다음과 같습니다.
- 앱이 프로덕션 환경에서 사용하는 실제 구성 요소를 사용합니다.
- 더 많은 코드와 데이터 처리가 필요합니다.
- 실행하는 데 시간이 더 오래 걸립니다.
따라서 통합 테스트 사용은 가장 중요한 인프라 시나리오로 제한합니다. 단위 테스트 또는 통합 테스트를 사용하여 동작을 테스트할 수 있는 경우 단위 테스트를 선택합니다.
통합 테스트를 논의할 때 테스트된 프로젝트를 주로 테스트 중인 시스템 또는 간단히 "SUT"라고 합니다. 이 문서 전체에서 테스트 중인 ASP.NET Core 앱을 참조하기 위해 사용합니다.
데이터베이스 및 파일 시스템을 사용하는 데이터 및 파일 액세스의 모든 데이터 순열에 대해 통합 테스트를 작성하지 마세요. 앱에서 데이터베이스 및 파일 시스템과 상호 작용하는 위치 수에 관계없이, 주요 읽기, 쓰기, 업데이트 및 삭제 통합 테스트 세트를 통해 일반적으로 데이터베이스 및 파일 시스템 구성 요소를 적절하게 테스트할 수 있습니다. 이러한 구성 요소와 상호 작용하는 메서드 논리의 루틴 테스트를 위해 단위 테스트를 사용합니다. 단위 테스트에서 인프라 가짜 또는 모의 시험을 사용하면 테스트 실행 속도가 더 빨라집니다.
ASP.NET Core의 통합 테스트
ASP.NET Core의 통합 테스트에는 다음이 필요합니다.
- 테스트 프로젝트는 테스트를 포함하고 실행하는 데 사용됩니다. 테스트 프로젝트에는 SUT에 대한 참조가 있습니다.
- 테스트 프로젝트는 SUT에 대한 테스트 웹 호스트를 만들고 테스트 서버 클라이언트를 사용하여 SUT에서 요청 및 응답을 처리합니다.
- Test Runner는 테스트를 실행하고 테스트 결과를 보고하는 데 사용됩니다.
통합 테스트는 일반적인 정렬, 실행및 어설션 테스트 단계를 포함하는 이벤트 시퀀스를 따릅니다.
- SUT의 웹 호스트가 구성되어 있습니다.
- 앱에 요청을 제출하는 테스트 서버 클라이언트가 만들어집니다.
- 테스트 정렬 단계가 실행됩니다. 테스트 앱이 요청을 준비합니다.
- Act 테스트 단계가 실행됩니다. 클라이언트가 요청을 제출하고 응답을 받습니다.
- Assert 테스트 단계가 실행됩니다. 실제 응답은 예상 응답에 따라 통과 또는 실패로 유효성을 검사합니다.
- 모든 테스트가 실행될 때까지 프로세스가 계속됩니다.
- 테스트 결과가 보고됩니다.
일반적으로 테스트 웹 호스트는 테스트 실행을 위해 앱의 일반 웹 호스트와는 다르게 구성됩니다. 예를 들어, 다른 데이터베이스 또는 다른 앱 설정을 테스트에 사용할 수 있습니다.
테스트 웹 호스트와 메모리 내 테스트 서버(TestServer)와 같은 인프라 구성 요소는 Microsoft.AspNetCore.Mvc.Testing 패키지에서 제공되거나 관리됩니다. 이 패키지를 사용하면 테스트 생성 및 실행이 간소화됩니다.
Microsoft.AspNetCore.Mvc.Testing
패키지는 다음 작업을 처리합니다.
- SUT의 종속성 파일(
.deps
)을 테스트 프로젝트의bin
디렉터리에 복사합니다. - 테스트를 실행하면 고정 파일 및 페이지/뷰를 찾을 수 있도록 콘텐츠 루트를 SUT의 프로젝트 루트로 설정합니다.
TestServer
에서 SUT 부트스트랩을 간소화하기 위해 WebApplicationFactory 클래스를 제공합니다.
단위 테스트 설명서에서는 테스트를 실행하는 방법에 대한 자세한 지침과 테스트 및 테스트 클래스의 이름을 설정하는 방법에 대한 권장 사항을 제공하고 테스트 프로젝트 및 Test Runner를 설정하는 방법을 설명합니다.
통합 테스트에서 다른 프로젝트로 단위 테스트를 분리합니다. 테스트를 분리하는 중:
- 인프라 테스트 구성 요소가 실수로 단위 테스트에 포함되지 않도록 할 수 있습니다.
- 실행되는 테스트 집합을 제어할 수 있습니다.
Razor Pages 앱과 MVC 앱의 테스트 구성 간에는 차이가 거의 없습니다. 유일한 차이점은 테스트의 이름을 지정하는 방법 뿐입니다. Razor Pages 앱에서 페이지 엔드포인트의 테스트 이름은 일반적으로 페이지 모델 클래스의 이름을 따서 지정합니다(예: 인덱스 페이지에 대한 구성 요소 통합을 테스트하려는 경우 IndexPageTests
). MVC 앱에서 테스트는 일반적으로 컨트롤러 클래스로 구성되며 해당 이름은 테스트하는 컨트롤러의 이름을 따서 지정합니다(예: Home 컨트롤러에 대한 구성 요소 통합을 테스트하려는 경우 HomeControllerTests
).
테스트 앱 필수 조건
테스트 프로젝트는 다음을 수행해야 합니다.
Microsoft.AspNetCore.Mvc.Testing
패키지를 참조합니다.- 프로젝트 파일(
<Project Sdk="Microsoft.NET.Sdk.Web">
)에 웹 SDK를 지정합니다.
이러한 필수 구성 요소는 샘플 앱에서 볼 수 있습니다. tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj
파일을 검사합니다. 샘플 앱은 xUnit 테스트 프레임워크 및 AngleSharp 파서 라이브러리를 사용하므로 샘플 앱은 다음을 참조합니다.
xunit.runner.visualstudio
버전 2.4.2 이상을 사용하는 앱에서 테스트 프로젝트는 Microsoft.NET.Test.Sdk
패키지를 참조해야 합니다.
Entity Framework Core도 테스트에 사용됩니다. GitHub의 프로젝트 파일을 참조하세요.
SUT 환경
SUT의 환경이 설정되지 않은 경우 환경은 기본적으로 Development입니다.
기본 WebApplicationFactory를 사용하는 기본 테스트
다음 중 하나를 수행하여 암시적으로 정의된 Program
클래스를 테스트 프로젝트에 노출합니다.
웹앱의 내부 형식을 테스트 프로젝트에 노출합니다. 이 작업은 SUT 프로젝트의 파일(
.csproj
)에서 수행할 수 있습니다.<ItemGroup> <InternalsVisibleTo Include="MyTestProject" /> </ItemGroup>
partial 클래스 선언을 사용하여
Program
클래스를 퍼블릭으로 만듭니다.var builder = WebApplication.CreateBuilder(args); // ... Configure services, routes, etc. app.Run(); + public partial class Program { }
샘플 앱은
Program
partial 클래스 방법을 사용합니다.
WebApplicationFactory<TEntryPoint>는 통합 테스트에 대한 TestServer를 만드는 데 사용됩니다. TEntryPoint
는 SUT의 진입점 클래스로, 일반적으로 Program.cs
입니다.
테스트 클래스는 클래스 픽스쳐 인터페이스(IClassFixture
)를 구현하여 클래스가 테스트를 포함함을 나타내고 클래스의 테스트에서 공유 개체 인스턴스를 제공합니다.
다음 테스트 클래스 BasicTests
에서는 WebApplicationFactory
를 사용하여 SUT를 부트스트랩하고 HttpClient를 테스트 메서드 Get_EndpointsReturnSuccessAndCorrectContentType
에 제공합니다. 메서드는 응답 상태 코드가 성공(200-299)인지 Content-Type
헤더가 여러 앱 페이지에 대해 text/html; charset=utf-8
인지를 확인합니다.
CreateClient() 는 리디렉션을 자동으로 따르고 쿠키를 처리하는 인스턴스 HttpClient
를 만듭니다.
public class BasicTests
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public BasicTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/About")]
[InlineData("/Privacy")]
[InlineData("/Contact")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync(url);
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
기본적으로 일반 데이터 보호 규정 동의 정책을 사용하는 경우 필수가 아닌 쿠키는 요청 간에 유지되지 않습니다. TempData 공급자가 사용하는 쿠키와 같이 필수가 아닌 쿠키를 유지하려면 테스트에서 필수로 표시합니다. 필수로 표시하는 cookie 방법에 대한 지침은 필수 쿠키를 참조 하세요.
위조 방지 검사를 위한 AngleSharp 및 Application Parts
이 문서에서는 AngleSharp 파서를 사용하여 페이지를 로드하고 HTML을 구문 분석하여 위조 방지 검사를 처리합니다. 컨트롤러 및 Razor 페이지 보기의 엔드포인트를 브라우저에서 렌더링하는 방법을 신경 쓰지 않고 하위 수준에서 테스트하려면 Application Parts
를 사용하는 것이 좋습니다. 애플리케이션 파트 접근 방식은 필요한 값을 가져오기 위해 JSON 요청을 만드는 데 사용할 수 있는 컨트롤러 또는 Razor 페이지를 앱에 삽입합니다. 자세한 내용은 블로그 Integration Testing ASP.NET Core Application Parts를 사용하여 위조 방지로 보호되는 리소스 및 Martin Costello의 연결된 GitHub 리포지토리를 참조하세요.
WebApplicationFactory 사용자 지정
웹 호스트 구성은 하나 이상의 사용자 지정 팩터리를 만들기 위해 WebApplicationFactory<TEntryPoint>에서 상속하여 테스트 클래스와는 별개로 만들 수 있습니다.
WebApplicationFactory
에서 상속하고 ConfigureWebHost를 재정의합니다. IWebHostBuilder는IWebHostBuilder.ConfigureServices
를 사용하여 서비스 컬렉션을 구성할 수 있도록 합니다.public class CustomWebApplicationFactory<TProgram> : WebApplicationFactory<TProgram> where TProgram : class { protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureServices(services => { var dbContextDescriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>)); services.Remove(dbContextDescriptor); var dbConnectionDescriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbConnection)); services.Remove(dbConnectionDescriptor); // Create open SqliteConnection so EF won't automatically close it. services.AddSingleton<DbConnection>(container => { var connection = new SqliteConnection("DataSource=:memory:"); connection.Open(); return connection; }); services.AddDbContext<ApplicationDbContext>((container, options) => { var connection = container.GetRequiredService<DbConnection>(); options.UseSqlite(connection); }); }); builder.UseEnvironment("Development"); } }
샘플 앱의 데이터베이스 시드는
InitializeDbForTests
메서드에서 수행됩니다. 이 방법은 통합 테스트 샘플: 테스트 앱 조직 섹션에 설명되어 있습니다.SUT의 데이터베이스 컨텍스트는
Program.cs
에 등록되어 있습니다. 테스트 앱의builder.ConfigureServices
콜백은 앱의Program.cs
코드가 실행된 후에 실행됩니다. 앱의 데이터베이스와는 다른 데이터베이스를 테스트에 사용하려면 앱의 데이터베이스 컨텍스트를builder.ConfigureServices
에서 바꾸어야 합니다.샘플 앱은 데이터베이스 컨텍스트에 대한 서비스 설명자를 찾고, 이 설명자를 사용하여 서비스 등록을 제거합니다. 그런 다음, 팩터리는 테스트를 위해 메모리 내 데이터베이스를 사용하는 새
ApplicationDbContext
를 추가합니다.다른 데이터베이스에 연결하려면 .를 변경합니다
DbConnection
. SQL Server 테스트 데이터베이스를 사용하려면 다음을 수행합니다.- 프로젝트 파일에서
Microsoft.EntityFrameworkCore.SqlServer
NuGet 패키지를 참조합니다. UseInMemoryDatabase
을 호출합니다.
- 프로젝트 파일에서
테스트 클래스에서 사용자 지정
CustomWebApplicationFactory
를 사용합니다. 다음 예제에서는IndexPageTests
클래스에서 팩터리를 사용합니다.public class IndexPageTests : IClassFixture<CustomWebApplicationFactory<Program>> { private readonly HttpClient _client; private readonly CustomWebApplicationFactory<Program> _factory; public IndexPageTests( CustomWebApplicationFactory<Program> factory) { _factory = factory; _client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); }
샘플 앱 클라이언트는
HttpClient
의 다음 리디렉션을 방지하도록 구성됩니다. 모의 인증 섹션 뒷부분에 설명된 것처럼 이를 통해 테스트는 앱의 첫 번째 응답 결과를 확인할 수 있습니다. 첫 번째 응답은Location
헤더를 사용하는 이러한 테스트 대부분에서 리디렉션입니다.일반적인 테스트는
HttpClient
및 도우미 메서드를 사용하여 요청 및 응답을 처리합니다.[Fact] public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot() { // Arrange var defaultPage = await _client.GetAsync("/"); var content = await HtmlHelpers.GetDocumentAsync(defaultPage); //Act var response = await _client.SendAsync( (IHtmlFormElement)content.QuerySelector("form[id='messages']"), (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']")); // Assert Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode); Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); Assert.Equal("/", response.Headers.Location.OriginalString); }
SUT에 대한 POST 요청은 앱의 데이터 보호 위조 방지 시스템에서 자동으로 수행하는 위조 방지 검사를 충족해야 합니다. 테스트의 POST 요청을 정렬하기 위해 테스트 앱은 다음을 수행해야 합니다.
- 페이지에 대한 요청을 만듭니다.
- 위조 방지 cookie를 구문 분석하고 응답에서 유효성 검사 토큰을 요청합니다.
- 위조 방지 cookie 및 요청 유효성 검사 토큰을 사용하여 POST 요청을 수행합니다.
샘플 앱의 SendAsync
도우미 확장 메서드(Helpers/HttpClientExtensions.cs
) 및 GetDocumentAsync
도우미 메서드(Helpers/HtmlHelpers.cs
)는 AngleSharp 파서를 사용하여 다음 메서드를 통한 위조 방지 확인을 처리합니다.
GetDocumentAsync
: HttpResponseMessage를 수신하고IHtmlDocument
를 반환합니다.GetDocumentAsync
는 원본HttpResponseMessage
에 따라 가상 응답을 준비하는 팩터리를 사용합니다. 자세한 내용은 AngleSharp 설명서를 참조하세요.HttpClient
에 대한SendAsync
확장 메서드는 HttpRequestMessage를 작성하고 SendAsync(HttpRequestMessage)를 호출하여 요청을 SUT에 제출합니다. HTML 양식(IHtmlFormElement
) 및 다음을 수락하기 위한SendAsync
오버로드:- 양식(
IHtmlElement
)의 제출 단추 - 양식 값 컬렉션(
IEnumerable<KeyValuePair<string, string>>
) - 제출 단추(
IHtmlElement
) 및 양식 값(IEnumerable<KeyValuePair<string, string>>
)
- 양식(
AngleSharp는 이 문서 및 샘플 앱에서 데모용으로 사용되는 타사 구문 분석 라이브러리입니다. AngleSharp는 ASP.NET Core 앱의 통합 테스트에 대해 지원되지 않거나 필요하지 않습니다. HAP(Html Agility Pack)와 같은 기타 파서를 사용할 수 있습니다. 또 다른 방법은 위조 방지 시스템의 요청 확인 토큰과 위조 방지 cookie를 직접 처리하는 코드를 작성하는 것입니다. 자세한 내용은 이 문서에서 위조 방지 검사에 대한 AngleSharp 및 Application Parts
를 참조하세요.
EF-Core 메모리 내 데이터베이스 공급자는 제한적이고 기본적인 테스트에 사용할 수 있지만 SQLite 공급자는 메모리 내 테스트에 권장되는 선택입니다.
테스트에 사용자 지정 서비스 또는 미들웨어가 필요한 경우 유용한 시작 필터를 사용하여 IStartupFilter시작 확장 필터를 참조하세요.
WithWebHostBuilder를 사용하여 클라이언트 사용자 지정
테스트 메서드 내에서 추가 구성이 필요한 경우 WithWebHostBuilder는 구성에서 추가로 사용자 지정되는 IWebHostBuilder를 사용하여 새 WebApplicationFactory
를 만듭니다.
샘플 코드는 구성된 서비스를 테스트 스텁으로 바꾸기 위해 호출 WithWebHostBuilder
합니다. 자세한 내용 및 사용 예제는 이 문서의 모의 서비스 삽입을 참조하세요.
샘플 앱의 Post_DeleteMessageHandler_ReturnsRedirectToRoot
테스트 메서드는 WithWebHostBuilder
의 사용을 보여 줍니다. 이 테스트는 SUT에서 양식 제출을 트리거하여 데이터베이스에서 레코드 삭제를 수행합니다.
IndexPageTests
클래스의 다른 테스트는 데이터베이스의 모든 레코드를 삭제하는 작업을 수행하고 Post_DeleteMessageHandler_ReturnsRedirectToRoot
메서드 이전에 실행될 수 있기 때문에, 이 테스트 메서드에서 데이터베이스가 다시 시드되어 SUT가 삭제할 레코드가 존재하게 됩니다. SUT에서 messages
양식의 첫 번째 삭제 단추를 선택하면 SUT에 대한 요청에서 시뮬레이트됩니다.
[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
// Arrange
using (var scope = _factory.Services.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<ApplicationDbContext>();
Utilities.ReinitializeDbForTests(db);
}
var defaultPage = await _client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
//Act
var response = await _client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("form[id='messages']")
.QuerySelector("div[class='panel-body']")
.QuerySelector("button"));
// Assert
Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.OriginalString);
}
클라이언트 옵션
HttpClient
인스턴스를 만들 때 기본값 및 사용 가능한 옵션은 WebApplicationFactoryClientOptions 페이지를 참조하세요.
WebApplicationFactoryClientOptions
클래스를 만들고 CreateClient() 메서드에 전달합니다.
public class IndexPageTests :
IClassFixture<CustomWebApplicationFactory<Program>>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory<Program>
_factory;
public IndexPageTests(
CustomWebApplicationFactory<Program> factory)
{
_factory = factory;
_client = factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
}
참고: HTTPS 리디렉션 미들웨어를 사용할 때 로그에서 HTTPS 리디렉션 경고를 방지하려면 다음을 설정합니다. BaseAddress = new Uri("https://localhost")
모의 서비스 주입
호스트 작성기에서 ConfigureTestServices에 대한 호출을 사용하여 테스트에서 서비스를 재정의할 수 있습니다. 재정의된 서비스의 범위를 테스트 자체 WithWebHostBuilder 로 지정하기 위해 이 메서드는 호스트 작성기를 검색하는 데 사용됩니다. 이 내용은 다음 테스트에서 확인할 수 있습니다.
- Get_QuoteService_ProvidesQuoteInPage
- Get_GithubProfilePageCanGetAGithubUser
- Get_SecurePageIsReturnedForAnAuthenticatedUser
샘플 SUT에는 따옴표를 반환하는 범위 지정 서비스가 포함되어 있습니다. 인덱스 페이지를 요청하면 따옴표가 인덱스 페이지의 숨겨진 필드에 포함됩니다.
Services/IQuoteService.cs
:
public interface IQuoteService
{
Task<string> GenerateQuote();
}
Services/QuoteService.cs
:
// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult<string>(
"Come on, Sarah. We've an appointment in London, " +
"and we're already 30,000 years late.");
}
}
Program.cs
:
services.AddScoped<IQuoteService, QuoteService>();
Pages/Index.cshtml.cs
:
public class IndexModel : PageModel
{
private readonly ApplicationDbContext _db;
private readonly IQuoteService _quoteService;
public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
{
_db = db;
_quoteService = quoteService;
}
[BindProperty]
public Message Message { get; set; }
public IList<Message> Messages { get; private set; }
[TempData]
public string MessageAnalysisResult { get; set; }
public string Quote { get; private set; }
public async Task OnGetAsync()
{
Messages = await _db.GetMessagesAsync();
Quote = await _quoteService.GenerateQuote();
}
Pages/Index.cs
:
<input id="quote" type="hidden" value="@Model.Quote">
다음 태그는 SUT 앱이 실행될 때 생성됩니다.
<input id="quote" type="hidden" value="Come on, Sarah. We've an appointment in
London, and we're already 30,000 years late.">
통합 테스트에서 서비스 및 따옴표 주입을 테스트하기 위해, 테스트를 통해 모의 서비스가 SUT에 주입됩니다. 모의 서비스는 앱의 QuoteService
를 TestQuoteService
이라는 테스트 앱에서 제공하는 서비스로 바꿉니다.
IntegrationTests.IndexPageTests.cs
:
// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult(
"Something's interfering with time, Mr. Scarman, " +
"and time is my business.");
}
}
ConfigureTestServices
가 호출되고 범위 지정 서비스가 등록됩니다.
[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddScoped<IQuoteService, TestQuoteService>();
});
})
.CreateClient();
//Act
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
var quoteElement = content.QuerySelector("#quote");
// Assert
Assert.Equal("Something's interfering with time, Mr. Scarman, " +
"and time is my business.", quoteElement.Attributes["value"].Value);
}
테스트 실행 중에 생성된 태그는 TestQuoteService
에서 제공하는 따옴표 텍스트를 반영하므로 어설션은 성공합니다.
<input id="quote" type="hidden" value="Something's interfering with time,
Mr. Scarman, and time is my business.">
모의 인증
AuthTests
클래스의 테스트는 보안 엔드포인트가 다음을 수행하는지 확인합니다.
- 인증되지 않은 사용자를 앱의 로그인 페이지로 리디렉션합니다.
- 인증된 사용자에 대한 콘텐츠를 반환합니다.
SUT에서 /SecurePage
페이지는 AuthorizePage 규칙을 사용하여 페이지에 AuthorizeFilter를 적용합니다. 자세한 내용은 Razor Pages 권한 부여 규칙을 참조하세요.
services.AddRazorPages(options =>
{
options.Conventions.AuthorizePage("/SecurePage");
});
Get_SecurePageRedirectsAnUnauthenticatedUser
테스트에서 WebApplicationFactoryClientOptions는 AllowAutoRedirect를 false
로 설정하여 리디렉션을 허용하지 않도록 설정됩니다.
[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
// Arrange
var client = _factory.CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
// Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.StartsWith("http://localhost/Identity/Account/Login",
response.Headers.Location.OriginalString);
}
클라이언트에서 리디렉션을 따르도록 허용하지 않으면 다음 검사를 수행할 수 있습니다.
- SUT에서 반환된 상태 코드는 로그인 페이지로 리디렉션된 후의 최종 상태 코드(HttpStatusCode.OK)가 아니라 예상된 HttpStatusCode.Redirect 결과에 따라 확인할 수 있습니다.
- 응답 헤더의
Location
헤더 값을 검토하여Location
헤더가 없는 마지막 로그인 페이지 응답이 아니라http://localhost/Identity/Account/Login
으로 시작하는지 확인합니다.
테스트 앱은 인증과 권한 부여 측면을 테스트하기 위해 ConfigureTestServices에서 AuthenticationHandler<TOptions>를 모의로 시험할 수 있습니다. 최소 시나리오는 AuthenticateResult.Success를 반환합니다.
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder)
: base(options, logger, encoder)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "TestScheme");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
TestAuthHandler
는 인증 체계가 TestScheme
로 설정된 경우 사용자를 인증하도록 호출됩니다. 여기서는 AddAuthentication
이 ConfigureTestServices
에 등록되어 있습니다. TestScheme
체계가 앱에 필요한 체계와 일치하는 것이 중요합니다. 그렇지 않으면 인증이 작동하지 않습니다.
[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication(defaultScheme: "TestScheme")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
"TestScheme", options => { });
});
})
.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false,
});
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue(scheme: "TestScheme");
//Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
WebApplicationFactoryClientOptions
에 대한 자세한 내용은 클라이언트 옵션 섹션을 참조하세요.
인증 미들웨어에 대한 기본 테스트
인증 미들웨어의 기본 테스트는 이 GitHub 리포지토리를 참조하세요. 여기에는 테스트 시나리오와 관련된 테스트 서버가 포함되어 있습니다.
환경 설정
사용자 지정 애플리케이션 팩터리에서 환경을 설정합니다.
public class CustomWebApplicationFactory<TProgram>
: WebApplicationFactory<TProgram> where TProgram : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var dbContextDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbContextOptions<ApplicationDbContext>));
services.Remove(dbContextDescriptor);
var dbConnectionDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbConnection));
services.Remove(dbConnectionDescriptor);
// Create open SqliteConnection so EF won't automatically close it.
services.AddSingleton<DbConnection>(container =>
{
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
return connection;
});
services.AddDbContext<ApplicationDbContext>((container, options) =>
{
var connection = container.GetRequiredService<DbConnection>();
options.UseSqlite(connection);
});
});
builder.UseEnvironment("Development");
}
}
테스트 인프라가 앱 콘텐츠 루트 경로를 유추하는 방법
WebApplicationFactory
생성자는 TEntryPoint
어셈블리 System.Reflection.Assembly.FullName
과 같은 키가 있는 통합 테스트를 포함하는 어셈블리에서 WebApplicationFactoryContentRootAttribute를 검색하여 앱 콘텐츠 루트 경로를 유추합니다. 올바른 키를 포함하는 특성을 찾을 수 없는 경우 WebApplicationFactory
는 솔루션 파일(.sln) 검색으로 대체되고 TEntryPoint
어셈블리 이름을 솔루션 디렉터리에 추가합니다. 앱 루트 디렉터리(콘텐츠 루트 경로)는 뷰와 콘텐츠 파일을 검색하는 데 사용됩니다.
섀도 복사 사용 안 함
섀도 복사를 수행하면 테스트가 출력 디렉터리와는 다른 디렉터리에서 실행됩니다. 테스트에서 Assembly.Location
을 기준으로 한 파일 로드를 사용하는 경우 문제가 발생하면 섀도 복사를 사용하지 않도록 설정해야 할 수 있습니다.
XUnit을 사용하는 경우 섀도 복사를 사용하지 않도록 설정하려면 올바른 구성 설정을 사용하여 테스트 프로젝트 디렉터리에 xunit.runner.json
파일을 만듭니다.
{
"shadowCopy": false
}
개체 삭제
IClassFixture
구현 테스트를 실행한 후 TestServer와 HttpClient는 xUnit이 WebApplicationFactory
를 삭제하면 삭제됩니다. 개발자가 인스턴스화한 개체를 삭제해야 하는 경우 IClassFixture
구현에서 삭제합니다. 자세한 내용은 Dispose 메서드 구현을 참조하세요.
통합 테스트 샘플
샘플 앱은 다음 두 앱으로 구성됩니다.
App | 프로젝트 디렉터리 | 설명 |
---|---|---|
메시지 앱(SUT) | src/RazorPagesProject |
사용자가 메시지를 추가, 메시지 1개 또는 모든 메시지를 삭제, 메시지를 분석할 수 있도록 허용합니다. |
앱 테스트 | tests/RazorPagesProject.Tests |
SUT에 대해 통합 테스트를 수행하는 데 사용됩니다. |
Visual Studio와 같은 IDE의 기본 제공 테스트 기능을 사용하여 테스트를 실행할 수 있습니다. Visual Studio Code 또는 명령줄을 사용하는 경우 tests/RazorPagesProject.Tests
디렉터리의 명령 프롬프트에서 다음 명령을 실행합니다.
dotnet test
메시지 앱(SUT) 조직
SUT는 다음과 같은 특징을 가진 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
데이터 액세스 계층)을 포함합니다. - 앱 시작 시 데이터베이스가 비어 있는 경우 메시지 저장소는 세 개의 메시지로 초기화됩니다.
- 앱에는 인증된 사용자만 액세스할 수 있는
/SecurePage
가 포함되어 있습니다.
†EF 문서 InMemory로 테스트에서는 MSTest를 사용하여 테스트에 메모리 내 데이터베이스를 사용하는 방법을 설명합니다. 이 항목에서는 xUnit 테스트 프레임워크를 사용합니다. 여러 테스트 프레임워크의 테스트 개념 및 테스트 구현은 비슷하지만 동일하지는 않습니다.
앱은 리포지토리 패턴을 사용하지 않고 UoW(작업 단위) 패턴의 효과적인 예가 아니지만 Razor Pages는 이러한 개발 패턴을 지원합니다. 자세한 내용은 인프라 지속성 계층 디자인 및 테스트 컨트롤러 논리(샘플에서 리포지토리 패턴 구현)를 참조하세요.
테스트 앱 구성
테스트 앱은 tests/RazorPagesProject.Tests
디렉터리에 있는 콘솔 앱입니다.
테스트 앱 디렉터리 | 설명 |
---|---|
AuthTests |
다음에 대한 테스트 메서드를 포함합니다.
|
BasicTests |
라우팅 및 콘텐츠 형식에 대한 테스트 메서드를 포함합니다. |
IntegrationTests |
사용자 지정 WebApplicationFactory 클래스를 사용하여 인덱스 페이지에 대한 통합 테스트를 포함합니다. |
Helpers/Utilities |
|
테스트 프레임워크는 xUnit입니다. 통합 테스트는 TestServer를 포함하는 Microsoft.AspNetCore.TestHost를 사용하여 수행됩니다. Microsoft.AspNetCore.Mvc.Testing
패키지를 사용하여 테스트 호스트와 테스트 서버를 구성하기 때문에 TestHost
및 TestServer
패키지에는 테스트 앱의 프로젝트 파일 또는 테스트 앱의 개발자 구성에서 직접 패키지 참조가 필요하지 않습니다.
통합 테스트에서는 일반적으로 테스트 실행 전에 데이터베이스에 작은 데이터 세트가 필요합니다. 예를 들어, 삭제 테스트는 데이터베이스 레코드 삭제를 요구하므로 삭제 요청이 성공하려면 데이터베이스에 적어도 하나 이상의 레코드가 있어야 합니다.
샘플 앱은 실행 시 사용할 수 있는 Utilities.cs
세 개의 메시지로 데이터베이스를 시드합니다.
public static void InitializeDbForTests(ApplicationDbContext db)
{
db.Messages.AddRange(GetSeedingMessages());
db.SaveChanges();
}
public static void ReinitializeDbForTests(ApplicationDbContext db)
{
db.Messages.RemoveRange(db.Messages);
InitializeDbForTests(db);
}
public static List<Message> GetSeedingMessages()
{
return new List<Message>()
{
new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
new Message(){ Text = "TEST RECORD: To the rational mind, " +
"nothing is inexplicable; only unexplained." }
};
}
SUT의 데이터베이스 컨텍스트는 Program.cs
에 등록되어 있습니다. 테스트 앱의 builder.ConfigureServices
콜백은 앱의 Program.cs
코드가 실행된 후에 실행됩니다. 다른 데이터베이스를 테스트에 사용하려면 앱의 데이터베이스 컨텍스트를 builder.ConfigureServices
에서 바꾸어야 합니다. 자세한 내용은 WebApplicationFactory 사용자 지정 섹션을 참조하세요.
추가 리소스
이 항목에서는 단위 테스트에 대한 기본적인 지식이 있다고 가정합니다. 테스트 개념을 잘 모르는 경우 ..NET Core 및 .NET Standard의 단위 테스트 항목 및 연결된 콘텐츠를 참조하세요.
샘플 앱은 Razor Pages 앱이며 Razor Pages를 기본적으로 이해하고 있다고 가정합니다. Razor Pages에 익숙하지 않은 경우 다음 항목을 참조하세요.
참고 항목
SPA 테스트의 경우 브라우저를 자동화할 수 있는 Playwright for .NET과 같은 도구를 사용하는 것이 좋습니다.
통합 테스트 소개
통합 테스트는 단위 테스트보다 광범위한 수준에서 앱의 구성 요소를 평가합니다. 단위 테스트는 개별 클래스 메서드와 같은 격리된 소프트웨어 구성 요소를 테스트하는 데 사용됩니다. 통합 테스트는 두 개 이상의 앱 구성 요소가 함께 작동하여 요청을 완전히 처리하는 데 필요한 모든 구성 요소를 포함하여 예상되는 결과를 생성하는지 확인합니다.
이러한 광범위한 테스트는 다음과 같은 구성 요소를 포함하여 앱의 인프라 및 전체 프레임워크를 테스트하는 데 사용됩니다.
- 데이터베이스
- 파일 시스템
- 네트워크 어플라이언스
- 요청-응답 파이프라인
단위 테스트는 인프라 구성 요소 대신 가짜 또는 모의 개체로 알려져 있는 제작된 구성 요소를 사용합니다.
단위 테스트와 달리, 통합 테스트는 다음과 같습니다.
- 앱이 프로덕션 환경에서 사용하는 실제 구성 요소를 사용합니다.
- 더 많은 코드와 데이터 처리가 필요합니다.
- 실행하는 데 시간이 더 오래 걸립니다.
따라서 통합 테스트 사용은 가장 중요한 인프라 시나리오로 제한합니다. 단위 테스트 또는 통합 테스트를 사용하여 동작을 테스트할 수 있는 경우 단위 테스트를 선택합니다.
통합 테스트를 논의할 때 테스트된 프로젝트를 주로 테스트 중인 시스템 또는 간단히 "SUT"라고 합니다. 이 문서 전체에서 테스트 중인 ASP.NET Core 앱을 참조하기 위해 사용합니다.
데이터베이스 및 파일 시스템을 사용하는 데이터 및 파일 액세스의 모든 데이터 순열에 대해 통합 테스트를 작성하지 마세요. 앱에서 데이터베이스 및 파일 시스템과 상호 작용하는 위치 수에 관계없이, 주요 읽기, 쓰기, 업데이트 및 삭제 통합 테스트 세트를 통해 일반적으로 데이터베이스 및 파일 시스템 구성 요소를 적절하게 테스트할 수 있습니다. 이러한 구성 요소와 상호 작용하는 메서드 논리의 루틴 테스트를 위해 단위 테스트를 사용합니다. 단위 테스트에서 인프라 가짜 또는 모의 시험을 사용하면 테스트 실행 속도가 더 빨라집니다.
ASP.NET Core의 통합 테스트
ASP.NET Core의 통합 테스트에는 다음이 필요합니다.
- 테스트 프로젝트는 테스트를 포함하고 실행하는 데 사용됩니다. 테스트 프로젝트에는 SUT에 대한 참조가 있습니다.
- 테스트 프로젝트는 SUT에 대한 테스트 웹 호스트를 만들고 테스트 서버 클라이언트를 사용하여 SUT에서 요청 및 응답을 처리합니다.
- Test Runner는 테스트를 실행하고 테스트 결과를 보고하는 데 사용됩니다.
통합 테스트는 일반적인 정렬, 실행및 어설션 테스트 단계를 포함하는 이벤트 시퀀스를 따릅니다.
- SUT의 웹 호스트가 구성되어 있습니다.
- 앱에 요청을 제출하는 테스트 서버 클라이언트가 만들어집니다.
- 테스트 정렬 단계가 실행됩니다. 테스트 앱이 요청을 준비합니다.
- Act 테스트 단계가 실행됩니다. 클라이언트가 요청을 제출하고 응답을 받습니다.
- Assert 테스트 단계가 실행됩니다. 실제 응답은 예상 응답에 따라 통과 또는 실패로 유효성을 검사합니다.
- 모든 테스트가 실행될 때까지 프로세스가 계속됩니다.
- 테스트 결과가 보고됩니다.
일반적으로 테스트 웹 호스트는 테스트 실행을 위해 앱의 일반 웹 호스트와는 다르게 구성됩니다. 예를 들어, 다른 데이터베이스 또는 다른 앱 설정을 테스트에 사용할 수 있습니다.
테스트 웹 호스트와 메모리 내 테스트 서버(TestServer)와 같은 인프라 구성 요소는 Microsoft.AspNetCore.Mvc.Testing 패키지에서 제공되거나 관리됩니다. 이 패키지를 사용하면 테스트 생성 및 실행이 간소화됩니다.
Microsoft.AspNetCore.Mvc.Testing
패키지는 다음 작업을 처리합니다.
- SUT의 종속성 파일(
.deps
)을 테스트 프로젝트의bin
디렉터리에 복사합니다. - 테스트를 실행하면 고정 파일 및 페이지/뷰를 찾을 수 있도록 콘텐츠 루트를 SUT의 프로젝트 루트로 설정합니다.
TestServer
에서 SUT 부트스트랩을 간소화하기 위해 WebApplicationFactory 클래스를 제공합니다.
단위 테스트 설명서에서는 테스트를 실행하는 방법에 대한 자세한 지침과 테스트 및 테스트 클래스의 이름을 설정하는 방법에 대한 권장 사항을 제공하고 테스트 프로젝트 및 Test Runner를 설정하는 방법을 설명합니다.
통합 테스트에서 다른 프로젝트로 단위 테스트를 분리합니다. 테스트를 분리하는 중:
- 인프라 테스트 구성 요소가 실수로 단위 테스트에 포함되지 않도록 할 수 있습니다.
- 실행되는 테스트 집합을 제어할 수 있습니다.
Razor Pages 앱과 MVC 앱의 테스트 구성 간에는 차이가 거의 없습니다. 유일한 차이점은 테스트의 이름을 지정하는 방법 뿐입니다. Razor Pages 앱에서 페이지 엔드포인트의 테스트 이름은 일반적으로 페이지 모델 클래스의 이름을 따서 지정합니다(예: 인덱스 페이지에 대한 구성 요소 통합을 테스트하려는 경우 IndexPageTests
). MVC 앱에서 테스트는 일반적으로 컨트롤러 클래스로 구성되며 해당 이름은 테스트하는 컨트롤러의 이름을 따서 지정합니다(예: Home 컨트롤러에 대한 구성 요소 통합을 테스트하려는 경우 HomeControllerTests
).
테스트 앱 필수 조건
테스트 프로젝트는 다음을 수행해야 합니다.
Microsoft.AspNetCore.Mvc.Testing
패키지를 참조합니다.- 프로젝트 파일(
<Project Sdk="Microsoft.NET.Sdk.Web">
)에 웹 SDK를 지정합니다.
이러한 필수 구성 요소는 샘플 앱에서 볼 수 있습니다. tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj
파일을 검사합니다. 샘플 앱은 xUnit 테스트 프레임워크 및 AngleSharp 파서 라이브러리를 사용하므로 샘플 앱은 다음을 참조합니다.
xunit.runner.visualstudio
버전 2.4.2 이상을 사용하는 앱에서 테스트 프로젝트는 Microsoft.NET.Test.Sdk
패키지를 참조해야 합니다.
Entity Framework Core도 테스트에 사용됩니다. 앱 참조:
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
Microsoft.AspNetCore.Identity.EntityFrameworkCore
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.InMemory
Microsoft.EntityFrameworkCore.Tools
SUT 환경
SUT의 환경이 설정되지 않은 경우 환경은 기본적으로 Development입니다.
기본 WebApplicationFactory를 사용하는 기본 테스트
WebApplicationFactory<TEntryPoint>는 통합 테스트에 대한 TestServer를 만드는 데 사용됩니다. TEntryPoint
는 SUT의 진입점 클래스로, 일반적으로 Startup
클래스입니다.
테스트 클래스는 클래스 픽스쳐 인터페이스(IClassFixture
)를 구현하여 클래스가 테스트를 포함함을 나타내고 클래스의 테스트에서 공유 개체 인스턴스를 제공합니다.
다음 테스트 클래스 BasicTests
에서는 WebApplicationFactory
를 사용하여 SUT를 부트스트랩하고 HttpClient를 테스트 메서드 Get_EndpointsReturnSuccessAndCorrectContentType
에 제공합니다. 메서드는 응답 상태 코드가 성공(200-299 범위의 상태 코드)인지 Content-Type
헤더가 여러 앱 페이지에 대해 text/html; charset=utf-8
인지를 확인합니다.
CreateClient() 는 리디렉션을 자동으로 따르고 쿠키를 처리하는 인스턴스 HttpClient
를 만듭니다.
public class BasicTests
: IClassFixture<WebApplicationFactory<RazorPagesProject.Startup>>
{
private readonly WebApplicationFactory<RazorPagesProject.Startup> _factory;
public BasicTests(WebApplicationFactory<RazorPagesProject.Startup> factory)
{
_factory = factory;
}
[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/About")]
[InlineData("/Privacy")]
[InlineData("/Contact")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync(url);
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
기본적으로 GDPR 동의 정책을 사용하도록 설정한 경우에는 필요하지 않은 쿠키가 요청에서 유지되지 않습니다. TempData 공급자가 사용하는 쿠키와 같이 필수가 아닌 쿠키를 유지하려면 테스트에서 필수로 표시합니다. 필수로 표시하는 cookie 방법에 대한 지침은 필수 쿠키를 참조 하세요.
WebApplicationFactory 사용자 지정
웹 호스트 구성은 하나 이상의 사용자 지정 팩터리를 만들기 위해 WebApplicationFactory
에서 상속하여 테스트 클래스와는 별개로 만들 수 있습니다.
WebApplicationFactory
에서 상속하고 ConfigureWebHost를 재정의합니다. IWebHostBuilder는 ConfigureServices를 사용하여 서비스 컬렉션을 구성할 수 있도록 합니다.public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup: class { protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureServices(services => { var descriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>)); services.Remove(descriptor); services.AddDbContext<ApplicationDbContext>(options => { options.UseInMemoryDatabase("InMemoryDbForTesting"); }); var sp = services.BuildServiceProvider(); using (var scope = sp.CreateScope()) { var scopedServices = scope.ServiceProvider; var db = scopedServices.GetRequiredService<ApplicationDbContext>(); var logger = scopedServices .GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>(); db.Database.EnsureCreated(); try { Utilities.InitializeDbForTests(db); } catch (Exception ex) { logger.LogError(ex, "An error occurred seeding the " + "database with test messages. Error: {Message}", ex.Message); } } }); } }
샘플 앱의 데이터베이스 시드는
InitializeDbForTests
메서드에서 수행됩니다. 이 방법은 통합 테스트 샘플: 테스트 앱 조직 섹션에 설명되어 있습니다.SUT의 데이터베이스 컨텍스트는
Startup.ConfigureServices
메서드에 등록되어 있습니다. 테스트 앱의builder.ConfigureServices
콜백은 앱의Startup.ConfigureServices
코드가 실행된 후에 실행됩니다. 실행 순서는 ASP.NET Core 3.0의 릴리스를 포함하는 일반 호스트의 호환성이 손상되는 변경에 해당합니다. 앱의 데이터베이스와는 다른 데이터베이스를 테스트에 사용하려면 앱의 데이터베이스 컨텍스트를builder.ConfigureServices
에서 바꾸어야 합니다.웹 호스트를 계속 사용하는 SUT의 경우 테스트 앱의
builder.ConfigureServices
콜백은 SUT 코드Startup.ConfigureServices
전에 실행됩니다. 테스트 앱의builder.ConfigureTestServices
콜백은 이후에 실행됩니다.샘플 앱은 데이터베이스 컨텍스트에 대한 서비스 설명자를 찾고, 이 설명자를 사용하여 서비스 등록을 제거합니다. 그런 다음, 팩터리는 테스트를 위해 메모리 내 데이터베이스를 사용하는 새
ApplicationDbContext
를 추가합니다.메모리 내 데이터베이스가 아닌 다른 데이터베이스에 연결하려면
UseInMemoryDatabase
호출을 변경하여 컨텍스트를 다른 데이터베이스에 연결합니다. SQL Server 테스트 데이터베이스를 사용하려면 다음을 수행합니다.- 프로젝트 파일에서
Microsoft.EntityFrameworkCore.SqlServer
NuGet 패키지를 참조합니다. - 데이터베이스에 대한 연결 문자열을 사용하여
UseSqlServer
를 호출합니다.
services.AddDbContext<ApplicationDbContext>((options, context) => { context.UseSqlServer( Configuration.GetConnectionString("TestingDbConnectionString")); });
- 프로젝트 파일에서
테스트 클래스에서 사용자 지정
CustomWebApplicationFactory
를 사용합니다. 다음 예제에서는IndexPageTests
클래스에서 팩터리를 사용합니다.public class IndexPageTests : IClassFixture<CustomWebApplicationFactory<RazorPagesProject.Startup>> { private readonly HttpClient _client; private readonly CustomWebApplicationFactory<RazorPagesProject.Startup> _factory; public IndexPageTests( CustomWebApplicationFactory<RazorPagesProject.Startup> factory) { _factory = factory; _client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); }
샘플 앱 클라이언트는
HttpClient
의 다음 리디렉션을 방지하도록 구성됩니다. 모의 인증 섹션 뒷부분에 설명된 것처럼 이를 통해 테스트는 앱의 첫 번째 응답 결과를 확인할 수 있습니다. 첫 번째 응답은Location
헤더를 사용하는 이러한 테스트 대부분에서 리디렉션입니다.일반적인 테스트는
HttpClient
및 도우미 메서드를 사용하여 요청 및 응답을 처리합니다.[Fact] public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot() { // Arrange var defaultPage = await _client.GetAsync("/"); var content = await HtmlHelpers.GetDocumentAsync(defaultPage); //Act var response = await _client.SendAsync( (IHtmlFormElement)content.QuerySelector("form[id='messages']"), (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']")); // Assert Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode); Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); Assert.Equal("/", response.Headers.Location.OriginalString); }
SUT에 대한 POST 요청은 앱의 데이터 보호 위조 방지 시스템에서 자동으로 수행하는 위조 방지 검사를 충족해야 합니다. 테스트의 POST 요청을 정렬하기 위해 테스트 앱은 다음을 수행해야 합니다.
- 페이지에 대한 요청을 만듭니다.
- 위조 방지 cookie를 구문 분석하고 응답에서 유효성 검사 토큰을 요청합니다.
- 위조 방지 cookie 및 요청 유효성 검사 토큰을 사용하여 POST 요청을 수행합니다.
샘플 앱의 SendAsync
도우미 확장 메서드(Helpers/HttpClientExtensions.cs
) 및 GetDocumentAsync
도우미 메서드(Helpers/HtmlHelpers.cs
)는 AngleSharp 파서를 사용하여 다음 메서드를 통한 위조 방지 확인을 처리합니다.
GetDocumentAsync
: HttpResponseMessage를 수신하고IHtmlDocument
를 반환합니다.GetDocumentAsync
는 원본HttpResponseMessage
에 따라 가상 응답을 준비하는 팩터리를 사용합니다. 자세한 내용은 AngleSharp 설명서를 참조하세요.HttpClient
에 대한SendAsync
확장 메서드는 HttpRequestMessage를 작성하고 SendAsync(HttpRequestMessage)를 호출하여 요청을 SUT에 제출합니다. HTML 양식(IHtmlFormElement
) 및 다음을 수락하기 위한SendAsync
오버로드:- 양식(
IHtmlElement
)의 제출 단추 - 양식 값 컬렉션(
IEnumerable<KeyValuePair<string, string>>
) - 제출 단추(
IHtmlElement
) 및 양식 값(IEnumerable<KeyValuePair<string, string>>
)
- 양식(
참고 항목
AngleSharp는 이 항목 및 샘플 앱에서 데모용으로 사용되는 타사 구문 분석 라이브러리입니다. AngleSharp는 ASP.NET Core 앱의 통합 테스트에 대해 지원되지 않거나 필요하지 않습니다. HAP(Html Agility Pack)와 같은 기타 파서를 사용할 수 있습니다. 또 다른 방법은 위조 방지 시스템의 요청 확인 토큰과 위조 방지 cookie를 직접 처리하는 코드를 작성하는 것입니다.
참고 항목
EF-Core 메모리 내 데이터베이스 공급자는 제한적이고 기본적인 테스트에 사용할 수 있지만 SQLite 공급자는 메모리 내 테스트에 권장되는 선택입니다.
WithWebHostBuilder를 사용하여 클라이언트 사용자 지정
테스트 메서드 내에서 추가 구성이 필요한 경우 WithWebHostBuilder는 구성에서 추가로 사용자 지정되는 IWebHostBuilder를 사용하여 새 WebApplicationFactory
를 만듭니다.
샘플 앱의 Post_DeleteMessageHandler_ReturnsRedirectToRoot
테스트 메서드는 WithWebHostBuilder
의 사용을 보여 줍니다. 이 테스트는 SUT에서 양식 제출을 트리거하여 데이터베이스에서 레코드 삭제를 수행합니다.
IndexPageTests
클래스의 다른 테스트는 데이터베이스의 모든 레코드를 삭제하는 작업을 수행하고 Post_DeleteMessageHandler_ReturnsRedirectToRoot
메서드 이전에 실행될 수 있기 때문에, 이 테스트 메서드에서 데이터베이스가 다시 시드되어 SUT가 삭제할 레코드가 존재하게 됩니다. SUT에서 messages
양식의 첫 번째 삭제 단추를 선택하면 SUT에 대한 요청에서 시뮬레이트됩니다.
[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
var serviceProvider = services.BuildServiceProvider();
using (var scope = serviceProvider.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices
.GetRequiredService<ApplicationDbContext>();
var logger = scopedServices
.GetRequiredService<ILogger<IndexPageTests>>();
try
{
Utilities.ReinitializeDbForTests(db);
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred seeding " +
"the database with test messages. Error: {Message}",
ex.Message);
}
}
});
})
.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
//Act
var response = await client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("form[id='messages']")
.QuerySelector("div[class='panel-body']")
.QuerySelector("button"));
// Assert
Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.OriginalString);
}
클라이언트 옵션
다음 표에서는 HttpClient
인스턴스를 만들 때 사용할 수 있는 기본 WebApplicationFactoryClientOptions를 보여 줍니다.
옵션 | 설명 | 기본값 |
---|---|---|
AllowAutoRedirect | HttpClient 인스턴스가 자동으로 리디렉션 응답을 따르는지 여부를 가져오거나 설정합니다. |
true |
BaseAddress | HttpClient 인스턴스의 기준 주소를 가져오거나 설정합니다. |
http://localhost |
HandleCookies | HttpClient 인스턴스에서 쿠키를 처리할지 여부를 가져오거나 설정합니다. |
true |
MaxAutomaticRedirections | HttpClient 인스턴스에서 따라야 하는 리디렉션 응답의 최대 수를 가져오거나 설정합니다. |
7 |
WebApplicationFactoryClientOptions
클래스를 만들고 CreateClient() 메서드에 전달합니다(기본값은 코드 예제에 표시됨).
// Default client option values are shown
var clientOptions = new WebApplicationFactoryClientOptions();
clientOptions.AllowAutoRedirect = true;
clientOptions.BaseAddress = new Uri("http://localhost");
clientOptions.HandleCookies = true;
clientOptions.MaxAutomaticRedirections = 7;
_client = _factory.CreateClient(clientOptions);
모의 서비스 주입
호스트 작성기에서 ConfigureTestServices에 대한 호출을 사용하여 테스트에서 서비스를 재정의할 수 있습니다. 모의 서비스를 주입하려면 SUT에 Startup.ConfigureServices
메서드를 포함하는 Startup
클래스가 있어야 합니다.
샘플 SUT에는 따옴표를 반환하는 범위 지정 서비스가 포함되어 있습니다. 인덱스 페이지를 요청하면 따옴표가 인덱스 페이지의 숨겨진 필드에 포함됩니다.
Services/IQuoteService.cs
:
public interface IQuoteService
{
Task<string> GenerateQuote();
}
Services/QuoteService.cs
:
// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult<string>(
"Come on, Sarah. We've an appointment in London, " +
"and we're already 30,000 years late.");
}
}
Startup.cs
:
services.AddScoped<IQuoteService, QuoteService>();
Pages/Index.cshtml.cs
:
public class IndexModel : PageModel
{
private readonly ApplicationDbContext _db;
private readonly IQuoteService _quoteService;
public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
{
_db = db;
_quoteService = quoteService;
}
[BindProperty]
public Message Message { get; set; }
public IList<Message> Messages { get; private set; }
[TempData]
public string MessageAnalysisResult { get; set; }
public string Quote { get; private set; }
public async Task OnGetAsync()
{
Messages = await _db.GetMessagesAsync();
Quote = await _quoteService.GenerateQuote();
}
Pages/Index.cs
:
<input id="quote" type="hidden" value="@Model.Quote">
다음 태그는 SUT 앱이 실행될 때 생성됩니다.
<input id="quote" type="hidden" value="Come on, Sarah. We've an appointment in
London, and we're already 30,000 years late.">
통합 테스트에서 서비스 및 따옴표 주입을 테스트하기 위해, 테스트를 통해 모의 서비스가 SUT에 주입됩니다. 모의 서비스는 앱의 QuoteService
를 TestQuoteService
이라는 테스트 앱에서 제공하는 서비스로 바꿉니다.
IntegrationTests.IndexPageTests.cs
:
// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult<string>(
"Something's interfering with time, Mr. Scarman, " +
"and time is my business.");
}
}
ConfigureTestServices
가 호출되고 범위 지정 서비스가 등록됩니다.
[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddScoped<IQuoteService, TestQuoteService>();
});
})
.CreateClient();
//Act
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
var quoteElement = content.QuerySelector("#quote");
// Assert
Assert.Equal("Something's interfering with time, Mr. Scarman, " +
"and time is my business.", quoteElement.Attributes["value"].Value);
}
테스트 실행 중에 생성된 태그는 TestQuoteService
에서 제공하는 따옴표 텍스트를 반영하므로 어설션은 성공합니다.
<input id="quote" type="hidden" value="Something's interfering with time,
Mr. Scarman, and time is my business.">
모의 인증
AuthTests
클래스의 테스트는 보안 엔드포인트가 다음을 수행하는지 확인합니다.
- 인증되지 않은 사용자를 앱의 로그인 페이지로 리디렉션합니다.
- 인증된 사용자에 대한 콘텐츠를 반환합니다.
SUT에서 /SecurePage
페이지는 AuthorizePage 규칙을 사용하여 페이지에 AuthorizeFilter를 적용합니다. 자세한 내용은 Razor Pages 권한 부여 규칙을 참조하세요.
services.AddRazorPages(options =>
{
options.Conventions.AuthorizePage("/SecurePage");
});
Get_SecurePageRedirectsAnUnauthenticatedUser
테스트에서 WebApplicationFactoryClientOptions는 AllowAutoRedirect를 false
로 설정하여 리디렉션을 허용하지 않도록 설정됩니다.
[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
// Arrange
var client = _factory.CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
// Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.StartsWith("http://localhost/Identity/Account/Login",
response.Headers.Location.OriginalString);
}
클라이언트에서 리디렉션을 따르도록 허용하지 않으면 다음 검사를 수행할 수 있습니다.
- SUT에서 반환된 상태 코드는 로그인 페이지로 리디렉션된 후의 최종 상태 코드(HttpStatusCode.OK)가 아니라 예상된 HttpStatusCode.Redirect 결과에 따라 확인할 수 있습니다.
- 응답 헤더의
Location
헤더 값을 검토하여Location
헤더가 없는 마지막 로그인 페이지 응답이 아니라http://localhost/Identity/Account/Login
으로 시작하는지 확인합니다.
테스트 앱은 인증과 권한 부여 측면을 테스트하기 위해 ConfigureTestServices에서 AuthenticationHandler<TOptions>를 모의로 시험할 수 있습니다. 최소 시나리오는 AuthenticateResult.Success를 반환합니다.
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "Test");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
TestAuthHandler
는 인증 체계가 Test
로 설정된 경우 사용자를 인증하도록 호출됩니다. 여기서는 AddAuthentication
이 ConfigureTestServices
에 등록되어 있습니다. Test
체계가 앱에 필요한 체계와 일치하는 것이 중요합니다. 그렇지 않으면 인증이 작동하지 않습니다.
[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication("Test")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
"Test", options => {});
});
})
.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false,
});
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Test");
//Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
WebApplicationFactoryClientOptions
에 대한 자세한 내용은 클라이언트 옵션 섹션을 참조하세요.
환경 설정
기본적으로 SUT의 호스트 및 앱 환경은 개발 환경을 사용하도록 구성됩니다. IHostBuilder
를 사용하여 SUT의 환경을 재정의하는 방법은 다음과 같습니다.
ASPNETCORE_ENVIRONMENT
환경 변수(예:Staging
,Production
또는Testing
과 같은 기타 사용자 지정 값)를 설정합니다.ASPNETCORE
를 접두사로 사용하는 환경 변수를 읽도록 테스트 앱의CreateHostBuilder
를 재정의합니다.
protected override IHostBuilder CreateHostBuilder() =>
base.CreateHostBuilder()
.ConfigureHostConfiguration(
config => config.AddEnvironmentVariables("ASPNETCORE"));
SUT에서 웹 호스트(IWebHostBuilder
)를 사용하는 경우 CreateWebHostBuilder
를 재정의합니다.
protected override IWebHostBuilder CreateWebHostBuilder() =>
base.CreateWebHostBuilder().UseEnvironment("Testing");
테스트 인프라가 앱 콘텐츠 루트 경로를 유추하는 방법
WebApplicationFactory
생성자는 TEntryPoint
어셈블리 System.Reflection.Assembly.FullName
과 같은 키가 있는 통합 테스트를 포함하는 어셈블리에서 WebApplicationFactoryContentRootAttribute를 검색하여 앱 콘텐츠 루트 경로를 유추합니다. 올바른 키를 포함하는 특성을 찾을 수 없는 경우 WebApplicationFactory
는 솔루션 파일(.sln) 검색으로 대체되고 TEntryPoint
어셈블리 이름을 솔루션 디렉터리에 추가합니다. 앱 루트 디렉터리(콘텐츠 루트 경로)는 뷰와 콘텐츠 파일을 검색하는 데 사용됩니다.
섀도 복사 사용 안 함
섀도 복사를 수행하면 테스트가 출력 디렉터리와는 다른 디렉터리에서 실행됩니다. 테스트에서 Assembly.Location
을 기준으로 한 파일 로드를 사용하는 경우 문제가 발생하면 섀도 복사를 사용하지 않도록 설정해야 할 수 있습니다.
XUnit을 사용하는 경우 섀도 복사를 사용하지 않도록 설정하려면 올바른 구성 설정을 사용하여 테스트 프로젝트 디렉터리에 xunit.runner.json
파일을 만듭니다.
{
"shadowCopy": false
}
개체 삭제
IClassFixture
구현 테스트를 실행한 후 TestServer와 HttpClient는 xUnit이 WebApplicationFactory
를 삭제하면 삭제됩니다. 개발자가 인스턴스화한 개체를 삭제해야 하는 경우 IClassFixture
구현에서 삭제합니다. 자세한 내용은 Dispose 메서드 구현을 참조하세요.
통합 테스트 샘플
샘플 앱은 다음 두 앱으로 구성됩니다.
App | 프로젝트 디렉터리 | 설명 |
---|---|---|
메시지 앱(SUT) | src/RazorPagesProject |
사용자가 메시지를 추가, 메시지 1개 또는 모든 메시지를 삭제, 메시지를 분석할 수 있도록 허용합니다. |
앱 테스트 | tests/RazorPagesProject.Tests |
SUT에 대해 통합 테스트를 수행하는 데 사용됩니다. |
Visual Studio와 같은 IDE의 기본 제공 테스트 기능을 사용하여 테스트를 실행할 수 있습니다. Visual Studio Code 또는 명령줄을 사용하는 경우 tests/RazorPagesProject.Tests
디렉터리의 명령 프롬프트에서 다음 명령을 실행합니다.
dotnet test
메시지 앱(SUT) 조직
SUT는 다음과 같은 특징을 가진 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
데이터 액세스 계층)을 포함합니다. - 앱 시작 시 데이터베이스가 비어 있는 경우 메시지 저장소는 세 개의 메시지로 초기화됩니다.
- 앱에는 인증된 사용자만 액세스할 수 있는
/SecurePage
가 포함되어 있습니다.
†EF 항목 InMemory로 테스트에서는 MSTest를 사용하여 테스트에 메모리 내 데이터베이스를 사용하는 방법을 설명합니다. 이 항목에서는 xUnit 테스트 프레임워크를 사용합니다. 여러 테스트 프레임워크의 테스트 개념 및 테스트 구현은 비슷하지만 동일하지는 않습니다.
앱은 리포지토리 패턴을 사용하지 않고 UoW(작업 단위) 패턴의 효과적인 예가 아니지만 Razor Pages는 이러한 개발 패턴을 지원합니다. 자세한 내용은 인프라 지속성 계층 디자인 및 테스트 컨트롤러 논리(샘플에서 리포지토리 패턴 구현)를 참조하세요.
테스트 앱 구성
테스트 앱은 tests/RazorPagesProject.Tests
디렉터리에 있는 콘솔 앱입니다.
테스트 앱 디렉터리 | 설명 |
---|---|
AuthTests |
다음에 대한 테스트 메서드를 포함합니다.
|
BasicTests |
라우팅 및 콘텐츠 형식에 대한 테스트 메서드를 포함합니다. |
IntegrationTests |
사용자 지정 WebApplicationFactory 클래스를 사용하여 인덱스 페이지에 대한 통합 테스트를 포함합니다. |
Helpers/Utilities |
|
테스트 프레임워크는 xUnit입니다. 통합 테스트는 TestServer를 포함하는 Microsoft.AspNetCore.TestHost를 사용하여 수행됩니다. Microsoft.AspNetCore.Mvc.Testing
패키지를 사용하여 테스트 호스트와 테스트 서버를 구성하기 때문에 TestHost
및 TestServer
패키지에는 테스트 앱의 프로젝트 파일 또는 테스트 앱의 개발자 구성에서 직접 패키지 참조가 필요하지 않습니다.
통합 테스트에서는 일반적으로 테스트 실행 전에 데이터베이스에 작은 데이터 세트가 필요합니다. 예를 들어, 삭제 테스트는 데이터베이스 레코드 삭제를 요구하므로 삭제 요청이 성공하려면 데이터베이스에 적어도 하나 이상의 레코드가 있어야 합니다.
샘플 앱은 실행 시 사용할 수 있는 Utilities.cs
세 개의 메시지로 데이터베이스를 시드합니다.
public static void InitializeDbForTests(ApplicationDbContext db)
{
db.Messages.AddRange(GetSeedingMessages());
db.SaveChanges();
}
public static void ReinitializeDbForTests(ApplicationDbContext db)
{
db.Messages.RemoveRange(db.Messages);
InitializeDbForTests(db);
}
public static List<Message> GetSeedingMessages()
{
return new List<Message>()
{
new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
new Message(){ Text = "TEST RECORD: To the rational mind, " +
"nothing is inexplicable; only unexplained." }
};
}
SUT의 데이터베이스 컨텍스트는 Startup.ConfigureServices
메서드에 등록되어 있습니다. 테스트 앱의 builder.ConfigureServices
콜백은 앱의 Startup.ConfigureServices
코드가 실행된 후에 실행됩니다. 다른 데이터베이스를 테스트에 사용하려면 앱의 데이터베이스 컨텍스트를 builder.ConfigureServices
에서 바꾸어야 합니다. 자세한 내용은 WebApplicationFactory 사용자 지정 섹션을 참조하세요.
웹 호스트를 계속 사용하는 SUT의 경우 테스트 앱의 builder.ConfigureServices
콜백은 SUT 코드 Startup.ConfigureServices
전에 실행됩니다. 테스트 앱의 builder.ConfigureTestServices
콜백은 이후에 실행됩니다.
추가 리소스
이 문서에서는 단위 테스트에 대한 기본적인 지식이 있다고 가정합니다. 테스트 개념을 잘 모르는 경우 .NET Core 및 .NET Standard의 단위 테스트 문서 및 연결된 콘텐츠를 참조하세요.
샘플 앱은 Razor Pages 앱이며 Razor Pages를 기본적으로 이해하고 있다고 가정합니다. Razor Pages에 익숙하지 않은 경우 다음 문서를 참조하세요.
SPA 테스트의 경우 브라우저를 자동화할 수 있는 Playwright for .NET과 같은 도구를 사용하는 것이 좋습니다.
통합 테스트 소개
통합 테스트는 단위 테스트보다 광범위한 수준에서 앱의 구성 요소를 평가합니다. 단위 테스트는 개별 클래스 메서드와 같은 격리된 소프트웨어 구성 요소를 테스트하는 데 사용됩니다. 통합 테스트는 두 개 이상의 앱 구성 요소가 함께 작동하여 요청을 완전히 처리하는 데 필요한 모든 구성 요소를 포함하여 예상되는 결과를 생성하는지 확인합니다.
이러한 광범위한 테스트는 다음과 같은 구성 요소를 포함하여 앱의 인프라 및 전체 프레임워크를 테스트하는 데 사용됩니다.
- 데이터베이스
- 파일 시스템
- 네트워크 어플라이언스
- 요청-응답 파이프라인
단위 테스트는 인프라 구성 요소 대신 가짜 또는 모의 개체로 알려져 있는 제작된 구성 요소를 사용합니다.
단위 테스트와 달리, 통합 테스트는 다음과 같습니다.
- 앱이 프로덕션 환경에서 사용하는 실제 구성 요소를 사용합니다.
- 더 많은 코드와 데이터 처리가 필요합니다.
- 실행하는 데 시간이 더 오래 걸립니다.
따라서 통합 테스트 사용은 가장 중요한 인프라 시나리오로 제한합니다. 단위 테스트 또는 통합 테스트를 사용하여 동작을 테스트할 수 있는 경우 단위 테스트를 선택합니다.
통합 테스트를 논의할 때 테스트된 프로젝트를 주로 테스트 중인 시스템 또는 간단히 "SUT"라고 합니다. 이 문서 전체에서 테스트 중인 ASP.NET Core 앱을 참조하기 위해 사용합니다.
데이터베이스 및 파일 시스템을 사용하는 데이터 및 파일 액세스의 모든 데이터 순열에 대해 통합 테스트를 작성하지 마세요. 앱에서 데이터베이스 및 파일 시스템과 상호 작용하는 위치 수에 관계없이, 주요 읽기, 쓰기, 업데이트 및 삭제 통합 테스트 세트를 통해 일반적으로 데이터베이스 및 파일 시스템 구성 요소를 적절하게 테스트할 수 있습니다. 이러한 구성 요소와 상호 작용하는 메서드 논리의 루틴 테스트를 위해 단위 테스트를 사용합니다. 단위 테스트에서 인프라 가짜 또는 모의 시험을 사용하면 테스트 실행 속도가 더 빨라집니다.
ASP.NET Core의 통합 테스트
ASP.NET Core의 통합 테스트에는 다음이 필요합니다.
- 테스트 프로젝트는 테스트를 포함하고 실행하는 데 사용됩니다. 테스트 프로젝트에는 SUT에 대한 참조가 있습니다.
- 테스트 프로젝트는 SUT에 대한 테스트 웹 호스트를 만들고 테스트 서버 클라이언트를 사용하여 SUT에서 요청 및 응답을 처리합니다.
- Test Runner는 테스트를 실행하고 테스트 결과를 보고하는 데 사용됩니다.
통합 테스트는 일반적인 정렬, 실행및 어설션 테스트 단계를 포함하는 이벤트 시퀀스를 따릅니다.
- SUT의 웹 호스트가 구성되어 있습니다.
- 앱에 요청을 제출하는 테스트 서버 클라이언트가 만들어집니다.
- 테스트 정렬 단계가 실행됩니다. 테스트 앱이 요청을 준비합니다.
- Act 테스트 단계가 실행됩니다. 클라이언트가 요청을 제출하고 응답을 받습니다.
- Assert 테스트 단계가 실행됩니다. 실제 응답은 예상 응답에 따라 통과 또는 실패로 유효성을 검사합니다.
- 모든 테스트가 실행될 때까지 프로세스가 계속됩니다.
- 테스트 결과가 보고됩니다.
일반적으로 테스트 웹 호스트는 테스트 실행을 위해 앱의 일반 웹 호스트와는 다르게 구성됩니다. 예를 들어, 다른 데이터베이스 또는 다른 앱 설정을 테스트에 사용할 수 있습니다.
테스트 웹 호스트와 메모리 내 테스트 서버(TestServer)와 같은 인프라 구성 요소는 Microsoft.AspNetCore.Mvc.Testing 패키지에서 제공되거나 관리됩니다. 이 패키지를 사용하면 테스트 생성 및 실행이 간소화됩니다.
Microsoft.AspNetCore.Mvc.Testing
패키지는 다음 작업을 처리합니다.
- SUT의 종속성 파일(
.deps
)을 테스트 프로젝트의bin
디렉터리에 복사합니다. - 테스트를 실행하면 고정 파일 및 페이지/뷰를 찾을 수 있도록 콘텐츠 루트를 SUT의 프로젝트 루트로 설정합니다.
TestServer
에서 SUT 부트스트랩을 간소화하기 위해 WebApplicationFactory 클래스를 제공합니다.
단위 테스트 설명서에서는 테스트를 실행하는 방법에 대한 자세한 지침과 테스트 및 테스트 클래스의 이름을 설정하는 방법에 대한 권장 사항을 제공하고 테스트 프로젝트 및 Test Runner를 설정하는 방법을 설명합니다.
통합 테스트에서 다른 프로젝트로 단위 테스트를 분리합니다. 테스트를 분리하는 중:
- 인프라 테스트 구성 요소가 실수로 단위 테스트에 포함되지 않도록 할 수 있습니다.
- 실행되는 테스트 집합을 제어할 수 있습니다.
Razor Pages 앱과 MVC 앱의 테스트 구성 간에는 차이가 거의 없습니다. 유일한 차이점은 테스트의 이름을 지정하는 방법 뿐입니다. Razor Pages 앱에서 페이지 엔드포인트의 테스트 이름은 일반적으로 페이지 모델 클래스의 이름을 따서 지정합니다(예: 인덱스 페이지에 대한 구성 요소 통합을 테스트하려는 경우 IndexPageTests
). MVC 앱에서 테스트는 일반적으로 컨트롤러 클래스로 구성되며 해당 이름은 테스트하는 컨트롤러의 이름을 따서 지정합니다(예: Home 컨트롤러에 대한 구성 요소 통합을 테스트하려는 경우 HomeControllerTests
).
테스트 앱 필수 조건
테스트 프로젝트는 다음을 수행해야 합니다.
Microsoft.AspNetCore.Mvc.Testing
패키지를 참조합니다.- 프로젝트 파일(
<Project Sdk="Microsoft.NET.Sdk.Web">
)에 웹 SDK를 지정합니다.
이러한 필수 구성 요소는 샘플 앱에서 볼 수 있습니다. tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj
파일을 검사합니다. 샘플 앱은 xUnit 테스트 프레임워크 및 AngleSharp 파서 라이브러리를 사용하므로 샘플 앱은 다음을 참조합니다.
xunit.runner.visualstudio
버전 2.4.2 이상을 사용하는 앱에서 테스트 프로젝트는 Microsoft.NET.Test.Sdk
패키지를 참조해야 합니다.
Entity Framework Core도 테스트에 사용됩니다. GitHub의 프로젝트 파일을 참조하세요.
SUT 환경
SUT의 환경이 설정되지 않은 경우 환경은 기본적으로 Development입니다.
기본 WebApplicationFactory를 사용하는 기본 테스트
다음 중 하나를 수행하여 암시적으로 정의된 Program
클래스를 테스트 프로젝트에 노출합니다.
웹앱의 내부 형식을 테스트 프로젝트에 노출합니다. 이 작업은 SUT 프로젝트의 파일(
.csproj
)에서 수행할 수 있습니다.<ItemGroup> <InternalsVisibleTo Include="MyTestProject" /> </ItemGroup>
partial 클래스 선언을 사용하여
Program
클래스를 퍼블릭으로 만듭니다.var builder = WebApplication.CreateBuilder(args); // ... Configure services, routes, etc. app.Run(); + public partial class Program { }
샘플 앱은
Program
partial 클래스 방법을 사용합니다.
WebApplicationFactory<TEntryPoint>는 통합 테스트에 대한 TestServer를 만드는 데 사용됩니다. TEntryPoint
는 SUT의 진입점 클래스로, 일반적으로 Program.cs
입니다.
테스트 클래스는 클래스 픽스쳐 인터페이스(IClassFixture
)를 구현하여 클래스가 테스트를 포함함을 나타내고 클래스의 테스트에서 공유 개체 인스턴스를 제공합니다.
다음 테스트 클래스 BasicTests
에서는 WebApplicationFactory
를 사용하여 SUT를 부트스트랩하고 HttpClient를 테스트 메서드 Get_EndpointsReturnSuccessAndCorrectContentType
에 제공합니다. 메서드는 응답 상태 코드가 성공(200-299)인지 Content-Type
헤더가 여러 앱 페이지에 대해 text/html; charset=utf-8
인지를 확인합니다.
CreateClient() 는 리디렉션을 자동으로 따르고 쿠키를 처리하는 인스턴스 HttpClient
를 만듭니다.
public class BasicTests
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public BasicTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/About")]
[InlineData("/Privacy")]
[InlineData("/Contact")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync(url);
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
기본적으로 일반 데이터 보호 규정 동의 정책을 사용하는 경우 필수가 아닌 쿠키는 요청 간에 유지되지 않습니다. TempData 공급자가 사용하는 쿠키와 같이 필수가 아닌 쿠키를 유지하려면 테스트에서 필수로 표시합니다. 필수로 표시하는 cookie 방법에 대한 지침은 필수 쿠키를 참조 하세요.
위조 방지 검사를 위한 AngleSharp 및 Application Parts
이 문서에서는 AngleSharp 파서를 사용하여 페이지를 로드하고 HTML을 구문 분석하여 위조 방지 검사를 처리합니다. 컨트롤러 및 Razor 페이지 보기의 엔드포인트를 브라우저에서 렌더링하는 방법을 신경 쓰지 않고 하위 수준에서 테스트하려면 Application Parts
를 사용하는 것이 좋습니다. 애플리케이션 파트 접근 방식은 필요한 값을 가져오기 위해 JSON 요청을 만드는 데 사용할 수 있는 컨트롤러 또는 Razor 페이지를 앱에 삽입합니다. 자세한 내용은 블로그 Integration Testing ASP.NET Core Application Parts를 사용하여 위조 방지로 보호되는 리소스 및 Martin Costello의 연결된 GitHub 리포지토리를 참조하세요.
WebApplicationFactory 사용자 지정
웹 호스트 구성은 하나 이상의 사용자 지정 팩터리를 만들기 위해 WebApplicationFactory<TEntryPoint>에서 상속하여 테스트 클래스와는 별개로 만들 수 있습니다.
WebApplicationFactory
에서 상속하고 ConfigureWebHost를 재정의합니다. IWebHostBuilder는IWebHostBuilder.ConfigureServices
를 사용하여 서비스 컬렉션을 구성할 수 있도록 합니다.public class CustomWebApplicationFactory<TProgram> : WebApplicationFactory<TProgram> where TProgram : class { protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureServices(services => { var dbContextDescriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>)); services.Remove(dbContextDescriptor); var dbConnectionDescriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbConnection)); services.Remove(dbConnectionDescriptor); // Create open SqliteConnection so EF won't automatically close it. services.AddSingleton<DbConnection>(container => { var connection = new SqliteConnection("DataSource=:memory:"); connection.Open(); return connection; }); services.AddDbContext<ApplicationDbContext>((container, options) => { var connection = container.GetRequiredService<DbConnection>(); options.UseSqlite(connection); }); }); builder.UseEnvironment("Development"); } }
샘플 앱의 데이터베이스 시드는
InitializeDbForTests
메서드에서 수행됩니다. 이 방법은 통합 테스트 샘플: 테스트 앱 조직 섹션에 설명되어 있습니다.SUT의 데이터베이스 컨텍스트는
Program.cs
에 등록되어 있습니다. 테스트 앱의builder.ConfigureServices
콜백은 앱의Program.cs
코드가 실행된 후에 실행됩니다. 앱의 데이터베이스와는 다른 데이터베이스를 테스트에 사용하려면 앱의 데이터베이스 컨텍스트를builder.ConfigureServices
에서 바꾸어야 합니다.샘플 앱은 데이터베이스 컨텍스트에 대한 서비스 설명자를 찾고, 이 설명자를 사용하여 서비스 등록을 제거합니다. 그런 다음, 팩터리는 테스트를 위해 메모리 내 데이터베이스를 사용하는 새
ApplicationDbContext
를 추가합니다.다른 데이터베이스에 연결하려면 .를 변경합니다
DbConnection
. SQL Server 테스트 데이터베이스를 사용하려면 다음을 수행합니다.
- 프로젝트 파일에서
Microsoft.EntityFrameworkCore.SqlServer
NuGet 패키지를 참조합니다. UseInMemoryDatabase
을 호출합니다.
테스트 클래스에서 사용자 지정
CustomWebApplicationFactory
를 사용합니다. 다음 예제에서는IndexPageTests
클래스에서 팩터리를 사용합니다.public class IndexPageTests : IClassFixture<CustomWebApplicationFactory<Program>> { private readonly HttpClient _client; private readonly CustomWebApplicationFactory<Program> _factory; public IndexPageTests( CustomWebApplicationFactory<Program> factory) { _factory = factory; _client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); }
샘플 앱 클라이언트는
HttpClient
의 다음 리디렉션을 방지하도록 구성됩니다. 모의 인증 섹션 뒷부분에 설명된 것처럼 이를 통해 테스트는 앱의 첫 번째 응답 결과를 확인할 수 있습니다. 첫 번째 응답은Location
헤더를 사용하는 이러한 테스트 대부분에서 리디렉션입니다.일반적인 테스트는
HttpClient
및 도우미 메서드를 사용하여 요청 및 응답을 처리합니다.[Fact] public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot() { // Arrange var defaultPage = await _client.GetAsync("/"); var content = await HtmlHelpers.GetDocumentAsync(defaultPage); //Act var response = await _client.SendAsync( (IHtmlFormElement)content.QuerySelector("form[id='messages']"), (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']")); // Assert Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode); Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); Assert.Equal("/", response.Headers.Location.OriginalString); }
SUT에 대한 POST 요청은 앱의 데이터 보호 위조 방지 시스템에서 자동으로 수행하는 위조 방지 검사를 충족해야 합니다. 테스트의 POST 요청을 정렬하기 위해 테스트 앱은 다음을 수행해야 합니다.
- 페이지에 대한 요청을 만듭니다.
- 위조 방지 cookie를 구문 분석하고 응답에서 유효성 검사 토큰을 요청합니다.
- 위조 방지 cookie 및 요청 유효성 검사 토큰을 사용하여 POST 요청을 수행합니다.
샘플 앱의 SendAsync
도우미 확장 메서드(Helpers/HttpClientExtensions.cs
) 및 GetDocumentAsync
도우미 메서드(Helpers/HtmlHelpers.cs
)는 AngleSharp 파서를 사용하여 다음 메서드를 통한 위조 방지 확인을 처리합니다.
GetDocumentAsync
: HttpResponseMessage를 수신하고IHtmlDocument
를 반환합니다.GetDocumentAsync
는 원본HttpResponseMessage
에 따라 가상 응답을 준비하는 팩터리를 사용합니다. 자세한 내용은 AngleSharp 설명서를 참조하세요.HttpClient
에 대한SendAsync
확장 메서드는 HttpRequestMessage를 작성하고 SendAsync(HttpRequestMessage)를 호출하여 요청을 SUT에 제출합니다. HTML 양식(IHtmlFormElement
) 및 다음을 수락하기 위한SendAsync
오버로드:- 양식(
IHtmlElement
)의 제출 단추 - 양식 값 컬렉션(
IEnumerable<KeyValuePair<string, string>>
) - 제출 단추(
IHtmlElement
) 및 양식 값(IEnumerable<KeyValuePair<string, string>>
)
- 양식(
AngleSharp는 이 문서 및 샘플 앱에서 데모용으로 사용되는 타사 구문 분석 라이브러리입니다. AngleSharp는 ASP.NET Core 앱의 통합 테스트에 대해 지원되지 않거나 필요하지 않습니다. HAP(Html Agility Pack)와 같은 기타 파서를 사용할 수 있습니다. 또 다른 방법은 위조 방지 시스템의 요청 확인 토큰과 위조 방지 cookie를 직접 처리하는 코드를 작성하는 것입니다. 자세한 내용은 이 문서에서 위조 방지 검사에 대한 AngleSharp 및 Application Parts
를 참조하세요.
EF-Core 메모리 내 데이터베이스 공급자는 제한적이고 기본적인 테스트에 사용할 수 있지만 SQLite 공급자는 메모리 내 테스트에 권장되는 선택입니다.
테스트에 사용자 지정 서비스 또는 미들웨어가 필요한 경우 유용한 시작 필터를 사용하여 IStartupFilter시작 확장 필터를 참조하세요.
WithWebHostBuilder를 사용하여 클라이언트 사용자 지정
테스트 메서드 내에서 추가 구성이 필요한 경우 WithWebHostBuilder는 구성에서 추가로 사용자 지정되는 IWebHostBuilder를 사용하여 새 WebApplicationFactory
를 만듭니다.
샘플 코드는 구성된 서비스를 테스트 스텁으로 바꾸기 위해 호출 WithWebHostBuilder
합니다. 자세한 내용 및 사용 예제는 이 문서의 모의 서비스 삽입을 참조하세요.
샘플 앱의 Post_DeleteMessageHandler_ReturnsRedirectToRoot
테스트 메서드는 WithWebHostBuilder
의 사용을 보여 줍니다. 이 테스트는 SUT에서 양식 제출을 트리거하여 데이터베이스에서 레코드 삭제를 수행합니다.
IndexPageTests
클래스의 다른 테스트는 데이터베이스의 모든 레코드를 삭제하는 작업을 수행하고 Post_DeleteMessageHandler_ReturnsRedirectToRoot
메서드 이전에 실행될 수 있기 때문에, 이 테스트 메서드에서 데이터베이스가 다시 시드되어 SUT가 삭제할 레코드가 존재하게 됩니다. SUT에서 messages
양식의 첫 번째 삭제 단추를 선택하면 SUT에 대한 요청에서 시뮬레이트됩니다.
[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
// Arrange
using (var scope = _factory.Services.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<ApplicationDbContext>();
Utilities.ReinitializeDbForTests(db);
}
var defaultPage = await _client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
//Act
var response = await _client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("form[id='messages']")
.QuerySelector("div[class='panel-body']")
.QuerySelector("button"));
// Assert
Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.OriginalString);
}
클라이언트 옵션
HttpClient
인스턴스를 만들 때 기본값 및 사용 가능한 옵션은 WebApplicationFactoryClientOptions 페이지를 참조하세요.
WebApplicationFactoryClientOptions
클래스를 만들고 CreateClient() 메서드에 전달합니다.
public class IndexPageTests :
IClassFixture<CustomWebApplicationFactory<Program>>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory<Program>
_factory;
public IndexPageTests(
CustomWebApplicationFactory<Program> factory)
{
_factory = factory;
_client = factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
}
참고: HTTPS 리디렉션 미들웨어를 사용할 때 로그에서 HTTPS 리디렉션 경고를 방지하려면 다음을 설정합니다. BaseAddress = new Uri("https://localhost")
모의 서비스 주입
호스트 작성기에서 ConfigureTestServices에 대한 호출을 사용하여 테스트에서 서비스를 재정의할 수 있습니다. 재정의된 서비스의 범위를 테스트 자체 WithWebHostBuilder 로 지정하기 위해 이 메서드는 호스트 작성기를 검색하는 데 사용됩니다. 이 내용은 다음 테스트에서 확인할 수 있습니다.
- Get_QuoteService_ProvidesQuoteInPage
- Get_GithubProfilePageCanGetAGithubUser
- Get_SecurePageIsReturnedForAnAuthenticatedUser
샘플 SUT에는 따옴표를 반환하는 범위 지정 서비스가 포함되어 있습니다. 인덱스 페이지를 요청하면 따옴표가 인덱스 페이지의 숨겨진 필드에 포함됩니다.
Services/IQuoteService.cs
:
public interface IQuoteService
{
Task<string> GenerateQuote();
}
Services/QuoteService.cs
:
// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult<string>(
"Come on, Sarah. We've an appointment in London, " +
"and we're already 30,000 years late.");
}
}
Program.cs
:
services.AddScoped<IQuoteService, QuoteService>();
Pages/Index.cshtml.cs
:
public class IndexModel : PageModel
{
private readonly ApplicationDbContext _db;
private readonly IQuoteService _quoteService;
public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
{
_db = db;
_quoteService = quoteService;
}
[BindProperty]
public Message Message { get; set; }
public IList<Message> Messages { get; private set; }
[TempData]
public string MessageAnalysisResult { get; set; }
public string Quote { get; private set; }
public async Task OnGetAsync()
{
Messages = await _db.GetMessagesAsync();
Quote = await _quoteService.GenerateQuote();
}
Pages/Index.cs
:
<input id="quote" type="hidden" value="@Model.Quote">
다음 태그는 SUT 앱이 실행될 때 생성됩니다.
<input id="quote" type="hidden" value="Come on, Sarah. We've an appointment in
London, and we're already 30,000 years late.">
통합 테스트에서 서비스 및 따옴표 주입을 테스트하기 위해, 테스트를 통해 모의 서비스가 SUT에 주입됩니다. 모의 서비스는 앱의 QuoteService
를 TestQuoteService
이라는 테스트 앱에서 제공하는 서비스로 바꿉니다.
IntegrationTests.IndexPageTests.cs
:
// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult(
"Something's interfering with time, Mr. Scarman, " +
"and time is my business.");
}
}
ConfigureTestServices
가 호출되고 범위 지정 서비스가 등록됩니다.
[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddScoped<IQuoteService, TestQuoteService>();
});
})
.CreateClient();
//Act
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
var quoteElement = content.QuerySelector("#quote");
// Assert
Assert.Equal("Something's interfering with time, Mr. Scarman, " +
"and time is my business.", quoteElement.Attributes["value"].Value);
}
테스트 실행 중에 생성된 태그는 TestQuoteService
에서 제공하는 따옴표 텍스트를 반영하므로 어설션은 성공합니다.
<input id="quote" type="hidden" value="Something's interfering with time,
Mr. Scarman, and time is my business.">
모의 인증
AuthTests
클래스의 테스트는 보안 엔드포인트가 다음을 수행하는지 확인합니다.
- 인증되지 않은 사용자를 앱의 로그인 페이지로 리디렉션합니다.
- 인증된 사용자에 대한 콘텐츠를 반환합니다.
SUT에서 /SecurePage
페이지는 AuthorizePage 규칙을 사용하여 페이지에 AuthorizeFilter를 적용합니다. 자세한 내용은 Razor Pages 권한 부여 규칙을 참조하세요.
services.AddRazorPages(options =>
{
options.Conventions.AuthorizePage("/SecurePage");
});
Get_SecurePageRedirectsAnUnauthenticatedUser
테스트에서 WebApplicationFactoryClientOptions는 AllowAutoRedirect를 false
로 설정하여 리디렉션을 허용하지 않도록 설정됩니다.
[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
// Arrange
var client = _factory.CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
// Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.StartsWith("http://localhost/Identity/Account/Login",
response.Headers.Location.OriginalString);
}
클라이언트에서 리디렉션을 따르도록 허용하지 않으면 다음 검사를 수행할 수 있습니다.
- SUT에서 반환된 상태 코드는 로그인 페이지로 리디렉션된 후의 최종 상태 코드(HttpStatusCode.OK)가 아니라 예상된 HttpStatusCode.Redirect 결과에 따라 확인할 수 있습니다.
- 응답 헤더의
Location
헤더 값을 검토하여Location
헤더가 없는 마지막 로그인 페이지 응답이 아니라http://localhost/Identity/Account/Login
으로 시작하는지 확인합니다.
테스트 앱은 인증과 권한 부여 측면을 테스트하기 위해 ConfigureTestServices에서 AuthenticationHandler<TOptions>를 모의로 시험할 수 있습니다. 최소 시나리오는 AuthenticateResult.Success를 반환합니다.
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "TestScheme");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
TestAuthHandler
는 인증 체계가 TestScheme
로 설정된 경우 사용자를 인증하도록 호출됩니다. 여기서는 AddAuthentication
이 ConfigureTestServices
에 등록되어 있습니다. TestScheme
체계가 앱에 필요한 체계와 일치하는 것이 중요합니다. 그렇지 않으면 인증이 작동하지 않습니다.
[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication(defaultScheme: "TestScheme")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
"TestScheme", options => { });
});
})
.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false,
});
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue(scheme: "TestScheme");
//Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
WebApplicationFactoryClientOptions
에 대한 자세한 내용은 클라이언트 옵션 섹션을 참조하세요.
인증 미들웨어에 대한 기본 테스트
인증 미들웨어의 기본 테스트는 이 GitHub 리포지토리를 참조하세요. 여기에는 테스트 시나리오와 관련된 테스트 서버가 포함되어 있습니다.
환경 설정
사용자 지정 애플리케이션 팩터리에서 환경을 설정합니다.
public class CustomWebApplicationFactory<TProgram>
: WebApplicationFactory<TProgram> where TProgram : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var dbContextDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbContextOptions<ApplicationDbContext>));
services.Remove(dbContextDescriptor);
var dbConnectionDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbConnection));
services.Remove(dbConnectionDescriptor);
// Create open SqliteConnection so EF won't automatically close it.
services.AddSingleton<DbConnection>(container =>
{
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
return connection;
});
services.AddDbContext<ApplicationDbContext>((container, options) =>
{
var connection = container.GetRequiredService<DbConnection>();
options.UseSqlite(connection);
});
});
builder.UseEnvironment("Development");
}
}
테스트 인프라가 앱 콘텐츠 루트 경로를 유추하는 방법
WebApplicationFactory
생성자는 TEntryPoint
어셈블리 System.Reflection.Assembly.FullName
과 같은 키가 있는 통합 테스트를 포함하는 어셈블리에서 WebApplicationFactoryContentRootAttribute를 검색하여 앱 콘텐츠 루트 경로를 유추합니다. 올바른 키를 포함하는 특성을 찾을 수 없는 경우 WebApplicationFactory
는 솔루션 파일(.sln) 검색으로 대체되고 TEntryPoint
어셈블리 이름을 솔루션 디렉터리에 추가합니다. 앱 루트 디렉터리(콘텐츠 루트 경로)는 뷰와 콘텐츠 파일을 검색하는 데 사용됩니다.
섀도 복사 사용 안 함
섀도 복사를 수행하면 테스트가 출력 디렉터리와는 다른 디렉터리에서 실행됩니다. 테스트에서 Assembly.Location
을 기준으로 한 파일 로드를 사용하는 경우 문제가 발생하면 섀도 복사를 사용하지 않도록 설정해야 할 수 있습니다.
XUnit을 사용하는 경우 섀도 복사를 사용하지 않도록 설정하려면 올바른 구성 설정을 사용하여 테스트 프로젝트 디렉터리에 xunit.runner.json
파일을 만듭니다.
{
"shadowCopy": false
}
개체 삭제
IClassFixture
구현 테스트를 실행한 후 TestServer와 HttpClient는 xUnit이 WebApplicationFactory
를 삭제하면 삭제됩니다. 개발자가 인스턴스화한 개체를 삭제해야 하는 경우 IClassFixture
구현에서 삭제합니다. 자세한 내용은 Dispose 메서드 구현을 참조하세요.
통합 테스트 샘플
샘플 앱은 다음 두 앱으로 구성됩니다.
App | 프로젝트 디렉터리 | 설명 |
---|---|---|
메시지 앱(SUT) | src/RazorPagesProject |
사용자가 메시지를 추가, 메시지 1개 또는 모든 메시지를 삭제, 메시지를 분석할 수 있도록 허용합니다. |
앱 테스트 | tests/RazorPagesProject.Tests |
SUT에 대해 통합 테스트를 수행하는 데 사용됩니다. |
Visual Studio와 같은 IDE의 기본 제공 테스트 기능을 사용하여 테스트를 실행할 수 있습니다. Visual Studio Code 또는 명령줄을 사용하는 경우 tests/RazorPagesProject.Tests
디렉터리의 명령 프롬프트에서 다음 명령을 실행합니다.
dotnet test
메시지 앱(SUT) 조직
SUT는 다음과 같은 특징을 가진 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
데이터 액세스 계층)을 포함합니다. - 앱 시작 시 데이터베이스가 비어 있는 경우 메시지 저장소는 세 개의 메시지로 초기화됩니다.
- 앱에는 인증된 사용자만 액세스할 수 있는
/SecurePage
가 포함되어 있습니다.
†EF 문서 InMemory로 테스트에서는 MSTest를 사용하여 테스트에 메모리 내 데이터베이스를 사용하는 방법을 설명합니다. 이 항목에서는 xUnit 테스트 프레임워크를 사용합니다. 여러 테스트 프레임워크의 테스트 개념 및 테스트 구현은 비슷하지만 동일하지는 않습니다.
앱은 리포지토리 패턴을 사용하지 않고 UoW(작업 단위) 패턴의 효과적인 예가 아니지만 Razor Pages는 이러한 개발 패턴을 지원합니다. 자세한 내용은 인프라 지속성 계층 디자인 및 테스트 컨트롤러 논리(샘플에서 리포지토리 패턴 구현)를 참조하세요.
테스트 앱 구성
테스트 앱은 tests/RazorPagesProject.Tests
디렉터리에 있는 콘솔 앱입니다.
테스트 앱 디렉터리 | 설명 |
---|---|
AuthTests |
다음에 대한 테스트 메서드를 포함합니다.
|
BasicTests |
라우팅 및 콘텐츠 형식에 대한 테스트 메서드를 포함합니다. |
IntegrationTests |
사용자 지정 WebApplicationFactory 클래스를 사용하여 인덱스 페이지에 대한 통합 테스트를 포함합니다. |
Helpers/Utilities |
|
테스트 프레임워크는 xUnit입니다. 통합 테스트는 TestServer를 포함하는 Microsoft.AspNetCore.TestHost를 사용하여 수행됩니다. Microsoft.AspNetCore.Mvc.Testing
패키지를 사용하여 테스트 호스트와 테스트 서버를 구성하기 때문에 TestHost
및 TestServer
패키지에는 테스트 앱의 프로젝트 파일 또는 테스트 앱의 개발자 구성에서 직접 패키지 참조가 필요하지 않습니다.
통합 테스트에서는 일반적으로 테스트 실행 전에 데이터베이스에 작은 데이터 세트가 필요합니다. 예를 들어, 삭제 테스트는 데이터베이스 레코드 삭제를 요구하므로 삭제 요청이 성공하려면 데이터베이스에 적어도 하나 이상의 레코드가 있어야 합니다.
샘플 앱은 실행 시 사용할 수 있는 Utilities.cs
세 개의 메시지로 데이터베이스를 시드합니다.
public static void InitializeDbForTests(ApplicationDbContext db)
{
db.Messages.AddRange(GetSeedingMessages());
db.SaveChanges();
}
public static void ReinitializeDbForTests(ApplicationDbContext db)
{
db.Messages.RemoveRange(db.Messages);
InitializeDbForTests(db);
}
public static List<Message> GetSeedingMessages()
{
return new List<Message>()
{
new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
new Message(){ Text = "TEST RECORD: To the rational mind, " +
"nothing is inexplicable; only unexplained." }
};
}
SUT의 데이터베이스 컨텍스트는 Program.cs
에 등록되어 있습니다. 테스트 앱의 builder.ConfigureServices
콜백은 앱의 Program.cs
코드가 실행된 후에 실행됩니다. 다른 데이터베이스를 테스트에 사용하려면 앱의 데이터베이스 컨텍스트를 builder.ConfigureServices
에서 바꾸어야 합니다. 자세한 내용은 WebApplicationFactory 사용자 지정 섹션을 참조하세요.
추가 리소스
ASP.NET Core