我有以下方法,我正在寻找有效的单元测试,这也让我能够很好地覆盖代码路径:
public TheResponse DoSomething(TheRequest request)
{
if (request == null)
throw new ArgumentNullException("request");
BeginRequest(request);
try
{
var result = Service.DoTheWork(request.Data);
var response = Mapper.Map<TheResult, TheResponse>(result);
return response;
}
catch (Exception ex)
{
Logger.LogError("This method failed.", ex);
throw;
}
finally
{
EndRequest();
}
}
方法使用的Service和Logger对象被注入到类构造函数(未显示)中。 BeginRequest和EndRequest在基类(未示出)中实现。 Mapper是用于对象到对象映射的AutoMapper类。
我的问题是,为这样的方法编写单元测试的好方法是什么,它还提供了完整的(或有意义的)代码覆盖?
我是一对一测试主体的信徒,并且在VS-Test中使用Moq作为模拟框架(尽管我并没有因为这种错误而挂断了这一部分)。虽然有些测试(例如确保传递null导致异常)很明显,但我发现自己想知道其他想到的是否有意义;特别是当他们以不同的方式行使相同的代码时。
答案 0 :(得分:2)
从您的帖子/评论来看,您似乎知道您应该编写哪些测试,并且它几乎与我在第一眼看到您的代码后测试的内容相匹配。开始时很少有明显的事情:
现在,困难的部分。根据您的基类是否是您可以访问的内容(例如,可以更改它而不会有太多麻烦),您可以尝试称为 Extract&amp; amp;覆盖强>:
代码看起来或多或少是这样的:
Base
{
protected virtual void BeginRequest(TheRequest request) { ... }
protected virtual void EndRequest() { ... }
}
Derived : Base // class you want to test
{
// your regular implementation goes here
// virtual methods remain the same
}
TestableDerived : Derived // class you'll actually test
{
// here you could for example expose some properties
// determining whether Begin/EndRequest were actually called,
// calls were made in correct order and so on - whatever makes
// it easier to verify later
protected override void BeginRequest(TheRequest request) { ... }
protected override void EndRequest() { ... }
}
您可以在Art of Unit Testing一书中找到有关此技术的更多信息,以及Working Effectively with Legacy Code。即便如此,我认为可能有更优雅的解决方案,这个应该使您能够测试流程并验证DoSomething方法中交互的一般正确性。
当您无法访问基类并且无法修改其代码时,会出现问题。不幸的是,我没有这种情况的“书外”解决方案,但也许你可以在Derived中围绕Begin / EndRequest创建虚拟包装器,并且仍然使用extract&amp;覆盖TestableDerived。
答案 1 :(得分:1)
这个问题被标记为TDD,但这个问题本身就是非常后续的问题,这也是为什么你很难兼顾两者的问题的一部分。如果你做了TDD,可能会出现一种不同的,更可测试的设计。为什么BeginRequest和EndRequest必须是继承的一部分。它们可以成为注入的TransactionManager的一部分吗?
无论如何,如果严格需要将BeginRequest和EndRequest作为该类的一部分,您可以将它们设置为虚拟并使用捕获这些方法调用的sublcass进行测试。
你也可以注入某种替代的TransactionManager(假设这是有意义的)只在测试中,委托,并在真正的代码中调用自己。
但是当我到达需要这些东西的地步时,我想知道测试是否真的告诉我这些方法是否需要与这些代码分离。
答案 2 :(得分:0)
我会添加断言,即BeginRequest和EndRequest被调用,因为它们可能非常重要。除此之外,这里可能没有其他任何重要的东西。你不想测试那些不重要的东西,比如记录错误。大多数测试可能会关注服务正在做什么。
答案 3 :(得分:0)
答案 4 :(得分:0)
我认为测试
var result = Service.DoTheWork(request.Data);
对于request.Data的各种咒语是高价值部分。其余的是基础设施,我正在考虑将其抽象为一个清洁工(参见下面的非常粗略和准备方法),更可测试的风格来处理Begin + End Request的明显价值,这将单独测试。
正如你所说,没有必要测试Mapper和logger做他们的事情。
你想要的最后一件事是很多脆弱的测试,当你决定重新加工物体的内部时会导致痛苦
public TheResponse DoSomething(TheRequest request)
{
Guard.NotNull( () => request );
BeginRequest( (request) =>
{
var result = Service.DoTheWork(request.Data);
var response = Mapper.Map<TheResult, TheResponse>(result);
return response;
});
}
// This is a base class method and can be tested elsewhere
void base::BeginRequest(Action<Request> execute)
{
BeginRequest();
try { execute(request);
return Mapper.Map<TheResult, TheResponse>(result); }
catch(Exception ex) { Logger.Log(ex);
throw; }
finally { EndRequest(); }
}