다음을 통해 공유


ASP.NET Web API 2의 유닛 테스트 컨트롤러

이 항목에서는 Web API 2의 단위 테스트 컨트롤러에 대한 몇 가지 특정 기술에 대해 설명합니다. 이 항목을 읽기 전에 솔루션에 단위 테스트 프로젝트를 추가하는 방법을 보여 주는 자습서 Unit Testing ASP.NET Web API 2를 읽을 수 있습니다.

자습서에서 사용되는 소프트웨어 버전

참고

Moq를 사용했지만 모든 모의 프레임워크에도 동일한 아이디어가 적용됩니다. Moq 4.5.30 이상에서는 Visual Studio 2017, Roslyn 및 .NET 4.5 이상 버전을 지원합니다.

단위 테스트의 일반적인 패턴은 "arrange-act-assert"입니다.

  • 정렬: 테스트를 실행하기 위한 필수 구성 요소를 설정합니다.
  • 작업: 테스트를 수행합니다.
  • 어설션: 테스트가 성공했는지 확인합니다.

정렬 단계에서는 종종 모의 개체 또는 스텁 개체를 사용합니다. 종속성 수를 최소화하므로 테스트는 한 가지 테스트에 중점을 줍니다.

다음은 Web API 컨트롤러에서 단위 테스트해야 하는 몇 가지 사항입니다.

  • 작업은 올바른 유형의 응답을 반환합니다.
  • 잘못된 매개 변수가 올바른 오류 응답을 반환합니다.
  • 작업은 리포지토리 또는 서비스 계층에서 올바른 메서드를 호출합니다.
  • 응답에 도메인 모델이 포함된 경우 모델 유형을 확인합니다.

이러한 항목은 테스트할 일반적인 몇 가지 사항이지만 세부 사항은 컨트롤러 구현에 따라 달라집니다. 특히 컨트롤러 작업이 HttpResponseMessage 또는 IHttpActionResult를 반환하는지에 따라 큰 차이가 있습니다. 이러한 결과 형식에 대한 자세한 내용은 Web Api 2의 작업 결과를 참조하세요.

HttpResponseMessage를 반환하는 테스트 작업

다음은 작업이 HttpResponseMessage를 반환하는 컨트롤러의 예입니다.

public class ProductsController : ApiController
{
    IProductRepository _repository;

    public ProductsController(IProductRepository repository)
    {
        _repository = repository;
    }

    public HttpResponseMessage Get(int id)
    {
        Product product = _repository.GetById(id);
        if (product == null)
        {
            return Request.CreateResponse(HttpStatusCode.NotFound);
        }
        return Request.CreateResponse(product);
    }

    public HttpResponseMessage Post(Product product)
    {
        _repository.Add(product);

        var response = Request.CreateResponse(HttpStatusCode.Created, product);
        string uri = Url.Link("DefaultApi", new { id = product.Id });
        response.Headers.Location = new Uri(uri);

        return response;
    }
}

컨트롤러는 종속성 주입을 사용하여 를 삽입합니다 IProductRepository. 따라서 모의 리포지토리를 삽입할 수 있으므로 컨트롤러를 더 테스트할 수 있습니다. 다음 단위 테스트는 메서드가 응답 본문에 GetProduct 쓰는지 확인합니다. 이 모 repositoryIProductRepository의 라고 가정합니다.

[TestMethod]
public void GetReturnsProduct()
{
    // Arrange
    var controller = new ProductsController(repository);
    controller.Request = new HttpRequestMessage();
    controller.Configuration = new HttpConfiguration();

    // Act
    var response = controller.Get(10);

    // Assert
    Product product;
    Assert.IsTrue(response.TryGetContentValue<Product>(out product));
    Assert.AreEqual(10, product.Id);
}

컨트롤러에서 요청구성 을 설정하는 것이 중요합니다. 그렇지 않으면 ArgumentNullException 또는 InvalidOperationException으로 테스트가 실패합니다.

메서드는 PostUrlHelper.Link 호출하여 응답에 링크를 만듭니다. 이렇게 하려면 단위 테스트에서 좀 더 설정해야 합니다.

[TestMethod]
public void PostSetsLocationHeader()
{
    // Arrange
    ProductsController controller = new ProductsController(repository);

    controller.Request = new HttpRequestMessage { 
        RequestUri = new Uri("http://localhost/api/products") 
    };
    controller.Configuration = new HttpConfiguration();
    controller.Configuration.Routes.MapHttpRoute(
        name: "DefaultApi", 
        routeTemplate: "api/{controller}/{id}",
        defaults: new { id = RouteParameter.Optional });

    controller.RequestContext.RouteData = new HttpRouteData(
        route: new HttpRoute(),
        values: new HttpRouteValueDictionary { { "controller", "products" } });

    // Act
    Product product = new Product() { Id = 42, Name = "Product1" };
    var response = controller.Post(product);

    // Assert
    Assert.AreEqual("http://localhost/api/products/42", response.Headers.Location.AbsoluteUri);
}

UrlHelper 클래스에는 요청 URL 및 경로 데이터가 필요하므로 테스트는 이러한 값에 대한 값을 설정해야 합니다. 또 다른 옵션은 모의 또는 스텁 UrlHelper입니다. 이 방법을 사용하면 ApiController.Url 의 기본값을 고정 값을 반환하는 모의 또는 스텁 버전으로 바꿉니다.

Moq 프레임워크를 사용하여 테스트를 다시 작성해 보겠습니다. Moq 테스트 프로젝트에 NuGet 패키지를 설치합니다.

[TestMethod]
public void PostSetsLocationHeader_MockVersion()
{
    // This version uses a mock UrlHelper.

    // Arrange
    ProductsController controller = new ProductsController(repository);
    controller.Request = new HttpRequestMessage();
    controller.Configuration = new HttpConfiguration();

    string locationUrl = "http://location/";

    // Create the mock and set up the Link method, which is used to create the Location header.
    // The mock version returns a fixed string.
    var mockUrlHelper = new Mock<UrlHelper>();
    mockUrlHelper.Setup(x => x.Link(It.IsAny<string>(), It.IsAny<object>())).Returns(locationUrl);
    controller.Url = mockUrlHelper.Object;

    // Act
    Product product = new Product() { Id = 42 };
    var response = controller.Post(product);

    // Assert
    Assert.AreEqual(locationUrl, response.Headers.Location.AbsoluteUri);
}

이 버전에서는 모의 UrlHelper 가 상수 문자열을 반환하므로 경로 데이터를 설정할 필요가 없습니다.

IHttpActionResult를 반환하는 테스트 작업

Web API 2에서 컨트롤러 작업은 ASP.NET MVC의 ActionResult와 유사한 IHttpActionResult를 반환할 수 있습니다. IHttpActionResult 인터페이스는 HTTP 응답을 만들기 위한 명령 패턴을 정의합니다. 컨트롤러는 응답을 직접 만드는 대신 IHttpActionResult를 반환합니다. 나중에 파이프라인은 IHttpActionResult 를 호출하여 응답을 만듭니다. 이 방법을 사용하면 HttpResponseMessage에 필요한 많은 설정을 건너뛸 수 있으므로 단위 테스트를 더 쉽게 작성할 수 있습니다.

다음은 작업이 IHttpActionResult를 반환하는 예제 컨트롤러입니다.

public class Products2Controller : ApiController
{
    IProductRepository _repository;

    public Products2Controller(IProductRepository repository)
    {
        _repository = repository;
    }

    public IHttpActionResult Get(int id)
    {
        Product product = _repository.GetById(id);
        if (product == null)
        {
            return NotFound();
        }
        return Ok(product);
    }

    public IHttpActionResult Post(Product product)
    {
        _repository.Add(product);
        return CreatedAtRoute("DefaultApi", new { id = product.Id }, product);
    }

    public IHttpActionResult Delete(int id)
    {
        _repository.Delete(id);
        return Ok();
    }

    public IHttpActionResult Put(Product product)
    {
        // Do some work (not shown).
        return Content(HttpStatusCode.Accepted, product);
    }    
}

이 예제에서는 IHttpActionResult를 사용하는 몇 가지 일반적인 패턴을 보여 줍니다. 단위 테스트 방법을 살펴보겠습니다.

작업은 응답 본문이 있는 200(OK)을 반환합니다.

Get 제품이 발견되면 메서드가 를 호출 Ok(product) 합니다. 단위 테스트에서 반환 형식이 OkNegotiatedContentResult 이고 반환된 제품에 올바른 ID가 있는지 확인합니다.

[TestMethod]
public void GetReturnsProductWithSameId()
{
    // Arrange
    var mockRepository = new Mock<IProductRepository>();
    mockRepository.Setup(x => x.GetById(42))
        .Returns(new Product { Id = 42 });

    var controller = new Products2Controller(mockRepository.Object);

    // Act
    IHttpActionResult actionResult = controller.Get(42);
    var contentResult = actionResult as OkNegotiatedContentResult<Product>;

    // Assert
    Assert.IsNotNull(contentResult);
    Assert.IsNotNull(contentResult.Content);
    Assert.AreEqual(42, contentResult.Content.Id);
}

단위 테스트는 작업 결과를 실행하지 않습니다. 작업 결과가 HTTP 응답을 올바르게 만든다고 가정할 수 있습니다. 웹 API 프레임워크에 자체 단위 테스트가 있는 이유입니다.)

Action은 404(찾을 수 없음)를 반환합니다.

메서드는 Get 제품을 찾을 수 없는 경우 를 호출 NotFound() 합니다. 이 경우 단위 테스트는 반환 형식이 NotFoundResult인지 확인합니다.

[TestMethod]
public void GetReturnsNotFound()
{
    // Arrange
    var mockRepository = new Mock<IProductRepository>();
    var controller = new Products2Controller(mockRepository.Object);

    // Act
    IHttpActionResult actionResult = controller.Get(10);

    // Assert
    Assert.IsInstanceOfType(actionResult, typeof(NotFoundResult));
}

동작은 응답 본문이 없는 200(OK)을 반환합니다.

메서드는 Delete 를 호출 Ok() 하여 빈 HTTP 200 응답을 반환합니다. 이전 예제와 마찬가지로 단위 테스트는 반환 형식(이 경우 OkResult)을 확인합니다.

[TestMethod]
public void DeleteReturnsOk()
{
    // Arrange
    var mockRepository = new Mock<IProductRepository>();
    var controller = new Products2Controller(mockRepository.Object);

    // Act
    IHttpActionResult actionResult = controller.Delete(10);

    // Assert
    Assert.IsInstanceOfType(actionResult, typeof(OkResult));
}

Action은 위치 헤더를 사용하여 201(생성됨)을 반환합니다.

메서드는 Post 를 호출 CreatedAtRoute 하여 위치 헤더에 URI를 사용하여 HTTP 201 응답을 반환합니다. 단위 테스트에서 작업이 올바른 라우팅 값을 설정하는지 확인합니다.

[TestMethod]
public void PostMethodSetsLocationHeader()
{
    // Arrange
    var mockRepository = new Mock<IProductRepository>();
    var controller = new Products2Controller(mockRepository.Object);

    // Act
    IHttpActionResult actionResult = controller.Post(new Product { Id = 10, Name = "Product1" });
    var createdResult = actionResult as CreatedAtRouteNegotiatedContentResult<Product>;

    // Assert
    Assert.IsNotNull(createdResult);
    Assert.AreEqual("DefaultApi", createdResult.RouteName);
    Assert.AreEqual(10, createdResult.RouteValues["id"]);
}

Action은 응답 본문이 있는 다른 2xx를 반환합니다.

메서드는 Put 를 호출 Content 하여 응답 본문을 사용하여 HTTP 202(수락됨) 응답을 반환합니다. 이 사례는 200(OK)을 반환하는 것과 비슷하지만 단위 테스트도 상태 코드를 검사 합니다.

[TestMethod]
public void PutReturnsContentResult()
{
    // Arrange
    var mockRepository = new Mock<IProductRepository>();
    var controller = new Products2Controller(mockRepository.Object);

    // Act
    IHttpActionResult actionResult = controller.Put(new Product { Id = 10, Name = "Product" });
    var contentResult = actionResult as NegotiatedContentResult<Product>;

    // Assert
    Assert.IsNotNull(contentResult);
    Assert.AreEqual(HttpStatusCode.Accepted, contentResult.StatusCode);
    Assert.IsNotNull(contentResult.Content);
    Assert.AreEqual(10, contentResult.Content.Id);
}

추가 리소스