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

Supercharge your testing experience with MSTest.Analyzers
; https://devblogs.microsoft.com/dotnet/supercharge-your-testing-experience-with-ms-test-analyzers/
정성태

... 151  152  153  154  155  156  157  158  159  160  161  162  [163]  164  165  ...
NoWriterDateCnt.TitleFile(s)
972정성태1/7/201124230개발 환경 구성: 95. SQL Server 2008 R2 이하 버전 정보 확인
971정성태1/5/201133803.NET Framework: 199. .NET 코드 - Named Pipe 닷넷 서버와 VC++ 클라이언트 제작 [2]파일 다운로드1
970정성태1/4/201134331.NET Framework: 198. 윈도우 응용 프로그램에 Facebook 로그인 연동 [1]파일 다운로드1
969정성태12/31/201040404VC++: 45. Winsock 2 Layered Service Provider - Visual Studio 2010용 프로젝트 [1]파일 다운로드1
968정성태12/30/201026698개발 환경 구성: 94. 개발자가 선택할 수 있는 윈도우에서의 네트워크 프로그래밍 기술 [2]
967정성태12/27/201028434.NET Framework: 197. .NET 코드 - 단일 Process 실행파일 다운로드1
966정성태12/26/201026381.NET Framework: 196. .NET 코드 - 창 흔드는 효과파일 다운로드1
965정성태12/25/201027905개발 환경 구성: 93. MSBuild를 이용한 닷넷 응용프로그램의 다중 어셈블리 출력 빌드파일 다운로드1
964정성태12/21/2010143068개발 환경 구성: 92. 윈도우 서버 환경에서, 최대 생성 가능한 소켓(socket) 연결 수는 얼마일까? [14]
963정성태12/13/201027947개발 환경 구성: 91. MSBuild를 이용한 닷넷 응용프로그램의 플랫폼(x86/x64)별 빌드 [2]파일 다운로드1
962정성태12/10/201022768오류 유형: 110. GAC 등록 - Failure adding assembly to the cache: Invalid file or assembly name.
961정성태12/10/201099830개발 환경 구성: 90. 닷넷에서 접근해보는 PostgreSQL DB [5]
960정성태12/8/201045153.NET Framework: 195. .NET에서 코어(Core) 관련 CPU 정보 알아내는 방법파일 다운로드1
959정성태12/8/201031965.NET Framework: 194. Facebook 연동 - API Error Description: Invalid OAuth 2.0 Access Token
958정성태12/7/201028978개발 환경 구성: 89. 배치(batch) 파일에서 또 다른 배치 파일을 동기 방식으로 실행 및 반환값 얻기 [2]
957정성태12/6/201031727디버깅 기술: 31. Windbg - Visual Studio 디버그 상태에서 종료해 버리는 응용 프로그램 [3]
953정성태11/28/201036960.NET Framework: 193. 페이스북(Facebook) 계정으로 로그인하는 C# 웹 사이트 제작 [5]
952정성태11/25/201025372.NET Framework: 192. GC의 부하는 상대적인 것! [4]
950정성태11/18/201076742.NET Framework: 191. ClickOnce - 관리자 권한 상승하는 방법 [17]파일 다운로드2
954정성태11/29/201048732    답변글 .NET Framework: 191.1. [답변] 클릭원스 - 요청한 작업을 수행하려면 권한 상승이 필요합니다. (Exception from HRESULT: 0x800702E4) [2]
949정성태11/16/201027267오류 유형: 109. System.ServiceModel.Security.SecurityNegotiationException
948정성태11/16/201036141.NET Framework: 190. 트위터 계정으로 로그인하는 C# 웹 사이트 제작 [7]파일 다운로드1
947정성태11/14/201041711.NET Framework: 189. Mono Cecil로 만들어 보는 .NET Decompiler [1]파일 다운로드1
946정성태11/11/201041586.NET Framework: 188. .NET 64비트 응용 프로그램에서 왜 (2GB) OutOfMemoryException 예외가 발생할까? [1]파일 다운로드1
945정성태11/11/201025083VC++: 44. C++/CLI 컴파일 오류 - error C4368: mixed types are not supported
944정성태11/11/201031616VC++: 43. C++/CLI 컴파일 오류 - error C2872: 'IServiceProvider' : ambiguous symbol could be ...
... 151  152  153  154  155  156  157  158  159  160  161  162  [163]  164  165  ...