你可能会用到的 Mock 小技巧

你可能会用到的 Mock 小技巧

Intro

最近看到阿迪分享了两篇 Mock 相关的文章,于是想把自己遇到的一些可能对你有帮助的一些小技巧分享一下,大概总结了一下,且看下文

AsyncEnumerable

在 C# 8 中引入了异步流,AsyncEnumerable,在有些类库中已经引入了这一语法,在 StackExchange.RedisHashScanAsync 的返回值就是 IAsyncEnumerable<HashEntry>

使用示例如下:

var dic = new Dictionary<string, string>();
await foreach (var entry in db.HashScanAsync(setName, "*"))
{
    dic[entry.Name] = entry.Value;
}

在 Mock 的时候,我们可以通过下面的 MockAsyncEnumerable 比较方便的指定一个 IEnumerable 对象来实现一个 IAsyncEnumerable 对象

private class MockAsyncEnumerable<T> : IAsyncEnumerable<T>
{
    private readonly IEnumerable<T> _data;

    public MockAsyncEnumerable(IEnumerable<T> data)
    {
        _data = data;
    }

    public IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = new CancellationToken())
    {
        return new MockAsyncEnumerator<T>(_data.GetEnumerator());
    }
}

private class MockAsyncEnumerator<T> : IAsyncEnumerator<T>
{
    private readonly IEnumerator<T> _enumerator;

    public MockAsyncEnumerator(IEnumerator<T> enumerator)
    {
        _enumerator = enumerator;
    }

    public ValueTask DisposeAsync()
    {
        _enumerator.Dispose();
        return default;
    }

    public ValueTask<bool> MoveNextAsync()
    {
        return new ValueTask<bool>(_enumerator.MoveNext());
    }

    public T Current => _enumerator.Current;
}

使用示例如下:

var entries = new HashEntry[10];
databaseMock.Setup(c => c.HashScanAsync(setName, "*", 200, 0, 0, CommandFlags.None))
    .Returns(new MockAsyncEnumerable<HashEntry>(entries));

HttpClient Mock

一个项目中经常会遇到调用第三方的 API,如何比较方便的 Mock 一个 HttpClient 的行为呢,我们可以通过自定义一个 HttpHandler 来实现自定义响应信息,通常我们需要根据不同的请求信息返回不同的响应,我们自定义了一个 MockHttpHandler 来实现比较方便的 Mock 第三方 API 的行为,实现如下:

internal class MockHttpHandler : DelegatingHandler
{
    private readonly Func<HttpRequestMessage, HttpResponseMessage> _getResponseFunc;

    public MockHttpHandler(Func<HttpRequestMessage, HttpResponseMessage> getResponseFunc)
    {
        _getResponseFunc = getResponseFunc;
    }

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return Task.FromResult(_getResponseFunc(request));
    }
}

使用示例如下:

using var client =
    new HttpClient(new MockHttpHandler(req => new HttpResponseMessage(HttpStatusCode.BadRequest)))
    {
        BaseAddress = new Uri("https://api.weihanli.xyz/")
    };
//
using var httpClient = new HttpClient(new MockHttpHandler(request =>
{
    var statusCode = request.RequestUri.AbsoluteUri.Contains("templateId=1") ? HttpStatusCode.NotFound : (request.RequestUri.AbsoluteUri.Contains("templateId=2") ? HttpStatusCode.BadRequest : HttpStatusCode.InternalServerError);
    return new HttpResponseMessage(statusCode)
    {
        Content = new StringContent(JsonConvert.SerializeObject(new
        {
            Code = statusCode.ToString(),
            Msg = "The template not exists"
        }))
    };
}))

MVC HttpContext Mock

HttpContext mock 示例:

var services = new ServiceCollection()
    .AddScoped<CurrentUser>(sp => new CurrentUser()
    {
        UserID = 1,
        UserName = "admin"
    })
    .BuildServiceProvider();

var mock = new Mock<HttpContext>();
// Mock HttpContext.User
mock.SetupGet(x => x.User)
    .Returns(new ClaimsPrincipal(new ClaimsIdentity(new Claim[]
    {
        new Claim(ClaimTypes.Name, "admin"),
        new Claim(ClaimTypes.Email, "weihan.li@iherb.com"),
    }, JwtBearerDefaults.AuthenticationScheme)));
// Mock RequestServices
mock.Setup(x => x.RequestServices).Returns(services);

var controller = new CommonController(NullLogger<CommonController>.Instance)
{
    ControllerContext = new ControllerContext() 
    {
        HttpContext = mock.Object 
    }
};

MVC ExceptionFilter test

有时我们会在项目里使用到 ExceptionFilter 来捕获 MVC 中未捕获的异常,如果想要针对自定义的 ExceptionFilter 写一些测试用例可以参考下面的测试用例:

[Fact]
public async Task ExceptionTest()
{
    var filters = new IFilterMetadata[]
    {
        new ResultExceptionFilter()
    };
    var exceptionContext = new ExceptionContext(new ActionContext()
    {
        HttpContext = new DefaultHttpContext()
        {
            RequestServices = new ServiceCollection()
                .AddLogging()
                .BuildServiceProvider()
        },
        RouteData = new RouteData(new RouteValueDictionary()
        {
            {"controller", "Test"},
            {"action", "Test"},
        }),
        ActionDescriptor = new ActionDescriptor(),
    }, filters)
    {
        Exception = new NotImplementedException()
    };

    var invoker = new Mock<IActionInvoker>();
    invoker.Setup(x => x.InvokeAsync())
        .Callback(() =>
        {
            new ResultExceptionFilter().OnException(exceptionContext);
        })
        .Returns(Task.CompletedTask);

    await invoker.Object.InvokeAsync();
    // ...
}

Mock Data

字符串

在我们的代码中经常会出现对输入参数进行校验是否为空,对于这样的数据每次都取写一遍就会有点烦,所以写了一个自定义测试数据,就是返回 null/空字符串,实现代码如下:

public class NullOrEmptyStringDataAttribute : DataAttribute
{
    public bool IncludeWhitespace { get; set; }

    public override IEnumerable<object[]> GetData(MethodInfo testMethod)
    {
        yield return new object[] { null };
        yield return new object[] { string.Empty };
        if (IncludeWhitespace)
        {
            yield return new object[] { " " };
        }
    }
}

使用示例如下:

[Theory]
[NullOrEmptyStringData]
public void Test(string name)
{
    Assert.True(string.IsNullOrEmpty(name));
}

[Theory]
[NullOrEmptyStringData(IncludeWhitespace=true)]
public void Test1(string name)
{
    Assert.True(string.IsNullOrWhitespace(name));
}

Number

对于 id 之类的数据,通过我们需要检查是否大于0,在写测试的时候需要考虑小于等于 0 的情况,通常我们也可以像上面那样做一个简单的封装,实现代码如下:

public class LessThanOrEqualDataAttribute : DataAttribute
{
    public int Value { get; set; }

    public override IEnumerable<object[]> GetData(MethodInfo testMethod)
    {
        yield return new object[] { Value };
        yield return new object[] { Value - 1 };
    }
}

使用实例如下:

[Theory]
[LessThanOrEqualData]
public async Task GetCategoryIdInfo_BadRequest(int id)
{
    var result = await _controller.GetCategoryIdInfo(id, null);
    result.AssertCode(ErrorCode.BadRequest);
}

More

上面是一些我写测试用例的时候可能会用到的一些帮助类或 Mock 方法,希望能对你有所帮助~

你在写测试用例的过程中还有哪些觉得比较实用或者有哪些测试用例觉得比较难写呢?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值