前言

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
requestin 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" /> <
PackageReferenceInclude="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.3"
/> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" /> <
PackageReferenceInclude="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或简单实现,达到了单元测试的目的。

技术
©2019-2020 Toolsou All rights reserved,
详解ubuntu14.04如何设置静态IPQCustomPlot系列(5)-实时动态曲线比尔·盖茨:疫情后彻底恢复正常可能要到2022年末华为认证HCIA-AI人工智能Python基础知识整理笔记百度、阿里、腾讯内部岗位级别和薪资结构,附带求职建议!Jsp+Ajax+Servlet+Mysql实现增删改查(一)2021年1月程序员工资统计,平均14915元Faster RCNN系列算法原理讲解(笔记)经典算法-递归(生兔子案例)