前言

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 = ;
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(),
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=""},
new Student{Name=""}
};
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=""},
new HelloRequest{Name=""},
};
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=""},
new HelloRequest{Name="abc"},
};
var stream = new TestStreamReader<HelloRequest>(data); Assert.ThrowsAsync<ArgumentException>(async () => await service.Sum(stream, scc));
}

总结

以上代码,通过对gRPC服务依赖的关键资源进行mock或简单实现,达到了单元测试的目的。

最新文章

  1. .Net Core MVC 网站开发(Ninesky) 2.3、项目架构调整(续)-使用配置文件动态注入
  2. dvd开发小程序
  3. Java源码分析系列
  4. (实用篇)php数组查找函数in_array()、array_search()、array_key_exists()使用
  5. 斯坦福iOS7公开课7-9笔记及演示Demo
  6. 青少年如何使用 Python 开始游戏开发
  7. Data Flow -&gt;&gt; Look up &amp; Merge Join
  8. cocos2dx 3.4 截图代码
  9. [转] Java内部类之闭包(closure)与回调(callback)
  10. Ant命令行操作
  11. scp命令和sftp命令
  12. Sql 知识点小结
  13. 关于在windows10中的vmware9.0里面安装的ubuntukylin15.04和windows共享目录的一些反思
  14. JAVA基础复习与总结&lt;五&gt; String类_File类_Date类
  15. (七)Knockout 创建自定义绑定
  16. ASP 运行结果显示空白 --- 是编码的原因。
  17. 队列java实现
  18. Xcode插件管理器Alcatraz的使用
  19. String.format()格式化日期(2)
  20. docker 环境

热门文章

  1. 初始化 RESTful API 风格的博客系统
  2. 一种特殊的生成器函数-Generator函数
  3. Mac文件上传下载到服务器指定命令
  4. Django入门2:路由系统
  5. SpringCloud之整合Feign
  6. IBM:向所有云平台开放Watson人工智能系统
  7. IDC:企业需求疲软 第三季度全球服务器市场收入下滑7%
  8. PLDroidPlayer 是七牛推出的一款免费的适用于 Android 平台的播放器 SDK,采用全自研的跨平台播放内核,拥有丰富的功能和优异的性能,可高度定制化和二次开发。 https://developer.qiniu.com/pili/sdk/…
  9. 初识DP动态规划
  10. Acmer 仅以此纪念最痛苦的一天