对“复杂”单元进行单元测试的正确方法应用服务
我有一个应用程序服务(即大型方法)负责协调多个业务对象之间的交互。本质上,它从一个包含客户信息和发票的系统中获取 DTO,并将其转换并根据各种业务规则将其导入到不同的系统中。
public void ProcessQueuedData()
{
var queuedItems = _importServiceDAL.LoadQueuedData();
foreach (var queuedItem in queuedItems)
{
var itemType = GetQueuedImportItemType(queuedItem);
if (itemType == QueuedImportItemType.Invoice)
{
var account = _accountDAL.GetAccountByAssocNo(queuedItem.Assoc);
int agentAccountID;
if (!account.IsValid)
{
agentAccountId = _accountDAL.AddAccount(queuedItem.Assoc);
}
else
{
agentAccountId = account.AgentAccountID;
}
/// Do additional processing TBD
}
}
}
对于单元测试,假设服务中的整个流程应该逐步进行细粒度测试是否正确,类似于以下内容?
ImportService_ProcessQueuedData_CallsDataAccessLayer_ToLoadQueue
ImportService_ProcessQueuedData_WithQueuedItemToProccess_ChecksIfAccountExists
ImportService_ProcessQueuedData_WithInvoice_CallsDALToCreateAccountIfOneDoesNotExist
这是一个典型的测试:
[TestMethod()]
public void ImportService_ProcessQueuedData_WithInvoice_CallsDALToCheckIfAgentAccountExists()
{
var accountDAL = MockRepository.GenerateStub<IAccountDAL>();
var importServiceDAL = MockRepository.GenerateStub<IImportServiceDAL>();
importServiceDAL.Stub(x => x.LoadQueuedData())
.Return(GetQueuedImportItemsWithInvoice());
accountDAL.Stub(x => x.GetAccountByAssocNo("FFFFF"))
.IgnoreArguments()
.Return(new Account() { AgentAccountId = 0 });
var importSvc = new ImportService(accountDAL, importServiceDAL);
importSvc.ProcessQueuedData();
accountDAL.AssertWasCalled(a => a.GetAccountByAssocNo("FFFFF"), o => o.IgnoreArguments());
accountDAL.VerifyAllExpectations();
}
我的问题是我最终在每个测试中都做了太多设置一点。这是正确的方法吗?如果是,有哪些指示可以避免在每个粒度测试中重复所有这些设置?
I have an application service (ie. large method) responsible for coordinating the interaction between several business objects. Essentially it takes a DTO from one system which contains customer information and an invoice, and translates it and imports it into a different system based on various business rules.
public void ProcessQueuedData()
{
var queuedItems = _importServiceDAL.LoadQueuedData();
foreach (var queuedItem in queuedItems)
{
var itemType = GetQueuedImportItemType(queuedItem);
if (itemType == QueuedImportItemType.Invoice)
{
var account = _accountDAL.GetAccountByAssocNo(queuedItem.Assoc);
int agentAccountID;
if (!account.IsValid)
{
agentAccountId = _accountDAL.AddAccount(queuedItem.Assoc);
}
else
{
agentAccountId = account.AgentAccountID;
}
/// Do additional processing TBD
}
}
}
For the unit tests, is it correct to assume the entire process within the service should be tested on a granular step by step basis, similar to the following?
ImportService_ProcessQueuedData_CallsDataAccessLayer_ToLoadQueue
ImportService_ProcessQueuedData_WithQueuedItemToProccess_ChecksIfAccountExists
ImportService_ProcessQueuedData_WithInvoice_CallsDALToCreateAccountIfOneDoesNotExist
Here's a typical test:
[TestMethod()]
public void ImportService_ProcessQueuedData_WithInvoice_CallsDALToCheckIfAgentAccountExists()
{
var accountDAL = MockRepository.GenerateStub<IAccountDAL>();
var importServiceDAL = MockRepository.GenerateStub<IImportServiceDAL>();
importServiceDAL.Stub(x => x.LoadQueuedData())
.Return(GetQueuedImportItemsWithInvoice());
accountDAL.Stub(x => x.GetAccountByAssocNo("FFFFF"))
.IgnoreArguments()
.Return(new Account() { AgentAccountId = 0 });
var importSvc = new ImportService(accountDAL, importServiceDAL);
importSvc.ProcessQueuedData();
accountDAL.AssertWasCalled(a => a.GetAccountByAssocNo("FFFFF"), o => o.IgnoreArguments());
accountDAL.VerifyAllExpectations();
}
My problem is that I end up doing so much setup in each of these tests it is becoming brittle. Is this the correct approach, and if so what are some pointers to avoid duplicating all of this setup within each granular test?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
我对你的具体应用了解不多,所以我无法提出任何具体的建议。也就是说,这种面向过程的测试听起来像是使用 Model- 的良好候选者基于测试技术。我熟悉 ModelJUnit 工具(公平披露:不久前,我被聘为该工具的 Java 开发人员,但是似乎有一个名为 NModel< 的等效工具/a> 适用于 C#,以及随附的书籍 C# 中基于模型的软件测试和分析。我这么说的原因是,也许您可以随机遍历整个过程,将您的设置代码全部放在一个位置,并允许抽象测试生成为您完成大部分其余的艰苦工作。
I don't know a lot about your particular application, so I can't really make any specific recommendations. That said, this kind of process-oriented testing sounds like it could be a good candidate for employing Model-based testing techniques. I'm familiar with the ModelJUnit tool (fair disclosure: I was employed as a developer of this tool some time ago) for Java, however there appears to be an equivalent tool called NModel for C#, and an accompanying book, Model-based Software Testing and Analysis in C#. The reason I say this is that perhaps you can randomise walks through the process, placing your setup code all in one place and allowing the abstract test generation to do most of the rest of the hard work for you.
就我个人而言,我会尝试测试所有代码段,但不一定将每个部分都作为自己的测试。一项测试检查具有有效帐户的发票是否通过。第二个测试检查具有无效帐户的发票是否创建新帐户。当然,我会模拟 dals,因此不会将数据添加到数据库中。这还允许模拟异常和不应执行任何操作的情况(队列中没有任何内容,或者可能没有发票。)
Personally I would try to test all pieces of code, but not necessarily each section as its own test. One test checks that an Invoice with a valid account goes through. A second test checks that an Invoice with an invalid account creates a new account. Of course I would mock out the dals so no data is added to the database. This also allows mocking exceptions and cases where no operations should be taken (nothing in the queue, or no invoices perhaps.)
我同意佩德罗的观点。您的第一个示例测试 (ImportService_ProcessQueuedData_CallsDataAccessLayer_ToLoadQueue) 是不必要的,因为其他测试将隐式测试 LoadQueuedData 是否被调用 - 如果没有,它们将没有可操作的数据。您希望对每个路径进行测试,但不需要对方法中的每一行代码进行单独的测试。如果条件分支有 6 条路径,则需要 6 次测试。
如果我想真正变得更有趣,我还可以考虑利用对象多态性来减少 if 语句的数量并简化测试。例如,您可以为每个 QueuedImportItemType 有一个不同的“处理程序”,并将处理该类型的项目的逻辑放在此处,而不是您的大 Process 方法。拆分迭代逻辑以及如何处理每种类型的项目使它们更容易单独测试。
I agree with Pedro. Your first example test (ImportService_ProcessQueuedData_CallsDataAccessLayer_ToLoadQueue) isn't necessary, because the other tests will implicitly test that LoadQueuedData was called -- if it wasn't, they would have no data to operate on. You want to have a test for every path, but you don't need a separate test for every line of code in the method. If there are six paths through the conditional branching, you want six tests.
If I wanted to get really fancy, I might also consider leveraging object polymorphism to reduce the number of if statements and simplify the tests. For example, you could have a different "handler" for every QueuedImportItemType, and put the logic for what to do with an item of that type there, instead of your big Process method. Splitting up the logic of iteration and of what to do with each type of item makes them easier to test separately.