gpt4 book ai didi

c# - 如何对该异步代码进行单元测试?

转载 作者:行者123 更新时间:2023-12-03 10:27:06 25 4
gpt4 key购买 nike

我正在一个将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属性 setter 中调用它。我的想法是,我在gui中具有可视指示器,以显示提供的路径是否有效,并且UpdateDefaultBackupPath方法根据所选数据库中的信息更新建议的备份文件名。

解释您在这里看到的内容,如果第一个IF块已经运行但尚未完成,则它取消了验证任务(我知道我还没有使用取消 token )。在第二个块中,如果没有提供路径(起始状态),我不想显示错误,也不需要进一步验证。在任务中,我首先指示该字段正在验证中,然后检查是否可以在磁盘上找到数据库文件,最后,如果找到该文件,我会在数据库中寻找要用于命名备份文件名的信息。我正在使用MVVM Light,这是Set方法的来源(它实现了INotifyPropertyChanged)。

到目前为止,在我使用过任务的其他所有地方,我都没有进行过问题测试。我等待有问题的方法并测试结果。这种情况是不同的。这在属性 setter 中被称为,它显然不能遵循异步等待模式,而且我也不想这样做,因为用户可以在第一个验证序列完成之前再次更改属性。我对测试感兴趣的主要内容是CustomDbPathValidation和CustomDbPathValidationMessage值。它是否设置为在验证之前进行验证。成功时将其设置为已验证,失败时是否设置为无效。我很高兴以一种使它可测试的方式重写该方法,我只是不知道该怎么做。有任何想法吗?

编辑#2:

本着@vendettamit建议遵循SRP的精神,我对功能进行了分割。 GetCompanyInfoFromDbAsync用于在适当时从数据库中获取公司信息。 GetCompanyInfoAsync确定是否在可能的情况下从数据库中检索信息(如果找不到数据库)。最终,这两种方法将移至另一个类,并公开进行测试。我将它们移到的类将通过构造函数注入(inject)到此处显示的类中。

关于@vendettamit提出的一些观点:

“您正在设置路径是否为空(不应成为验证方法的一部分)”

我认为您误读了代码。如果路径为空白,我将公司设置为空白。这段代码的重点是获取公司名称并使用它来编写文件名。

我不确定GetCompanyInfoAsync是否满足您的SRP标准,但是尝试将其分解得比本次编辑中的要多,这对我来说似乎很奇怪。

'在所有路径“代码气味”中调用UpdateDefaultBackupPath()”

我猜你是在写我的第一篇文章之前写的。回顾一下代码,我得出了相同的结论,并且已经对其进行了重构,因此被称为一次。

“最后我看到了您的修改,即ref _customDbPathValidationMessage上帝与您同在。”

虽然我同意通常很少使用ref,但我觉得这里是适当的。 Set方法来自此类派生的MVVM Light ViewModelBase基类。它有助于使用INotifyPropertyChanged“模式”。它们具有不会更新您的后备字段的功能,但是当我需要更改后备字段并通知我时,我选择使用Set方法来减少所需的代码。第一个参数是一个Expression,该表达式使您可以指定引发通知的属性,编译器可以帮助您捕获错别字。 ref参数是您提供属性的后备字段的位置,下一个参数是要分配给后备字段的新值。我可能没有使用过Set,而是使用了ViewModelBase中提供的其他帮助程序方法来引发通知,然后在此之前手动设置后备字段。但为什么?为什么要添加更多代码?我不知道它将实现什么。

根据先前调用的状态(Task和TaskCancellationSource)对函数的注释,我看不到任何解决方法。我需要能够在不让 setter 等待任务完成的情况下启动此功能。在他们输入绑定(bind)到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提出的一些建议中,我将方法重写为异步方法,并将某些逻辑分解为另一个方法,该方法以后可放入另一个类中并在测试期间进行伪造。我必须添加一个编译指示才能使编译器退出,并警告我我没有在等待异步方法(在属性 setter 中无法做到)。我认为重写后现在可以更好地显示我遇到的问题,而我之前可能还不太清楚。我知道我可以将其编写为异步并等待它,并且可以测试它是否正确标记为无效或已验证。我的问题是,在执行验证之前,如何测试它是否首先将其标记为验证?它会在执行验证之前就执行此操作,但是一旦您等待该函数的结果,则基本上为时已晚,因为您获得的结果将无效或经过验证。我不确定该如何测试。在我看来,我想可能有一种方法可以伪造新修订的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,

  • _validateCustomDbPathTask是类变量,因此此方法的输出取决于对象的状态。
  • 您正在检查Task是否已在运行(验证状态)
  • 您正在设置路径是否为空(不应作为验证方法的一部分)
  • 在“任务”中,您再次验证路径是否存在于磁盘

  • 在所有路径中调用
  • UpdateDefaultBackupPath() “代码气味”
  • ..最后我看到您的编辑,即ref _customDbPathValidationMessage上帝与您同在。

  • 其次,看完此方法后,您似乎需要更多地编写单元测试。

    Before you write unit tests, write a good testable code.



    尽管单元测试促进了 Refactoring,所以首先您应该重构您的方法,然后开始编写可以自己解决的测试。

    提示-对其进行重构(删除异步代码),因为它是一种同步方法而编写测试。然后将其更改为异步并修复您的测试。

    关于c# - 如何对该异步代码进行单元测试?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36845804/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com