성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] 그냥 RSS Reader 기능과 약간의 UI 편의성 때문에 사용...
[이종효] 오래된 소프트웨어는 보안 위협이 되기도 합니다. 혹시 어떤 기능...
[정성태] @Keystroke IEEE의 문서를 소개해 주시다니... +_...
[손민수 (Keystroke)] 괜히 듀얼채널 구성할 때 한번에 같은 제품 사라고 하는 것이 아...
[정성태] 전각(Full-width)/반각(Half-width) 기능을 토...
[정성태] Vector에 대한 내용은 없습니다. Vector가 닷넷 BCL...
[orion] 글 읽고 찾아보니 디자인 타임에는 InitializeCompon...
[orion] 연휴 전에 재현 프로젝트 올리자 생각해 놓고 여의치 않아서 못 ...
[정성태] 아래의 글에 정리했으니 참고하세요. C# - Typed D...
[정성태] 간단한 재현 프로젝트라도 있을까요? 저런 식으로 설명만 해...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>C# - .NET Core Web API 단위 테스트 방법</h1> <p> 다음과 같은 글이 있군요. ^^<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C# Web API: How to call your endpoint through integration tests ; <a target='tab' href='https://dev.to/willianantunes/c-web-api-how-to-call-your-endpoint-through-integration-tests-li1'>https://dev.to/willianantunes/c-web-api-how-to-call-your-endpoint-through-integration-tests-li1</a> </pre> <br /> 위의 글은 <a target='tab' href='https://www.sysnet.pe.kr/2/0/12729'>xUnit으로 설명</a>하고 있는데, 그대로 하면 재미가 없으니 저는 위의 설명을 <a target='tab' href='https://www.sysnet.pe.kr/2/0/12728'>MSTest로 바꿔서 정리</a>해 보겠습니다. ^^<br /> <br /> <hr style='width: 50%' /><br /> <br /> 우선 ASP.NET Core Web API 프로젝트를 하나 생성하고, "<a target='tab' href='https://dev.to/willianantunes/c-web-api-how-to-call-your-endpoint-through-integration-tests-li1'>C# Web API: How to call your endpoint through integration tests</a>" 글에서 공개한 예제 코드를 다음과 같이 적용해 보겠습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System; namespace WebApplication1.Controllers { [ApiController] [Route("api/v1/[controller]")] <span style='color: blue; font-weight: bold'>public class MoviesController : ControllerBase</span> { private readonly IFilmSpecialist _filmSpecialist; private readonly ILogger<WeatherForecastController> _logger; public MoviesController(ILogger<WeatherForecastController> logger, IFilmSpecialist filmSpecialist) { _logger = logger; _filmSpecialist = filmSpecialist; } [HttpGet] public Movie Get() { _logger.Log(LogLevel.Information, "Let me ask the film specialist..."); <span style='color: blue; font-weight: bold'>var movie = _filmSpecialist.SuggestSomeMovie();</span> _logger.Log(LogLevel.Information, "Suggested movie: {Movie}", movie); <span style='color: blue; font-weight: bold'>return movie;</span> } } <span style='color: blue; font-weight: bold'>public interface IFilmSpecialist { Movie SuggestSomeMovie(); }</span> public class Movie { public string Title { get; } public string Release { get; } public string[] Genres { get; } public string Duration { get; } public Movie(string title, string release, string[] genres, string duration) { Title = title; Release = release; Genres = genres; Duration = duration; } } } </pre> <br /> 실제 업무에서는 _filmSpecialist.SuggestSomeMovie()의 호출은 Startup.cs의 ConfigureServices에서 설정한 IFilmSpecialist 구현체가 주입이 될 것이므로 대개의 경우 DB와 같은 storage와 연계가 될 것입니다. 그리고 그렇게 구현이 완료되면 "http://localhost:7732/api/v1/Movies"라는 호출에 대해 영화 추천 결과가 하나 나올 것입니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 자, 그럼 이제부터 단위 테스트를 작성해 볼까요? 가장 먼저 처리해야 할 것은 "_filmSpecialist.SuggestSomeMovie()" 호출에서 storage를 직접 접근하는 것이 아닌, fake 동작을 수행할 타입을 마련하는 것입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public class FilmSpecialist : IFilmSpecialist { private static readonly Movie[] Films = { new("RoboCop", "10/08/1987", new[] {"Action", "Thriller", "Science Fiction"}, "1h 42m"), new("The Matrix", "05/21/1999", new[] {"Action", "Science Fiction"}, "2h 16m"), new("Soul", "12/25/2020", new[] {"Family", "Animation", "Comedy", "Drama", "Music", "Fantasy"}, "1h 41m"), new("Space Jam", "12/25/1996", new[] {"Adventure", "Animation", "Comedy", "Family"}, "1h 28m"), new("Aladdin", "07/03/1993", new[] {"Animation", "Family", "Adventure", "Fantasy", "Romance"}, "1h 28m"), new("The World of Dragon Ball Z", "01/21/2000", new[] {"Action"}, "20m"), }; public Movie SuggestSomeMovie() { Random random = new(); var filmIndexThatIWillSuggest = random.Next(0, Films.Length); return Films[filmIndexThatIWillSuggest]; } } </pre> <br /> 그리고 이 상태에서 Web API가 아닌, MoviesController.Get 내부의 _filmSpecialist.SuggestSomeMovie(); 메서드를 단위 테스트 하는 것이라면 다음과 같은 식으로 처리하게 될 것입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [TestClass] public class MoviesControllerTests { private readonly IFilmSpecialist _filmSpecialist = new FilmSpecialist(); [TestMethod()] public void GetTest() { var suggestedMovie = _filmSpecialist.SuggestSomeMovie(); var expectedTiles = new[] { "RoboCop", "The Matrix", "Soul", "Space Jam", "Aladdin", "The World of Dragon Ball Z" }; CollectionAssert.Contains(expectedTiles, suggestedMovie.Title); } } </pre> <br /> 하지만, 우리가 원하는 것은 해당 Web API의 [Route("api/v1/[controller]")] 설정까지 반영된 MoviesController.Get 메서드가 호출되는 것을 단위 테스트 범위로 넣고 싶은 것입니다.<br /> <br /> 이를 위해 Microsoft는 <a target='tab' href='https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1'>WebApplicationFactory</a> 타입을 제공하고 있으며 그 방법도 아래의 문서에서 자세하게 소개하고 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > ASP.NET Core MVC 앱 테스트 ; <a target='tab' href='https://learn.microsoft.com/ko-kr/dotnet/architecture/modern-web-apps-azure/test-asp-net-core-mvc-apps'>https://learn.microsoft.com/ko-kr/dotnet/architecture/modern-web-apps-azure/test-asp-net-core-mvc-apps</a> Integration tests in ASP.NET Core ; <a target='tab' href='https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests'>https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests</a> </pre> <br /> 해당 타입을 사용하려면 우선 Microsoft.AspNetCore.Mvc.Testing 패키지를 참조 추가하고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Install-Package Microsoft.AspNetCore.Mvc.Testing </pre> <br /> 이후, ClassInitialize로 WebApplicationFactory<Startup> 인스턴스를 생성해 테스트 대상인 Web API와 유사한 환경을 제공한 후,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > static WebApplicationFactory<Startup> _factory; static HttpClient _httpClient; [ClassInitialize] public static void TestInitialize(TestContext _) { <span style='color: blue; font-weight: bold'>_factory = new WebApplicationFactory<Startup>(); _httpClient = _factory.WithWebHostBuilder(builder => { builder.ConfigureTestServices(services => { services.AddScoped(typeof(IFilmSpecialist), typeof(FilmSpecialist)); }); }).CreateClient();</span> } </pre> <br /> 다음과 같이 Web API를 HttpClient로 호출하면서 실제 Web Server에서 호스팅 중인 Web API를 접근하는 것처럼 단위 테스트를 만들 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [TestMethod()] public async Task GetTest() { <span style='color: blue; font-weight: bold'>var requestPath = "/api/v1/movies";</span> var response = <span style='color: blue; font-weight: bold'>await _httpClient.GetAsync(requestPath);</span> Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); var movie = await response.Content.ReadFromJsonAsync<Movie>(); var expectedTiles = new[] { "RoboCop", "The Matrix", "Soul", "Space Jam", "Aladdin", "The World of Dragon Ball Z" }; CollectionAssert.Contains(expectedTiles, movie.Title); } </pre> <br /> <hr style='width: 50%' /><br /> <br /> "<a target='tab' href='https://dev.to/willianantunes/c-web-api-how-to-call-your-endpoint-through-integration-tests-li1'>C# Web API: How to call your endpoint through integration tests</a>" 글을 보면 Mock을 사용해,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Install-Package Moq </pre> <br /> class FilmSpecialist 정의까지 없애고 있는데요, 이것 역시 동일하게 MSTest에서도 적용할 수 있습니다. 아래는 이를 반영한 단위 테스트 전체 소스 코드입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using Microsoft.VisualStudio.TestTools.UnitTesting; using WebApplication1.Controllers; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; using System.Net.Http; using Microsoft.Extensions.DependencyInjection; using System.Net.Http.Json; using System.Net; using Moq; using Microsoft.Extensions.DependencyInjection.Extensions; namespace WebApplication1.Controllers.Tests { [TestClass] public class MoviesControllerTests { static IFilmSpecialist _filmSpecialist; static WebApplicationFactory<Startup> _factory; static HttpClient _httpClient; [ClassInitialize] public static void TestInitialize(TestContext _) { <span style='color: blue; font-weight: bold'>_filmSpecialist = Mock.Of<IFilmSpecialist>();</span> _factory = new WebApplicationFactory<Startup>(); _httpClient = _factory.WithWebHostBuilder(builder => { builder.ConfigureTestServices(services => { services.RemoveAll<IFilmSpecialist>(); <span style='color: blue; font-weight: bold'>services.TryAddTransient(_ => _filmSpecialist);</span> }); }).CreateClient(); } [TestMethod()] public async Task GetTestWithMock() { var requestPath = "/api/v1/movies"; var movieToBeSuggested = new Movie("Schindler's List", "12/31/1993", new[] { "Drama", "History", "War" }, "3h 15m"); <span style='color: blue; font-weight: bold'>Mock.Get(_filmSpecialist) .Setup(f => f.SuggestSomeMovie()) .Returns(movieToBeSuggested) .Verifiable();</span> var response = await _httpClient.GetAsync(requestPath); Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); var movie = await response.Content.ReadFromJsonAsync<Movie>(); Assert.AreEqual(movieToBeSuggested, movie); } } } </pre> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=1835&boardid=331301885'>첨부 파일은 이 글의 예제 코드를 포함</a>합니다.)<br /> <br /> 그러니까, 결국 이 글 하나를 쓰려고 했는데 어쩌다가 글 타래가 엮여 다음의 글들까지 써진 것입니다. ^^;<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 비주얼 스튜디오 - 단위 테스트 선택 시 MSTestv2 외의 xUnit, NUnit 사용법 ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12726'>https://www.sysnet.pe.kr/2/0/12726</a> MSTestv2 단위 테스트에 메서드/클래스/어셈블리 수준의 문맥 제공 ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12728'>https://www.sysnet.pe.kr/2/0/12728</a> xUnit 단위 테스트에 메서드/클래스 수준의 문맥 제공 - Fixture ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12729'>https://www.sysnet.pe.kr/2/0/12729</a> C# 단위 테스트 - MSTestv2/NUnit의 Assert.Inconclusive 사용법(?) ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12727'>https://www.sysnet.pe.kr/2/0/12727</a> Visual Studio 2017부터 단위 테스트에 DataRow 특성 지원 ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12749'>https://www.sysnet.pe.kr/2/0/12749</a> MSTest - 단위 테스트에 static/instance 유형의 private 멤버 접근 방법 ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12755'>https://www.sysnet.pe.kr/2/0/12755</a> </pre> <br /> <hr style='width: 50%' /><br /> <br /> 참고로, 단위 테스트 메서드를 비동기로 하려는 경우 단순히 async로만 지정하면 안 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [TestMethod()] public <span style='color: blue; font-weight: bold'>async void</span> GetTestWithMock() { // ...[생략]... } </pre> <br /> 저렇게 하면 단위 테스트의 Run/Debug 시에 아예 해당 메서드가 실행조차 안 되는 것을 확인할 수 있습니다. 반드시 "async Task"로 해줘야 합니다. ^^<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [TestMethod()] public <span style='color: blue; font-weight: bold'>async Task</span> GetTestWithMock() { // ...[생략]... } </pre> </p><br /> <br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
8602
(왼쪽의 숫자를 입력해야 합니다.)