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

わかりました、これは例です。ロガーの実装内にマッパーのものを含め、インターフェースに 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 をマップするように要求します (Logger をスタブ化 - 無関係)
  2. テスト E.DoSomething は Logger にマップされた文字列をログに記録するように指示します (スタブ/フェイクアウト マッパーは message1 と message2 を返します)

聞かないで教えて

これが実際の例である場合、あなた自身がほのめかしたように。ロガーは、ハッシュテーブルまたはマッパーを使用して内部的に変換を処理することを期待しています。それで、私は E.DoSomething の簡単なテストをします

  1. テスト E.DoSomethingLogger に string1 と string2 をログに記録するよう指示します

Logger のテストは、L.Log がマッパーに s1 を変換して結果をログに記録するように要求することを確認します。

共同作業者を結合することで、Ask メソッドはテストを複雑にします (Mapper に s1 と s2 を変換するように依頼します。次に、戻り値 m1 と m2 を Logger に渡します)。

無関係なオブジェクトを無視する

テストの相互作用による分離のトレードオフは、テストが実装を認識していることです。秘訣は、これを最小限に抑えることです (インターフェイスを作成しない/意のままに期待値を指定することにより)。DRYは期待にも当てはまります。期待値が指定される場所の量を最小限に抑えます...理想的には1回です。

カップリングを最小限に抑える

多くの共同作業者がいる場合、カップリングが高くなりますが、これは悪いことです。そのため、同じ抽象化レベルに属していない共同作業者を確認するために、設計をやり直す必要がある場合があります。

于 2012-08-05T08:30:57.513 に答える
0

あなたの困難は、状態ではなく行動をテストすることから来ています。ログへの呼び出しが行われたことを確認するのではなく、ログの内容を確認するようにテストを書き直しても、実装の変更によってテストが壊れることはありません。

于 2012-08-04T16:26:35.200 に答える