Microsoft MVP성태의 닷넷 이야기
.NET Framework: 1084. C# - .NET Core Web API 단위 테스트 방법 [링크 복사], [링크+제목 복사]
조회: 7935
글쓴 사람
정성태 (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
정성태

1  2  3  4  5  6  7  8  9  10  11  12  13  14  [15]  ...
NoWriterDateCnt.TitleFile(s)
13249정성태2/7/20234956오류 유형: 842. 리눅스 - You must wait longer to change your password
13248정성태2/7/20234080오류 유형: 841. 리눅스 - [사용자 계정] is not in the sudoers file. This incident will be reported.
13247정성태2/7/20234985VS.NET IDE: 180. Visual Studio - 닷넷 소스 코드 디버깅 중 "Decompile source code"가 동작하는 않는 문제
13246정성태2/6/20234112개발 환경 구성: 664. Hyper-V에 설치한 리눅스 VM의 VHD 크기 늘리는 방법 - 두 번째 이야기
13245정성태2/6/20234660.NET Framework: 2093. C# - PEM 파일을 이용한 RSA 개인키/공개키 설정 방법파일 다운로드1
13244정성태2/5/20234016VS.NET IDE: 179. Visual Studio - External Tools에 Shell 내장 명령어 등록
13243정성태2/5/20234866디버깅 기술: 190. windbg - Win32 API 호출 시점에 BP 거는 방법 [1]
13242정성태2/4/20234308디버깅 기술: 189. ASP.NET Web Application (.NET Framework) 프로젝트의 숨겨진 예외 - System.UnauthorizedAccessException
13241정성태2/3/20233832디버깅 기술: 188. ASP.NET Web Application (.NET Framework) 프로젝트의 숨겨진 예외 - System.IO.FileNotFoundException
13240정성태2/1/20233988디버깅 기술: 187. ASP.NET Web Application (.NET Framework) 프로젝트의 숨겨진 예외 - System.Web.HttpException
13239정성태2/1/20233633디버깅 기술: 186. C# - CacheDependency의 숨겨진 예외 - System.Web.HttpException
13238정성태1/31/20235653.NET Framework: 2092. IIS 웹 사이트를 TLS 1.2 또는 TLS 1.3 프로토콜로만 운영하는 방법
13237정성태1/30/20235338.NET Framework: 2091. C# - 웹 사이트가 어떤 버전의 TLS/SSL을 지원하는지 확인하는 방법
13236정성태1/29/20234975개발 환경 구성: 663. openssl을 이용해 인트라넷 IIS 사이트의 SSL 인증서 생성
13235정성태1/29/20234529개발 환경 구성: 662. openssl - 윈도우 환경의 명령행에서 SAN 적용하는 방법
13234정성태1/28/20235568개발 환경 구성: 661. dnSpy를 이용해 소스 코드가 없는 .NET 어셈블리의 코드를 변경하는 방법 [1]
13233정성태1/28/20236916오류 유형: 840. C# - WebClient로 https 호출 시 "The request was aborted: Could not create SSL/TLS secure channel" 예외 발생
13232정성태1/27/20234719스크립트: 43. uwsgi의 --processes와 --threads 옵션
13231정성태1/27/20233625오류 유형: 839. python - TypeError: '...' object is not callable
13230정성태1/26/20234036개발 환경 구성: 660. WSL 2 내부로부터 호스트 측의 네트워크로 UDP 데이터가 1개의 패킷으로만 제한되는 문제
13229정성태1/25/20234986.NET Framework: 2090. C# - UDP Datagram의 최대 크기
13228정성태1/24/20235122.NET Framework: 2089. C# - WMI 논리 디스크가 속한 물리 디스크의 정보를 얻는 방법 [2]파일 다운로드1
13227정성태1/23/20234834개발 환경 구성: 659. Windows - IP MTU 값을 바꿀 수 있을까요? [1]
13226정성태1/23/20234518.NET Framework: 2088. .NET 5부터 지원하는 GetRawSocketOption 사용 시 주의할 점
13225정성태1/21/20233760개발 환경 구성: 658. Windows에서 실행 중인 소켓 서버를 다른 PC 또는 WSL에서 접속할 수 없는 경우
13224정성태1/21/20234120Windows: 221. Windows - Private/Public/Domain이 아닌 네트워크 어댑터 단위로 방화벽을 on/off하는 방법
1  2  3  4  5  6  7  8  9  10  11  12  13  14  [15]  ...