.net core grpc單元測試 - 服務器端


前言

gRPC憑借其嚴謹的接口定義、高效的傳輸效率、多樣的調用方式等優點,在微服務開發方面占據了一席之地。dotnet core正式支持gRPC也有一段時間了,官方文檔也對如何使用gRPC進行了比較詳細的說明,但是關於如何對gRPC的服務器和客戶端進行單元測試,卻沒有描述。經過查閱官方代碼,找到了一些解決方法,總結在此,供大家參考。

本文重點介紹gRPC服務器端代碼的單元測試,包括普通調用、服務器端流、客戶端流等調用方式的單元測試,另外,引入sqlite的內存數據庫模式,對數據庫相關操作進行測試。

准備gRPC服務端項目

使用dotnet new grpc命令創建一個gRPC服務器項目。

修改protos/greeter.proto, 添加兩個接口方法:

//服務器流
rpc SayHellos (HelloRequest) returns (stream HelloReply);

//客戶端流
rpc Sum (stream HelloRequest) returns (HelloReply);
 
在GreeterService中添加方法的實現:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Grpc.Core;
using GrpcTest.Server.Models;
using Microsoft.Extensions.Logging;

namespace GrpcTest.Server
{
    public class GreeterService : Greeter.GreeterBase
    {
        private readonly ILogger<GreeterService> _logger;
        private readonly ApplicationDbContext _db;

        public GreeterService(ILogger<GreeterService> logger,
            ApplicationDbContext db)
        {
            _logger = logger;
            _db = db;
        }

        public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
        {
            return Task.FromResult(new HelloReply
            {
                Message = "Hello " + request.Name
            });
        }

        public override async Task SayHellos(HelloRequest request,
            IServerStreamWriter<HelloReply> responseStream,
            ServerCallContext context)
        {
            foreach (var student in _db.Students)
            {
                if (context.CancellationToken.IsCancellationRequested)
                    break;

                var message = student.Name;
                _logger.LogInformation($"Sending greeting {message}.");

                await responseStream.WriteAsync(new HelloReply { Message = message });
            }
        }

        public override async Task<HelloReply> Sum(IAsyncStreamReader<HelloRequest> requestStream, ServerCallContext context)
        {
            var sum = 0;
            await foreach (var request in requestStream.ReadAllAsync())
            {
                if (int.TryParse(request.Name, out var number))
                    sum += number;
                else
                    throw new ArgumentException("參數必須是可識別的數字");
            }

            return new HelloReply { Message = $"sum is {sum}" };
        }
    }
}

SayHello: 簡單的返回一個文本消息。

SayHellos: 從數據庫的表中讀取所有數據,並且使用服務器端流的方式返回。

Sum:從客戶端流獲取輸入數據,並計算所有數據的和,如果輸入的文本無法轉換為數字,拋出異常。

單元測試

新建xunit項目,並引用剛才建立的gRPC項目,引入如下包:

<ItemGroup>
    <PackageReference Include="Grpc.Core.Testing" Version="2.28.1" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.3" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
    <PackageReference Include="moq" Version="4.14.1" />
    <PackageReference Include="xunit" Version="2.4.0" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
    <PackageReference Include="coverlet.collector" Version="1.2.0" />
</ItemGroup>

偽造Logger

使用如下命令偽造service需要的logger:
var logger = Mock.Of<ILogger<GreeterService>>();

使用sqlite inmemory的DbContext

public static ApplicationDbContext CreateDbContext(){
            var db = new ApplicationDbContext(new DbContextOptionsBuilder<ApplicationDbContext>()
                .UseSqlite(CreateInMemoryDatabase()).Options);
            db.Database.EnsureCreated();
            return db;
        }

        private static DbConnection CreateInMemoryDatabase()
        {
            var connection = new SqliteConnection("Filename=:memory:");
            connection.Open();
            return connection;
        }

重點:雖然是內存模式,數據庫也必須是open的,並且需要運行EnsureCreated,否則調用數據庫功能是會報告找不到表。

偽造ServerCallContext

使用如下代碼偽造:

public static ServerCallContext CreateTestContext(){
            return TestServerCallContext.Create("fooMethod", 
                null, 
                DateTime.UtcNow.AddHours(1), 
                new Metadata(), 
                CancellationToken.None, 
                "127.0.0.1", 
                null,
                null, 
                (metadata) => TaskUtils.CompletedTask, 
                () => new WriteOptions(), 
                (writeOptions) => { });
}

里面的具體參數要依據實際測試需要進行調整,比如測試客戶端取消操作時,修改CancellationToken參數。

普通調用的測試

[Fact]
        public void SayHello()
        {     
            var service = new GreeterService(logger, null);
            var request = new HelloRequest{Name="world"};
            var response = service.SayHello(request, scc).Result;

            var expected = "Hello world";
            var actual = response.Message;
            Assert.Equal(expected, actual);
        }

其中scc = 偽造的ServerCallContext,如果被測方法中沒有實際使用它,也可以直接傳入null。

服務器端流的測試

服務器端流的方法包含一個IServerStreamWriter<HelloReply>類型的參數,該參數被用於將方法的計算結果逐個返回給調用方,可以創建一個通用的類實現此接口,將寫入的消息存儲為一個list,以便測試。

public class TestServerStreamWriter<T> : IServerStreamWriter<T>
{
    public WriteOptions WriteOptions { get; set; }
    public List<T> Responses { get; } = new List<T>();
    public Task WriteAsync(T message)
    {
        this.Responses.Add(message);
        return Task.CompletedTask;
    }
}

測試時,向數據庫表中插入兩條記錄,然后測試對比,看接口方法是否返回兩條記錄。

public  async Task SayHellos(){            
            var db = TestTools.CreateDbContext();

            var students = new List<Student>{
                new Student{Name="1"},
                new Student{Name="2"}
            };
            db.AddRange(students);
            db.SaveChanges();

            var service = new GreeterService(logger, db);
            var request = new HelloRequest{Name="world"};
            
            var sw = new TestServerStreamWriter<HelloReply>();
            await service.SayHellos(request, sw, scc);
            
            var expected = students.Count;
            var actual = sw.Responses.Count;
            Assert.Equal(expected, actual);
}

客戶端流的測試

與服務器流類似,客戶端流方法也有一個參數類型為IAsyncStreamReader<HelloRequest>,簡單實現一個類用於測試。

該類通過直接將客戶端要傳入的數據通過IEnumable<T>參數傳入,模擬客戶端的流式請求多個數據。

public class TestStreamReader<T> : IAsyncStreamReader<T>
{
    private readonly IEnumerator<T> _stream;

    public TestStreamReader(IEnumerable<T> list){
        _stream = list.GetEnumerator();
    }

    public T Current => _stream.Current;

    public Task<bool> MoveNext(CancellationToken cancellationToken)
    {
        return Task.FromResult(_stream.MoveNext());
    }
}

正常流程測試代碼

[Fact]
        public void Sum_NormalInput_ReturnSum()
        {
            var service = new GreeterService(null, null);
            var data = new List<HelloRequest>{
                new HelloRequest{Name="1"},
                new HelloRequest{Name="2"},
            };
            var stream = new TestStreamReader<HelloRequest>(data);

            var response = service.Sum(stream, scc).Result;
            var expected = "sum is 3";
            var actual = response.Message;
            Assert.Equal(expected, actual);
        }

參數錯誤的測試代碼

[Fact]
        public void Sum_BadInput_ThrowException()
        {
            var service = new GreeterService(null, null);
            var data = new List<HelloRequest>{
                new HelloRequest{Name="1"},
                new HelloRequest{Name="abc"},
            };
            var stream = new TestStreamReader<HelloRequest>(data);

            Assert.ThrowsAsync<ArgumentException>(async () => await service.Sum(stream, scc));
        }

總結

以上代碼,通過對gRPC服務依賴的關鍵資源進行mock或簡單實現,達到了單元測試的目的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM