Go 言語: TDD テスト駆動開発を通じてモックの考え方を学ぶ

一定の学習と実践を経て、Golang 言語基盤の TDD 駆動開発テストトレーニングは終了し、次はこれをベースに TDD を使用してアプリケーションを構築していきます。詳細:クリックして入手

文章

ここで、3 から始まり下に向かって進むプログラムを作成する必要があります。0 に達すると、「GO!」と出力して終了します。各出力は新しい行から開始し、出力間に 1 秒の休止時間が必要です。

3

2

1

行く!

Countdown これは、関数を作成して main プログラムに組み込むことで処理します 。次のようになります。

package main

func main() {
    Countdown()
}

これは非常に単純なプログラムですが、完全にテストするには、通常どおり、反復的なテスト駆動のアプローチを採用する必要があります。

反復とは、ソフトウェアを使用できるようにするために最小のステップを踏むことを意味します。

攻撃されても理論的にはまだ実行できるコードの作成にあまり時間をかけたくありません。これは、開発者を開発の底なしの深淵に導くことがよくあるからです。ソフトウェアを動作させるために、要件を可能な限り細分化することは重要なスキルです

作業と反復をどのように分割するかは次のとおりです。

  •  印刷 3

  •  3 枚印刷してすぐに使えます!

  • 各行の間に 1 秒待ちます

 最初にテストを書く

ソフトウェアは結果を標準出力インターフェイスに出力する必要があります。DI (Dependency Injection) セクションでは、便利なテストに DI を使用する方法を説明しました。

func TestCountdown(t *testing.T) {
    buffer := &bytes.Buffer{}

    Countdown(buffer)

    got := buffer.String()
    want := "3"

    if got != want {
        t.Errorf("got '%s' want '%s'", got, want)
    }
}

よくわからない場合は buffer 、前のセクションをもう一度読んでください。

私たちの目的は、 Countdown 関数にデータをどこかに書き込むことであることはわかっていますが、io.writer就是作为 Go 的一个接口来抓取数据的一种方式。

  • では main 、情報を に送信し os.Stdout、ユーザーは Countdown 端末に出力された結果を確認できます。

  •  テストでは、 に送信する bytes.Bufferので、テストは生成されるデータを取得できます。

 テストを実行してみる

./countdown_test.go:11:2: 未定義: カウントダウン

テストを実行するための最小限のコードを作成し、失敗したテストの出力を検査します。

Countdown 関数を定義する 

func Countdown() {}

もう一度実行してみてください

./countdown_test.go:11:11: Countdown の呼び出しの引数が多すぎます

have (*bytes.Buffer)

欲しい () ()

コンパイラは関数の問題を通知しているので、それを修正してください

func Countdown(out *bytes.Buffer) {}

countdown_test.go:17: 「3」が欲しい

結果は完璧です!

プログラムを通過させるのに十分なコードを作成する

func Countdown(out *bytes.Buffer) {
    fmt.Fprint(out, "3")
}

fmt.Fprint pass in one  io.Writer(たとえば *bytes.Buffer) and send one を 使用しています stringこのテストはパスするはずです。

コードを再構築する

それが機能することは誰もが知っていますが *bytes.Buffer 、代わりに汎用インターフェイスを使用することをお勧めします。

func Countdown(out io.Writer) {
    fmt.Fprint(out, "3")
}

テストを再実行すると、テストは成功するはずです。

このタスクを達成するために、関数を次のオブジェクトに適用しましょう。 main中。这样的话,我们就有了一些可工作的软件来确保我们的工作正在取得进展。

package main

import (
    "fmt"
    "io"
    "os"
)

func Countdown(out io.Writer) {
    fmt.Fprint(out, "3")
}

func main() {
    Countdown(os.Stdout)
}

プログラムを実行してみると、その結果に驚かれるでしょう。

もちろん、これでも単純に見えますが、どのプロジェクトにもこのアプローチをお勧めします。

テストのサポートにより、機能は小さな機能ポイントに分割され、エンドツーエンドで接続されてスムーズに実行されます。

次に、2、1 を出力してから、「Go!」を出力させます。

最初にテストを書く

時間をかけてプロセス全体を正しく行うことで、ソリューションを安全かつ簡単に繰り返すことができます。プログラムを停止して再実行する必要はなくなり、すべてのロジックがテストされているため、プログラムが機能することを確信できます。

func TestCountdown(t *testing.T) {
    buffer := &bytes.Buffer{}

    Countdown(buffer)

    got := buffer.String()
    want := `3
2
1
Go!`

    if got != want {
        t.Errorf("got '%s' want '%s'", got, want)
    }
}

バックティック構文も string 作成方法の 1 つですが、新しい行などに内容を配置することができ、テストには最適でした。

テストを実行してみる

countdown_test.go:21: '3' を獲得しました '3' が欲しいです

2

1

行く!'

 テストに合格するのに十分なコードを作成する

テストに合格するのに十分なコードを作成する

func Countdown(out io.Writer) {
    for i := 3; i > 0; i-- {
        fmt.Fprintln(out, i)
    }
    fmt.Fprint(out, "Go!")
}

for ループを 使用し てi-- 逆方向にカウントし、 fmt.println 数値を に出力し out、その後に改行を続けます。最後に fmt.Fprint 「Go!」を送信します。

コードを再構築する

ここではリファクタリングするものは何もありません。変数を名前付き定数にリファクタリングするだけです。

const finalWord = "Go!"
const countdownStart = 3

func Countdown(out io.Writer) {
    for i := countdownStart; i > 0; i-- {
        fmt.Fprintln(out, i)
    }
    fmt.Fprint(out, finalWord)
}

ここでプログラムを実行すると、目的の出力が得られるはずですが、出力のカウントダウンのための 1 秒の一時停止はありません。

Go は time.Sleep を通じてこの関数を実装できます。これをコードに追加してみてください。

func Countdown(out io.Writer) {
    for i := countdownStart; i > 0; i-- {
        time.Sleep(1 * time.Second)
        fmt.Fprintln(out, i)
    }

    time.Sleep(1 * time.Second)
    fmt.Fprint(out, finalWord)
}

プログラムを実行すると、期待どおりに動作します。

嘲笑する

テストに合格し、ソフトウェアは期待どおりに動作します。しかし、いくつかの問題があります。

  • テストの実行には 4 秒かかりました

  •  ソフトウェア開発に関するあらゆる最先端の考え方では、迅速なフィードバック ループの重要性が強調されています。

  •  テストが遅いと開発者の生産性が損なわれます。

  •  要件がより複雑になった場合、より多くのテストが必要になることを想像してください。新しいテストごとに Countdown テスト実行に 4 秒追加されても満足でしょうか?

  •  この関数にはまだテストしていない重要な特性があります。

Sleepテストで制御する前に抽出する必要がある ing の注射があります 。

モック できる場合は 、依存関係注入を使用して 「実際の」ものを置き換えるtime.Sleepことができ 、アサーションを使用して 呼び出しを監視できます。time.Sleep

 最初にテストを書く

依存関係をインターフェースとして定義しましょう。このようにして、 main本物のもの を使用し 、テストで スパイスリーパーを使用することができます。インターフェイスを使用することで、  関数はこれを無視し、呼び出し元に柔軟性を与えます。 SleeperCountdown

type Sleeper interface {
    Sleep()
}

 私は、私たちの関数が時間の長さにCountdown 関与しない という設計上の決定を下しました 。sleepこれにより、少なくともコードが簡素化され、関数のユーザーが好みに応じてスリープ期間を構成できるようになります。

次に、使用するテスト用に モックを生成する必要があります。

type SpySleeper struct {
    Calls int
}

func (s *SpySleeper) Sleep() {
    s.Calls++
}

スパイは、依存関係がどのように使用されるかを記録するモックです 渡されたパラメータや回数などを記録できます。Sleep() 私たちの場合は、テストで確認できるように、呼び出された回数を追跡します 。

テストを更新してモニターに依存関係を挿入し、 sleepそれが 4 回呼び出されることをアサートします。

func TestCountdown(t *testing.T) {
    buffer := &bytes.Buffer{}
    spySleeper := &SpySleeper{}

    Countdown(buffer, spySleeper)

    got := buffer.String()
    want := `3
2
1
Go!`

    if got != want {
        t.Errorf("got '%s' want '%s'", got, want)
    }

    if spySleeper.Calls != 4 {
        t.Errorf("not enough calls to sleeper, want 4 got %d", spySleeper.Calls)
    }
}

テストを実行してみる

Countdown の呼び出しに引数が多すぎます

have (*bytes.Buffer, Sleeper)

欲しい (io.Writer)

 テストを実行するための最小限のコードを作成し、失敗したテストの出力を検査します。

Countdow 私たちのものを受け入れるには 更新が必要です Sleeper

func Countdown(out io.Writer, sleeper Sleeper) {
    for i := countdownStart; i > 0; i-- {
        time.Sleep(1 * time.Second)
        fmt.Fprintln(out, i)
    }

    time.Sleep(1 * time.Second)
    fmt.Fprint(out, finalWord)
}

再試行しても、 main 同じコンパイル エラーは発生しません。

./main.go:26:11: Countdown の呼び出しに十分な引数がありません

(*os.ファイル) がある

欲しい (io.Writer、Sleeper)

 必要なインターフェイスを実装する実際のスリーパーを作成しましょう 

type ConfigurableSleeper struct {
    duration time.Duration
}

func (o *ConfigurableSleeper) Sleep() {
    time.Sleep(o.duration)
}

私はさらに一歩進んで、これを真に構成可能なスリーパーにすることにしました。ただし、1秒で簡単に書くこともできます。

次のように実際のアプリケーションで使用できます。

func main() {
    sleeper := &ConfigurableSleeper{1 * time.Second}
    Countdown(os.Stdout, sleeper)
}

テストに合格するのに十分なコード

現在、テストはコンパイルされていますが、 time.Sleep 依存関係注入の代わりに呼び出しを行っているため、合格しません。この問題を解決しましょう。

func Countdown(out io.Writer, sleeper Sleeper) {
    for i := countdownStart; i > 0; i-- {
        sleeper.Sleep()
        fmt.Fprintln(out, i)
    }

    sleeper.Sleep()
    fmt.Fprint(out, finalWord)
}

テストは成功し、4 秒もかかりません。

まだいくつか問題があります

まだテストしていない重要な機能が 1 つあります。

Countdown

 最初の印刷の前にスリープし、その後最後の印刷までスリープする必要があります。例:

  •  Sleep

  •  Print N

  •  Sleep

  •  Print N-1

  •  Sleep

私たちの最新の修正では、それが sleep 4 回発生したとのみ主張していますが、それらは sleeps 順序どおりに発生したわけではない可能性があります。

 テストを書くときに自信がなかったとしても、テストはそれを覆すほどの自信を与えてくれます。(ただし、最初に必ず変更をソース管理にコミットしてください)。コードを次のように変更します。

func Countdown(out io.Writer, sleeper Sleeper) {
    for i := countdownStart; i > 0; i-- {
        sleeper.Sleep()
    }

    for i := countdownStart; i > 0; i-- {
        fmt.Fprintln(out, i)
    }

    sleeper.Sleep()
    fmt.Fprint(out, finalWord)
}

テストを実行すると、たとえ実装が間違っていたとしても、テストは合格するはずです。

新しいテストを使用して、操作の順序が正しいことを確認してみましょう。

2 つの異なる依存関係があり、それらのすべての操作をリストに記録したいと考えています。したがって、両方に同じモニターを作成します 

type CountdownOperationsSpy struct {
    Calls []string
}

func (s *CountdownOperationsSpy) Sleep() {
    s.Calls = append(s.Calls, sleep)
}

func (s *CountdownOperationsSpy) Write(p []byte) (n int, err error) {
    s.Calls = append(s.Calls, write)
    return
}

const write = "write"
const sleep = "sleep"

私たちのものは  と の CountdownOperationsSpy 両方を実装し 、 へのすべての呼び出しを記録します このテストでは、操作の順序のみを考慮するため、操作の代名詞のリストを記録するだけで十分です。io.writerSleeperslice

 これで、テスト スイートにサブテストを追加できるようになりました。

t.Run("sleep after every print", func(t *testing.T) {
    spySleepPrinter := &CountdownOperationsSpy{}
    Countdown(spySleepPrinter, spySleepPrinter)

    want := []string{
        sleep,
        write,
        sleep,
        write,
        sleep,
        write,
        sleep,
        write,
    }

    if !reflect.DeepEqual(want, spySleepPrinter.Calls) {
        t.Errorf("wanted calls %v got %v", want, spySleepPrinter.Calls)
    }
})

これでテストは失敗するはずです。新しいテストを復元すると、再び合格するはずです。

サーバー上 Sleeper に 2 つのテスト モニターがあるので、テストをリファクタリングして、1 つは印刷される内容をテストし、もう 1 つはいつ印刷されるかを確認できるようになりました。 sleep。最后我们可以删除第一个监视器,因为它已经不需要了。

func TestCountdown(t *testing.T) {

    t.Run("prints 3 to Go!", func(t *testing.T) {
        buffer := &bytes.Buffer{}
        Countdown(buffer, &CountdownOperationsSpy{})

        got := buffer.String()
        want := `3
2
1
Go!`

        if got != want {
            t.Errorf("got '%s' want '%s'", got, want)
        }
    })

    t.Run("sleep after every print", func(t *testing.T) {
        spySleepPrinter := &CountdownOperationsSpy{}
        Countdown(spySleepPrinter, spySleepPrinter)

        want := []string{
            sleep,
            write,
            sleep,
            write,
            sleep,
            write,
            sleep,
            write,
        }

        if !reflect.DeepEqual(want, spySleepPrinter.Calls) {
            t.Errorf("wanted calls %v got %v", want, spySleepPrinter.Calls)
        }
    })
}

これで関数が完成し、その 2 つの重要なプロパティが適切なテストに合格しました。

 嘲笑は悪ではないのか?

嘲笑は悪だということを聞いたことがあるかもしれません。ソフトウェア開発のあらゆるものと同様、DRY (繰り返さないでください) のような悪に利用される可能性があります。

人々がテストに耳を傾けず 、 リファクタリング段階を尊重しない場合 、通常は悪い状況に陥ります。

モック コードが複雑になったり、何かをテストするために多くのものをモックする必要がある場合は、 その不快な感情に耳を傾け 、コードについて考えるべきです。通常、これは次のような兆候です。

  • 実行中のテストでは実行する必要があることが多すぎます

  •  モジュールを分離するとテスト内容が減ります

  •  依存関係が詳細すぎる

  •  これらの依存関係を意味のあるモジュールに結合する方法を検討します。

  • テストが実装の詳細に焦点を当てすぎている

  •  機能の実装ではなく、予想される動作をテストする方が良い

多くの場合、コードには間違った抽象化を指すモックが大量に含まれています 

ここで人々が目にしているのは、テスト駆動開発の弱点ですが、実際には長所でもあります。多くの場合、テストが不十分なコードは設計が不十分な結果であるのに対し、適切に設計されたコードはテストが容易です。

しかし、モックとテストは依然として私を苦戦させます。

このような状況に遭遇したことがありますか?

  •  リファクタリングをしたい

  •  これを行うには、多くのテストを変更することになります

  • あなたはテスト駆動開発に疑問を抱き、「嘲笑は有害である」というタイトルの記事をメディアに掲載しました。

これは通常、実装の詳細をテストしすぎていることを示しています  。システムの実行にとって実装が非常に重要でない限り、テストで有用な動作がテストされるように、この問題を克服するように努めてください 。

どのレベルまでテストすれば よいのかを正確に知るのは難しい場合もありますが、私が従うようにしている思考プロセスとルールをいくつか紹介します。

  • リファクタリングの定義は、コードを変更しても動作は変わらないことです。 理論的にリファクタリングを行うことに決めた場合は、テストを変更せずにコミットできるはずです。したがって、テストを作成するときは、次のことを自問してください。

  •  必要な動作をテストしているのか、それとも実装の詳細をテストしているのか?

  •  このコードをリファクタリングする場合、テストに多くの変更を加える必要がありますか?

  •  Go ではプライベート関数をテストできますが、プライベート関数は実装に依存するため、これは避けたいと思います。 

  • テストに 3 つを超えるモックが含まれている場合、それは警告であり 、設計を再考する時期が来ているように感じます。

  •  モニターは注意して使用してください。モニターを使用すると、作成中のアルゴリズムの内部詳細を確認でき、非常に便利ですが、テスト コードと実装の間により密接な関係があることを意味します。これらの詳細を監視する場合は、それらを本当に気にするようにしてください。

いつものことですが、ソフトウェア開発におけるルールは実際にはルールではなく、例外があります。Uncle Bob の記事 「いつ嘲笑するか」 には、いくつかの優れたガイドラインが記載されています。

 要約する

テスト駆動開発の詳細:

  •  それほど単純ではない例に直面した場合は、問題を「単純なモジュール」に分割します。ウサギの穴に落ちたり、「ビッグバン」アプローチを取ったりすることを避けるために、できるだけ早くテストして、動作するソフトウェアのサポートを取得するようにしてください。

  •  いくつかの動作するソフトウェアを作成したら、小さなステップで繰り返します

モッキング: 開発者がモックを学習すると、システムのあらゆる側面を、その動作 ではなく 動作 に対して過剰テストするのが簡単になります テストの価値と、テストが将来のリファクタリングにどのような影響を与えるかを常に意識してください モックに関するこの記事では、 モックの一種であるスパイについてのみ言及しました。モックにもさまざまな種類があります。これらのタイプについては、Uncle Bob による非常に読みやすい記事で説明されています。後の章では、他のデータに依存するコードを記述する必要があり、そのときのスタブの動作を示します。 

記事の転載元:slowdance2me

元のリンク: https://www.cnblogs.com/slowlydance2me/p/17261292.html

おすすめ

転載: blog.csdn.net/sdgfafg_25/article/details/131592764