前言

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或简单实现,达到了单元测试的目的。

.net core grpc单元测试 - 服务器端的更多相关文章

  1. .NET Core ❤ gRPC

    这篇内容主要来自Microsoft .NET团队程序经理Sourabh Shirhatti的博客文章:https://grpc.io/blog/grpc-on-dotnetcore/, .NET Co ...

  2. Asp.Net Core Grpc 入门实践

    Grpc简介 gRPC 是一种与语言无关的高性能远程过程调用 (RPC) 框架. 在 gRPC 中,客户端应用程序可以直接调用不同计算机上的服务器应用程序上的方法,就像它是本地对象一样,从而更轻松地创 ...

  3. .net core grpc consul 实现服务注册 服务发现 负载均衡(二)

    在上一篇 .net core grpc 实现通信(一) 中,我们实现的grpc通信在.net core中的可行性,但要在微服务中真正使用,还缺少 服务注册,服务发现及负载均衡等,本篇我们将在 .net ...

  4. NetCore服务虚拟化01(集群组件Sodao.Core.Grpc)

    一. 起始 去年.NetCore2.0的发布,公司决定新项目采用.NetCore开发,当作试验.但是问题在于当前公司内部使用的RPC服务为Thrift v0.9 + zookeeper版本,经过个性化 ...

  5. ASP.NET Core gRPC 入门全家桶

    一. 说明 本全家桶现在只包含了入门级别的资料,实战资料更新中. 二.官方文档 gRPC in Asp.Net Core :官方文档 gRPC 官网:点我跳转 三.入门全家桶 正片: ASP.NET ...

  6. ASP.NET Core gRPC 健康检查的实现方式

    一. 前言 gRPC 服务实现健康检查有两种方式,前面在此文 ASP.NET Core gRPC 使用 Consul 服务注册发现 中有提到过,这里归纳整理一下.gRPC 的健康检查,官方是定义了标准 ...

  7. .Net Core Grpc Consul 实现服务注册 服务发现 负载均衡

    本文是基于..net core grpc consul 实现服务注册 服务发现 负载均衡(二)的,很多内容是直接复制过来的,..net core grpc consul 实现服务注册 服务发现 负载均 ...

  8. .Net Core Grpc 实现通信

    .Net Core 3.0已经把Grpc作为一个默认的模板引入,所以我认为每一个.Net程序员都有学习Grpc的必要,当然这不是必须的. 我在我的前一篇文章中介绍并创建了一个.Net Core 3.0 ...

  9. 旧 WCF 项目迁移到 asp.net core + gRPC 的尝试

    一个月前,公司的运行WCF的windows服务器down掉了,由于 AWS 没有通知,没有能第一时间发现问题. 所以,客户提出将WCF服务由C#改为JAVA,在Linux上面运行:一方面,AWS对Li ...

随机推荐

  1. STL入门大全(待编辑)

    前言:这个暑假才接触STL,仿佛开启了新世界的大门(如同学完结构体排序一般的快乐\(≧▽≦)/),终于彻底领悟了大佬们说的“STL大法好”(虽然我真的很菜www现在只学会了一点点...)这篇blog主 ...

  2. 原生Js贪吃蛇游戏实战开发笔记

    前言 本课程是通过JavaScript结合WebAPI DOM实现的一版网页游戏---贪吃蛇的开发全过程,采用面向以象的思想设计开发.通过这个小游戏的开发, 不仅可以掌握JS的语法的应用,还可以学会D ...

  3. 4、flink自定义source、sink

    一.Source 代码地址:https://gitee.com/nltxwz_xxd/abc_bigdata 1.1.flink内置数据源 1.基于文件 env.readTextFile(" ...

  4. php sprintf() 函数把格式化的字符串写入一个变量中。

    来源:https://blog.csdn.net/zxh1220/article/details/79709207 HP sprintf() 函数用到的参数 printf — 输出格式化字符串 spr ...

  5. 3.k均值的算法

    一.课堂练习 # 课堂练习 from sklearn.datasets import load_iris # 导入鸢尾花数据 iris=load_iris() iris iris.keys() dat ...

  6. Spring5参考指南:组件扫描

    文章目录 组件扫描 @Component 元注解和组合注解 组件内部定义Bean元数据 为自动检测组件命名 为自动检测的组件提供作用域 生成候选组件的索引 组件扫描 上一篇文章我们讲到了annotat ...

  7. if __name__ == '__main__'到底是什么?

    引子 要搞清楚这个问题,可以先听一个故事~~~ 像我们做事一样,都需要一个起始点,终点存在与否无关紧要.编程也是一样,任何程序都有一个入口,在所谓的静态编译语言中,如Java的入口是一个名字叫做Mai ...

  8. 自定义View之Canvas使用

    自定义View的绘制流程一般都是这样:提前创建好Paint对象,重写onDraw(),把绘制代码卸载ondraw()里面,大致如下: Paint paint = new Paint(); @Overr ...

  9. 修复.NET的HttpClient

    \ 看新闻很累?看技术新闻更累?试试下载InfoQ手机客户端,每天上下班路上听新闻,有趣还有料! \ \\ 早在2016年我们就报道过 ,.NET的HttpClient存在一些问题.随着.NET Co ...

  10. 状态压缩DP(大佬写的很好,转来看)

    奉上大佬博客 https://blog.csdn.net/accry/article/details/6607703 动态规划本来就很抽象,状态的设定和状态的转移都不好把握,而状态压缩的动态规划解决的 ...