ASP.NET Core에서 컨트롤러 논리를 테스트합니다.Testing controller logic in ASP.NET Core

으로 Steve SmithBy Steve Smith

ASP.NET MVC 응용 프로그램의 컨트롤러는 작고 사용자 인터페이스 문제에 초점을 맞추고 이어야 합니다.Controllers in ASP.NET MVC apps should be small and focused on user-interface concerns. UI가 아닌 문제를 처리 하는 큰 컨트롤러는 테스트 및 유지 관리 하기가 더 어렵습니다.Large controllers that deal with non-UI concerns are more difficult to test and maintain.

GitHub에서 보거나 다운로드 샘플View or download sample from GitHub

테스트 컨트롤러Testing controllers

컨트롤러는 모든 ASP.NET Core MVC 응용 프로그램의 핵심 요소로입니다.Controllers are a central part of any ASP.NET Core MVC application. 따라서 응용 프로그램에 대 한 의도 한 대로 동작 하므로 신뢰도 있어야 합니다.As such, you should have confidence they behave as intended for your app. 자동화 된 테스트가이 신뢰도 제공할 수 있습니다 및 프로덕션에 도달 하기 전에 오류를 검색할 수 있습니다.Automated tests can provide you with this confidence and can detect errors before they reach production. 컨트롤러 내에서 불필요 한 책임을 배치 하지 컨트롤러 책임에만 테스트 포커스를 확인 하는 것이 유용 합니다.It's important to avoid placing unnecessary responsibilities within your controllers and ensure your tests focus only on controller responsibilities.

컨트롤러 논리 최소화 해야 및 비즈니스 논리 나 인프라 문제나 (예를 들어 데이터 액세스)에 대해 수행 되지 않습니다.Controller logic should be minimal and not be focused on business logic or infrastructure concerns (for example, data access). 테스트 컨트롤러 논리를 프레임 워크 없습니다.Test controller logic, not the framework. 테스트 방법 컨트롤러 동작 잘못 되었거나 잘못 된 입력에 기반 합니다.Test how the controller behaves based on valid or invalid inputs. 수행 하는 비즈니스 작업의 결과에 따라 컨트롤러 응답을 테스트 합니다.Test controller responses based on the result of the business operation it performs.

일반적인 컨트롤러 책임:Typical controller responsibilities:

  • 확인 ModelState.IsValid합니다.Verify ModelState.IsValid.
  • 경우에 대 한 오류 응답을 반환 ModelState 올바르지 않습니다.Return an error response if ModelState is invalid.
  • 지 속성 저장소에서 비즈니스 엔터티를 검색 합니다.Retrieve a business entity from persistence.
  • 비즈니스 엔터티에서 작업을 수행 합니다.Perform an action on the business entity.
  • 지 속성을 비즈니스 엔터티를 저장 합니다.Save the business entity to persistence.
  • 적절 한 반환 IActionResult합니다.Return an appropriate IActionResult.

유닛 테스트Unit testing

단위 테스트 는 인프라와 종속성에서 격리 된 상태에서 응용 프로그램의 일부를 테스트 해야 합니다.Unit testing involves testing a part of an app in isolation from its infrastructure and dependencies. 단위 테스트 컨트롤러 논리를 단일 작업의 내용만 검사 될 때, 프레임 워크 자체 또는 해당 종속 항목의 동작 하지 않습니다.When unit testing controller logic, only the contents of a single action is tested, not the behavior of its dependencies or of the framework itself. 테스트 컨트롤러 작업 하면 단위로 동작에만 집중 있는지 확인 합니다.As you unit test your controller actions, make sure you focus only on its behavior. 등을 방지 하는 컨트롤러 단위 테스트 필터, 라우팅, 또는 모델 바인딩합니다.A controller unit test avoids things like filters, routing, or model binding. 테스트 한 것에 초점을 맞추면, 단위 테스트는 일반적으로 쓰려고 쉽고 빠르게 실행 하는.By focusing on testing just one thing, unit tests are generally simple to write and quick to run. 많은 오버 헤드 없이 자주 잘 작성 된 일련의 단위 테스트를 실행할 수 있습니다.A well-written set of unit tests can be run frequently without much overhead. 그러나 단위 테스트 문제 검색 하지 않는 구성 요소 간의 상호 작용의 용도 통합 테스트합니다.However, unit tests do not detect issues in the interaction between components, which is the purpose of integration testing.

단위 테스트를 사용자 지정 필터, 경로 등을 작성 하는 경우, 되지만 특정 컨트롤러 작업에서 테스트의 일부로 되지는 않습니다.If you're writing custom filters, routes, etc, you should unit test them, but not as part of your tests on a particular controller action. 격리 된 상태에서 테스트 해야 합니다.They should be tested in isolation.

단위 테스트를 보여 주기 위해 다음 컨트롤러를 검토 합니다.To demonstrate unit testing, review the following controller. 브레인스토밍 세션의 목록을 표시 하 고 새로운 브레인스토밍 POST를 사용 하 여 만들어야 하는 세션을 허용 합니다.It displays a list of brainstorming sessions and allows new brainstorming sessions to be created with a POST:

using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using TestingControllersSample.Core.Interfaces;
using TestingControllersSample.Core.Model;
using TestingControllersSample.ViewModels;

namespace TestingControllersSample.Controllers
{
    public class HomeController : Controller
    {
        private readonly IBrainstormSessionRepository _sessionRepository;

        public HomeController(IBrainstormSessionRepository sessionRepository)
        {
            _sessionRepository = sessionRepository;
        }

        public async Task<IActionResult> Index()
        {
            var sessionList = await _sessionRepository.ListAsync();

            var model = sessionList.Select(session => new StormSessionViewModel()
            {
                Id = session.Id,
                DateCreated = session.DateCreated,
                Name = session.Name,
                IdeaCount = session.Ideas.Count
            });

            return View(model);
        }

        public class NewSessionModel
        {
            [Required]
            public string SessionName { get; set; }
        }

        [HttpPost]
        public async Task<IActionResult> Index(NewSessionModel model)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
            else
            {
                await _sessionRepository.AddAsync(new BrainstormSession()
                {
                    DateCreated = DateTimeOffset.Now,
                    Name = model.SessionName
                });
            }

            return RedirectToAction(actionName: nameof(Index));
        }
    }
}

컨트롤러는 다음의 명시적 종속 관계 원칙의 인스턴스를 제공 하는 종속성 주입 예상 IBrainstormSessionRepository합니다.The controller is following the explicit dependencies principle, expecting dependency injection to provide it with an instance of IBrainstormSessionRepository. 이렇게 하면 like는 모의 개체 프레임 워크를 사용 하 여 테스트를 매우 쉽게 Moq합니다.This makes it fairly easy to test using a mock object framework, like Moq. HTTP GET Index 메서드는 루프 또는 분기 및 유일한 호출 한 메서드가 없습니다.The HTTP GET Index method has no looping or branching and only calls one method. 이 테스트 하려면 Index 메서드를 여부를 확인 해야 하는 ViewResult 반환 되 면와 ViewModel 저장소의에서 List 메서드.To test this Index method, we need to verify that a ViewResult is returned, with a ViewModel from the repository's List method.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Moq;
using TestingControllersSample.Controllers;
using TestingControllersSample.Core.Interfaces;
using TestingControllersSample.Core.Model;
using TestingControllersSample.ViewModels;
using Xunit;

namespace TestingControllersSample.Tests.UnitTests
{
    public class HomeControllerTests
    {
        [Fact]
        public async Task Index_ReturnsAViewResult_WithAListOfBrainstormSessions()
        {
            // Arrange
            var mockRepo = new Mock<IBrainstormSessionRepository>();
            mockRepo.Setup(repo => repo.ListAsync()).Returns(Task.FromResult(GetTestSessions()));
            var controller = new HomeController(mockRepo.Object);

            // Act
            var result = await controller.Index();

            // Assert
            var viewResult = Assert.IsType<ViewResult>(result);
            var model = Assert.IsAssignableFrom<IEnumerable<StormSessionViewModel>>(
                viewResult.ViewData.Model);
            Assert.Equal(2, model.Count());
        }

        private List<BrainstormSession> GetTestSessions()
        {
            var sessions = new List<BrainstormSession>();
            sessions.Add(new BrainstormSession()
            {
                DateCreated = new DateTime(2016, 7, 2),
                Id = 1,
                Name = "Test One"
            });
            sessions.Add(new BrainstormSession()
            {
                DateCreated = new DateTime(2016, 7, 1),
                Id = 2,
                Name = "Test Two"
            });
            return sessions;
        }
    }
}

HomeController HTTP POST Index 메서드 (위에 표시)을 확인 해야 합니다.The HomeController HTTP POST Index method (shown above) should verify:

  • 동작 메서드는 잘못 된 요청 반환 ViewResult 적절 한 데이터와 함께 때 ModelState.IsValidfalseThe action method returns a Bad Request ViewResult with the appropriate data when ModelState.IsValid is false

  • Add 리포지토리의 메서드는 및 RedirectToActionResult 올바른 인수와 함께 반환 됩니다 때 ModelState.IsValid 가 true입니다.The Add method on the repository is called and a RedirectToActionResult is returned with the correct arguments when ModelState.IsValid is true.

잘못 된 모델 상태를 사용 하 여 오류를 추가 하 여 테스트할 수 AddModelError 아래 첫 번째 테스트와 같이 합니다.Invalid model state can be tested by adding errors using AddModelError as shown in the first test below.

[Fact]
public async Task IndexPost_ReturnsBadRequestResult_WhenModelStateIsInvalid()
{
    // Arrange
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.ListAsync()).Returns(Task.FromResult(GetTestSessions()));
    var controller = new HomeController(mockRepo.Object);
    controller.ModelState.AddModelError("SessionName", "Required");
    var newSession = new HomeController.NewSessionModel();

    // Act
    var result = await controller.Index(newSession);

    // Assert
    var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
    Assert.IsType<SerializableError>(badRequestResult.Value);
}

[Fact]
public async Task IndexPost_ReturnsARedirectAndAddsSession_WhenModelStateIsValid()
{
    // Arrange
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.AddAsync(It.IsAny<BrainstormSession>()))
        .Returns(Task.CompletedTask)
        .Verifiable();
    var controller = new HomeController(mockRepo.Object);
    var newSession = new HomeController.NewSessionModel()
    {
        SessionName = "Test Name"
    };

    // Act
    var result = await controller.Index(newSession);

    // Assert
    var redirectToActionResult = Assert.IsType<RedirectToActionResult>(result);
    Assert.Null(redirectToActionResult.ControllerName);
    Assert.Equal("Index", redirectToActionResult.ActionName);
    mockRepo.Verify();
}

첫 번째 테스트를 확인 하는 경우 ModelState 유효 하지 않을 경우 동일한 ViewResult 에 대 한 반환 되는 GET 요청 합니다.The first test confirms when ModelState is not valid, the same ViewResult is returned as for a GET request. 테스트 하지 않는 잘못 된 모델에 전달 하려고 하는 참고 합니다.Note that the test doesn't attempt to pass in an invalid model. 작동 하지 않습니다 모델 바인딩 실행 되 고 있지 않습니다 (경우에 통합 테스트 연습 모델 바인딩을 사용).That wouldn't work anyway since model binding isn't running (though an integration test would use exercise model binding). 이 경우 모델 바인딩을 테스트 중입니다.In this case, model binding is not being tested. 이러한 단위 테스트 작업 메서드의 코드에서에서 수행 하는 작업 테스트만 합니다.These unit tests are only testing what the code in the action method does.

두 번째 테스트 확인 때 ModelState 올바른지, 새 BrainstormSession (리포지토리)를 통해 추가 메서드에서 반환는 RedirectToActionResult 예상된 속성을 사용 합니다.The second test verifies that when ModelState is valid, a new BrainstormSession is added (via the repository), and the method returns a RedirectToActionResult with the expected properties. 모의 호출 되지 호출은 일반적으로 무시 되었지만 호출 Verifiable 설치 프로그램의 끝에 호출 수 있도록 테스트에서 확인할 수 있습니다.Mocked calls that aren't called are normally ignored, but calling Verifiable at the end of the setup call allows it to be verified in the test. 호출 하 여 그렇게 mockRepo.Verify, 예상 되는 메서드를 호출 하지 않았습니다 테스트가 실패 합니다.This is done with the call to mockRepo.Verify, which will fail the test if the expected method was not called.

참고

이 샘플에 사용 된 Moq 라이브러리를 사용 비안정형 모의 개체 ("느슨한" mocks 또는 스텁 라고도 함)를 사용한 확인할 또는 "strict" mocks 혼합할 수 있습니다.The Moq library used in this sample makes it easy to mix verifiable, or "strict", mocks with non-verifiable mocks (also called "loose" mocks or stubs). 에 대 한 자세한 내용은 Moq 모의 동작을 사용자 지정합니다.Learn more about customizing Mock behavior with Moq.

응용 프로그램의 다른 컨트롤러 브레인스토밍 특정 세션에 관련 된 정보를 표시 합니다.Another controller in the app displays information related to a particular brainstorming session. 잘못 된 id 값으로 처리 하는 일부 논리를 포함 합니다.It includes some logic to deal with invalid id values:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using TestingControllersSample.Core.Interfaces;
using TestingControllersSample.ViewModels;

namespace TestingControllersSample.Controllers
{
    public class SessionController : Controller
    {
        private readonly IBrainstormSessionRepository _sessionRepository;

        public SessionController(IBrainstormSessionRepository sessionRepository)
        {
            _sessionRepository = sessionRepository;
        }

        public async Task<IActionResult> Index(int? id)
        {
            if (!id.HasValue)
            {
                return RedirectToAction(actionName: nameof(Index), controllerName: "Home");
            }

            var session = await _sessionRepository.GetByIdAsync(id.Value);
            if (session == null)
            {
                return Content("Session not found.");
            }

            var viewModel = new StormSessionViewModel()
            {
                DateCreated = session.DateCreated,
                Name = session.Name,
                Id = session.Id
            };

            return View(viewModel);
        }
    }
}

컨트롤러 동작을 테스트 하려면 각각에 대해 하나씩 세 개의 사례에 return 문:The controller action has three cases to test, one for each return statement:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Moq;
using TestingControllersSample.Controllers;
using TestingControllersSample.Core.Interfaces;
using TestingControllersSample.Core.Model;
using TestingControllersSample.ViewModels;
using Xunit;

namespace TestingControllersSample.Tests.UnitTests
{
    public class SessionControllerTests
    {
        [Fact]
        public async Task IndexReturnsARedirectToIndexHomeWhenIdIsNull()
        {
            // Arrange
            var controller = new SessionController(sessionRepository: null);

            // Act
            var result = await controller.Index(id: null);

            // Assert
            var redirectToActionResult = Assert.IsType<RedirectToActionResult>(result);
            Assert.Equal("Home", redirectToActionResult.ControllerName);
            Assert.Equal("Index", redirectToActionResult.ActionName);
        }

        [Fact]
        public async Task IndexReturnsContentWithSessionNotFoundWhenSessionNotFound()
        {
            // Arrange
            int testSessionId = 1;
            var mockRepo = new Mock<IBrainstormSessionRepository>();
            mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
                .Returns(Task.FromResult((BrainstormSession)null));
            var controller = new SessionController(mockRepo.Object);

            // Act
            var result = await controller.Index(testSessionId);

            // Assert
            var contentResult = Assert.IsType<ContentResult>(result);
            Assert.Equal("Session not found.", contentResult.Content);
        }

        [Fact]
        public async Task IndexReturnsViewResultWithStormSessionViewModel()
        {
            // Arrange
            int testSessionId = 1;
            var mockRepo = new Mock<IBrainstormSessionRepository>();
            mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
                .Returns(Task.FromResult(GetTestSessions().FirstOrDefault(s => s.Id == testSessionId)));
            var controller = new SessionController(mockRepo.Object);

            // Act
            var result = await controller.Index(testSessionId);

            // Assert
            var viewResult = Assert.IsType<ViewResult>(result);
            var model = Assert.IsType<StormSessionViewModel>(viewResult.ViewData.Model);
            Assert.Equal("Test One", model.Name);
            Assert.Equal(2, model.DateCreated.Day);
            Assert.Equal(testSessionId, model.Id);
        }

        private List<BrainstormSession> GetTestSessions()
        {
            var sessions = new List<BrainstormSession>();
            sessions.Add(new BrainstormSession()
            {
                DateCreated = new DateTime(2016, 7, 2),
                Id = 1,
                Name = "Test One"
            });
            sessions.Add(new BrainstormSession()
            {
                DateCreated = new DateTime(2016, 7, 1),
                Id = 2,
                Name = "Test Two"
            });
            return sessions;
        }
    }
}

웹 API (브레인스토밍 세션 및 세션에 새로운 아이디어를 추가 하는 방법을 관련 된 아이디어 목록) 기능을 노출 하는 응용 프로그램:The app exposes functionality as a web API (a list of ideas associated with a brainstorming session and a method for adding new ideas to a session):

<a name=ideas-controller>

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using TestingControllersSample.ClientModels;
using TestingControllersSample.Core.Interfaces;
using TestingControllersSample.Core.Model;

namespace TestingControllersSample.Api
{
    [Route("api/ideas")]
    public class IdeasController : Controller
    {
        private readonly IBrainstormSessionRepository _sessionRepository;

        public IdeasController(IBrainstormSessionRepository sessionRepository)
        {
            _sessionRepository = sessionRepository;
        }

        [HttpGet("forsession/{sessionId}")]
        public async Task<IActionResult> ForSession(int sessionId)
        {
            var session = await _sessionRepository.GetByIdAsync(sessionId);
            if (session == null)
            {
                return NotFound(sessionId);
            }

            var result = session.Ideas.Select(idea => new IdeaDTO()
            {
                Id = idea.Id,
                Name = idea.Name,
                Description = idea.Description,
                DateCreated = idea.DateCreated
            }).ToList();

            return Ok(result);
        }

        [HttpPost("create")]
        public async Task<IActionResult> Create([FromBody]NewIdeaModel model)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            var session = await _sessionRepository.GetByIdAsync(model.SessionId);
            if (session == null)
            {
                return NotFound(model.SessionId);
            }

            var idea = new Idea()
            {
                DateCreated = DateTimeOffset.Now,
                Description = model.Description,
                Name = model.Name
            };
            session.AddIdea(idea);

            await _sessionRepository.UpdateAsync(session);

            return Ok(session);
        }
    }
}

ForSession 메서드 목록을 반환 IdeaDTO 형식입니다.The ForSession method returns a list of IdeaDTO types. API 호출을 통해 직접 비즈니스 도메인 엔터티를 반환 하지 않도록, 자주 이후 API 클라이언트 걸리며 외부적으로 노출 하는 API와 응용 프로그램의 내부 도메인 모델 불필요 하 게 결합 것 보다 많은 데이터 변경 내용이 해당 됩니다.Avoid returning your business domain entities directly via API calls, since frequently they include more data than the API client requires, and they unnecessarily couple your app's internal domain model with the API you expose externally. 도메인 엔터티 및 연결을 통해 돌아갑니다 형식 간의 매핑을 수동으로 수행할 수 있습니다 (LINQ를 사용 하 여 Select 다음과 같이)와 같은 라이브러리를 사용 하 여 또는 AutoMapperMapping between domain entities and the types you will return over the wire can be done manually (using a LINQ Select as shown here) or using a library like AutoMapper

에 대 한 단위 테스트는 CreateForSession API 메서드:The unit tests for the Create and ForSession API methods:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Moq;
using TestingControllersSample.Api;
using TestingControllersSample.ClientModels;
using TestingControllersSample.Core.Interfaces;
using TestingControllersSample.Core.Model;
using Xunit;

namespace TestingControllersSample.Tests.UnitTests
{
    public class ApiIdeasControllerTests
    {
        [Fact]
        public async Task Create_ReturnsBadRequest_GivenInvalidModel()
        {
            // Arrange & Act
            var mockRepo = new Mock<IBrainstormSessionRepository>();
            var controller = new IdeasController(mockRepo.Object);
            controller.ModelState.AddModelError("error","some error");

            // Act
            var result = await controller.Create(model: null);

            // Assert
            Assert.IsType<BadRequestObjectResult>(result);
        }

        [Fact]
        public async Task Create_ReturnsHttpNotFound_ForInvalidSession()
        {
            // Arrange
            int testSessionId = 123;
            var mockRepo = new Mock<IBrainstormSessionRepository>();
            mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
                .Returns(Task.FromResult((BrainstormSession)null));
            var controller = new IdeasController(mockRepo.Object);

            // Act
            var result = await controller.Create(new NewIdeaModel());

            // Assert
            Assert.IsType<NotFoundObjectResult>(result);
        }

        [Fact]
        public async Task Create_ReturnsNewlyCreatedIdeaForSession()
        {
            // Arrange
            int testSessionId = 123;
            string testName = "test name";
            string testDescription = "test description";
            var testSession = GetTestSession();
            var mockRepo = new Mock<IBrainstormSessionRepository>();
            mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
                .Returns(Task.FromResult(testSession));
            var controller = new IdeasController(mockRepo.Object);

            var newIdea = new NewIdeaModel()
            {
                Description = testDescription,
                Name = testName,
                SessionId = testSessionId
            };
            mockRepo.Setup(repo => repo.UpdateAsync(testSession))
                .Returns(Task.CompletedTask)
                .Verifiable();

            // Act
            var result = await controller.Create(newIdea);

            // Assert
            var okResult = Assert.IsType<OkObjectResult>(result);
            var returnSession = Assert.IsType<BrainstormSession>(okResult.Value);
            mockRepo.Verify();
            Assert.Equal(2, returnSession.Ideas.Count());
            Assert.Equal(testName, returnSession.Ideas.LastOrDefault().Name);
            Assert.Equal(testDescription, returnSession.Ideas.LastOrDefault().Description);
        }

        private BrainstormSession GetTestSession()
        {
            var session = new BrainstormSession()
            {
                DateCreated = new DateTime(2016, 7, 2),
                Id = 1,
                Name = "Test One"
            };

            var idea = new Idea() { Name = "One" };
            session.AddIdea(idea);
            return session;
        }
    }
}

메서드의 동작을 테스트 하려면 이전에 설명한 것 처럼 때 ModelState 유효 하지 않을 경우 컨트롤러에는 테스트의 일부로 모델 오류를 추가 합니다.As stated previously, to test the behavior of the method when ModelState is invalid, add a model error to the controller as part of the test. 모델 유효성 검사 또는 모델 바인딩 단위 테스트에서 테스트-방금 특정으로 하는 경우 작업 메서드의 동작을 테스트 하지 마십시오 ModelState 값입니다.Don't try to test model validation or model binding in your unit tests - just test your action method's behavior when confronted with a particular ModelState value.

두 번째 테스트는 모의 리포지토리에서 null을 반환 하도록 구성 되어 있으므로 null을 반환 하는 저장소에 따라 달라 집니다.The second test depends on the repository returning null, so the mock repository is configured to return null. 테스트 데이터베이스를 만들 필요가 없습니다 (메모리에 또는 그렇지 않은 경우)이이 결과 반환 하는 쿼리를 작성 및-표시 된 것 처럼 단일 문으로 수행할 수 있습니다.There's no need to create a test database (in memory or otherwise) and construct a query that will return this result - it can be done in a single statement as shown.

마지막 테스트를 확인 하는 저장소의 Update 메서드를 호출 합니다.The last test verifies that the repository's Update method is called. 모의로 호출 되 고 이전에 수행한 것 처럼 Verifiable 와 다음 모의 리포지토리의 Verify 메서드는 안정형 메서드가 실행 되었음을 확인 합니다.As we did previously, the mock is called with Verifiable and then the mocked repository's Verify method is called to confirm the verifiable method was executed. 단위 테스트 과제는에 없으면는 Update 메서드 데이터를 저장, 통합 테스트와 함께 수행할 수 있습니다.It's not a unit test responsibility to ensure that the Update method saved the data; that can be done with an integration test.

통합 테스트Integration testing

통합 테스트 올바르게 함께 응용 프로그램 작업 내에서 별도 모듈을 보장 하기 위해 수행 됩니다.Integration testing is done to ensure separate modules within your app work correctly together. 일반적으로 모든 단위 테스트와 함께 테스트할 수 있습니다를 테스트할 수도 있습니다는 통합 테스트와 함께 하지만 반대의 제한이 적용 되지 않습니다.Generally, anything you can test with a unit test, you can also test with an integration test, but the reverse isn't true. 그러나 통합 테스트에 단위 테스트 보다 느릴 수는 있습니다.However, integration tests tend to be much slower than unit tests. 따라서 모든 단위 테스트 하는 여러 공동 작업자와 관련 된 시나리오에 대 한 통합 테스트를 사용 하 여 테스트 하는 것이 좋습니다.Thus, it's best to test whatever you can with unit tests, and use integration tests for scenarios that involve multiple collaborators.

여전히 유용 하지만, 모의 개체는 통합 테스트에서 거의 사용 됩니다.Although they may still be useful, mock objects are rarely used in integration tests. 단위 테스트를 모의 개체는 테스트의 용도 대 한 테스트 하는 단위 외부 공동 작업자 해야 작동 하는 방식을 제어 하는 효과적인 방법.In unit testing, mock objects are an effective way to control how collaborators outside of the unit being tested should behave for the purposes of the test. 통합 테스트에서 실제 공동 작업자 전체 하위 시스템 작동 함께 제대로 확인 하는 데 사용 됩니다.In an integration test, real collaborators are used to confirm the whole subsystem works together correctly.

응용 프로그램 상태Application state

한 중요 한 고려 사항은 통합 테스트를 수행 하는 경우에 응용 프로그램 상태를 설정 하는 방법입니다.One important consideration when performing integration testing is how to set your app's state. 각 테스트에서 알려진된 상태로 응용 프로그램으로 시작 해야 하므로 테스트를 서로 독립적으로 실행 해야 합니다.Tests need to run independent of one another, and so each test should start with the app in a known state. 응용 프로그램 데이터베이스를 사용 하지 못하거나 모든 지 속성은 갖지, 문제가 아닐 수 있습니다.If your app doesn't use a database or have any persistence, this may not be an issue. 그러나 대부분의 실제 응용 프로그램 데이터 저장소를 다시 설정 하지 않는 한 변경 내용은 모두 하나의 테스트 하 여 다른 테스트 영향을 줄 수 있으므로 상태 일부 종류의 데이터 저장소로 유지 합니다.However, most real-world apps persist their state to some kind of data store, so any modifications made by one test could impact another test unless the data store is reset. 기본 제공을 사용 하 여 TestServer, 작업은 통합이 테스트에서는 내에서 호스트 ASP.NET Core 응용 프로그램에 매우 간단 하지만 반드시 사용 데이터에 대 한 액세스를 부여 하지 하 합니다.Using the built-in TestServer, it's very straightforward to host ASP.NET Core apps within our integration tests, but that doesn't necessarily grant access to the data it will use. 실제 데이터베이스를 사용 하는 경우 한 가지 방법은 테스트 데이터베이스에 연결 하는 앱을 각 테스트를 실행 하기 전에 알려진 상태로 다시 설정 됩니다 테스트에 액세스 하 고 확인 수 있습니다.If you're using an actual database, one approach is to have the app connect to a test database, which your tests can access and ensure is reset to a known state before each test executes.

이 샘플 응용 프로그램에서 사용 하 고 Entity Framework Core InMemoryDatabase 지원, 테스트 프로젝트 내에서 것에 연결할 수 있도록 합니다.In this sample application, I'm using Entity Framework Core's InMemoryDatabase support, so I can't just connect to it from my test project. 노출 대신는 InitializeDatabase 응용 프로그램의 메서드에서 Startup 에 있으면 응용 프로그램 시작 될 때 호출 하는 클래스는 Development 환경입니다.Instead, I expose an InitializeDatabase method from the app's Startup class, which I call when the app starts up if it's in the Development environment. 통합 테스트에서이 환경으로 설정 것으로 자동으로 활용 Development합니다.My integration tests automatically benefit from this as long as they set the environment to Development. InMemoryDatabase은 앱이 다시 시작 될 때마다 다시 설정한 후 데이터베이스를 다시 설정에 대해 걱정 하지 않아도 됩니다.I don't have to worry about resetting the database, since the InMemoryDatabase is reset each time the app restarts.

Startup 클래스:The Startup class:

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using TestingControllersSample.Core.Interfaces;
using TestingControllersSample.Core.Model;
using TestingControllersSample.Infrastructure;

namespace TestingControllersSample
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<AppDbContext>(
                optionsBuilder => optionsBuilder.UseInMemoryDatabase("InMemoryDb"));

            services.AddMvc();

            services.AddScoped<IBrainstormSessionRepository,
                EFStormSessionRepository>();
        }

        public void Configure(IApplicationBuilder app,
            IHostingEnvironment env,
            ILoggerFactory loggerFactory)
        {
            if (env.IsDevelopment())
            {
                var repository = app.ApplicationServices.GetService<IBrainstormSessionRepository>();
                InitializeDatabaseAsync(repository).Wait();
            }

            app.UseStaticFiles();

            app.UseMvcWithDefaultRoute();
        }

        public async Task InitializeDatabaseAsync(IBrainstormSessionRepository repo)
        {
            var sessionList = await repo.ListAsync();
            if (!sessionList.Any())
            {
                await repo.AddAsync(GetTestSession());
            }
        }

        public static BrainstormSession GetTestSession()
        {
            var session = new BrainstormSession()
            {
                Name = "Test Session 1",
                DateCreated = new DateTime(2016, 8, 1)
            };
            var idea = new Idea()
            {
                DateCreated = new DateTime(2016, 8, 1),
                Description = "Totally awesome idea",
                Name = "Awesome idea"
            };
            session.AddIdea(idea);
            return session;
        }
    }
}

표시는 GetTestSession 아래 통합 테스트에서 자주 사용 하는 방법입니다.You'll see the GetTestSession method used frequently in the integration tests below.

보기에 액세스Accessing views

각 통합 테스트 클래스에 구성 된 TestServer ASP.NET Core 응용 프로그램 실행 합니다.Each integration test class configures the TestServer that will run the ASP.NET Core app. 기본적으로 TestServer 실행 중인 위치-이 경우 테스트 프로젝트 폴더는 폴더에 웹 응용 프로그램 호스트 합니다.By default, TestServer hosts the web app in the folder where it's running - in this case, the test project folder. 따라서 하려고 하면 테스트 컨트롤러 작업을 반환 하는 ViewResult,이 오류가 표시 될 수 있습니다.Thus, when you attempt to test controller actions that return ViewResult, you may see this error:

The view 'Index' was not found. The following locations were searched:
(list of locations)

이 문제를 해결 하려면 테스트 중인 프로젝트에 대 한 보기를 찾을 수 있도록 서버의 콘텐츠 루트를 구성 해야 합니다.To correct this issue, you need to configure the server's content root, so it can locate the views for the project being tested. 호출 하 여 이렇게 UseContentRootTestFixture 아래에 표시 된 클래스:This is done by a call to UseContentRoot in the TestFixture class, shown below:

using System;
using System.IO;
using System.Net.Http;
using System.Reflection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;

namespace TestingControllersSample.Tests.IntegrationTests
{
    /// <summary>
    /// A test fixture which hosts the target project (project we wish to test) in an in-memory server.
    /// </summary>
    /// <typeparam name="TStartup">Target project's startup type</typeparam>
    public class TestFixture<TStartup> : IDisposable
    {
        private readonly TestServer _server;

        public TestFixture()
            : this(Path.Combine("src"))
        {
        }

        protected TestFixture(string relativeTargetProjectParentDir)
        {
            var startupAssembly = typeof(TStartup).GetTypeInfo().Assembly;
            var contentRoot = GetProjectPath(relativeTargetProjectParentDir, startupAssembly);

            var builder = new WebHostBuilder()
                .UseContentRoot(contentRoot)
                .ConfigureServices(InitializeServices)
                .UseEnvironment("Development")
                .UseStartup(typeof(TStartup));

            _server = new TestServer(builder);

            Client = _server.CreateClient();
            Client.BaseAddress = new Uri("http://localhost");
        }

        public HttpClient Client { get; }

        public void Dispose()
        {
            Client.Dispose();
            _server.Dispose();
        }

        protected virtual void InitializeServices(IServiceCollection services)
        {
            var startupAssembly = typeof(TStartup).GetTypeInfo().Assembly;

            // Inject a custom application part manager. 
            // Overrides AddMvcCore() because it uses TryAdd().
            var manager = new ApplicationPartManager();
            manager.ApplicationParts.Add(new AssemblyPart(startupAssembly));
            manager.FeatureProviders.Add(new ControllerFeatureProvider());
            manager.FeatureProviders.Add(new ViewComponentFeatureProvider());

            services.AddSingleton(manager);
        }

        /// <summary>
        /// Gets the full path to the target project that we wish to test
        /// </summary>
        /// <param name="projectRelativePath">
        /// The parent directory of the target project.
        /// e.g. src, samples, test, or test/Websites
        /// </param>
        /// <param name="startupAssembly">The target project's assembly.</param>
        /// <returns>The full path to the target project.</returns>
        private static string GetProjectPath(string projectRelativePath, Assembly startupAssembly)
        {
            // Get name of the target project which we want to test
            var projectName = startupAssembly.GetName().Name;

            // Get currently executing test project path
            var applicationBasePath = System.AppContext.BaseDirectory;

            // Find the path to the target project
            var directoryInfo = new DirectoryInfo(applicationBasePath);
            do
            {
                directoryInfo = directoryInfo.Parent;

                var projectDirectoryInfo = new DirectoryInfo(Path.Combine(directoryInfo.FullName, projectRelativePath));
                if (projectDirectoryInfo.Exists)
                {
                    var projectFileInfo = new FileInfo(Path.Combine(projectDirectoryInfo.FullName, projectName, $"{projectName}.csproj"));
                    if (projectFileInfo.Exists)
                    {
                        return Path.Combine(projectDirectoryInfo.FullName, projectName);
                    }
                }
            }
            while (directoryInfo.Parent != null);

            throw new Exception($"Project root could not be located using the application root {applicationBasePath}.");
        }
    }
}

TestFixture 클래스는 구성 및 만들기는 TestServer, 설정 하는 HttpClient 와 통신 하는 TestServer합니다.The TestFixture class is responsible for configuring and creating the TestServer, setting up an HttpClient to communicate with the TestServer. 사용 하 여 각 통합 테스트는 Client 속성을 테스트 서버에 연결 하 고 요청을 만드세요.Each of the integration tests uses the Client property to connect to the test server and make a request.

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Xunit;

namespace TestingControllersSample.Tests.IntegrationTests
{
    public class HomeControllerTests : IClassFixture<TestFixture<TestingControllersSample.Startup>>
    {
        private readonly HttpClient _client;

        public HomeControllerTests(TestFixture<TestingControllersSample.Startup> fixture)
        {
            _client = fixture.Client;
        }

        [Fact]
        public async Task ReturnsInitialListOfBrainstormSessions()
        {
            // Arrange - get a session known to exist
            var testSession = Startup.GetTestSession();

            // Act
            var response = await _client.GetAsync("/");

            // Assert
            response.EnsureSuccessStatusCode();
            var responseString = await response.Content.ReadAsStringAsync();
            Assert.Contains(testSession.Name, responseString);
        }

        [Fact]
        public async Task PostAddsNewBrainstormSession()
        {
            // Arrange
            string testSessionName = Guid.NewGuid().ToString();
            var data = new Dictionary<string, string>();
            data.Add("SessionName", testSessionName);
            var content = new FormUrlEncodedContent(data);

            // Act
            var response = await _client.PostAsync("/", content);

            // Assert
            Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
            Assert.Equal("/", response.Headers.Location.ToString());
        }
    }
}

위의 첫 번째 테스트에는 responseString 실제 예상된 결과 포함 되어 있는지 확인 하는 감사 될 수 있는 뷰에서 HTML 렌더링을 보유 합니다.In the first test above, the responseString holds the actual rendered HTML from the View, which can be inspected to confirm it contains expected results.

두 번째 테스트 고유한 세션 이름 포함 하는 폼 POST를 생성 하 고, 앱에 게시 다음 예상된 리디렉션이 반환 되는지 확인 합니다.The second test constructs a form POST with a unique session name and POSTs it to the app, then verifies that the expected redirect is returned.

API 메서드API methods

응용 프로그램 웹 것 것 테스트 자동화 하는 것을 예상 대로 실행 확인 Api를 노출할 경우.If your app exposes web APIs, it's a good idea to have automated tests confirm they execute as expected. 기본 제공 TestServer 웹 Api를 테스트 하기가 쉬워졌습니다.The built-in TestServer makes it easy to test web APIs. 항상 확인 해야 API 메서드를 바인딩하는 모델을 사용 하는 경우 ModelState.IsValid, 이며 통합 테스트는 적절 한 위치 하 여 모델 유효성 검사가 제대로 작동 하는지 확인 합니다.If your API methods are using model binding, you should always check ModelState.IsValid, and integration tests are the right place to confirm that your model validation is working properly.

다음과 같은 테스트 대상 집합이 Create 에서 메서드는 IdeasController 위에 표시 된 클래스:The following set of tests target the Create method in the IdeasController class shown above:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json;
using TestingControllersSample.ClientModels;
using TestingControllersSample.Core.Model;
using Xunit;

namespace TestingControllersSample.Tests.IntegrationTests
{
    public class ApiIdeasControllerTests : IClassFixture<TestFixture<TestingControllersSample.Startup>>
    {
        internal class NewIdeaDto
        {
            public NewIdeaDto(string name, string description, int sessionId)
            {
                Name = name;
                Description = description;
                SessionId = sessionId;
            }

            public string Name { get; set; }
            public string Description { get; set; }
            public int SessionId { get; set; }
        }

        private readonly HttpClient _client;

        public ApiIdeasControllerTests(TestFixture<TestingControllersSample.Startup> fixture)
        {
            _client = fixture.Client;
        }

        [Fact]
        public async Task CreatePostReturnsBadRequestForMissingNameValue()
        {
            // Arrange
            var newIdea = new NewIdeaDto("", "Description", 1);

            // Act
            var response = await _client.PostAsJsonAsync("/api/ideas/create", newIdea);

            // Assert
            Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
        }

        [Fact]
        public async Task CreatePostReturnsBadRequestForMissingDescriptionValue()
        {
            // Arrange
            var newIdea = new NewIdeaDto("Name", "", 1);

            // Act
            var response = await _client.PostAsJsonAsync("/api/ideas/create", newIdea);

            // Assert
            Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
        }

        [Fact]
        public async Task CreatePostReturnsBadRequestForSessionIdValueTooSmall()
        {
            // Arrange
            var newIdea = new NewIdeaDto("Name", "Description", 0);

            // Act
            var response = await _client.PostAsJsonAsync("/api/ideas/create", newIdea);

            // Assert
            Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
        }

        [Fact]
        public async Task CreatePostReturnsBadRequestForSessionIdValueTooLarge()
        {
            // Arrange
            var newIdea = new NewIdeaDto("Name", "Description", 1000001);

            // Act
            var response = await _client.PostAsJsonAsync("/api/ideas/create", newIdea);

            // Assert
            Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
        }

        [Fact]
        public async Task CreatePostReturnsNotFoundForInvalidSession()
        {
            // Arrange
            var newIdea = new NewIdeaDto("Name", "Description", 123);

            // Act
            var response = await _client.PostAsJsonAsync("/api/ideas/create", newIdea);

            // Assert
            Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
        }

        [Fact]
        public async Task CreatePostReturnsCreatedIdeaWithCorrectInputs()
        {
            // Arrange
            var testIdeaName = Guid.NewGuid().ToString();
            var newIdea = new NewIdeaDto(testIdeaName, "Description", 1);

            // Act
            var response = await _client.PostAsJsonAsync("/api/ideas/create", newIdea);

            // Assert
            response.EnsureSuccessStatusCode();
            var returnedSession = await response.Content.ReadAsJsonAsync<BrainstormSession>();
            Assert.Equal(2, returnedSession.Ideas.Count);
            Assert.Contains(testIdeaName, returnedSession.Ideas.Select(i => i.Name).ToList());
        }

        [Fact]
        public async Task ForSessionReturnsNotFoundForBadSessionId()
        {
            // Arrange & Act
            var response = await _client.GetAsync("/api/ideas/forsession/500");

            // Assert
            Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
        }

        [Fact]
        public async Task ForSessionReturnsIdeasForValidSessionId()
        {
            // Arrange
            var testSession = Startup.GetTestSession();

            // Act
            var response = await _client.GetAsync("/api/ideas/forsession/1");

            // Assert
            response.EnsureSuccessStatusCode();
            var ideaList = JsonConvert.DeserializeObject<List<IdeaDTO>>(
                await response.Content.ReadAsStringAsync());
            var firstIdea = ideaList.First();
            Assert.Equal(testSession.Ideas.First().Name, firstIdea.Name);
        }
    }
}

HTML 뷰를 반환 하는 동작의 통합 테스트를 달리 API 웹 메서드 결과 반환 하는 일반적으로 deserialize 할 수 강력한 형식의 개체로 위의 마지막 테스트와 같이 합니다.Unlike integration tests of actions that returns HTML views, web API methods that return results can usually be deserialized as strongly typed objects, as the last test above shows. 테스트 결과를 역직렬화 하는 경우에 BrainstormSession 인스턴스를 선택한 아이디어 아이디어의 해당 컬렉션에 올바르게 추가 되었는지 확인 합니다.In this case, the test deserializes the result to a BrainstormSession instance, and confirms that the idea was correctly added to its collection of ideas.

이 문서에서 통합 테스트의 추가 예제를 찾을 수 샘플 프로젝트합니다.You'll find additional examples of integration tests in this article's sample project.