需要帮助确定要编写的单元测试

时间:2011-03-31 19:07:05

标签: c# unit-testing testing tdd

我有以下方法,我正在寻找有效的单元测试,这也让我能够很好地覆盖代码路径:

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导致异常)很明显,但我发现自己想知道其他想到的是否有意义;特别是当他们以不同的方式行使相同的代码时。

5 个答案:

答案 0 :(得分:2)

从您的帖子/评论来看,您似乎知道您应该编写哪些测试,并且它几乎与我在第一眼看到您的代码后测试的内容相匹配。开始时很少有明显的事情:

  • 空参数例外检查
  • 模拟服务和记录器,检查是否使用正确的数据调用
  • Stub Mapper(以及可能的Service)以检查是否实际返回了正确的结果

现在,困难的部分。根据您的基类是否是您可以访问的内容(例如,可以更改它而不会有太多麻烦),您可以尝试称为 Extract&amp; amp;覆盖

  1. 将Mark Beginquest / EndRequest标记为基类中的虚拟
  2. 在派生类中不执行任何操作
  3. 介绍从您想要测试的类派生的新的可测试类;从基类(BeginRequest / EndRequest)覆盖方法,使它们成为例如。更改一些内部值,以后可以轻松验证
  4. 代码看起来或多或少是这样的:

    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(); }
}