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

... 76  77  78  79  80  81  82  [83]  84  85  86  87  88  89  90  ...
NoWriterDateCnt.TitleFile(s)
11899정성태5/17/201921372Linux: 9. Linux에서 윈도우의 OutputDebugString 대신 사용할 수 있는 syslog [1]
11898정성태5/16/201922977VC++: 130. C++ string의 c_str과 data 함수의 차이점 [3]
11897정성태5/16/201929615오류 유형: 536. Visual Studio - "Developer Pack"을 설치했는데도 "대상 프레임워크" 목록에 나오지 않는 경우 [2]
11896정성태5/15/201925500개발 환경 구성: 440. C#, C++ - double의 Infinity, NaN 표현 방식파일 다운로드1
11895정성태5/12/201922855.NET Framework: 832. ML.NET Model Builder - 회귀(Regression), 다중 분류(Multi-class classification) 예제파일 다운로드1
11894정성태5/10/201924882VS.NET IDE: 135. Visual Studio - ML.NET Model Builder 소개 [5]
11893정성태5/10/201920816오류 유형: 535. C# 6.0 이상의 문법을 컴파일 시 오류가 발생한다면?
11892정성태5/10/201920713웹: 38. HTTP Cookie의 expires 시간 형식(RFC7231)
11891정성태5/9/201923866.NET Framework: 831. (번역글) .NET Internals Cookbook Part 12 - Memory structure, attributes, handles
11890정성태5/8/201919758개발 환경 구성: 439. "Visual Studio Enterprise is required to execute the test." 메시지와 관련된 코드 기록
11889정성태5/8/201919612개발 환경 구성: 438. mstest, QTAgent의 로그 파일 설정 방법
11888정성태5/8/201937665.NET Framework: 830. C# - 비동기 호출을 취소하는 CancellationToken의 간단한 예제 코드 [1]파일 다운로드1
11887정성태5/8/201923529.NET Framework: 829. C# - yield 문을 사용할 수 있는 메서드의 조건
11886정성태5/7/201920370오류 유형: 534. mstest.exe 실행 시 "Visual Studio Enterprise is required to execute the test." 오류 [2]
11885정성태5/7/201917970오류 유형: 533. mstest.exe 실행 시 "File extension specified '.loadtest' is not a valid test extension." 오류 발생
11884정성태5/5/201922962.NET Framework: 828. C# DLL에서 Win32 C/C++처럼 dllexport 함수를 제공하는 방법 - 두 번째 이야기
11883정성태5/3/201927895.NET Framework: 827. C# - 인터넷 시간 서버로부터 받은 시간을 윈도우에 적용하는 방법파일 다운로드1
11882정성태5/2/201924520.NET Framework: 826. (번역글) .NET Internals Cookbook Part 11 - Various C# riddles파일 다운로드1
11881정성태4/28/201924658오류 유형: 532. .NET Core 프로젝트로 마이그레이션 시 "CS0579 Duplicate 'System.Reflection.AssemblyCompanyAttribute' attribute" 오류 발생
11880정성태4/25/201920375오류 유형: 531. 이벤트 로그 오류 - Task Scheduling Error: m->NextScheduledSPRetry 1547, m->NextScheduledEvent 1547
11879정성태4/24/201929533.NET Framework: 825. (번역글) .NET Internals Cookbook Part 10 - Threads, Tasks, asynchronous code and others파일 다운로드2
11878정성태4/22/201924509.NET Framework: 824. (번역글) .NET Internals Cookbook Part 9 - Finalizers, queues, card tables and other GC stuff파일 다운로드1
11877정성태4/22/201924697.NET Framework: 823. (번역글) .NET Internals Cookbook Part 8 - C# gotchas파일 다운로드1
11876정성태4/21/201922695.NET Framework: 822. (번역글) .NET Internals Cookbook Part 7 - Word tearing, locking and others파일 다운로드1
11875정성태4/21/201924753오류 유형: 530. Visual Studo에서 .NET Core 프로젝트를 열 때 "One or more errors occurred." 오류 발생
11874정성태4/20/201924780.NET Framework: 821. (번역글) .NET Internals Cookbook Part 6 - Object internals파일 다운로드1
... 76  77  78  79  80  81  82  [83]  84  85  86  87  88  89  90  ...