zoukankan      html  css  js  c++  java
  • .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项目,引入如下包:

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

  • 相关阅读:
    从 Qt 的 delete 说开来
    Qt信号槽的一些事
    Qt 线程基础(QThread、QtConcurrent等)
    QThread使用——关于run和movetoThread的区别
    重点:怎样正确的使用QThread类(注:包括推荐使用QThread线程的新方法QObject::moveToThread)
    重要:C/C++变量的自动初始化
    C++中基类的析构函数为什么要用virtual虚析构函数
    如何打印Qt中的枚举所对应的字符串
    用route命令解决多出口的问题
    C/C++预处理指令
  • 原文地址:https://www.cnblogs.com/wjsgzcn/p/12883169.html
Copyright © 2011-2022 走看看