ASP.NET Web API 2의 유닛 테스트 컨트롤러Unit Testing Controllers in ASP.NET Web API 2

Mike Wassonby Mike Wasson

이 항목에서는 Web API 2의 유닛 테스트 컨트롤러에 대 한 몇 가지 특정 기술에 대해 설명 합니다.This topic describes some specific techniques for unit testing controllers in Web API 2. 이 항목을 읽기 전에 솔루션에 단위 테스트 프로젝트를 추가 하는 방법을 보여 주는 자습서 단위 테스트 ASP.NET Web API 2를 읽어 볼 수 있습니다.Before reading this topic, you might want to read the tutorial Unit Testing ASP.NET Web API 2, which shows how to add a unit-test project to your solution.

자습서에서 사용 되는 소프트웨어 버전Software versions used in the tutorial

Note

Moq를 사용 했지만 모든 모의 프레임 워크에 동일한 아이디어가 적용 됩니다.I used Moq, but the same idea applies to any mocking framework. Moq 4.5.30 이상 버전은 Visual Studio 2017, Roslyn 및 .NET 4.5 이상 버전을 지원 합니다.Moq 4.5.30 (and later) supports Visual Studio 2017, Roslyn and .NET 4.5 and later versions.

단위 테스트의 일반적인 패턴은 " 정렬-동작 어설션입니다 " .A common pattern in unit tests is "arrange-act-assert":

  • 정렬: 테스트를 실행 하기 위한 모든 필수 구성 요소를 설정 합니다.Arrange: Set up any prerequisites for the test to run.
  • Act: 테스트를 수행 합니다.Act: Perform the test.
  • Assert: 테스트에 성공 했는지 확인 합니다.Assert: Verify that the test succeeded.

정렬 단계에서 모의 개체 또는 스텁 개체를 사용 하는 경우가 많습니다.In the arrange step, you will often use mock or stub objects. 이렇게 하면 종속성 수가 최소화 되므로 테스트는 한 가지 테스트를 중심으로 합니다.That minimizes the number of dependencies, so the test is focused on testing one thing.

웹 API 컨트롤러에서 단위 테스트를 수행 해야 하는 몇 가지 사항은 다음과 같습니다.Here are some things that you should unit test in your Web API controllers:

  • 작업은 올바른 응답 유형을 반환 합니다.The action returns the correct type of response.
  • 잘못 된 매개 변수는 올바른 오류 응답을 반환 합니다.Invalid parameters return the correct error response.
  • 작업은 리포지토리 또는 서비스 계층에서 올바른 메서드를 호출 합니다.The action calls the correct method on the repository or service layer.
  • 응답이 도메인 모델을 포함 하는 경우 모델 유형을 확인 합니다.If the response includes a domain model, verify the model type.

이러한 항목은 몇 가지 일반적인 테스트 이지만 컨트롤러 구현에 따라 구체적입니다.These are some of the general things to test, but the specifics depend on your controller implementation. 특히 컨트롤러 작업에서 HttpResponseMessage 또는 IHttpActionResult를 반환 하는지 여부에 따라 큰 차이가 있습니다.In particular, it makes a big difference whether your controller actions return HttpResponseMessage or IHttpActionResult. 이러한 결과 형식에 대 한 자세한 내용은 Web Api 2의 작업 결과를 참조 하세요.For more information about these result types, see Action Results in Web Api 2.

HttpResponseMessage를 반환 하는 테스트 작업Testing Actions that Return HttpResponseMessage

다음은 작업에서 HttpResponseMessage를 반환 하는 컨트롤러의 예입니다.Here is an example of a controller whose actions return 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 합니다.Notice the controller uses dependency injection to inject an IProductRepository. 이렇게 하면 모의 리포지토리를 주입할 수 있으므로 컨트롤러를 더 쉽게 테스트할 수 있습니다.That makes the controller more testable, because you can inject a mock repository. 다음 단위 테스트는 Get 메서드가 응답 본문에를 기록 하는지 확인 합니다 Product .The following unit test verifies that the Get method writes a Product to the response body. repository가 모의 라고 가정 IProductRepository 합니다.Assume that repository is a mock IProductRepository.

[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);
}

컨트롤러에서 요청구성을 설정 하는 것이 중요 합니다.It's important to set Request and Configuration on the controller. 그렇지 않으면 Argumentnullexception 또는 InvalidOperationException를 사용 하 여 테스트가 실패 합니다.Otherwise, the test will fail with an ArgumentNullException or InvalidOperationException.

Post메서드는 urlhelper 링크 를 호출 하 여 응답에 링크를 만듭니다.The Post method calls UrlHelper.Link to create links in the response. 이렇게 하려면 단위 테스트에서 약간의 설정이 필요 합니다.This requires a little more setup in the unit test:

[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 및 경로 데이터를 요구 하므로 테스트에서 이러한 값을 설정 해야 합니다.The UrlHelper class needs the request URL and route data, so the test has to set values for these. 또 다른 옵션은 모의 또는 스텁 Urlhelper입니다.Another option is mock or stub UrlHelper. 이 방법을 사용 하면 ApiController 의 기본값을 고정 값을 반환 하는 모의 또는 스텁 버전으로 바꿀 수 있습니다.With this approach, you replace the default value of ApiController.Url with a mock or stub version that returns a fixed value.

Moq 프레임 워크를 사용 하 여 테스트를 다시 작성해 보겠습니다.Let's rewrite the test using the Moq framework. Moq테스트 프로젝트에 NuGet 패키지를 설치 합니다.Install the Moq NuGet package in the test project.

[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 가 상수 문자열을 반환 하므로 경로 데이터를 설정할 필요가 없습니다.In this version, you don't need to set up any route data, because the mock UrlHelper returns a constant string.

IHttpActionResult를 반환 하는 테스트 작업Testing Actions that Return IHttpActionResult

Web API 2에서 컨트롤러 동작은 ASP.NET MVC의 Actionresult 와 유사한 IHttpActionResult를 반환할 수 있습니다.In Web API 2, a controller action can return IHttpActionResult, which is analogous to ActionResult in ASP.NET MVC. IHttpActionResult 인터페이스는 HTTP 응답을 만들기 위한 명령 패턴을 정의 합니다.The IHttpActionResult interface defines a command pattern for creating HTTP responses. 컨트롤러는 직접 응답을 만드는 대신 IHttpActionResult를 반환 합니다.Instead of creating the response directly, the controller returns an IHttpActionResult. 나중에 파이프라인은 IHttpActionResult 를 호출 하 여 응답을 만듭니다.Later, the pipeline invokes the IHttpActionResult to create the response. HttpResponseMessage에 필요한 많은 설치를 건너뛸 수 있으므로이 방법을 사용 하면 단위 테스트를 보다 쉽게 작성할 수 있습니다.This approach makes it easier to write unit tests, because you can skip a lot of the setup that is needed for HttpResponseMessage.

작업에서 IHttpActionResult를 반환 하는 예제 컨트롤러는 다음과 같습니다.Here is an example controller whose actions return 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를 사용 하는 몇 가지 일반적인 패턴을 보여 줍니다.This example shows some common patterns using IHttpActionResult. 테스트를 단위 테스트 하는 방법을 알아보겠습니다.Let's see how to unit test them.

작업은 응답 본문으로 200 (OK)를 반환 합니다.Action returns 200 (OK) with a response body

Get제품이 발견 되 면 메서드는를 호출 합니다 Ok(product) .The Get method calls Ok(product) if the product is found. 단위 테스트에서 반환 형식이 Oknegotiatedcontentresult 이 고 반환 된 제품에 올바른 ID가 있는지 확인 합니다.In the unit test, make sure the return type is OkNegotiatedContentResult and the returned product has the right 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);
}

단위 테스트는 작업 결과를 실행 하지 않습니다.Notice that the unit test doesn't execute the action result. 작업 결과에서 HTTP 응답을 올바르게 생성 하는 것으로 간주할 수 있습니다.You can assume the action result creates the HTTP response correctly. Web API 프레임 워크에는 고유한 단위 테스트가 포함 되어 있습니다.(That's why the Web API framework has its own unit tests!)

작업에서 404 (찾을 수 없음)을 반환 합니다.Action returns 404 (Not Found)

Get제품을 찾을 수 없는 경우 메서드는를 호출 합니다 NotFound() .The Get method calls NotFound() if the product is not found. 이 경우 단위 테스트는 반환 형식이 NotFoundResult인지만 확인 합니다.For this case, the unit test just checks if the return type is 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)를 반환 합니다.Action returns 200 (OK) with no response body

Delete메서드는 Ok() 를 호출 하 여 빈 HTTP 200 응답을 반환 합니다.The Delete method calls Ok() to return an empty HTTP 200 response. 이전 예제와 마찬가지로 단위 테스트는 반환 형식 (이 경우 Okresult)을 확인 합니다.Like the previous example, the unit test checks the return type, in this case 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));
}

작업은 Location 헤더를 사용 하 여 201 (Created)을 반환 합니다.Action returns 201 (Created) with a Location header

Post메서드는 CreatedAtRoute 를 호출 하 여 Location 헤더에 URI가 있는 HTTP 201 응답을 반환 합니다.The Post method calls CreatedAtRoute to return an HTTP 201 response with a URI in the Location header. 단위 테스트에서 작업이 올바른 라우팅 값을 설정 하는지 확인 합니다.In the unit test, verify that the action sets the correct routing values.

[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"]);
}

작업은 응답 본문이 있는 다른 2xx를 반환 합니다.Action returns another 2xx with a response body

Put메서드는 Content 를 호출 하 여 응답 본문이 포함 된 HTTP 202 (수락 됨) 응답을 반환 합니다.The Put method calls Content to return an HTTP 202 (Accepted) response with a response body. 이 경우 200 (OK)을 반환 하는 것과 유사 하지만 단위 테스트 에서도 상태 코드를 확인 해야 합니다.This case is similar to returning 200 (OK), but the unit test should also check the status code.

[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);
}

추가 리소스Additional Resources