[英]How do I unit test this asynchronous code?
我正在做一个将gui放入第三方数据库备份实用程序的项目。 这是我在编写单元测试时所做的第一个项目。 到目前为止,我一直在使用TDD方法编写几乎整个项目,但是我对此功能感到困惑,只是在没有TDD的情况下编写了该功能。 回去时,我仍然不知道如何测试它。
注意如果您是第一次阅读此帖子, 请忽略下面的代码,并检出下面的“编辑#2”,这是该代码的重构版本。
private void ValidateCustomDbPath()
{
if (_validateCustomDbPathTask != null && !_validateCustomDbPathTask.IsCompleted)
{
_validateCustomDbPathCancellationTokenSource.Cancel();
}
if (string.IsNullOrEmpty(_customDbPath))
{
Set(() => CustomDbPathValidation, ref _customDbPathValidation, ValidationState.Validated);
Set(() => CustomDbPathValidationMessage, ref _customDbPathValidationMessage, "");
_customDbPathCompany = string.Empty;
UpdateDefaultBackupPath();
return;
}
_validateCustomDbPathCancellationTokenSource = new CancellationTokenSource();
var ct = _validateCustomDbPathCancellationTokenSource.Token;
_validateCustomDbPathTask = Task.Run(async () =>
{
Set(() => CustomDbPathValidation, ref _customDbPathValidation, ValidationState.Validating);
Set(() => CustomDbPathValidationMessage, ref _customDbPathValidationMessage, "");
try
{
if (!_diskUtils.File.Exists(_customDbPath))
{
Set(() => CustomDbPathValidation, ref _customDbPathValidation, ValidationState.Invalid);
Set(() => CustomDbPathValidationMessage, ref _customDbPathValidationMessage, "File not found");
_customDbPathCompany = string.Empty;
UpdateDefaultBackupPath();
return;
}
using (var conn = _connectionProvider.GetConnection(_customDbPath, false))
using (var trxn = conn.BeginTransaction())
{
var dbSetup = await _dbSetupRepo.GetAsync(conn, trxn);
_customDbPathCompany = dbSetup.Company;
Set(() => CustomDbPathValidation, ref _customDbPathValidation, ValidationState.Validated);
Set(() => CustomDbPathValidationMessage, ref _customDbPathValidationMessage, "");
UpdateDefaultBackupPath();
}
}
catch
{
Set(() => CustomDbPathValidation, ref _customDbPathValidation, ValidationState.Invalid);
Set(() => CustomDbPathValidationMessage, ref _customDbPathValidationMessage, "Error getting company");
_customDbPathCompany = string.Empty;
UpdateDefaultBackupPath();
}
}, ct);
}
此方法在屏幕的ViewModel中。 设置新值后,将在CustomDbPath属性设置器中调用它。 我的想法是,我在gui中具有可视指示器,以显示提供的路径是否有效,并且UpdateDefaultBackupPath方法根据所选数据库中的信息更新建议的备份文件名。
解释您在这里看到的内容,如果第一个IF块已经运行但尚未完成,则它取消了验证任务(我知道我还没有使用取消令牌)。 在第二个块中,如果没有提供路径(起始状态),我不想显示错误,也不需要进一步验证。 在任务中,我首先指出该字段正在验证中,然后检查是否可以在磁盘上找到数据库文件,最后检查是否在数据库中寻找要用于命名备份文件名的信息。 我正在使用MVVM Light,这是Set方法的来源(它实现了INotifyPropertyChanged)。
到目前为止,在我使用过任务的其他所有地方,我都没有进行过问题测试。 我等待有问题的方法并测试结果。 这种情况是不同的。 这在属性设置器中被称为,它显然不能遵循异步等待模式,而且我也不想这样做,因为用户可以在第一个验证序列完成之前再次更改属性。 我对测试感兴趣的主要内容是CustomDbPathValidation和CustomDbPathValidationMessage值。 它是否设置为在验证之前进行验证。 成功时将其设置为已验证,失败时是否设置为无效。 我很乐意以一种使其可测试的方式重写此方法,我只是不知道该怎么做。 有任何想法吗?
编辑#2:
本着@vendettamit建议遵循SRP的精神,我对功能进行了细分。 GetCompanyInfoFromDbAsync用于在适当时从数据库中获取公司信息。 GetCompanyInfoAsync确定是否在可能的情况下从数据库中检索信息(如果找不到数据库)。 最终,这两种方法将移至另一个类,并公开进行测试。 我将它们移到的类将通过构造函数注入到此处显示的类中。
关于@vendettamit提出的一些观点:
“您正在设置路径是否为空(不应作为验证方法的一部分)”
我认为您误读了代码。 如果路径为空白,我将公司设置为空白。 这段代码的重点是获取公司名称并使用它来编写文件名。
我不确定GetCompanyInfoAsync是否满足您的SRP标准,但是尝试将其分解得比本次编辑中的要多,这对我来说似乎很奇怪。
'在所有路径“代码气味”中调用UpdateDefaultBackupPath()”
我猜您是在写完第一篇文章之前写的。 回顾一下代码,我得出了相同的结论,并且已经对其进行了重构,因此被称为一次。
“最后我看到了您的编辑,即ref _customDbPathValidationMessage上帝与您同在。”
虽然我同意通常很少使用ref,但我觉得这里是适当的。 Set方法来自该类派生的MVVM Light ViewModelBase基类。 它有助于使用INotifyPropertyChanged“模式”。 它们具有不会更新后备字段的功能,但是当我需要更改后备字段并通知我时,我选择使用Set方法来减少所需的代码。 第一个参数是一个Expression,它使您可以指定属性,针对该属性,将以编译器可以帮助您捕获错别字的方式引发通知。 ref参数是您提供属性的后备字段的位置,下一个参数是要分配给后备字段的新值。 我可能没有使用Set,而是使用了ViewModelBase中提供的其他帮助程序方法来引发通知,然后在此之前手动设置后备字段。 但为什么? 为什么要添加更多代码? 我不知道它将实现什么。
根据先前调用的状态(Task和TaskCancellationSource)对函数的注释,我看不到任何解决方法。 我需要能够在不让安装员等待任务完成的情况下启动此功能。 在他们输入绑定到CustomDbPath字段的编辑框的字母后,我无法“暂停”它。 当按下“备份”按钮(VM上的命令)时,我将需要检查任务是否正在运行,并等待任务完成。
我仍然担心ValidateCustomDbPathAsync中的代码。 我可以将其更改为protected等,然后在测试中等待它,但是我仍然遇到的问题是,在执行验证之前,我不知道如何测试将其设置为Validating,因为到了等待返回时结果来不及检查。 这最终是我遇到的问题,即使在重构后,我也看不到实现这一目标的简便方法。
注意 -这越来越长。 StackOverflow是否愿意保留以前的编辑,还是应该删除第一个编辑以缩短此问题的时间?
public string CustomDbPath
{
get { return _customDbPath; }
set
{
if (_customDbPath != value)
{
Set(() => CustomDbPath, ref _customDbPath, value);
ValidateCustomDbPath();
}
}
}
private void ValidateCustomDbPath()
{
if (_validateCustomDbPathCts != null)
{
_validateCustomDbPathCts.Cancel();
_validateCustomDbPathCts.Dispose();
}
_validateCustomDbPathCts = new CancellationTokenSource();
_validateCustomDbPathTask = ValidateCustomDbPathAndUpdateDefaultBackupPathAsync(_validateCustomDbPathCts.Token);
}
private async Task ValidateCustomDbPathAndUpdateDefaultBackupPathAsync(CancellationToken ct)
{
var companyInfo = await ValidateCustomDbPathAsync(ct);
_customDbPathCompany = companyInfo.Company;
UpdateDefaultBackupPath();
}
private async Task<CompanyInfo> ValidateCustomDbPathAsync(CancellationToken ct)
{
Set(() => CustomDbPathValidation, ref _customDbPathValidation, ValidationState.Validating);
Set(() => CustomDbPathValidationMessage, ref _customDbPathValidationMessage, string.Empty);
var companyInfo = await GetCompanyInfoAsync(_customDbPath, ct);
Set(() => CustomDbPathValidation, ref _customDbPathValidation, companyInfo.Error ? ValidationState.Invalid : ValidationState.Validated);
Set(() => CustomDbPathValidationMessage, ref _customDbPathValidationMessage, companyInfo.ErrorMsg);
return companyInfo;
}
private async Task<CompanyInfo> GetCompanyInfoAsync(string dbPath, CancellationToken ct)
{
if (string.IsNullOrEmpty(dbPath))
{
return new CompanyInfo
{
Company = string.Empty,
Error = false,
ErrorMsg = string.Empty
};
}
if (_diskUtils.File.Exists(dbPath))
{
return await GetCompanyInfoFromDbAsync(dbPath, ct);
}
ct.ThrowIfCancellationRequested();
return new CompanyInfo
{
Company = string.Empty,
Error = true,
ErrorMsg = "File not found"
};
}
private async Task<CompanyInfo> GetCompanyInfoFromDbAsync(string dbPath, CancellationToken ct)
{
try
{
using (var conn = _connectionProvider.GetConnection(dbPath, false))
using (var trxn = conn.BeginTransaction())
{
ct.ThrowIfCancellationRequested();
var dbSetup = await _dbSetupRepo.GetAsync(conn, trxn);
ct.ThrowIfCancellationRequested();
return new CompanyInfo
{
Company = dbSetup.Company,
Error = false,
ErrorMsg = string.Empty
};
}
}
catch (OperationCanceledException)
{
throw;
}
catch
{
return new CompanyInfo
{
Company = string.Empty,
Error = true,
ErrorMsg = "Error getting company"
};
}
}
编辑#1:
从@BerinLoritsch提出的一些建议中,我将方法重写为异步方法,并将某些逻辑分解为另一个方法,该方法以后可放入另一个类中并在测试期间进行伪造。 我必须添加一条pragma语句以使编译器退出,并警告我我没有在等待async方法(在属性设置器中无法做到)。 我认为重写后现在可以更好地显示我遇到的问题,而我之前可能还不太清楚。 我知道我可以将其编写为异步并等待它,并且可以测试它是否已正确标记为无效或已验证。 我的问题是,如何在执行验证之前测试它是否首先将其标记为验证? 它在执行验证之前就执行了此操作,但是一旦您等待该函数的结果,则基本上为时已晚,因为您获得的结果将无效或经过验证。 我不确定该如何测试。 在我看来,我想可能有一种方法可以伪造新修订的GetCompanyInfoAsync方法,以返回在测试中“停顿”的任务,直到我希望它完成。 如果我可以控制它的完成时间,那么也许我可以在完成之前测试ViewModel状态。
public string CustomDbPath
{
get { return _customDbPath; }
set
{
if (_customDbPath != value)
{
Set(() => CustomDbPath, ref _customDbPath, value);
#pragma warning disable 4014
ValidateCustomDbPathAsync();
#pragma warning restore 4014
}
}
}
private async Task ValidateCustomDbPathAsync()
{
if (_validateCustomDbPathTask != null && !_validateCustomDbPathTask.IsCompleted)
{
_validateCustomDbPathCancellationTokenSource.Cancel();
}
_validateCustomDbPathCancellationTokenSource = new CancellationTokenSource();
var ct = _validateCustomDbPathCancellationTokenSource.Token;
_validateCustomDbPathTask = Task.Run(async () =>
{
Set(() => CustomDbPathValidation, ref _customDbPathValidation, ValidationState.Validating);
Set(() => CustomDbPathValidationMessage, ref _customDbPathValidationMessage, string.Empty);
var companyInfo = await GetCompanyInfoAsync(_customDbPath, ct);
Set(() => CustomDbPathValidation, ref _customDbPathValidation, companyInfo.Error ? ValidationState.Invalid : ValidationState.Validated);
Set(() => CustomDbPathValidationMessage, ref _customDbPathValidationMessage, companyInfo.ErrorMsg);
_customDbPathCompany = companyInfo.Company;
UpdateDefaultBackupPath();
}, ct);
await _validateCustomDbPathTask;
}
private async Task<CompanyInfo> GetCompanyInfoAsync(string dbPath, CancellationToken ct)
{
if (string.IsNullOrEmpty(dbPath))
{
return new CompanyInfo
{
Company = string.Empty,
Error = false,
ErrorMsg = string.Empty
};
}
if (!_diskUtils.File.Exists(dbPath))
{
return new CompanyInfo
{
Company = string.Empty,
Error = true,
ErrorMsg = "File not found"
};
}
try
{
using (var conn = _connectionProvider.GetConnection(dbPath, false))
using (var trxn = conn.BeginTransaction())
{
var dbSetup = await _dbSetupRepo.GetAsync(conn, trxn);
return new CompanyInfo
{
Company = dbSetup.Company,
Error = false,
ErrorMsg = string.Empty
};
}
}
catch
{
return new CompanyInfo
{
Company = string.Empty,
Error = true,
ErrorMsg = "Error getting company"
};
}
}
首先,您应该真正重构此方法。 我看到一个单元中发生了很多事情,这就是为什么您面对为此编写测试的问题。 让我们做一点RCA(根本原因分析)
很少违反SRP,
UpdateDefaultBackupPath()
ref _customDbPathValidationMessage
上帝与您同在。 其次,看完此方法后,您似乎需要更多地编写单元测试。
在编写单元测试之前,请编写良好的可测试代码。
尽管单元测试促进了Refactoring
,所以首先应该重构方法,然后开始编写可以自己解决的测试。
提示 -重构它(删除异步代码),编写测试,因为它是同步方法。 然后将其更改为异步并修复您的测试。
通过最初的重构,您已经迈出了一步,可以对这些功能进行单元测试。 现在,您必须在可访问性与可测试性之间取得平衡。 简而言之,您可能不希望您想要的任何代码调用这些方法,但是您确实希望由单元测试调用这些方法。
如果将private
更改为protected
则可以选择扩展类并公开受保护的方法。 如果将private
更改为internal
,则只要单元测试位于相同的名称空间(如果程序集是密封的,则在程序集)中,它们就可以访问代码。 或者,您可以将它们public
并且所有人都可以访问它。
单元测试将如下所示:
[Test]
public async Task TestMyDbCode()
{
string dbPath = "path/to/db";
// do your set up
CompanyInfo info = await myObject.GetCompanyInfoAsync(dbPath, CancellationToken.None);
Assert.That(info, Is.NotNull());
// and finish your assertions.
}
这样做的目的是分解测试,以便最小的单元是稳定的,并且依赖于它的代码可以同样地可预测。
您已经非常简单地设置了属性设置程序,以至于只要验证代码已经过良好的测试,它就真的不值得测试。
您有几个选择,但是在这种情况下,最不麻烦的方法是将ValidateCustomDbPath()
声明为async
返回任务。 它看起来像这样:
private async Task ValidateCustomDbPath()
{
// prep work
_validateCustomDbPathTask = Task.Run(async () =>
{
// all your DB stuff
});
await _validateCustomDbPathTask;
}
通过使async
代码一直异步到可以调用它的地方,那么您已经有了一种机制来await
它在单元测试中完成。 还有其他一些选项,如果ValidateCustomDbPath
必须是Action,或者本身是从事件中调用的,则这些选项很有用。
该方法要求您有两种方法:
async
方法(通常是Task) 好处是您可以将单元测试时间花费在实际的async
方法上,而无需理会回调方法。
async void
方法的问题在于,虽然您可以await
其他异步方法,但不能等待返回void
,这就是为什么要测试代码时需要2个方法的原因。
最后一个选择是将数据库内容放入其自己的async Task
方法中,您可以直接从单元测试中调用该方法。 您可以测试设置方法的路径,这些方法实际上并没有将数据库工作与数据库工作本身分开。 重要的是,您公开了一个async Task
方法,您的单元测试可以利用该方法,并实际上等待工作完成。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.