ASP.NET Core에서 컨트롤러 논리 단위 테스트
작성자 Steve Smith
단위 테스트는 앱의 일부분을 인프라 및 종속성과 분리하여 테스트를 수행합니다. 컨트롤러 논리를 단위 테스트할 때 단일 작업의 콘텐츠만 테스트되고, 작업의 종속성 또는 프레임워크 자체의 동작은 테스트되지 않습니다.
컨트롤러 단위 테스트
컨트롤러의 동작에 초점을 맞춰 컨트롤러 동작의 단위 테스트를 설정합니다. 컨트롤러 단위 테스트는 필터, 라우팅, 모델 바인딩 같은 시나리오를 방지합니다. 전체적으로 요청에 응답하는 구성 요소 간 상호 작용을 포함하는 테스트는 ‘통합 테스트’에서 처리합니다. 통합 테스트에 대한 자세한 내용은 ASP.NET Core의 통합 테스트를 참조하세요.
사용자 지정 필터 및 경로를 작성할 때, 특정 컨트롤러 작업에 대한 테스트의 일부로서가 아니라, 별도로 단위 테스트를 수행하세요.
컨트롤러 단위 테스트를 시연하려면 예제 앱에서 다음 컨트롤러를 검토하세요.
Home 컨트롤러는 브레인스토밍 세션 목록을 표시하고 POST 요청을 사용하여 새로운 브레인스토밍 세션을 만들 수 있습니다.
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));
}
}
이전 컨트롤러는 다음과 같습니다.
- 명시적 종속성 원칙을 따릅니다.
- DI(종속성 주입)에서
IBrainstormSessionRepository
의 인스턴스를 제공할 것으로 기대합니다. - Moq와 같은 모의 개체 프레임워크를 사용하여 모의
IBrainstormSessionRepository
서비스로 테스트할 수 있습니다. ‘모의 개체’는 테스트에 사용되는 사전 결정된 속성 및 메서드 동작 집합이 있는 제작된 개체입니다. 자세한 내용은 통합 테스트 소개를 참조하세요.
HTTP GET Index
메서드는 반복 또는 분기가 없으며 한 가지 메서드만 호출합니다. 이 작업의 단위 테스트는 다음을 수행합니다.
GetTestSessions
메서드를 사용하여IBrainstormSessionRepository
서비스를 모방합니다.GetTestSessions
는 날짜 및 세션 이름이 있는 두 개의 모의 브레인스토밍 세션을 작성합니다.Index
메서드를 실행합니다.- 메서드에서 반환된 결과에 대해 어설션을 만듭니다.
- ViewResult가 반환됩니다.
- ViewDataDictionary.Model은
StormSessionViewModel
입니다. 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;
}
Home 컨트롤러의 HTTP POST Index
메서드 테스트는 다음을 확인합니다.
- ModelState.IsValid인 경우 작업 메서드는
false
적절한 데이터가 포함된 400 잘못된 요청을 ViewResult 반환합니다. - 시기
ModelState.IsValid
true
:- 리포지토리의
Add
메서드를 호출합니다. - RedirectToActionResult가 올바른 인수와 함께 반환됩니다.
- 리포지토리의
아래의 첫 번째 테스트처럼 AddModelError로 오류를 추가하여 잘못된 모델 상태를 테스트할 수 있습니다.
[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
가 반환됩니다. 이 테스트는 잘못된 모델을 전달하려고 시도하지 않습니다. 모델 바인딩이 실행되지 않기 때문에(통합 테스트는 모델 바인딩을 사용하는 반면), 잘못된 모델을 전달하는 것은 유효한 접근 방식이 아닙니다. 이번 경우에는 모델 바인딩을 테스트하지 않습니다. 이러한 단위 테스트는 작업 메서드의 코드만 테스트합니다.
두 번째 테스트는 ModelState
가 유효한 시기를 확인합니다.
- 새
BrainstormSession
이 (리포지토리를 통해) 추가됩니다. - 메서드가 예상 속성과 함께
RedirectToActionResult
를 반환함.
호출되지 않은 모의 호출은 일반적으로 무시되지만, 설정 호출의 끝부분에서 Verifiable
을 호출하면 테스트에서 모의 유효성 검사가 가능합니다. 이는 mockRepo.Verify
호출을 통해 수행되며, 예상된 메서드가 호출되지 않았으면 테스트가 실패합니다.
참고 항목
이 샘플에 사용된 Moq 라이브러리를 사용하면 확인 가능한 또는 “엄격한” 모의 개체를 확인 불가능한 모의 개체(“느슨한” 모의 개체 또는 스텁이라고도 함)와 혼합할 수 있습니다. Moq를 사용하여 모의 동작 사용자 지정에 대해 자세히 알아보세요.
예제 앱의 SessionController는 특정 브레인스토밍 세션과 관련된 정보를 표시합니다. 이 컨트롤러에는 잘못된 id
값을 처리하는 논리가 포함되어 있습니다(다음 예제에는 이러한 시나리오를 다루는 두 가지 return
시나리오가 있습니다). 최종 return
문은 뷰Controllers/SessionController.cs
()에 대한 새 StormSessionViewModel
값을 반환합니다.
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
시나리오에 대한 하나의 테스트가 포함되어 있습니다.
[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);
}
Ideas 컨트롤러로 이동해보면 앱은 api/ideas
경로에서 기능을 웹 API로 노출합니다.
- 브레인스토밍 세션과 연관된 아이디어(
IdeaDTO
)의 목록이ForSession
메서드에 의해 반환됩니다. Create
메서드는 세션에 새 아이디어를 추가합니다.
[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 호출을 통해 직접 비즈니스 도메인 엔터티를 반환하지 마세요. 도메인 엔터티는 다음과 같습니다.
- 종종 클라이언트에 필요한 것보다 많은 데이터를 포함합니다.
- 공개적으로 노출된 API와 앱의 내부 도메인 모델을 불필요하게 결합합니다.
도메인 엔터티와 클라이언트에 반환되는 형식 간의 매핑을 수행할 수 있습니다.
- 예제 앱에서 사용하는 것처럼 LINQ
Select
를 사용하여 수동으로 수행합니다. 자세한 내용은 LINQ(Language-Integrated Query)를 참조하세요. - AutoMapper와 같은 라이브러리를 사용하여 자동으로 수행합니다.
다음으로 예제 앱은 Ideas 컨트롤러의 Create
및 ForSession
API 메서드에 대한 단위 테스트를 보여줍니다.
예제 앱에는 두 개의 ForSession
테스트가 포함되어 있습니다. 첫 번째 테스트는 ForSession
이 잘못된 세션에 대해 NotFoundObjectResult(HTTP를 찾을 수 없음)를 반환하는지 여부를 판별합니다.
[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>>
) 목록을 반환하는지 여부를 판별합니다. 또한 이 검사는 첫 번째 아이디어를 검사하여 Name
속성이 올바른지도 확인합니다.
[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
메서드의 동작을 테스트하기 위해 예제 앱은 테스트의 일부로 컨트롤러에 모델 오류를 추가합니다. 단위 테스트에서 모델 유효성 검사 또는 모델 바인딩을 테스트하지 말고, 잘못된 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
을 반환하도록 구성됩니다. 테스트 데이터베이스를 만들고(메모리 내부에 또는 다른 위치에) 이 결과를 반환하는 쿼리를 작성할 필요가 없습니다. 예제 코드에서 볼 수 있는 것처럼 단일 명령문으로 테스트할 수 있습니다.
[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
메서드가 호출되는지 확인합니다. Verifiable
을 통해 모의 개체가 호출된 후 확인 가능한 메서드가 실행되었는지 확인하기 위해 모의 리포지토리의 Verify
메서드가 호출됩니다. UpdateAsync
메서드가 데이터를 저장하도록 보장하는 것은 단위 테스트의 책임이 아니며 통합 테스트로 수행할 수 있습니다.
[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>
ActionResult<T>(ActionResult<TValue>)는 ActionResult
에서 파생된 형식을 반환하거나 특정 형식을 반환할 수 있습니다.
예제 앱에는 지정된 세션 id
에 대한 List<IdeaDTO>
를 반환하는 메서드가 포함되어 있습니다. 세션 id
가 없으면 컨트롤러는 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
에 포함되어 있습니다.
첫 번째 테스트는 컨트롤러가 ActionResult
를 반환하지만 존재하지 않는 세션 id
에 대한 존재하지 않는 아이디어 목록을 반환하는지 확인합니다.
ActionResult
형식이ActionResult<List<IdeaDTO>>
입니다.- Result는 NotFoundObjectResult임.
[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
에 대한 두 번째 테스트는 메서드가 다음을 반환하는지 확인합니다.
ActionResult
형식이List<IdeaDTO>
입니다.- ActionResult<T>.Value가
List<IdeaDTO>
유형임. - 목록의 첫 번째 항목은 모의 세션(
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
를 작성하는 메서드도 포함되어 있습니다. 컨트롤러는 다음을 반환합니다.
- 잘못된 모델에 대한 BadRequest.
- NotFound(세션이 없는 경우).
- CreatedAtAction(세션이 새 아이디어로 수정될 때).
[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
에 포함되어 있습니다.
첫 번째 테스트는 잘못된 모델에 대해 BadRequest가 반환되는지 확인합니다.
[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가 반환되는지 확인합니다.
[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
에 대한, 마지막 테스트는 다음을 확인합니다.
- 메서드가
BrainstormSession
유형의ActionResult
를 반환함. - ActionResult<T>.Result가 CreatedAtActionResult임.
CreatedAtActionResult
가Location
헤더가 있는 201 생성됨 응답과 유사함. - ActionResult<T>.Value가
BrainstormSession
유형임. - 세션을 업데이트하기 위한 모의 호출
UpdateAsync(testSession)
가 실행됨.Verifiable
메서드 호출은 어설션에서mockRepo.Verify()
를 실행하여 확인됩니다. - 세션에 대해 두 개의
Idea
개체가 반환됩니다. - 마지막 항목(
UpdateAsync
에 대한 모의 호출에 의해 추가된Idea
)이 테스트의 세션에 추가된newIdea
와 일치합니다.
[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);
}
컨트롤러는 모든 ASP.NET Core MVC 앱에서 중심적인 역할을 합니다. 따라서 컨트롤러가 의도한 대로 동작한다고 확신할 수 있어야 합니다. 자동화된 테스트는 앱이 프로덕션 환경에 배포되기 전에 오류를 발견할 수 있습니다.
컨트롤러 논리의 단위 테스트
단위 테스트는 앱의 일부분을 인프라 및 종속성과 분리하여 테스트를 수행합니다. 컨트롤러 논리를 단위 테스트할 때 단일 작업의 콘텐츠만 테스트되고, 작업의 종속성 또는 프레임워크 자체의 동작은 테스트되지 않습니다.
컨트롤러의 동작에 초점을 맞춰 컨트롤러 동작의 단위 테스트를 설정합니다. 컨트롤러 단위 테스트는 필터, 라우팅, 모델 바인딩 같은 시나리오를 방지합니다. 전체적으로 요청에 응답하는 구성 요소 간 상호 작용을 포함하는 테스트는 ‘통합 테스트’에서 처리합니다. 통합 테스트에 대한 자세한 내용은 ASP.NET Core의 통합 테스트를 참조하세요.
사용자 지정 필터 및 경로를 작성할 때, 특정 컨트롤러 작업에 대한 테스트의 일부로서가 아니라, 별도로 단위 테스트를 수행하세요.
컨트롤러 단위 테스트를 시연하려면 예제 앱에서 다음 컨트롤러를 검토하세요. Home 컨트롤러는 브레인스토밍 세션 목록을 표시하고 POST 요청을 사용하여 새로운 브레인스토밍 세션을 만들 수 있습니다.
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));
}
}
이전 컨트롤러는 다음과 같습니다.
- 명시적 종속성 원칙을 따릅니다.
- DI(종속성 주입)에서
IBrainstormSessionRepository
의 인스턴스를 제공할 것으로 기대합니다. - Moq와 같은 모의 개체 프레임워크를 사용하여 모의
IBrainstormSessionRepository
서비스로 테스트할 수 있습니다. ‘모의 개체’는 테스트에 사용되는 사전 결정된 속성 및 메서드 동작 집합이 있는 제작된 개체입니다. 자세한 내용은 통합 테스트 소개를 참조하세요.
HTTP GET Index
메서드는 반복 또는 분기가 없으며 한 가지 메서드만 호출합니다. 이 작업의 단위 테스트는 다음을 수행합니다.
GetTestSessions
메서드를 사용하여IBrainstormSessionRepository
서비스를 모방합니다.GetTestSessions
는 날짜 및 세션 이름이 있는 두 개의 모의 브레인스토밍 세션을 작성합니다.Index
메서드를 실행합니다.- 메서드에서 반환된 결과에 대해 어설션을 만듭니다.
- ViewResult가 반환됩니다.
- ViewDataDictionary.Model은
StormSessionViewModel
입니다. 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;
}
Home 컨트롤러의 HTTP POST Index
메서드 테스트는 다음을 확인합니다.
- ModelState.IsValid인 경우 작업 메서드는
false
적절한 데이터가 포함된 400 잘못된 요청을 ViewResult 반환합니다. - 시기
ModelState.IsValid
true
:- 리포지토리의
Add
메서드를 호출합니다. - RedirectToActionResult가 올바른 인수와 함께 반환됩니다.
- 리포지토리의
아래의 첫 번째 테스트처럼 AddModelError로 오류를 추가하여 잘못된 모델 상태를 테스트할 수 있습니다.
[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
가 반환됩니다. 이 테스트는 잘못된 모델을 전달하려고 시도하지 않습니다. 모델 바인딩이 실행되지 않기 때문에(통합 테스트는 모델 바인딩을 사용하는 반면), 잘못된 모델을 전달하는 것은 유효한 접근 방식이 아닙니다. 이번 경우에는 모델 바인딩을 테스트하지 않습니다. 이러한 단위 테스트는 작업 메서드의 코드만 테스트합니다.
두 번째 테스트는 ModelState
가 유효한 시기를 확인합니다.
- 새
BrainstormSession
이 (리포지토리를 통해) 추가됩니다. - 메서드가 예상 속성과 함께
RedirectToActionResult
를 반환함.
호출되지 않은 모의 호출은 일반적으로 무시되지만, 설정 호출의 끝부분에서 Verifiable
을 호출하면 테스트에서 모의 유효성 검사가 가능합니다. 이는 mockRepo.Verify
호출을 통해 수행되며, 예상된 메서드가 호출되지 않았으면 테스트가 실패합니다.
참고 항목
이 샘플에 사용된 Moq 라이브러리를 사용하면 확인 가능한 또는 “엄격한” 모의 개체를 확인 불가능한 모의 개체(“느슨한” 모의 개체 또는 스텁이라고도 함)와 혼합할 수 있습니다. Moq를 사용하여 모의 동작 사용자 지정에 대해 자세히 알아보세요.
예제 앱의 SessionController는 특정 브레인스토밍 세션과 관련된 정보를 표시합니다. 이 컨트롤러에는 잘못된 id
값을 처리하는 논리가 포함되어 있습니다(다음 예제에는 이러한 시나리오를 다루는 두 가지 return
시나리오가 있습니다). 최종 return
문은 뷰Controllers/SessionController.cs
()에 대한 새 StormSessionViewModel
값을 반환합니다.
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
시나리오에 대한 하나의 테스트가 포함되어 있습니다.
[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);
}
Ideas 컨트롤러로 이동해보면 앱은 api/ideas
경로에서 기능을 웹 API로 노출합니다.
- 브레인스토밍 세션과 연관된 아이디어(
IdeaDTO
)의 목록이ForSession
메서드에 의해 반환됩니다. Create
메서드는 세션에 새 아이디어를 추가합니다.
[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 호출을 통해 직접 비즈니스 도메인 엔터티를 반환하지 마세요. 도메인 엔터티는 다음과 같습니다.
- 종종 클라이언트에 필요한 것보다 많은 데이터를 포함합니다.
- 공개적으로 노출된 API와 앱의 내부 도메인 모델을 불필요하게 결합합니다.
도메인 엔터티와 클라이언트에 반환되는 형식 간의 매핑을 수행할 수 있습니다.
- 예제 앱에서 사용하는 것처럼 LINQ
Select
를 사용하여 수동으로 수행합니다. 자세한 내용은 LINQ(Language-Integrated Query)를 참조하세요. - AutoMapper와 같은 라이브러리를 사용하여 자동으로 수행합니다.
다음으로 예제 앱은 Ideas 컨트롤러의 Create
및 ForSession
API 메서드에 대한 단위 테스트를 보여줍니다.
예제 앱에는 두 개의 ForSession
테스트가 포함되어 있습니다. 첫 번째 테스트는 ForSession
이 잘못된 세션에 대해 NotFoundObjectResult(HTTP를 찾을 수 없음)를 반환하는지 여부를 판별합니다.
[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>>
) 목록을 반환하는지 여부를 판별합니다. 또한 이 검사는 첫 번째 아이디어를 검사하여 Name
속성이 올바른지도 확인합니다.
[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
메서드의 동작을 테스트하기 위해 예제 앱은 테스트의 일부로 컨트롤러에 모델 오류를 추가합니다. 단위 테스트에서 모델 유효성 검사 또는 모델 바인딩을 테스트하지 말고, 잘못된 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
을 반환하도록 구성됩니다. 테스트 데이터베이스를 만들고(메모리 내부에 또는 다른 위치에) 이 결과를 반환하는 쿼리를 작성할 필요가 없습니다. 예제 코드에서 볼 수 있는 것처럼 단일 명령문으로 테스트할 수 있습니다.
[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
메서드가 호출되는지 확인합니다. Verifiable
을 통해 모의 개체가 호출된 후 확인 가능한 메서드가 실행되었는지 확인하기 위해 모의 리포지토리의 Verify
메서드가 호출됩니다. UpdateAsync
메서드가 데이터를 저장하도록 보장하는 것은 단위 테스트의 책임이 아니며 통합 테스트로 수행할 수 있습니다.
[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>
ASP.NET Core 2.1 이상에서 ActionResult<T>(ActionResult<TValue>)는 ActionResult
에서 파생된 형식을 반환하거나 특정 형식을 반환할 수 있습니다.
예제 앱에는 지정된 세션 id
에 대한 List<IdeaDTO>
를 반환하는 메서드가 포함되어 있습니다. 세션 id
가 없으면 컨트롤러는 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
에 포함되어 있습니다.
첫 번째 테스트는 컨트롤러가 ActionResult
를 반환하지만 존재하지 않는 세션 id
에 대한 존재하지 않는 아이디어 목록을 반환하는지 확인합니다.
ActionResult
형식이ActionResult<List<IdeaDTO>>
입니다.- Result는 NotFoundObjectResult임.
[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
에 대한 두 번째 테스트는 메서드가 다음을 반환하는지 확인합니다.
ActionResult
형식이List<IdeaDTO>
입니다.- ActionResult<T>.Value가
List<IdeaDTO>
유형임. - 목록의 첫 번째 항목은 모의 세션(
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
를 작성하는 메서드도 포함되어 있습니다. 컨트롤러는 다음을 반환합니다.
- 잘못된 모델에 대한 BadRequest.
- NotFound(세션이 없는 경우).
- CreatedAtAction(세션이 새 아이디어로 수정될 때).
[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
에 포함되어 있습니다.
첫 번째 테스트는 잘못된 모델에 대해 BadRequest가 반환되는지 확인합니다.
[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가 반환되는지 확인합니다.
[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
에 대한, 마지막 테스트는 다음을 확인합니다.
- 메서드가
BrainstormSession
유형의ActionResult
를 반환함. - ActionResult<T>.Result가 CreatedAtActionResult임.
CreatedAtActionResult
가Location
헤더가 있는 201 생성됨 응답과 유사함. - ActionResult<T>.Value가
BrainstormSession
유형임. - 세션을 업데이트하기 위한 모의 호출
UpdateAsync(testSession)
가 실행됨.Verifiable
메서드 호출은 어설션에서mockRepo.Verify()
를 실행하여 확인됩니다. - 세션에 대해 두 개의
Idea
개체가 반환됩니다. - 마지막 항목(
UpdateAsync
에 대한 모의 호출에 의해 추가된Idea
)이 테스트의 세션에 추가된newIdea
와 일치합니다.
[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);
}
추가 리소스
- ASP.NET Core의 통합 테스트
- Visual Studio를 사용하여 단위 테스트를 만들고 실행
- MyTested.AspNetCore.Mvc - ASP.NET Core MVC용 흐름 테스트 라이브러리: MVC 및 웹 API 앱을 테스트하기 위한 유연한 인터페이스를 제공하는 강력한 형식의 단위 테스트 라이브러리입니다. (Microsoft에서 유지 관리하거나 지원하지 않습니다.)
- JustMockLite: .NET 개발자를 위한 모의 프레임워크입니다. (Microsoft에서 유지 관리하거나 지원하지 않습니다.)
ASP.NET Core