xUnit을 사용하여 .NET에서 기능 테스트 구현
전제 조건:
테스트된 프로젝트는 종종 테스트 중인 시스템 또는 줄여서 "SUT"라고 합니다.
기능 테스트는 일반적인 Arrange, Act 및 Assert 테스트 단계를 포함하는 일련의 이벤트를 따릅니다.
개발을 시작해 봅시다.
다음과 같은 구조를 가져야 합니다.
테스트 폴더에서 마우스 오른쪽 버튼을 클릭합니다. Add/New Project .../Store.FunctionalTests라는 xUnit Test 프로젝트를 추가하고 대상 프레임워크로 .NET 6을 선택합니다.
Store.SharedDatabaseSetup
및 Store.WebApi
프로젝트에 대한 참조를 추가합니다.다음 패키지를 설치합니다.
ASP.NET Core 6은
Startup
클래스에 대한 필요성을 제거한 WebApplication을 도입했습니다. WebApplicationFactory
클래스 없이 Startup
로 테스트하려면 ASP.NET Core 6 앱이 암시적으로 정의된 Program
클래스를 노출해야 합니다. 따라서 Store.WebApi 프로젝트에서 부분 클래스 선언을 사용하여 Program
클래스를 공개해야 합니다....
public partial class Program { }
웹 애플리케이션을 변경한 후 테스트 프로젝트는 이제
Program
에 대해 WebApplicationFactory
클래스를 사용할 수 있습니다.WebApplicationFactory<TEntryPoint>
는 기능 테스트를 위한 TestServer를 만드는 데 사용됩니다. TEntryPoint
는 SUT의 진입점 클래스이며 일반적으로 Startup 클래스입니다.Store.FunctionalTests 프로젝트에서 CustomWebApplicationFactory 클래스를 생성합니다.
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Store.Infrastructure.Persistence.Contexts;
using Store.SharedDatabaseSetup;
using System;
using System.Linq;
namespace Store.FunctionalTests
{
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// Remove the app's StoreContext registration.
var descriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbContextOptions<StoreContext>));
if (descriptor != null)
{
services.Remove(descriptor);
}
// Add StoreContext using an in-memory database for testing.
services.AddDbContext<StoreContext>(options =>
{
options.UseInMemoryDatabase("InMemoryDbForFunctionalTesting");
});
// Get service provider.
var serviceProvider = services.BuildServiceProvider();
using (var scope = serviceProvider.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var logger = scopedServices.GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>();
var storeDbContext = scopedServices.GetRequiredService<StoreContext>();
storeDbContext.Database.EnsureCreated();
try
{
DatabaseSetup.SeedData(storeDbContext);
}
catch (Exception ex)
{
logger.LogError(ex, $"An error occurred seeding the Store database with test messages. Error: {ex.Message}");
}
}
});
}
public void CustomConfigureServices(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// Get service provider.
var serviceProvider = services.BuildServiceProvider();
using (var scope = serviceProvider.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var logger = scopedServices.GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>();
var storeDbContext = scopedServices.GetRequiredService<StoreContext>();
try
{
DatabaseSetup.SeedData(storeDbContext);
}
catch (Exception ex)
{
logger.LogError(ex, $"An error occurred seeding the Store database with test messages. Error: {ex.Message}");
}
}
});
}
}
}
CustomWebApplicationFactory
는 WebApplicationFactory
에서 상속하고 ConfigureWebHost
를 재정의합니다. IWebHostBuilder
는 ConfigureServices
를 사용하여 서비스 컬렉션을 구성할 수 있습니다.SUT의 데이터베이스 컨텍스트는
Startup.ConfigureServices
메서드에 등록됩니다. 테스트 앱의 builder.ConfigureServices
콜백은 앱의 Startup.ConfigureServices
코드가 실행된 후 실행됩니다.샘플 앱은 데이터베이스 컨텍스트에 대한 서비스 설명자를 찾고 설명자를 사용하여 서비스 등록을 제거합니다. 다음으로, 공장은 테스트를 위해 메모리 내 데이터베이스를 사용하는 새로운
StoreContext
을 추가합니다.테스트 클래스는 클래스 고정 인터페이스
(IClassFixture)
를 구현하여 클래스에 테스트가 포함되어 있음을 표시하고 클래스의 테스트 전반에 걸쳐 공유 객체 인스턴스를 제공합니다.Controller 폴더를 만들고 이 안에 BaseControllerTests 클래스를 만듭니다.
using System.Net.Http;
using Xunit;
namespace Store.FunctionalTests.Controllers
{
public class BaseControllerTests : IClassFixture<CustomWebApplicationFactory<Program>>
{
private readonly CustomWebApplicationFactory<Program> _factory;
public BaseControllerTests(CustomWebApplicationFactory<Program> factory)
{
_factory = factory;
}
public HttpClient GetNewClient()
{
var newClient = _factory.WithWebHostBuilder(builder =>
{
_factory.CustomConfigureServices(builder);
}).CreateClient();
return newClient;
}
}
}
CustomConfigureServices
클래스의 CustomWebApplicationFactory
메소드는 WithWebHostBuilder
로 클라이언트를 사용자 정의하기 위해 생성되었습니다. 다른 테스트가 데이터베이스를 변경하는 작업을 수행할 수 있고 다른 테스트보다 먼저 실행될 수 있기 때문입니다.ProductsControllerTests 클래스를 만들고 BaseControllerTests에서 확장합니다.
using Newtonsoft.Json;
using Store.ApplicationCore.DTOs;
using Store.FunctionalTests.Models;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Xunit;
namespace Store.FunctionalTests.Controllers
{
public class ProductsControllerTests : BaseControllerTests
{
public ProductsControllerTests(CustomWebApplicationFactory<Program> factory) : base(factory)
{
}
[Fact]
public async Task GetProducts_ReturnsAllRecords()
{
var client = this.GetNewClient();
var response = await client.GetAsync("/api/Products");
response.EnsureSuccessStatusCode();
var stringResponse = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<IEnumerable<ProductResponse>>(stringResponse).ToList();
var statusCode = response.StatusCode.ToString();
Assert.Equal("OK", statusCode);
Assert.True(result.Count == 10);
}
[Fact]
public async Task GetProductById_ProductExists_ReturnsCorrectProduct()
{
var productId = 5;
var client = this.GetNewClient();
var response = await client.GetAsync($"/api/Products/{productId}");
response.EnsureSuccessStatusCode();
var stringResponse = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<SingleProductResponse>(stringResponse);
var statusCode = response.StatusCode.ToString();
Assert.Equal("OK", statusCode);
Assert.Equal(productId, result.Id);
Assert.NotNull(result.Name);
Assert.True(result.Price > 0);
Assert.True(result.Stock > 0);
}
[Theory]
[InlineData(0)]
[InlineData(20)]
public async Task GetProductById_ProductDoesntExist_ReturnsNotFound(int productId)
{
var client = this.GetNewClient();
var response = await client.GetAsync($"/api/Products/{productId}");
var statusCode = response.StatusCode.ToString();
Assert.Equal("NotFound", statusCode);
}
[Fact]
public async Task PostProduct_ReturnsCreatedProduct()
{
var client = this.GetNewClient();
// Create product
var request = new CreateProductRequest
{
Description = "Description",
Name = "Test product",
Price = 25.3
};
var stringContent = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");
var response1 = await client.PostAsync("/api/Products", stringContent);
response1.EnsureSuccessStatusCode();
var stringResponse1 = await response1.Content.ReadAsStringAsync();
var createdProduct = JsonConvert.DeserializeObject<SingleProductResponse>(stringResponse1);
var statusCode1 = response1.StatusCode.ToString();
Assert.Equal("Created", statusCode1);
// Get created product
var response2 = await client.GetAsync($"/api/Products/{createdProduct.Id}");
response2.EnsureSuccessStatusCode();
var stringResponse2 = await response2.Content.ReadAsStringAsync();
var result2 = JsonConvert.DeserializeObject<SingleProductResponse>(stringResponse2);
var statusCode2 = response2.StatusCode.ToString();
Assert.Equal("OK", statusCode2);
Assert.Equal(createdProduct.Id, result2.Id);
Assert.Equal(createdProduct.Name, result2.Name);
Assert.Equal(createdProduct.Description, result2.Description);
Assert.Equal(createdProduct.Stock, result2.Stock);
}
[Fact]
public async Task PostProduct_InvalidData_ReturnsErrors()
{
var client = this.GetNewClient();
// Create product
var request = new CreateProductRequest
{
Description = "Description",
Name = null,
Price = 0
};
var stringContent = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");
var response = await client.PostAsync("/api/Products", stringContent);
var stringResponse = await response.Content.ReadAsStringAsync();
var badRequest = JsonConvert.DeserializeObject<BadRequestModel>(stringResponse);
var statusCode = response.StatusCode.ToString();
Assert.Equal("BadRequest", statusCode);
Assert.NotNull(badRequest.Title);
Assert.NotNull(badRequest.Errors);
Assert.Equal(2, badRequest.Errors.Count);
Assert.Contains(badRequest.Errors.Keys, k => k == "Name");
Assert.Contains(badRequest.Errors.Keys, k => k == "Price");
}
[Fact]
public async Task PutProduct_ReturnsUpdatedProduct()
{
var client = this.GetNewClient();
// Update product
var productId = 6;
var request = new UpdateProductRequest
{
Description = "Description",
Name = "Test product",
Price = 17.67,
Stock = 94
};
var stringContent = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");
var response1 = await client.PutAsync($"/api/Products/{productId}", stringContent);
response1.EnsureSuccessStatusCode();
var stringResponse1 = await response1.Content.ReadAsStringAsync();
var updatedProduct = JsonConvert.DeserializeObject<SingleProductResponse>(stringResponse1);
var statusCode1 = response1.StatusCode.ToString();
Assert.Equal("OK", statusCode1);
// Get updated product
var response2 = await client.GetAsync($"/api/Products/{updatedProduct.Id}");
response2.EnsureSuccessStatusCode();
var stringResponse2 = await response2.Content.ReadAsStringAsync();
var result2 = JsonConvert.DeserializeObject<SingleProductResponse>(stringResponse2);
var statusCode2 = response2.StatusCode.ToString();
Assert.Equal("OK", statusCode2);
Assert.Equal(updatedProduct.Id, result2.Id);
Assert.Equal(updatedProduct.Name, result2.Name);
Assert.Equal(updatedProduct.Description, result2.Description);
Assert.Equal(updatedProduct.Stock, result2.Stock);
}
[Fact]
public async Task DeleteProductById_ReturnsNoContent()
{
var client = this.GetNewClient();
var productId = 5;
// Delete product
var response1 = await client.DeleteAsync($"/api/Products/{productId}");
var statusCode1 = response1.StatusCode.ToString();
Assert.Equal("NoContent", statusCode1);
// Get deleted product
var response2 = await client.GetAsync($"/api/Products/{productId}");
var statusCode2 = response2.StatusCode.ToString();
Assert.Equal("NotFound", statusCode2);
}
}
}
솔루션을 마우스 오른쪽 버튼으로 클릭하고 테스트 실행을 클릭합니다.
테스트 탐색기에서 볼 수 있듯이 모든 테스트가 통과했습니다.
소스 코드here를 찾을 수 있습니다.
읽어 주셔서 감사합니다
읽어주셔서 대단히 감사합니다. 이 기사가 흥미롭고 앞으로 유용할 수 있기를 바랍니다. 토론해야 할 질문이나 아이디어가 있으면 함께 협력하고 지식을 교환할 수 있어 기쁩니다.
Reference
이 문제에 관하여(xUnit을 사용하여 .NET에서 기능 테스트 구현), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/cristofima/implement-functional-tests-on-net-with-xunit-78텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)