2

我有一个用于获取一些数据的 MVC 控制器的接口。为了简单起见,到目前为止的界面看起来像这样:

public interface IDataProvider
{
    DataModel GetData();
}

我对此接口进行了适当的单元测试,该接口在操作中被调用。但是,在实际实现中,这将调用一个 Web 服务,当然这可能会引发异常,因此,如果它确实发生了,我想编写一个测试以确保在发生错误时记录一条消息。

为此,我有一个记录器接口,它实际上是一个名为 ILogger 的 NLog 接口。我可以这样做:

public interface IDataProvider
{
    DataModel GetData(ILogger logger);
}

这将允许我为记录器运行单元测试,使其变得简单易用。但是,我认为这不是正确的方法,因为记录器实际上与此方法无关。此外,如果我开始向此接口添加其他需要记录的方法,那么我也必须将记录器包含在所有这些方法的参数中。

我现在能想到的最好方法是将记录器包含在我的实现的构造函数中,它可能看起来像这样:

public class DataProvider : IDataProvider
{
    private readonly ILogger _logger;

    public DataProvider(ILogger logger)
    {
        _logger = logger;
    }

    public DataModel GetData()
    {
        // CODE GOES HERE
    }
}

但是,这意味着我无法在单元测试中测试记录器。实现此目的的最佳方法是什么,以便我可以将记录器与方法分开并使其可测试?

我会很感激任何帮助,谢谢。

编辑:

我意识到我错过了单元测试代码,这就是我的意思:

目前,我确保在我的操作中以这种方式调用 GetData:

var controller = new DataController(_dataProvider.Object);

controller.Index();

_dataProvider.Verify(dataProvider => dataProvider.GetData());

我想做的是相同的,但对于记录器,但前提是抛出这样的异常:

_dataProvider.Setup(dataProvider => dataProvider.GetData()).Throws<WebException>();

var controller = new DataController(_dataProvider.Object);

controller.Index();

_logger.Verify(logger => logger.ErrorException(It.IsAny<string>(), It.IsAny<Exception>());

显然记录器将在设置中提供给数据提供者。我希望这更有意义。

4

4 回答 4

6

您可以尝试使用工厂模式。

这里发生的是在您的生产代码中,您从Factory获取记录器。在这个工厂中,它要么返回你的真实记录器,要么返回一个在你的单元测试中设置的假记录器。对于您的生产代码,它没有任何区别。

在您的单元测试中,您使用的是使用Moq创建的假记录器。在这种情况下,这个假使您可以测试是否调用了接口方法ILogger.Log()。这是通过使用.Verify方法完成的。

尝试这样的事情:

ILogger.cs

public interface ILogger
{
    void Log(string message);
}

记录器工厂.cs

public static class LoggerFactory
{
    public static ILogger Logger
    {
        get
        {
            return LoggerFactory._logger == null ? new Logger() : LoggerFactory._logger;
        }
        set
        {
            LoggerFactory._logger = value;
        }
    }
    private static ILogger _logger = null;
}

数据提供者.cs

public void GetData()
{
    var logger = LoggerFactory.Logger;

    logger.Log("..."); // etc...
}

单元测试.cs

private void Mock<ILogger> _mockLogger = null;

public void Load()
{
    this._mockLogger = new Mock<ILogger>();

    LoggerFactory.Logger = _mockLogger.Object;
}

public void UnitTest()
{
    // test as required

    this._mockLogger.Verify(m => m.Log(It.IsAny<string>()));
}
于 2013-03-27T16:14:11.317 回答
1

如果您需要测试记录器是否被调用,我建议使用称为“间谍”的测试替身。这不会进行任何日志记录,但会跟踪调用了哪些方法(如果有)。然后您可以验证记录器是否在特定实例中被调用。

您可以通过使用模拟框架为您创建双重(或模拟)来做到这一点。或者您可以ILogger自己创建实现。例如:

class LoggerSpy : ILogger
{
    public string LogWasCalled;

    public void Log(string message)
    {
        LogWasCalled = true;;
    }
}

以下似乎有一个使用 Moq 模拟 ILogger 的示例:How to Mock ILogger / ILoggerService using Moq

于 2013-03-27T16:11:04.603 回答
1

使用模拟框架(例如 Moq 或 RhinoMocks)来验证记录器是否被调用。然后,您发布的最终代码块(通过构造函数传入记录器)将起作用。

于 2013-03-27T16:11:08.283 回答
1

在构造函数中传递记录器(或任何其他依赖项)是非常标准的做法,并允许您在需要时使用依赖注入框架。

我不确定为什么您将在构造函数中传递记录器视为单元测试的限制:您有 3 个可以单独测试的组件

  • 控制器(取决于提供的数据,模拟此依赖项进行测试),
  • 数据提供者(取决于日志记录和其他一些让您调用 Web 服务的类 - 模拟所有依赖项,以便您知道何时调用日志记录并且无需调用 Web 服务)
  • 日志记录 - 不确定它依赖于什么,但应该可以单独测试。

笔记:

  • 使用模拟框架(即moq)进行测试 - 您将能够非常轻松地提供接口的任何实现(包括异常)。
  • 看看依赖注入框架(即Unity)是否适合您。MVC4 非常适合它。
于 2013-03-27T16:17:46.360 回答