3

我们做 TDD 已经有一段时间了,我们在重构时遇到了一些问题。由于我们试图尽可能多地尊重 SRP(单一责任原则),我们创建了许多组合,我们的类使用这些组合来处理常见责任(例如验证、日志记录等)。让我们举一个非常简单的例子:

public class Executioner
{
    public ILogger Logger { get; set; }
    public void DoSomething()
    {
        Logger.DoLog("Starting doing something");
        Thread.Sleep(1000);
        Logger.DoLog("Something was done!");
    }
}

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

当我们使用模拟框架时,我们会针对这种情况进行的测试类似于

[TestClass]
public class ExecutionerTests
{
    [TestMethod]
    public void Test_DoSomething()
    {
        var objectUnderTests = new Executioner();

        #region Mock setup

        var loggerMock = new Mock<ILogger>(MockBehavior.Strict);
        loggerMock.Setup(l => l.DoLog("Starting doing something"));
        loggerMock.Setup(l => l.DoLog("Something was done!"));

        objectUnderTests.Logger = loggerMock.Object;

        #endregion

        objectUnderTests.DoSomething();

        loggerMock.VerifyAll();
    }
}

如您所见,测试清楚地知道我们正在测试的方法实现。我不得不承认这个例子太简单了,但我们有时会有一些包含职责的组合,这些职责不会为测试增加任何价值。

让我们为这个例子增加一些复杂性

public interface ILogger
{
    void DoLog(LoggingMessage message);
}

public interface IMapper
{
    TTarget DoMap<TSource, TTarget>(TSource source);
}

public class LoggingMessage
{
    public string Message { get; set; }
}

public class Executioner
{
    public ILogger Logger { get; set; }
    public IMapper Mapper { get; set; }
    public void DoSomething()
    {
        DoLog("Starting doing something");

        Thread.Sleep(1000);

        DoLog("Something was done!");
    }

    private void DoLog(string message)
    {
        var startMessage = Mapper.DoMap<string, LoggingMessage>(message);
        Logger.DoLog(startMessage);
    }
}

好的,这是一个例子。我会在我的 Logger 的实现中包含 Mapper 的东西,并在我的界面中保留一个 DoLog(string message) 方法,但这是一个证明我的担忧的例子

相应的测试引导我们

[TestClass]
public class ExecutionerTests
{
    [TestMethod]
    public void Test_DoSomething()
    {
        var objectUnderTests = new Executioner();

        #region Mock setup

        var loggerMock = new Mock<ILogger>(MockBehavior.Strict);
        var mapperMock = new Mock<IMapper>(MockBehavior.Strict);
        var mockedMessage = new LoggingMessage();

        mapperMock.Setup(m => m.DoMap<string, LoggingMessage>("Starting doing something")).Returns(mockedMessage);
        mapperMock.Setup(m => m.DoMap<string, LoggingMessage>("Something was done!")).Returns(mockedMessage);

        loggerMock.Setup(l => l.DoLog(mockedMessage));

        objectUnderTests.Logger = loggerMock.Object;
        objectUnderTests.Mapper = mapperMock.Object;

        #endregion

        objectUnderTests.DoSomething();

        mapperMock.VerifyAll();
        loggerMock.Verify(l => l.DoLog(mockedMessage), Times.Exactly(2));
        loggerMock.VerifyAll();
    }
}

哇...假设我们将使用另一种方式来翻译我们的实体,我将不得不更改每个具有使用映射器服务的方法的测试。

无论如何,当我们进行重大重构时,我们真的会感到有些痛苦,因为我们需要更改一堆测试。

我很乐意讨论这类问题。我错过了什么吗?我们是否测试了太多东西?

4

2 回答 2

3

尖端:

确切地指定应该发生的事情,仅此而已。

在你捏造的例子中,

  1. 测试 E.DoSomething 要求 Mapper 映射 string1 和 string2(存根记录器 - 无关)
  2. 测试 E.DoSomething 告诉 Logger 记录映射的字符串(Stub/Fake out Mapper 以返回 message1 和 message2)

告诉不要问

就像你自己暗示的那样,如果这是一个真实的例子。我希望 Logger 通过哈希表或使用 Mapper 在内部处理翻译。那么我会对 E.DoSomething 进行一个简单的测试

  1. 测试 E.DoSomething告诉Logger 记录 string1 和 string2

Logger 的测试将确保 L.Log 要求 mapper 翻译 s1 并记录结果

Ask 方法通过耦合协作者使测试复杂化(让 Mapper 翻译 s1 和 s2。然后将返回值 m1 和 m2 传递给 Logger)。

忽略不相关的对象

通过测试交互进行隔离的权衡是测试知道实现。诀窍是尽量减少这一点(通过不创建接口/指定期望值)。DRY 也适用于期望。最小化指定期望的位置数量……理想情况下是一次。

最小化耦合

如果有很多合作者,耦合度很高,这是一件坏事。因此,您可能需要重新设计您的设计,以查看哪些协作者不属于同一抽象级别

于 2012-08-05T08:30:57.513 回答
0

你的困难来自测试行为而不是状态。如果您要重写测试以便查看日志中的内容而不是验证是否对日志进行了调用,那么您的测试不会因实现的更改而中断。

于 2012-08-04T16:26:35.200 回答