[C#] C# gRPC 단위테스트 하기

참고


목적

  • C# 으로 gRPC Server 를 구현합니다.
  • 원래는 Client 가 Server 에게 메시지를 전송해서 서로 통신을 해야 하지만, Client 를 C# 에서 단위테스트를 통해서 구현하여 C# 에서 gRPC 단위테스트 하는 방법에 대해서 알아 봅니다.

gRPC 서버 구현

  • C# 에서 gRPC 서버 구현하는 방법은 ASP.NET Core gRPC 서비스 프로젝트를 생성하게 되면, 기본으로 greet.proto 파일과 함께 GreeterService 가 생성됩니다.
  • 아무런 작업을 하지 않아도 기본으로 gRPC 서버가 생성된 것입니다.
  • 하지만, 여기서 저는 테스트를 위해 2가지 작업을 진행해 주었습니다.
  • 첫 번째로, IGreeter.cs 인터페이스를 생성하였고, 생성한 IGreeter 인터페이스를 GreeterService 에 생성자로 의존성 주입을 시켜 주었습니다.
  • 추가 및 변경된 소스 코드는 다음과 같습니다.

IGreeter.cs

namespace gRPC_UnitTest
{
    public interface IGreeter
    {
        string Greet(string name);
    }
}

GreeterService.cs

using Grpc.Core;

namespace gRPC_UnitTest.Services
{
    public class GreeterService : Greeter.GreeterBase
    {
        private readonly ILogger<GreeterService> _logger;
        private readonly IGreeter _service;
        public GreeterService(ILogger<GreeterService> logger, IGreeter service)
        {
            _logger = logger;
            _service = service;
        }

        public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
        {
            var result = _service.Greet(request.Name);

            return Task.FromResult(new HelloReply
            {
                Message = result
            });
        }
    }
}
  • 위와 같이 IGreeter 인터페이스와 인터페이스를 생성자에 서비스로 주입해 주었습니다.
  • 그럼 이제 위 gRPC 서버 코드를 단위테스트 하도록 단위 테스트 코드를 작성해 보도록 하겠습니다.

단위 테스트 준비

  • 단위 테스트를 준비하려면 xUnit 테스트 프로젝트를 하나 생성합니다.
  • 그리고 gRPC 단위 테스트를 위한 Helper 클래스인 TestServerCallContext.cs 를 생성해 줍니다.
  • 그리고 아래와 같이 Mocking 을 이용하여 SayHello 메서드를 테스트 진행하는 코드를 작성해 보도록 하겠습니다.

TestServerCallContext.cs

using Grpc.Core;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace UnitTest
{
    public class TestServerCallContext : ServerCallContext
    {
        private readonly Metadata _requestHeaders;
        private readonly CancellationToken _cancellationToken;
        private readonly Metadata _responseTrailers;
        private readonly AuthContext _authContext;
        private readonly Dictionary<object, object> _userState;
        private WriteOptions? _writeOptions;

        public Metadata? ResponseHeaders { get; private set; }

        private TestServerCallContext(Metadata requestHeaders, CancellationToken cancellationToken)
        {
            _requestHeaders = requestHeaders;
            _cancellationToken = cancellationToken;
            _responseTrailers = new Metadata();
            _authContext = new AuthContext(string.Empty, new Dictionary<string, List<AuthProperty>>());
            _userState = new Dictionary<object, object>();
        }

        protected override string MethodCore => "MethodName";
        protected override string HostCore => "HostName";
        protected override string PeerCore => "PeerName";
        protected override DateTime DeadlineCore { get; }
        protected override Metadata RequestHeadersCore => _requestHeaders;
        protected override CancellationToken CancellationTokenCore => _cancellationToken;
        protected override Metadata ResponseTrailersCore => _responseTrailers;
        protected override Status StatusCore { get; set; }
        protected override WriteOptions? WriteOptionsCore { get => _writeOptions; set { _writeOptions = value; } }
        protected override AuthContext AuthContextCore => _authContext;

        protected override ContextPropagationToken CreatePropagationTokenCore(ContextPropagationOptions options)
        {
            throw new NotImplementedException();
        }

        protected override Task WriteResponseHeadersAsyncCore(Metadata responseHeaders)
        {
            if (ResponseHeaders != null)
            {
                throw new InvalidOperationException("Response headers have already been written.");
            }

            ResponseHeaders = responseHeaders;
            return Task.CompletedTask;
        }

        protected override IDictionary<object, object> UserStateCore => _userState;

        public static TestServerCallContext Create(Metadata? requestHeaders = null, CancellationToken cancellationToken = default)
        {
            return new TestServerCallContext(requestHeaders ?? new Metadata(), cancellationToken);
        }
    }
}

단위 테스트 코드

  • 위에서 단위 테스트에 필요한 Helper 클래스를 작성하였습니다.
  • 그럼 이제 앞서 생성한 gRPC Server 인 GreeterService 를 단위 테스트 할 수 있도록 코드를 작성해 보겠습니다.
  • 다음 코드에서는 Mocking 을 이용한 단위 테스트 코드를 작성하였습니다.
  • Act 부분에 Joe 라는 메시지를 전송하고, 최종적으로 Hello Joe 하고 결과값이 맞는지 비교하여 맞으면 테스트를 통과하는 로직을 작성하였습니다.
  • 실제로 테스트를 실행하여 정상적으로 테스트가 진행 되는지 확인 하였습니다.
using gRPC_UnitTest;
using gRPC_UnitTest.Services;
using Microsoft.Extensions.Logging;
using Moq;
using System.Threading.Tasks;
using Xunit;

namespace UnitTest
{
    public class UnitTest1
    {
        [Fact]
        public async Task SayHelloTest()
        {
            // Arrange
            var loggerFactory = new LoggerFactory();
            ILogger<GreeterService> logger = loggerFactory.CreateLogger<GreeterService>();
            var mockGreeter = new Mock<IGreeter>();
            mockGreeter.Setup(
                m => m.Greet(It.IsAny<string>())).Returns((string s) => $"Hello {s}");
            var service = new GreeterService(logger, mockGreeter.Object);

            // Act
            var response = await service.SayHello(
        new HelloRequest { Name = "Joe" }, TestServerCallContext.Create());

            // Assert
            mockGreeter.Verify(v => v.Greet("Joe"));
            Assert.Equal("Hello Joe", response.Message);
        }
    }
}

실행 결과

  • 실행 결과, 정상적으로 테스트 통과 되는 것을 확인할 수 있습니다.

728x90

이 글을 공유하기

댓글

Designed by JB FACTORY