TDD:测试方法实现过于接近

时间:2012-08-03 15:18:23

标签: tdd

我们正在进行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(字符串消息)方法,但这是一个证明我关注的例子

相应的测试引导我们

[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();
    }
}
哇...想象一下,我们会用另一种方式翻译我们的实体,我将不得不改变每个使用mapper服务的方法。

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

我想讨论这类问题。我错过了什么吗?我们测试的东西太多了吗?

2 个答案:

答案 0 :(得分:3)

提示:

准确指定应该发生什么,不再指定。

在你的例子中,

  1. 测试E.DoSomething要求Mapper映射string1和string2(Stub out Logger - 不相关)
  2. 测试E.DoSomething告诉Logger记录映射的字符串(Stub / Fake out Mapper返回message1和message2)
  3. Tell don't ask

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

    1. 测试E.DoSomething 告诉记录器记录string1和string2
    2. Logger的测试将确保L.Log要求mapper转换s1并记录结果

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

      忽略不相关的对象

      通过测试交互进行隔离的权衡是测试意识到实现。 诀窍是最小化这个(通过不创建接口/无条件地指定期望)。 DRY也适用于期望。最小化指定期望的位置数量...理想情况下一次。

      最小化耦合

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

答案 1 :(得分:0)

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