Microsoft MVP성태의 닷넷 이야기
.NET Framework: 1084. C# - .NET Core Web API 단위 테스트 방법 [링크 복사], [링크+제목 복사],
조회: 8107
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

(시리즈 글이 8개 있습니다.)
VS.NET IDE: 105. Visual Studio의 단위 테스트 작성 시 Fakes를 이용한 메서드 재정의 방법
; https://www.sysnet.pe.kr/2/0/10858

VS.NET IDE: 169. 비주얼 스튜디오 - 단위 테스트 선택 시 MSTestv2 외의 xUnit, NUnit 사용법
; https://www.sysnet.pe.kr/2/0/12726

.NET Framework: 1078. C# 단위 테스트 - MSTestv2/NUnit의 Assert.Inconclusive 사용법(?)
; https://www.sysnet.pe.kr/2/0/12727

.NET Framework: 1079. MSTestv2 단위 테스트에 메서드/클래스/어셈블리 수준의 문맥 제공
; https://www.sysnet.pe.kr/2/0/12728

.NET Framework: 1080. xUnit 단위 테스트에 메서드/클래스 수준의 문맥 제공 - Fixture
; https://www.sysnet.pe.kr/2/0/12729

개발 환경 구성: 590. Visual Studio 2017부터 단위 테스트에 DataRow 특성 지원
; https://www.sysnet.pe.kr/2/0/12749

개발 환경 구성: 593. MSTest - 단위 테스트에 static/instance 유형의 private 멤버 접근 방법
; https://www.sysnet.pe.kr/2/0/12755

.NET Framework: 1084. C# - .NET Core Web API 단위 테스트 방법
; https://www.sysnet.pe.kr/2/0/12756




C# - .NET Core Web API 단위 테스트 방법

다음과 같은 글이 있군요. ^^

C# Web API: How to call your endpoint through integration tests
; https://dev.to/willianantunes/c-web-api-how-to-call-your-endpoint-through-integration-tests-li1

위의 글은 xUnit으로 설명하고 있는데, 그대로 하면 재미가 없으니 저는 위의 설명을 MSTest로 바꿔서 정리해 보겠습니다. ^^




우선 ASP.NET Core Web API 프로젝트를 하나 생성하고, "C# Web API: How to call your endpoint through integration tests" 글에서 공개한 예제 코드를 다음과 같이 적용해 보겠습니다.

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;

namespace WebApplication1.Controllers
{
    [ApiController]
    [Route("api/v1/[controller]")]
    public class MoviesController : ControllerBase
    {
        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...");
            var movie = _filmSpecialist.SuggestSomeMovie();
            _logger.Log(LogLevel.Information, "Suggested movie: {Movie}", movie);
            return movie;
        }
    }

    public interface IFilmSpecialist
    {
        Movie SuggestSomeMovie();
    }

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

실제 업무에서는 _filmSpecialist.SuggestSomeMovie()의 호출은 Startup.cs의 ConfigureServices에서 설정한 IFilmSpecialist 구현체가 주입이 될 것이므로 대개의 경우 DB와 같은 storage와 연계가 될 것입니다. 그리고 그렇게 구현이 완료되면 "http://localhost:7732/api/v1/Movies"라는 호출에 대해 영화 추천 결과가 하나 나올 것입니다.




자, 그럼 이제부터 단위 테스트를 작성해 볼까요? 가장 먼저 처리해야 할 것은 "_filmSpecialist.SuggestSomeMovie()" 호출에서 storage를 직접 접근하는 것이 아닌, fake 동작을 수행할 타입을 마련하는 것입니다.

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

그리고 이 상태에서 Web API가 아닌, MoviesController.Get 내부의 _filmSpecialist.SuggestSomeMovie(); 메서드를 단위 테스트 하는 것이라면 다음과 같은 식으로 처리하게 될 것입니다.

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

하지만, 우리가 원하는 것은 해당 Web API의 [Route("api/v1/[controller]")] 설정까지 반영된 MoviesController.Get 메서드가 호출되는 것을 단위 테스트 범위로 넣고 싶은 것입니다.

이를 위해 Microsoft는 WebApplicationFactory 타입을 제공하고 있으며 그 방법도 아래의 문서에서 자세하게 소개하고 있습니다.

ASP.NET Core MVC 앱 테스트
; https://learn.microsoft.com/ko-kr/dotnet/architecture/modern-web-apps-azure/test-asp-net-core-mvc-apps

Integration tests in ASP.NET Core
; https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests

해당 타입을 사용하려면 우선 Microsoft.AspNetCore.Mvc.Testing 패키지를 참조 추가하고,

Install-Package Microsoft.AspNetCore.Mvc.Testing

이후, ClassInitialize로 WebApplicationFactory<Startup> 인스턴스를 생성해 테스트 대상인 Web API와 유사한 환경을 제공한 후,

static WebApplicationFactory<Startup> _factory;
static HttpClient _httpClient;

[ClassInitialize]
public static void TestInitialize(TestContext _)
{
    _factory = new WebApplicationFactory<Startup>();
    _httpClient = _factory.WithWebHostBuilder(builder =>
    {
        builder.ConfigureTestServices(services =>
        {
            services.AddScoped(typeof(IFilmSpecialist), typeof(FilmSpecialist));
        });
    }).CreateClient();
}

다음과 같이 Web API를 HttpClient로 호출하면서 실제 Web Server에서 호스팅 중인 Web API를 접근하는 것처럼 단위 테스트를 만들 수 있습니다.

[TestMethod()]
public async Task GetTest()
{
    var requestPath = "/api/v1/movies";
    var response = await _httpClient.GetAsync(requestPath);
    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);
}




"C# Web API: How to call your endpoint through integration tests" 글을 보면 Mock을 사용해,

Install-Package Moq

class FilmSpecialist 정의까지 없애고 있는데요, 이것 역시 동일하게 MSTest에서도 적용할 수 있습니다. 아래는 이를 반영한 단위 테스트 전체 소스 코드입니다.

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 _)
        {
            _filmSpecialist = Mock.Of<IFilmSpecialist>();

            _factory = new WebApplicationFactory<Startup>();
            _httpClient = _factory.WithWebHostBuilder(builder =>
            {
                builder.ConfigureTestServices(services =>
                {
                    services.RemoveAll<IFilmSpecialist>();
                    services.TryAddTransient(_ => _filmSpecialist);
                });
            }).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");
            Mock.Get(_filmSpecialist)
                .Setup(f => f.SuggestSomeMovie())
                .Returns(movieToBeSuggested)
                .Verifiable();

            var response = await _httpClient.GetAsync(requestPath);
            Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);

            var movie = await response.Content.ReadFromJsonAsync<Movie>();
            Assert.AreEqual(movieToBeSuggested, movie);
        }
    }
}

(첨부 파일은 이 글의 예제 코드를 포함합니다.)

그러니까, 결국 이 글 하나를 쓰려고 했는데 어쩌다가 글 타래가 엮여 다음의 글들까지 써진 것입니다. ^^;

비주얼 스튜디오 - 단위 테스트 선택 시 MSTestv2 외의 xUnit, NUnit 사용법
; https://www.sysnet.pe.kr/2/0/12726

MSTestv2 단위 테스트에 메서드/클래스/어셈블리 수준의 문맥 제공
; https://www.sysnet.pe.kr/2/0/12728

xUnit 단위 테스트에 메서드/클래스 수준의 문맥 제공 - Fixture
; https://www.sysnet.pe.kr/2/0/12729

C# 단위 테스트 - MSTestv2/NUnit의 Assert.Inconclusive 사용법(?)
; https://www.sysnet.pe.kr/2/0/12727

Visual Studio 2017부터 단위 테스트에 DataRow 특성 지원
; https://www.sysnet.pe.kr/2/0/12749

MSTest - 단위 테스트에 static/instance 유형의 private 멤버 접근 방법
; https://www.sysnet.pe.kr/2/0/12755




참고로, 단위 테스트 메서드를 비동기로 하려는 경우 단순히 async로만 지정하면 안 됩니다.

[TestMethod()]
public async void GetTestWithMock()
{
    // ...[생략]...
}

저렇게 하면 단위 테스트의 Run/Debug 시에 아예 해당 메서드가 실행조차 안 되는 것을 확인할 수 있습니다. 반드시 "async Task"로 해줘야 합니다. ^^

[TestMethod()]
public async Task GetTestWithMock()
{
    // ...[생략]...
}





[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 4/13/2023]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2024-02-16 09시44분
WebApplicationFactory In ASP.NET Core: Practical Tips For C# Developers
; https://dev.to/devleader/webapplicationfactory-in-aspnet-core-practical-tips-for-c-developers-2mo
정성태

... 16  17  18  19  20  21  22  23  [24]  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
13035정성태4/22/20227670Windows: 204. Windows 10부터 바뀐 QueryPerformanceFrequency, QueryPerformanceCounter
13034정성태4/21/20227022.NET Framework: 1996. C# XingAPI - 주식 종목에 따른 PBR, PER, ROE, ROA 구하는 방법(t3320, t8430 예제)파일 다운로드1
13033정성태4/18/20227621.NET Framework: 1195. C# - Thread.Yield와 Thread.Sleep(0)의 차이점(?)
13032정성태4/17/20227350오류 유형: 805. Github의 50MB 파일 크기 제한 - warning: GH001: Large files detected. You may want to try Git Large File Storage
13031정성태4/15/20226888.NET Framework: 1194. C# - IdealProcessor와 ProcessorAffinity의 차이점
13030정성태4/15/20226540오류 유형: 804. 정규 표현식 오류 - Quantifier {x,y} following nothing.
13029정성태4/14/20226952Windows: 203. iisreset 후에도 이전에 설정한 전역 환경 변수가 w3wp.exe에 적용되는 문제
13028정성태4/13/20226870.NET Framework: 1193. (appsettings.json처럼) web.config의 Debug/Release에 따른 설정 적용
13027정성태4/12/20227132.NET Framework: 1192. C# - 환경 변수의 변화를 알리는 WM_SETTINGCHANGE Win32 메시지 사용법파일 다운로드1
13026정성태4/11/20228677.NET Framework: 1191. C 언어로 작성된 FFmpeg Examples의 C# 포팅 전체 소스 코드 [3]
13025정성태4/11/20228024.NET Framework: 1190. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 vaapi_encode.c, vaapi_transcode.c 예제 포팅
13024정성태4/7/20226514.NET Framework: 1189. C# - 런타임 환경에 따라 달라진 AppDomain.GetCurrentThreadId 메서드
13023정성태4/6/20226811.NET Framework: 1188. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 transcoding.c 예제 포팅 [3]
13022정성태3/31/20226721Windows: 202. 윈도우 11 업그레이드 - "PC Health Check"를 통과했지만 여전히 업그레이드가 안 되는 경우 해결책
13021정성태3/31/20226907Windows: 201. Windows - INF 파일을 이용한 장치 제거 방법
13020정성태3/30/20226662.NET Framework: 1187. RDP 접속 시 WPF UserControl의 Unloaded 이벤트 발생파일 다운로드1
13019정성태3/30/20226629.NET Framework: 1186. Win32 Message를 Code로부터 메시지 이름 자체를 구하고 싶다면?파일 다운로드1
13018정성태3/29/20227169.NET Framework: 1185. C# - Unsafe.AsPointer가 반환한 포인터는 pinning 상태일까요? [5]
13017정성태3/28/20226962.NET Framework: 1184. C# - GC Heap에 위치한 참조 개체의 주소를 알아내는 방법 - 두 번째 이야기 [3]
13016정성태3/27/20227846.NET Framework: 1183. C# 11에 추가된 ref 필드의 (우회) 구현 방법파일 다운로드1
13015정성태3/26/20229182.NET Framework: 1182. C# 11 - ref struct에 ref 필드를 허용 [1]
13014정성태3/23/20227753VC++: 155. CComPtr/CComQIPtr과 Conformance mode 옵션의 충돌 [1]
13013정성태3/22/20226060개발 환경 구성: 641. WSL 우분투 인스턴스에 파이썬 2.7 개발 환경 구성하는 방법
13012정성태3/21/20225388오류 유형: 803. C# - Local '...' or its members cannot have their address taken and be used inside an anonymous method or lambda expression
13011정성태3/21/20226907오류 유형: 802. 윈도우 운영체제에서 웹캠 카메라 인식이 안 되는 경우
13010정성태3/21/20225831오류 유형: 801. Oracle.ManagedDataAccess.Core - GetTypes 호출 시 "Could not load file or assembly 'System.DirectoryServices.Protocols...'" 오류
... 16  17  18  19  20  21  22  23  [24]  25  26  27  28  29  30  ...