我目前正在使用Moq来帮助我进行单元测试,但是我遇到了一个我不知道如何解决的问题。
例如,假设我想验证CancellationToken.ThrowIfCancellationRequested()
每Upload(
次呼叫被调用一次
public UploadEngine(IUploader uploader)
{
_uploader = uploader;
}
public void PerformUpload(CancellationToken token)
{
token.ThrowIfCancellationRequested();
_uploader.Upload(token, "Foo");
token.ThrowIfCancellationRequested();
_uploader.Upload(token, "Bar");
}
如果token
是引用类型,我通常会执行类似
[TestMethod()]
public void PerformUploadTest()
{
var uploader = new Mock<IUploader>();
var token = new Mock<CancellationToken>();
int callCount = 0;
uploader.Setup(a => a.Upload(token.Object, It.IsAny<string>())).Callback(() => callCount++);
token.Setup(a => a.ThrowIfCancellationRequested());
var engine = new UploadEngine(uploader.Object);
engine.PerformUpload(token.Object);
token.Verify(a => a.ThrowIfCancellationRequested(), Times.Exactly(callCount));
}
然而,据我所知,Moq不支持值类型。测试这个的正确方法是什么,或者没有办法通过Moq做我想要的东西,而不先将CancellationToken
装入容器中,然后传递给PerformUpload(
?
答案 0 :(得分:3)
你可能已经离开了这一点,但我想到你想要测试的东西似乎并没有多大意义。测试ThrowIfCancellationRequested
被称为与Upload
相同的次数并不能确保它们以正确的顺序被调用,我假设在这种情况下它实际上是相关的。你不希望这样的代码通过,但我很确定它会:
_uploader.Upload(token, "Foo");
token.ThrowIfCancellationRequested();
token.ThrowIfCancellationRequested();
_uploader.Upload(token, "Bar");
正如评论中所说,解决此问题的最简单方法是将token.ThrowIfCancellationRequested
电话推送到Upload
电话。假设无论出于何种原因这是不可能的,我可能会采用以下方法来测试您的场景。
首先,我将封装检查是否已请求取消的功能,如果没有,则将操作调用为可测试的内容。首先想到的是,这可能是这样的:
public interface IActionRunner {
void ExecIfNotCancelled(CancellationToken token, Action action);
}
public class ActionRunner : IActionRunner{
public void ExecIfNotCancelled(CancellationToken token, Action action) {
token.ThrowIfCancellationRequested();
action();
}
}
这可以通过两次测试进行相当简单的测试。一个用于检查是否在未取消令牌时调用该操作,另一个用于验证是否取消该令牌。这些测试看起来像:
[TestMethod]
public void TestActionRunnerExecutesAction() {
bool run = false;
var runner = new ActionRunner();
var token = new CancellationToken();
runner.ExecIfNotCancelled(token, () => run = true);
// Validate action has been executed
Assert.AreEqual(true, run);
}
[TestMethod]
public void TestActionRunnerDoesNotExecuteIfCancelled() {
bool run = false;
var runner = new ActionRunner();
var token = new CancellationToken(true);
try {
runner.ExecIfNotCancelled(token, () => run = true);
Assert.Fail("Exception not thrown");
}
catch (OperationCanceledException) {
// Swallow only the expected exception
}
// Validate action hasn't been executed
Assert.AreEqual(false, run);
}
然后我会将IActionRunner
注入UploadEngine
并验证它是否被正确调用。因此,您的PerformUpload
方法将更改为:
public void PerformUpload(CancellationToken token) {
_actionRunner.ExecIfNotCancelled(token, () => _uploader.Upload(token, "Foo"));
_actionRunner.ExecIfNotCancelled(token, () => _uploader.Upload(token, "Bar"));
}
然后,您可以编写一对测试来验证PerformUpload
。第一个检查是否已设置ActionRunner
模拟执行提供的操作,然后Upload
被调用至少一次。第二个测试验证如果ActionRunner
模拟已设置为忽略该操作,则不会调用Upload
。这基本上可以确保方法中的所有 Upload
调用都通过ActionRunner
完成。这些测试看起来像这样:
[TestMethod]
public void TestUploadCallsMadeThroughActionRunner() {
var uploader = new Mock<IUploader>();
var runner = new Mock<IActionRunner>();
var token = new CancellationToken();
int callCount = 0;
uploader.Setup(a => a.Upload(token, It.IsAny<string>())).Callback(() => callCount++);
// Use callback to invoke actions supplied to runner
runner.Setup(x => x.ExecIfNotCancelled(token, It.IsAny<Action>()))
.Callback<CancellationToken, Action>((tok,act)=>act());
var engine = new UploadEngine(uploader.Object, runner.Object);
engine.PerformUpload(token);
Assert.IsTrue(callCount > 0);
}
[TestMethod]
public void TestNoUploadCallsMadeThroughWithoutActionRunner() {
var uploader = new Mock<IUploader>();
var runner = new Mock<IActionRunner>();
var token = new CancellationToken();
int callCount = 0;
uploader.Setup(a => a.Upload(token, It.IsAny<string>())).Callback(() => callCount++);
// NOP callback on runner prevents uploader action being run
runner.Setup(x => x.ExecIfNotCancelled(token, It.IsAny<Action>()))
.Callback<CancellationToken, Action>((tok, act) => { });
var engine = new UploadEngine(uploader.Object, runner.Object);
engine.PerformUpload(token);
Assert.AreEqual(0, callCount);
}
显然你可能想为你的UploadEngine
编写其他测试,但它们似乎超出了当前问题的范围......