天天看點

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

.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項目,引入如下包:

<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" />           

僞造Logger

使用如下指令僞造service需要的logger:

var logger = Mock.Of>();

使用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類型的參數,該參數被用于将方法的計算結果逐個傳回給調用方,可以建立一個通用的類實作此接口,将寫入的消息存儲為一個list,以便測試。

public class TestServerStreamWriter : IServerStreamWriter

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,簡單實作一個類用于測試。

該類通過直接将用戶端要傳入的資料通過IEnumable參數傳入,模拟用戶端的流式請求多個資料。

public class TestStreamReader : IAsyncStreamReader

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());
}           

正常流程測試代碼

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);
    }
           

參數錯誤的測試代碼

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或簡單實作,達到了單元測試的目的。

原文位址

https://www.cnblogs.com/wjsgzcn/p/12883169.html