gRPC凭借其严谨的接口定义、高效的传输效率、多样的调用方式等优点,在微服务开发方面占据了一席之地。dotnet core正式支持gRPC也有一段时间了,官方文档也对如何使用gRPC进行了比较详细的说明,但是关于如何对gRPC的服务器和客户端进行单元测试,却没有描述。经过查阅官方代码,找到了一些解决方法,总结在此,供大家参考。

本文重点介绍gRPC服务器端代码的单元测试,包括普通调用、服务器端流、客户端流等调用方式的单元测试,另外,引入sqlite的内存数据库模式,对数据库相关操作进行测试。

使用dotnet new grpc命令创建一个gRPC服务器项目。

修改protos/greeter.proto, 添加两个接口方法:

  1. //服务器流
  2. rpc SayHellos (HelloRequest) returns (stream HelloReply);
  3. //客户端流
  4. rpc Sum (stream HelloRequest) returns (HelloReply);
 
在GreeterService中添加方法的实现:
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Threading.Tasks;
  5. using Grpc.Core;
  6. using GrpcTest.Server.Models;
  7. using Microsoft.Extensions.Logging;
  8. namespace GrpcTest.Server
  9. {
  10. public class GreeterService : Greeter.GreeterBase
  11. {
  12. private readonly ILogger<GreeterService> _logger;
  13. private readonly ApplicationDbContext _db;
  14. public GreeterService(ILogger<GreeterService> logger,
  15. ApplicationDbContext db)
  16. {
  17. _logger = logger;
  18. _db = db;
  19. }
  20. public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
  21. {
  22. return Task.FromResult(new HelloReply
  23. {
  24. Message = "Hello " + request.Name
  25. });
  26. }
  27. public override async Task SayHellos(HelloRequest request,
  28. IServerStreamWriter<HelloReply> responseStream,
  29. ServerCallContext context)
  30. {
  31. foreach (var student in _db.Students)
  32. {
  33. if (context.CancellationToken.IsCancellationRequested)
  34. break;
  35. var message = student.Name;
  36. _logger.LogInformation($"Sending greeting {message}.");
  37. await responseStream.WriteAsync(new HelloReply { Message = message });
  38. }
  39. }
  40. public override async Task<HelloReply> Sum(IAsyncStreamReader<HelloRequest> requestStream, ServerCallContext context)
  41. {
  42. var sum = 0;
  43. await foreach (var request in requestStream.ReadAllAsync())
  44. {
  45. if (int.TryParse(request.Name, out var number))
  46. sum += number;
  47. else
  48. throw new ArgumentException("参数必须是可识别的数字");
  49. }
  50. return new HelloReply { Message = $"sum is {sum}" };
  51. }
  52. }
  53. }

SayHello: 简单的返回一个文本消息。

SayHellos: 从数据库的表中读取所有数据,并且使用服务器端流的方式返回。

Sum:从客户端流获取输入数据,并计算所有数据的和,如果输入的文本无法转换为数字,抛出异常。

新建xunit项目,并引用刚才建立的gRPC项目,引入如下包:

  1. <ItemGroup>
  2. <PackageReference Include="Grpc.Core.Testing" Version="2.28.1" />
  3. <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.3" />
  4. <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
  5. <PackageReference Include="moq" Version="4.14.1" />
  6. <PackageReference Include="xunit" Version="2.4.0" />
  7. <PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
  8. <PackageReference Include="coverlet.collector" Version="1.2.0" />
  9. </ItemGroup>
使用如下命令伪造service需要的logger:
var logger = Mock.Of<ILogger<GreeterService>>();
  1. public static ApplicationDbContext CreateDbContext(){
  2. var db = new ApplicationDbContext(new DbContextOptionsBuilder<ApplicationDbContext>()
  3. .UseSqlite(CreateInMemoryDatabase()).Options);
  4. db.Database.EnsureCreated();
  5. return db;
  6. }
  7. private static DbConnection CreateInMemoryDatabase()
  8. {
  9. var connection = new SqliteConnection("Filename=:memory:");
  10. connection.Open();
  11. return connection;
  12. }

重点:虽然是内存模式,数据库也必须是open的,并且需要运行EnsureCreated,否则调用数据库功能是会报告找不到表。

使用如下代码伪造:

  1. public static ServerCallContext CreateTestContext(){
  2. return TestServerCallContext.Create("fooMethod",
  3. null,
  4. DateTime.UtcNow.AddHours(1),
  5. new Metadata(),
  6. CancellationToken.None,
  7. "127.0.0.1",
  8. null,
  9. null,
  10. (metadata) => TaskUtils.CompletedTask,
  11. () => new WriteOptions(),
  12. (writeOptions) => { });
  13. }

里面的具体参数要依据实际测试需要进行调整,比如测试客户端取消操作时,修改CancellationToken参数。

  1. [Fact]
  2. public void SayHello()
  3. {
  4. var service = new GreeterService(logger, null);
  5. var request = new HelloRequest{Name="world"};
  6. var response = service.SayHello(request, scc).Result;
  7. var expected = "Hello world";
  8. var actual = response.Message;
  9. Assert.Equal(expected, actual);
  10. }

其中scc = 伪造的ServerCallContext,如果被测方法中没有实际使用它,也可以直接传入null。

服务器端流的方法包含一个IServerStreamWriter<HelloReply>类型的参数,该参数被用于将方法的计算结果逐个返回给调用方,可以创建一个通用的类实现此接口,将写入的消息存储为一个list,以便测试。

  1. public class TestServerStreamWriter<T> : IServerStreamWriter<T>
  2. {
  3. public WriteOptions WriteOptions { get; set; }
  4. public List<T> Responses { get; } = new List<T>();
  5. public Task WriteAsync(T message)
  6. {
  7. this.Responses.Add(message);
  8. return Task.CompletedTask;
  9. }
  10. }

测试时,向数据库表中插入两条记录,然后测试对比,看接口方法是否返回两条记录。

  1. public async Task SayHellos(){
  2. var db = TestTools.CreateDbContext();
  3. var students = new List<Student>{
  4. new Student{Name="1"},
  5. new Student{Name="2"}
  6. };
  7. db.AddRange(students);
  8. db.SaveChanges();
  9. var service = new GreeterService(logger, db);
  10. var request = new HelloRequest{Name="world"};
  11. var sw = new TestServerStreamWriter<HelloReply>();
  12. await service.SayHellos(request, sw, scc);
  13. var expected = students.Count;
  14. var actual = sw.Responses.Count;
  15. Assert.Equal(expected, actual);
  16. }

与服务器流类似,客户端流方法也有一个参数类型为IAsyncStreamReader<HelloRequest>,简单实现一个类用于测试。

该类通过直接将客户端要传入的数据通过IEnumable<T>参数传入,模拟客户端的流式请求多个数据。

  1. public class TestStreamReader<T> : IAsyncStreamReader<T>
  2. {
  3. private readonly IEnumerator<T> _stream;
  4. public TestStreamReader(IEnumerable<T> list){
  5. _stream = list.GetEnumerator();
  6. }
  7. public T Current => _stream.Current;
  8. public Task<bool> MoveNext(CancellationToken cancellationToken)
  9. {
  10. return Task.FromResult(_stream.MoveNext());
  11. }
  12. }

正常流程测试代码

  1. [Fact]
  2. public void Sum_NormalInput_ReturnSum()
  3. {
  4. var service = new GreeterService(null, null);
  5. var data = new List<HelloRequest>{
  6. new HelloRequest{Name="1"},
  7. new HelloRequest{Name="2"},
  8. };
  9. var stream = new TestStreamReader<HelloRequest>(data);
  10. var response = service.Sum(stream, scc).Result;
  11. var expected = "sum is 3";
  12. var actual = response.Message;
  13. Assert.Equal(expected, actual);
  14. }

参数错误的测试代码

  1. [Fact]
  2. public void Sum_BadInput_ThrowException()
  3. {
  4. var service = new GreeterService(null, null);
  5. var data = new List<HelloRequest>{
  6. new HelloRequest{Name="1"},
  7. new HelloRequest{Name="abc"},
  8. };
  9. var stream = new TestStreamReader<HelloRequest>(data);
  10. Assert.ThrowsAsync<ArgumentException>(async () => await service.Sum(stream, scc));
  11. }

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

版权声明:本文为wjsgzcn原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/wjsgzcn/p/12883169.html