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.

단위 테스트에서 일반적인 패턴은 "정렬 act assert":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: Verify that the test succeeded.

정렬 단계에는 자주 mock를 사용 하거나 스텁 개체입니다.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.

Web 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 mock는 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.Link 응답에 링크를 만들려고 합니다.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. 또 다른 옵션은 stub 또는 mock UrlHelper합니다.Another option is mock or stub UrlHelper. 이 방법의 기본 값을 바꿔야 ApiController.Url 고정된 값을 반환 하는 mock 또는 스텁 버전을 사용 하 여 합니다.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);
}

이 버전에서는 필요가 경로 데이터를 설정 하기 때문에 mock 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 컨트롤러 작업을 반환할 수 있습니다 IHttpActionResult에 비슷합니다 ActionResult ASP.NET MVC에서.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 (정상)를 반환합니다.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 (정상)를 반환합니다.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));
}

작업 위치 헤더를 사용 하 여 201 (만들어짐)를 반환합니다.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 (정상)를 반환 하도록 유사 하지만 단위 테스트 상태 코드를 확인 해야 합니다.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