web-dev-qa-db-ja.com

実装/アルゴリズムが単体テストに漏れるのを防ぐ方法は?

実装とテストの間の結合を防ぐ方法に関するここでの質問のほとんどは、スパイ/スタブまたはモックの使用に関するものであるため、アルゴリズムと実装の間で躊躇しています。

典型的な問題は、テストがstateではなくbehaviourの検証に焦点を合わせているため、テストが単にSUTをミラーリングしていることです。

私が話している結合は、テストがSUTの状態を検証しているところですが、アサーションを形成するために必要なデータを収集するためにコードをミラーリングする必要があります。

たとえば、キャンバスの寸法が正しいことをテストする次のコードを見てください。

it(
    'should set the width and height of the canvas', 
    fakeAsync(
        () => {
            fixture.detectChanges();
            getTestScheduler().flush();
            tick();

            let bordersWrapperStyle = getComputedStyle(fixture.nativeElement);
            let bordersWidth = bordersWrapperStyle
                .getPropertyValue('border-width')
                .split(' ');
            let yBordersTotalWidth = parseInt(bordersWidth[0]) * 2;
            let xBordersTotalWidth = parseInt(bordersWidth[1]) * 2;

            expect(page.canvasEl.width).toEqual(
                fixture.nativeElement.offsetWidth - xBordersTotalWidth
            );

            expect(page.canvasEl.height).toEqual(
                fixture.nativeElement.offsetHeight - yBordersTotalWidth
            );
        }
    )
);

今SUT:

setCanvasRect() {
    let rootEl = this.elRef.nativeElement;
    let bordersWrapperStyle = getComputedStyle(rootEl);
    let bordersWidth = bordersWrapperStyle.getPropertyValue('border-width').split(' ');
    let yBordersTotalWidth = parseInt(bordersWidth[0]) * 2;
    let xBordersTotalWidth = parseInt(bordersWidth[1]) * 2;

    this.canvasEl.width = rootEl.offsetWidth - xBordersTotalWidth;
    this.canvasEl.height = rootEl.offsetHeight - yBordersTotalWidth;
}

たぶん答えはテストとSUTの間にこの種の結合があることが避けられない場合があります

1
maximedupre

この重複を回避する方法は、明示的に定義された値を使用してフィクスチャを作成することです。この例では、fixture.nativeElement.offsetWidthxBordersTotalWidthの値を明示的に定義してキャンバスを設定します。次に、2つの違いを事前に計算します。次に、アサーションは期待される結果が得られたかどうかを確認します。これにより、言及した重複が回避され、テストコードで誤った計算が重複するのを防ぐことができます(したがって、期待される結果を実際にテストすることはできません)。たとえば、fixture.nativeElement.offsetWidthが310で、xBordersTotalWidthが10になるようにフィクスチャを定義した場合、期待値は次のように記述されます。

expect(page.canvasEl.width).toEqual(300);
8
Dirk Herrmann

あなたの問題は、setCanvasRectが2つのことをしていることだと思います:それは両方ともプロパティを解析していますそしてキャンバスにプロパティを設定しています。解析を個別の関数/クラス/その他に分割すると、2つのことを個別にテストできます。

  1. parseBorders(element)が指定された要素の境界線を正しく返すことをテストします(おそらくこれをさらに分解して、スタイルをフェッチして解析することができます)。
  2. setCanvasRect(w, h)が高さと幅を正しく設定することをテストします。
  3. setCanvasRect(element)parseBorders(element)setCanvasRect(w, h)を正しく呼び出し、parseBorders(element)setCanvasRect(w, h)をモックアウトすることをテストします。 。
3
Philip Kendall

私が話している結合は、テストがSUTの状態を検証しているところですが、アサーションを形成するために必要なデータを収集するためにコードをミラーリングする必要があります。

TL; DR:調べたい検索語は_property based testing_です。

あなたが遭遇したのは、「例に基づく」テストの一般的な問題です。既知の入力があるため、ターゲットとする特定の出力があります。テストコードで十分な「重複の削除」リファクタリングを適用すると、チェックしようとしている実装が見つかります。

プロパティベースのテストは別の考え方です。詳細についてあまり心配することなく、回答に適切なプロパティがあることを確認するテストを進めることができますか。

Scott Wlaschin は、病的な敵にadd(x,y)の正しい実装を提供するように強制するテストを作成する試みを特徴として、私が本当に好きな紹介を書きました。

常に真実であるはずのことを掘り下げることが中心的なアイデアです。幅/高さは常に> = 0になりますか?それらは常にネイティブ要素の範囲よりも小さくなりますか?違いは常に2の倍数になりますか?ネイティブ要素の高さが幅よりも大きい場合、それは予想されるキャンバスの高さが常に予想されるキャンバスの幅よりも大きいことを意味しますか?等々。

これは、契約による設計における事後条件の処理にいくぶん類似しています。操作の結果が不変条件のコレクションを満たしていることを確認できますか?

1
VoiceOfUnreason

次のように、計算部分を純粋関数に抽出することもできます。

setCanvasRect() {
    let rootEl = this.elRef.nativeElement;
    let bordersWrapperStyle = getComputedStyle(rootEl);

    let widthAndHeight = calculateWidthAndHeight(
        bordersWrapperStyle.getPropertyValue('border-width'),
        rootEl.offsetWidth,
        rootEl.offsetHeight);

    this.canvasEl.width = widthAndHeight[0];
    this.canvasEl.height = widthAndHeight[1];
}

calculateWidthAndHeight(bordersWidth, rootOffsetWidth, rootOffsetHeight) {
    let bordersWidthElems = bordersWidth.split(' ')
    let yBordersTotalWidth = parseInt(bordersWidthElems[0]) * 2;
    let xBordersTotalWidth = parseInt(bordersWidthElems[1]) * 2;
    return [rootOffsetWidth - xBordersTotalWidth, rootOffsetHeight - yBordersTotalWidth];
}

次に、具体的な例でテストできます

expect(calculateWidthAndHeight("10 5", 100, 200).toEqual([80, 190])
1
max630