ASP.NET Core에서 컨트롤러 논리 테스트Test controller logic in ASP.NET Core

작성자: Steve SmithBy Steve Smith

컨트롤러는 임의의 ASP.NET Core MVC 앱에서 중심적인 역할을 수행합니다.Controllers play a central role in any ASP.NET Core MVC app. 따라서 컨트롤러가 의도한 대로 동작한다고 확신할 수 있어야 합니다.As such, you should have confidence that controllers behave as intended. 자동화된 테스트는 앱이 프로덕션 환경에 배포되기 전에 오류를 발견할 수 있습니다.Automated tests can detect errors before the app is deployed to a production environment.

예제 코드 살펴보기 및 다운로드(다운로드 방법)View or download sample code (how to download)

컨트롤러 논리의 단위 테스트Unit tests of controller logic

단위 테스트는 인프라 및 종속성으로부터 격리된 상태에서 앱의 일부를 테스트하는 것입니다.Unit tests involve 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 are tested, not the behavior of its dependencies or of the framework itself.

컨트롤러의 동작에 초점을 맞춰 컨트롤러 동작의 단위 테스트를 설정합니다.Set up unit tests of controller actions to focus on the controller's behavior. 컨트롤러 단위 테스트는 필터, 라우팅, 모델 바인딩 같은 시나리오를 방지합니다.A controller unit test avoids scenarios such as filters, routing, and model binding. 전체적으로 요청에 응답하는 구성 요소 간 상호 작용을 포함하는 테스트는 ‘통합 테스트’에서 처리합니다.Tests that cover the interactions among components that collectively respond to a request are handled by integration tests. 통합 테스트에 대한 자세한 내용은 ASP.NET Core에서 통합 테스트를 참조하세요.For more information on integration tests, see ASP.NET Core에서 통합 테스트.

사용자 지정 필터 및 경로를 작성할 때 특정 컨트롤러 작업에 대한 테스트의 일부가 아닌 별도로 단위 테스트를 수행합니다.If you're writing custom filters and routes, unit test them in isolation, not as part of tests on a particular controller action.

컨트롤러 단위 테스트를 데모하려면 샘플 앱에서 다음 컨트롤러를 검토하세요.To demonstrate controller unit tests, review the following controller in the sample app. 홈 컨트롤러는 브레인스토밍 세션 목록을 표시하며 POST 요청을 사용하여 새로운 브레인스토밍 세션을 만들 수 있게 합니다.The Home controller displays a list of brainstorming sessions and allows the creation of new brainstorming sessions with a POST request:

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

이전 컨트롤러는 다음과 같습니다.The preceding controller:

  • 명시적 종속성 원칙을 따릅니다.Follows the Explicit Dependencies Principle.
  • DI(종속성 주입)에서 IBrainstormSessionRepository의 인스턴스를 제공할 것을 예상합니다.Expects dependency injection (DI) to provide an instance of IBrainstormSessionRepository.
  • Moq와 같은 모의 개체 프레임워크를 사용하여 모의 IBrainstormSessionRepository 서비스로 테스트할 수 있습니다.Can be tested with a mocked IBrainstormSessionRepository service using a mock object framework, such as Moq. ‘모의 개체’는 테스트에 사용되는 사전 결정된 속성 및 메서드 동작 집합이 있는 제작된 개체입니다.A mocked object is a fabricated object with a predetermined set of property and method behaviors used for testing. 자세한 내용은 통합 테스트 소개를 참조하세요.For more information, see Introduction to integration tests.

HTTP GET Index 메서드는 반복 또는 분기가 없으며 한 가지 메서드만 호출합니다.The HTTP GET Index method has no looping or branching and only calls one method. 이 동작의 단위 테스트는 다음을 수행합니다.The unit test for this action:

  • GetTestSessions 메서드를 사용하여 IBrainstormSessionRepository 서비스를 모방합니다.Mocks the IBrainstormSessionRepository service using the GetTestSessions method. GetTestSessions는 날짜 및 세션 이름이 있는 두 개의 모의 브레인스토밍 세션을 작성합니다.GetTestSessions creates two mock brainstorm sessions with dates and session names.
  • Index 메서드를 실행합니다.Executes the Index method.
  • 메서드에서 반환한 결과에 대한 어설션을 작성합니다.Makes assertions on the result returned by the method:
    • ViewResult가 반환됩니다.A ViewResult is returned.
    • ViewDataDictionary.ModelStormSessionViewModel입니다.The ViewDataDictionary.Model is a StormSessionViewModel.
    • ViewDataDictionary.Model에 저장된 두 개의 브레인스토밍 세션이 있습니다.There are two brainstorming sessions stored in the ViewDataDictionary.Model.
[Fact]
public async Task Index_ReturnsAViewResult_WithAListOfBrainstormSessions()
{
    // Arrange
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.ListAsync())
        .ReturnsAsync(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;
}

홈 컨트롤러의 HTTP POST Index 메서드는 다음을 확인합니다.The Home controller's HTTP POST Index method tests verifies that:

  • ModelState.IsValidfalse인 경우 작업 메서드가 적절한 데이터와 함께 ‘400 잘못된 요청’ ViewResult를 반환함.When ModelState.IsValid is false, the action method returns a 400 Bad Request ViewResult with the appropriate data.
  • ModelState.IsValidtrue인 경우:When ModelState.IsValid is true:
    • 리포지토리에서 Add 메서드를 호출함.The Add method on the repository is called.
    • RedirectToActionResult가 올바른 인수와 함께 반환됨.A RedirectToActionResult is returned with the correct arguments.

아래의 첫 번째 테스트처럼 AddModelError를 사용해 오류를 추가하여 잘못된 모델 상태를 테스트할 수 있습니다.An invalid model state is 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())
        .ReturnsAsync(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가 유효하지 않으면 GET 요청의 경우와 동일한 ViewResult가 반환됩니다.When ModelState isn't valid, the same ViewResult is returned as for a GET request. 이 테스트는 잘못된 모델을 전달하려고 시도하지 않습니다.The test doesn't attempt to pass in an invalid model. (통합 테스트에서 모델 바인딩을 사용하는데도 불구하고) 모델 바인딩이 실행되지 않기 때문에, 잘못된 모델을 전달하는 것은 유효한 접근 방식이 아닙니다.Passing an invalid model isn't a valid approach, since model binding isn't running (although an integration test does use model binding). 이 예에서는 모델 바인딩을 테스트하지 않습니다.In this case, model binding isn't tested. 이러한 단위 테스트는 작업 메서드의 코드만 테스트합니다.These unit tests are only testing the code in the action method.

두 번째 테스트는 ModelState가 유효한 시기를 확인합니다.The second test verifies that when the ModelState is valid:

  • BrainstormSession이 (리포지토리를 통해) 추가됨.A new BrainstormSession is added (via the repository).
  • 메서드가 예상 속성과 함께 RedirectToActionResult를 반환함.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 mock validation in the test. 이것은 mockRepo.Verify 호출을 통해 수행되며, 예상된 메서드가 호출되지 않으면 테스트가 실패합니다.This is performed with the call to mockRepo.Verify, which fails the test if the expected method wasn't called.

참고

이 샘플에 사용된 Moq 라이브러리를 사용하면 확인 가능한 또는 “엄격한” 모의 개체를 확인 불가능한 모의 개체(“느슨한” 모의 개체 또는 스텁이라고도 함)와 혼합할 수 있습니다.The Moq library used in this sample makes it possible 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 sample app displays information related to a particular brainstorming session. 컨트롤러에는 잘못된 id 값(다음 예에는 이러한 시나리오를 다루는 두 개의 return 시나리오가 있음)을 처리하는 논리가 포함되어 있습니다.The controller includes logic to deal with invalid id values (there are two return scenarios in the following example to cover these scenarios). 최종 return 문은 보기에 새 StormSessionViewModel을 반환합니다.The final return statement returns a new StormSessionViewModel to the view:

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

단위 테스트에는 세션 컨트롤러 Index 작업에 각 return 시나리오에 대한 하나의 테스트가 포함되어 있습니다.The unit tests include one test for each return scenario in the Session controller Index action:

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

아이디어 컨트롤러로 이동하면 앱은 api/ideas 경로에서 기능을 웹 API로 노출합니다.Moving to the Ideas controller, the app exposes functionality as a web API on the api/ideas route:

  • 브레인스토밍 세션과 연관된 아이디어(IdeaDTO)의 목록이 ForSession 메서드에 의해 반환됩니다.A list of ideas (IdeaDTO) associated with a brainstorming session is returned by the ForSession method.
  • Create 메서드는 세션에 새 아이디어를 추가합니다.The Create method adds new ideas to a session.
[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);
}

API 호출을 통해 직접 비즈니스 도메인 엔터티를 반환하지 마세요.Avoid returning business domain entities directly via API calls. 도메인 엔터티는 다음과 같습니다.Domain entities:

  • 종종 클라이언트에 필요한 것보다 많은 데이터를 포함합니다.Often include more data than the client requires.
  • 공개적으로 노출된 API와 앱의 내부 도메인 모델을 불필요하게 결합합니다.Unnecessarily couple the app's internal domain model with the publicly exposed API.

도메인 엔터티와 클라이언트에 반환된 유형 사이의 매핑을 수행할 수 있습니다.Mapping between domain entities and the types returned to the client can be performed:

다음으로 샘플 앱은 아이디어 컨트롤러의 CreateForSession API 메서드에 대한 단위 테스트를 보여 줍니다.Next, the sample app demonstrates unit tests for the Create and ForSession API methods of the Ideas controller.

샘플 앱에는 두 개의 ForSession 테스트가 포함되어 있습니다.The sample app contains two ForSession tests. 첫 번째 테스트에서 ForSession이 잘못된 세션에 대해 NotFoundObjectResult(HTTP를 찾을 수 없음)를 반환하는지 여부를 판별합니다.The first test determines if ForSession returns a NotFoundObjectResult (HTTP Not Found) for an invalid session:

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

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

    // Assert
    var notFoundObjectResult = Assert.IsType<NotFoundObjectResult>(result);
    Assert.Equal(testSessionId, notFoundObjectResult.Value);
}

두 번째 ForSession 테스트는 ForSession에서 유효한 세션에 대한 세션 아이디어(<List<IdeaDTO>>) 목록을 반환하는지 여부를 판별합니다.The second ForSession test determines if ForSession returns a list of session ideas (<List<IdeaDTO>>) for a valid session. 또한 검사는 첫 번째 아이디어를 검사하여 Name 속성이 올바른지 확인합니다.The checks also examine the first idea to confirm its Name property is correct:

[Fact]
public async Task ForSession_ReturnsIdeasForSession()
{
    // Arrange
    int testSessionId = 123;
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync(GetTestSession());
    var controller = new IdeasController(mockRepo.Object);

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

    // Assert
    var okResult = Assert.IsType<OkObjectResult>(result);
    var returnValue = Assert.IsType<List<IdeaDTO>>(okResult.Value);
    var idea = returnValue.FirstOrDefault();
    Assert.Equal("One", idea.Name);
}

ModelState가 유효하지 않을 때 Create 메서드의 동작을 테스트하기 위해 샘플 앱은 테스트의 일부로 컨트롤러에 모델 오류를 추가합니다.To test the behavior of the Create method when the ModelState is invalid, the sample app adds a model error to the controller as part of the test. 단위 테스트에서 모델 유효성 검사 또는 모델 바인딩을 테스트하지 말고, —잘못된 값이 나타나면 작업 메서드의 동작만 테스트ModelState하세요.Don't try to test model validation or model binding in unit tests—just test the action method's behavior when confronted with an invalid ModelState:

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

두 번째 Create 테스트는 null을 반환하는 리포지토리를 사용하므로 모의 리포지토리가 null을 반환하도록 구성됩니다.The second test of Create 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 returns this result. 샘플 코드에 나온 것과 같이 단일 명령문으로 테스트할 수 있습니다.The test can be accomplished in a single statement, as the sample code illustrates:

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

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

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

세 번째 Create 테스트인 Create_ReturnsNewlyCreatedIdeaForSession에서는 리포지토리의 UpdateAsync 메서드가 호출되는지 확인합니다.The third Create test, Create_ReturnsNewlyCreatedIdeaForSession, verifies that the repository's UpdateAsync method is called. Verifiable을 통해 모의 개체가 호출된 후 확인 가능한 메서드가 실행되었는지 확인하기 위해 모의 리포지토리의 Verify 메서드가 호출됩니다.The mock is called with Verifiable, and the mocked repository's Verify method is called to confirm the verifiable method is executed. UpdateAsync 메서드가 데이터를 저장하도록 보장하는 것은 단위 테스트의 책임이 아니며 통합 테스트로 수행할 수 있습니다.It's not the unit test's responsibility to ensure that the UpdateAsync method saved the data—that can be performed with an integration test.

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

테스트 ActionResult<T>Test ActionResult<T>

ASP.NET Core 2.1 이상에서 ActionResult<T>(ActionResult<TValue>)는 ActionResult에서 파생된 형식을 반환하거나 특정 형식을 반환할 수 있습니다.In ASP.NET Core 2.1 or later, ActionResult<T> (ActionResult<TValue>) enables you to return a type deriving from ActionResult or return a specific type.

샘플 앱에는 지정된 세션 id에 대한 List<IdeaDTO>를 반환하는 메서드가 포함되어 있습니다.The sample app includes a method that returns a List<IdeaDTO> for a given session id. 세션 id가 없으면 컨트롤러는 NotFound를 반환합니다.If the session id doesn't exist, the controller returns NotFound:

[HttpGet("forsessionactionresult/{sessionId}")]
[ProducesResponseType(200)]
[ProducesResponseType(404)]
public async Task<ActionResult<List<IdeaDTO>>> ForSessionActionResult(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 result;
}

ForSessionActionResult 컨트롤러에 대한 두 테스트는 ApiIdeasControllerTests에 포함되어 있습니다.Two tests of the ForSessionActionResult controller are included in the ApiIdeasControllerTests.

첫 번째 테스트는 컨트롤러가 ActionResult를 반환하지만 존재하지 않는 세션 id에 대한 존재하지 않는 아이디어 목록을 반환하는지 확인합니다.The first test confirms that the controller returns an ActionResult but not a nonexistent list of ideas for a nonexistent session id:

[Fact]
public async Task ForSessionActionResult_ReturnsNotFoundObjectResultForNonexistentSession()
{
    // Arrange
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    var controller = new IdeasController(mockRepo.Object);
    var nonExistentSessionId = 999;

    // Act
    var result = await controller.ForSessionActionResult(nonExistentSessionId);

    // Assert
    var actionResult = Assert.IsType<ActionResult<List<IdeaDTO>>>(result);
    Assert.IsType<NotFoundObjectResult>(actionResult.Result);
}

유효한 세션 id에 대한 두 번째 테스트는 메서드가 다음을 반환하는지 확인합니다.For for a valid session id, the second test confirms that the method returns:

  • List<IdeaDTO> 유형의 ActionResult.An ActionResult with a List<IdeaDTO> type.
  • ActionResult<T>.ValueList<IdeaDTO> 유형임.The ActionResult<T>.Value is a List<IdeaDTO> type.
  • 목록의 첫 번째 항목은 모의 세션(GetTestSession 호출로 얻음)에 저장된 아이디어와 일치하는 유효한 아이디어임.The first item in the list is a valid idea matching the idea stored in the mock session (obtained by calling GetTestSession).
[Fact]
public async Task ForSessionActionResult_ReturnsIdeasForSession()
{
    // Arrange
    int testSessionId = 123;
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync(GetTestSession());
    var controller = new IdeasController(mockRepo.Object);

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

    // Assert
    var actionResult = Assert.IsType<ActionResult<List<IdeaDTO>>>(result);
    var returnValue = Assert.IsType<List<IdeaDTO>>(actionResult.Value);
    var idea = returnValue.FirstOrDefault();
    Assert.Equal("One", idea.Name);
}

샘플 앱에는 지정된 세션에 대한 새 Idea를 작성하는 메서드도 포함되어 있습니다.The sample app also includes a method to create a new Idea for a given session. 컨트롤러는 다음을 반환합니다.The controller returns:

[HttpPost("createactionresult")]
[ProducesResponseType(201)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
public async Task<ActionResult<BrainstormSession>> CreateActionResult([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 CreatedAtAction(nameof(CreateActionResult), new { id = session.Id }, session);
}

세 개의 CreateActionResult 테스트는 ApiIdeasControllerTests에 포함되어 있습니다.Three tests of CreateActionResult are included in the ApiIdeasControllerTests.

첫 번째 텍스트는 잘못된 모델에 대해 BadRequest가 반환되는지 확인합니다.The first text confirms that a BadRequest is returned for an invalid model.

[Fact]
public async Task CreateActionResult_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.CreateActionResult(model: null);

    // Assert
    var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
    Assert.IsType<BadRequestObjectResult>(actionResult.Result);
}

두 번째 테스트는 세션이 존재하지 않는 경우 NotFound가 반환되는지 확인합니다.The second test checks that a NotFound is returned if the session doesn't exist.

[Fact]
public async Task CreateActionResult_ReturnsNotFoundObjectResultForNonexistentSession()
{
    // Arrange
    var nonExistentSessionId = 999;
    string testName = "test name";
    string testDescription = "test description";
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    var controller = new IdeasController(mockRepo.Object);

    var newIdea = new NewIdeaModel()
    {
        Description = testDescription,
        Name = testName,
        SessionId = nonExistentSessionId
    };

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

    // Assert
    var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
    Assert.IsType<NotFoundObjectResult>(actionResult.Result);
}

유효한 세션 id의 경우 최종 테스트는 다음을 확인합니다.For a valid session id, the final test confirms that:

  • 메서드가 BrainstormSession 유형의 ActionResult를 반환함.The method returns an ActionResult with a BrainstormSession type.
  • ActionResult<T>.ResultCreatedAtActionResult임.The ActionResult<T>.Result is a CreatedAtActionResult. CreatedAtActionResultLocation 헤더가 있는 201 생성됨 응답과 유사함.CreatedAtActionResult is analogous to a 201 Created response with a Location header.
  • ActionResult<T>.ValueBrainstormSession 유형임.The ActionResult<T>.Value is a BrainstormSession type.
  • 세션을 업데이트하기 위한 모의 호출 UpdateAsync(testSession)가 실행됨.The mock call to update the session, UpdateAsync(testSession), was invoked. Verifiable 메서드 호출은 어설션에서 mockRepo.Verify()를 실행하여 확인됨.The Verifiable method call is checked by executing mockRepo.Verify() in the assertions.
  • 세션에 대해 두 개의 Idea 개체가 반환됨.Two Idea objects are returned for the session.
  • 마지막 항목(UpdateAsync에 대한 모의 호출에 의해 추가된 Idea)이 테스트의 세션에 추가된 newIdea와 일치함.The last item (the Idea added by the mock call to UpdateAsync) matches the newIdea added to the session in the test.
[Fact]
public async Task CreateActionResult_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))
        .ReturnsAsync(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.CreateActionResult(newIdea);

    // Assert
    var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
    var createdAtActionResult = Assert.IsType<CreatedAtActionResult>(actionResult.Result);
    var returnValue = Assert.IsType<BrainstormSession>(createdAtActionResult.Value);
    mockRepo.Verify();
    Assert.Equal(2, returnValue.Ideas.Count());
    Assert.Equal(testName, returnValue.Ideas.LastOrDefault().Name);
    Assert.Equal(testDescription, returnValue.Ideas.LastOrDefault().Description);
}

추가 자료Additional resources