你可能会用到的 Mock 小技巧
Intro
最近看到阿迪分享了两篇 Mock 相关的文章,于是想把自己遇到的一些可能对你有帮助的一些小技巧分享一下,大概总结了一下,且看下文
AsyncEnumerable
在 C# 8 中引入了异步流,AsyncEnumerable
,在有些类库中已经引入了这一语法,在 StackExchange.Redis
中 HashScanAsync
的返回值就是 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 方法,希望能对你有所帮助~
你在写测试用例的过程中还有哪些觉得比较实用或者有哪些测试用例觉得比较难写呢?