为单元测试添加代码是不是很糟糕?

时间:2010-09-02 17:24:00

标签: c# unit-testing

我正在编写一个类来帮助我对代码进行单元测试。它看起来像这样:

/// <summary>
/// Wrapper for the LogManager class to allow us to stub the logger
/// </summary>
public class Logger
{
    private static ILogger _logger = null;

    /// <summary>
    /// This should be called to get a valid logger.
    /// </summary>
    /// <returns>A valid logger to log issues to file.</returns>
    public static ILogger GetLogger()
    {
        if (_logger == null)
          _logger = LogManager.GetLogger("logger");

        return _logger;
    }

    /// <summary>
    /// This is used by unit tests to allow a stub to be used as a logger.
    /// </summary>
    /// <param name="logger"></param>
    /// <returns></returns>
    public static ILogger GetLogger(ILogger logger)
    {
        _logger = logger;
        return _logger;
    }
}

第二种方法仅用于单元测试。我从不打算在我的生产代码中调用它。

这是不好的做法吗?我应该找到另一种不这样做的方式吗?

11 个答案:

答案 0 :(得分:12)

在我看来是的,这是一种不好的做法。单元测试旨在测试代码的实现,而不是真正影响它。在某些情况下,我发现以一种方式组织我的代码/方法以使其更容易/彻底测试是切实可行的,但是在测试特定用途的类中编写代码是一个不同的故事。

答案 1 :(得分:11)

通常,代码仅用于促进单元测试hides a design flaw

在这种情况下,您隐藏的缺陷是使用global state。使用您的记录器的类是秘密依赖它的:除非您阅读它们的源代码,否则您无法告诉他们需要记录器!

这些类应该在其构造函数中需要ILogger,这使得它们显然需要工作并使它们易于测试。

答案 2 :(得分:3)

你可以考虑这个:

/// <summary>
/// Wrapper for the LogManager class to allow us to stub the logger
/// </summary>
public class Logger
{
    private static ILogger _logger = null;

    /// <summary>
    /// This should be called to get a valid logger.
    /// </summary>
    /// <returns>A valid logger to log issues to file.</returns>
    public static ILogger Logger
    {
        get
        {
            if (_logger == null)
            {
                _logger = LogManager.GetLogger("logger");
            }
            return _logger
        }
        set
        {
            _logger = value;
        }
    }
}

如果没有使用setter设置,则第一次调用getter时设置记录器。

答案 3 :(得分:2)

IMO,是的,这是糟糕的代码。有两个原因:

  • Logger了解LogManager。这将Logger与LogManager紧密结合,并呈现您遇到的问题;您必须使用LogManager来获取ILogger对象,并且不能存根或模拟LogManager。
  • 您已将代码添加到生产类中,仅用于单元测试。这会让开发人员感到困惑,他们会看到测试钩子并认为他们可以在生产代码中使用它。

我会推荐以下一项或多项:

  • 定义一个公开GetLogger()的接口ILogManager,并在LogManager上实现它。然后,在ILogManager类型的Logger上创建一个属性(或者在实例化Logger时需要传入一个属性),并从Logger范围之外(通过IoC框架,或者简单地从任何实例化Logger)向Logger提供LogManager。这是松耦合;你现在可以提供任何实现ILogManager的类,包括MockLogManager,而Logger不知道它们的区别。
  • 出于单元测试的目的,从Logger派生“代理”并在那里实现任何仅测试方法。我使用这个方法主要是为了测试一个类的受保护方法,但是如果你发现自己无法将LogManager与Logger分离,你至少可以在测试程序集中隐藏这个派生代理及其钩子,其中生产代码不应该是引用它们。

答案 4 :(得分:1)

任何时候你在文件/库中的代码/不打算在生产容量中使用的代码都只是代码膨胀。您的生产对象不应编码为支持您的单元测试,应实施单元测试以支持您的生产对象。

答案 5 :(得分:1)

是的,设计并不完美:( GetLogger方法实际上设置了记录器!使用setter方法 - 这看起来更合适。

顺便说一句,你测试什么?如果你使用.NET标准记录器 - 你可以添加或删除监听器(我记得),如果你不需要测试来编写某些东西 - 配置适当的日志监听器。

并且看过Moq图书馆? http://code.google.com/p/moq/我喜欢它嘲笑。

答案 6 :(得分:1)

我没有任何奇怪的构造函数。相反,我在视图中有一个私有变量:

private readonly ILog _log = LogManager.GetLogger(typeof(MyViewClassName));

然后可以这样使用,在这种情况下,它在catch块中被调用:

_log.Error("SomePage.aspx.cs Page_Load failed", ex);

没有log4net的包装器。 ILog是log4net的一部分。我很好奇你为什么不使用Mocks。这是有道理的,然后你不需要做你正在做的事情。

答案 7 :(得分:1)

回答一般问题“为单元测试添加代码是不是很糟糕?”,一般来说不是没有坏。可测试性是系统的一个重要特征,因为它有助于保证系统的正确性(通过实际测试),并且与其他良好的设计原则高度相关,如松耦合和高内聚。

以另一种方式看待它;如果您有两种类似的方法来构建一些代码,主要区别在于可测试性,那么请选择更可测试的选项。

但至于你的代码的具体情况,我同意这里的大多数其他帖子;有一种更好的方法来编写该代码。

答案 8 :(得分:0)

我不会说这段代码是问题。只要您在xmldoc注释中声明方法仅用于单元测试,没有(理智的!)人员会在生产代码中使用它。

尽管如此,尽量在单元测试库中保留这些内容。正如Joel Etherton所说,无论如何......这都是代码膨胀......

答案 9 :(得分:0)

这里的错误是获取记录器的代码必须知道它想要的记录器。事实上,它没有理由关心。

这就是控件反转 海报孩子的原因。最简单的方法是,在配置文件中粘贴记录器的名称,并在测试代码中使用不同的值。在更复杂的级别,使用类似MEF的内容来决定加载哪个记录器。

答案 10 :(得分:0)

不,这绝对不是件坏事。

好的代码应该满足所有要求而不做任何其他事情。

但是,需求有多种形式,所有代码的非功能性要求是您可以向其他人证明其有效。

从这个角度来看,暴露对测试日志记录的访问是一件好事。比未经测试的所有日志要好得多,即使它们的内容很重要。更重要的是,在难以在运行时以“黑匣子”方式测试的罕见情况下,日志记录往往是最重要的 - 单元测试可能是在不良情况下测试故障模式的唯一方法,例如,当内存中有重要数据时,暂时失去与持久层的连接。

那就是说 - 你需要努力调和竞争要求。 API设计中的一个重要要求是不要泄漏对您不打算使用的API部分的访问权限。实现这一目标的一种方法是将您为测试做出的API决策明显地显而易见 - 例如通过重命名方法GetLoggerForUnitTesting