web-dev-qa-db-ja.com

テスト可能なコードはより良いコードですか?

私は自分のコードで定期的に単体テストを書く習慣をつけようとしていますが、それを読みました最初に書くことが重要ですテスト可能なコードこの質問 テスト可能なコードの作成の原則SOLIDの原則に触れますが、テストの作成を計画せずに、これらの設計原則が有益であるか(または少なくとも有害ではないか)知りたいです明確にするために-私はテストを書くことの重要性を理解しています;これはそれらの有用性に関する問題ではありません。

私の混乱を説明するために、この質問に影響を与えた piece で、ライターは現在の時刻をチェックし、時刻に応じていくつかの値を返す関数の例を示します。作成者は、内部で使用するデータ(時間)を生成し、テストを困難にするため、これを悪いコードとして指摘します。しかし、私にとっては、時間を引数として渡すのはやり過ぎのようです。ある時点で値を初期化する必要があります。なぜ消費に最も近くないのでしょうか。加えて、私の心のメソッドの目的は、現在の時間に基づいていくつかの値を返すことです。これをパラメータとして、この目的を変更できる/すべきであることを示します。これと他の質問は、テスト可能なコードが「より良い」コードと同義であるかどうか疑問に思います。

テストが不可能な場合でも、テスト可能なコードの記述は良い習慣ですか?


テスト可能なコードは実際により安定していますか? は重複として提案されています。ただし、その質問はコードの「安定性」に関するものですが、読みやすさ、パフォーマンス、カップリングなど、他の理由でもコードが優れているかどうかについて、もっと広く質問しています。

103
WannabeCoder

単体テストの一般的な定義に関しては、私はノーだと思います。テストフレームワークに適合するようにコードをひねる必要があるため、複雑なコードが複雑になっているのを見てきました(たとえば、インターフェイスと IoC すべての場所で、インターフェイス呼び出しとデータのレイヤーをたどるのが難しいため、わかりにくいはずです魔法で渡された)。理解しやすいコードとユニットテストが簡単なコードのどちらかを選択できるので、毎回保守可能なコードを使用します。

これはテストすることを意味するのではなく、ツールをあなたに合うように適合させることを意味します。テストする方法は他にもあります(ただし、理解しにくいコードは常に悪いコードです)。たとえば、より細かいユニットテストを作成できます(例: Martin Fowler ユニットは通常、メソッドではなくクラスであるという態度)、または自動統合テストでプログラムを実行できます代わりに。テストフレームワークが緑色の目盛りで点灯するほどきれいではないかもしれませんが、プロセスのゲーミフィケーションではなく、テスト済みのコードを使用しています。

それらの間の適切なインターフェースを定義し、コンポーネントのパブリックインターフェースを実行するテストを作成することにより、コードを維持しやすく、ユニットテストに適したものにすることができます。または、より優れたテストフレームワークを取得することもできます(モックを配置してコードをコンパイルする必要がなく、実行時に関数を置き換えてモック化します)。より優れた単体テストフレームワークを使用すると、実行時にシステムのGetCurrentTime()機能を独自のフレームワークに置き換えることができるため、テストツールに合わせるためだけにこれに人工的なラッパーを導入する必要はありません。

118
gbjbaanb

テストが不可能な場合でも、テスト可能なコードを書くことは依然として良い習慣ですか?

まず最初に、テストがないことは、コードがテスト可能かどうかよりもはるかに大きな問題です。単体テストがないことは、コード/機能が完了していないことを意味します。

それが邪魔になっても、テスト可能なコードを書くことが重要だとは言いません。flexibleコードを書くことが重要です。柔軟性のないコードはテストするのが難しいため、アプローチとそれを人々が呼ぶものには多くの重複があります。

だから私にとって、コードを書く際には常に優先事項のセットがあります:

  1. 機能させる-コードが必要なことを行わない場合、それは価値がありません。
  2. メンテナンス可能にする-コードがメンテナンス可能でない場合、すぐに動作を停止します。
  3. 柔軟にする-コードが柔軟でない場合、ビジネスが必然的にやって来てコードがXYZを実行できるかどうかを尋ねると、コードは機能を停止します。
  4. Make it fast-ベースの許容レベルを超えて、パフォーマンスはただのグレイビーです。

ユニットテストはコードの保守性を助けますが、それはある程度のことです。コードを読みにくくしたり、単体テストを機能させるために脆弱にしたりすると、生産性が低下します。 「テスト可能なコード」は一般に柔軟なコードであるため、機能性や保守性ほど重要ではありませんが、それで十分です。現在のような状況では、柔軟にすることは良いことですが、コードを正しく使いにくく複雑にすることにより、保守性を損なうことになります。保守性の方が重要であるため、テストしにくい場合でも、通常はより単純なアプローチに誤りを犯します。

69
Telastyn

はい、それは良い習慣です。理由は、テスト容易性はテストのためではないからです。それがもたらすのは、明確さと理解しやすさのためです。

テスト自体は誰も気にしません。絶えず足元をチェックせずに完璧なコードを書くには十分な才能がないため、大きな回帰テストスイートが必要になるのは悲しい事実です。できれば、テストの概念は不明であり、これらすべてが問題になることはありません。できればいいのに。しかし、経験上、ほとんどすべての人ができないことを示しています。したがって、ビジネスコードの作成に時間を費やしても、コードをカバーするテストは良いことです。

テストを行うことで、テストとは関係なくビジネスコードをどのように改善できますか機能が正しいことが簡単に示されるユニットに分割するように強制する。これらのユニットは、他の方法で作成したくなったユニットよりも、正しく理解するのが簡単です。

あなたの時間の例は良い点です。あなたがonlyに現在の時間を返す関数がある限り、それをプログラム可能にすることに意味がないと考えるかもしれません。これを正しく行うのはどれほど難しいでしょうか?しかし、必然的にあなたのプログラムは他のコードでこの関数をuseし、あなたは間違いなくテストしたいそれ異なる時間を含む、異なる条件下でのコード。したがって、関数が戻る時間を操作できるようにするのは良い考えです。1行のcurrentMillis()呼び出しを信用していないからではなく、を確認する必要があるためです。制御された状況下でのその呼び出しの呼び出し元。つまり、コードをテスト可能にしておくことは、それだけでは役に立ちますが、それほど注目に値するものではないようです。

51
Kilian Foth

ある時点で値を初期化する必要があります。なぜ消費に最も近くないのでしょうか。

内部で生成されたものとは異なる値で、そのコードを再利用する必要がある場合があるためです。パラメーターとして使用する値を挿入する機能により、「今」だけでなく(コードを呼び出すときの「今」という意味で)、いつでも好きなときにそれらの値を生成できます。

コードを実際にテスト可能にすることは、(最初​​から)2つの異なるシナリオ(運用とテスト)で使用できるコードを作成することを意味します。

基本的に、テストがない場合にコードをテスト可能にするインセンティブはないと主張することはできますが、再利用可能なコードを作成することには大きな利点があり、2つは同義語です。

さらに、私の考えでは、このメソッドの目的は、現在の時刻に基づいて値を返すことです。これをパラメーターとして、この目的を変更できるか、または変更する必要があることを示します。

また、このメソッドの目的は時間値に基づいて値を返すことであり、「今」に基づいてそれを生成する必要があると主張することもできます。それらの1つはより柔軟であり、そのバリアントの選択に慣れれば、やがてコードの再利用率が上がります。

12
utnapistim

このように言うのはばかげているように見えるかもしれませんが、コードをテストできるようにしたい場合は、そうです、テスト可能なコードを書く方が良いです。あなたが尋ねる:

ある時点で値を初期化する必要があります。なぜ消費に最も近くないのでしょうか。

なぜなら、あなたが参照している例では、そのコードをテストできなくなるからです。 1日の異なる時間にテストのサブセットのみを実行する場合を除きます。または、システムクロックをリセットします。または他の回避策。これらすべては、単にコードを柔軟にするよりも悪いものです。

柔軟性がないことに加えて、問題のこの小さなメソッドには2つの責任があります。(1)システム時間を取得し、(2)それに基づいて値を返すことです。

public static string GetTimeOfDay()
{
    DateTime time = DateTime.Now;
    if (time.Hour >= 0 && time.Hour < 6)
    {
        return "Night";
    }
    if (time.Hour >= 6 && time.Hour < 12)
    {
        return "Morning";
    }
    if (time.Hour >= 12 && time.Hour < 18)
    {
        return "Afternoon";
    }
    return "Evening";
}

責任をさらに分解することは理にかなっているので、あなたの制御不能な部分(DateTime.Now)は、残りのコードへの影響が最小です。そうすることで、上記のコードがよりシンプルになり、体系的にテストできるという副作用があります。

10
Eric King

それは確かにコストがかかりますが、一部の開発者はそれを支払うことに慣れているため、コストがそこにあることを忘れていました。あなたの例では、1つではなく2つのユニットがあり、追加の依存関係を初期化して管理するために呼び出しコードが必要であり、GetTimeOfDayの方がテストしやすい一方で、同じボートに戻って新しいIDateTimeProvider。良いテストがあれば、利益は通常コストを上回るだけです。

また、ある程度、テスト可能なコードを作成することで、より保守しやすい方法でコードを設計することができます。新しい依存関係管理コードは煩わしいので、可能であれば、すべての時間依存関数をグループ化する必要があります。これは、たとえば、ページを時間境界に直接ロードする場合など、いくつかの要素を前の時刻を使用してレンダリングし、一部の要素を後の時刻を使用してレンダリングする場合などのバグを軽減および修正するのに役立ちます。また、現在の時刻を取得するために繰り返されるシステムコールを回避することで、プログラムを高速化できます。

もちろん、これらのアーキテクチャの改善は、誰かがその機会に気づき、それを実装することに大きく依存しています。ユニットに集中することの最大の危険の1つは、全体像を見失うことです。

多くの単体テストフレームワークを使用すると、実行時にモックオブジェクトにモンキーパッチを適用できます。私はそれがC++で行われるのを見たことさえある。テスト可能性のコストが価値がないように見える状況で、その能力を調べてください。

9
Karl Bielefeldt

テスト可能性に貢献するすべての特性がテスト容易性のコンテキスト以外で望ましいとは限らない可能性があります-たとえば、引用する時間パラメーターについてテスト関連ではない正当化を思い付くのに苦労しますが、広く言えば、テスト容易性に貢献する特性テスト可能性に関係なく、優れたコードにも貢献します。

大まかに言えば、テスト可能なコードは柔軟なコードです。それは小さく、離散的で、まとまりのある塊になっているので、再利用のために個々のビットを呼び出すことができます。これはよく整理されており、名前が付けられています(一部の機能をテストできるようにするには、名前付けにより注意を払います。テストを作成していなければ、使い捨て関数の名前はそれほど重要ではありません)。 (時間の例のように)よりパラメトリックになる傾向があるため、本来の目的以外のコンテキストからも使用できます。それは乾燥しているので、散らかりにくく、理解しやすいです。

はいテストに関係なく、テスト可能なコードを書くことは良い習慣です。

8
Carl Manaster

テスト可能なコードを書くことは、コードが実際に機能することを証明したい場合に重要です

コードを特定のテストフレームワークに適合させるためだけに、コードをひどいゆがみに変形させることについての否定的な意見に同意する傾向があります。

一方、ここの誰もが、どこかで、処理する必要があるだけの凶悪な1,000行の長いマジック関数を処理する必要があり、事実上、1つ以上の不明瞭な、非他のどこか(または依存関係を視覚化することがほぼ不可能である、それ自体のどこかにある)明らかな依存関係。定義上、ほとんどテスト不可能です。私の意見では、テストフレームワークが誇張されたという概念(メリットがないわけではありません)を、品質が低くテスト不可能なコードを書くための無料ライセンスと見なすべきではありません。

たとえば、テスト駆動型の開発の理想は、単一責任の手順を作成するようにあなたをプッシュする傾向があり、thatは間違いなく良いことです。個人的には、私は単一の責任、単一の真の情報源、制御されたスコープ(異常なグローバル変数なし)を受け入れ、脆弱な依存関係を最小限に抑え、コードテスト可能である。特定のテストフレームワークでテスト可能ですか?知るか。しかし、多分それはそれ自身を良いコードに合わせる必要があるテストフレームワークであり、逆ではありません。

しかし、明確にするために、他の人間が簡単に理解できないほど賢く、または非常に長く相互依存しているコードは、良いコードではありません。また、偶然にも、簡単にテストできるコードではありません。

testablecodebettercode?

わからない、多分違う。ここの人々はいくつかの有効なポイントを持っています。

betterコードもtestableコードである傾向があると私は確信しています。

そして真面目な努力で使用するための真面目なソフトウェアについて話している場合、テストされていないコードを出荷することはあなたの雇用主と行うことができる最も責任のあることではありませんまたはあなたの顧客のお金。

また、一部のコードは他のコードよりも厳密なテストを必要とし、それ以外のふりをするのは少しばかげています。シャトルの重要なシステムと接続するメニューシステムがテストされていなかった場合、どのようにしてスペースシャトルの宇宙飛行士になりたいですか?または、原子炉の温度を監視するソフトウェアシステムがテストされなかった原子力発電所の従業員?一方、単純な読み取り専用レポートを生成する少しのコードでは、ドキュメントと完全なユニットテストが満載のコンテナートラックが必要ですか?私は間違いなく願っています。ただ言って...

8
Craig

しかし、私にとっては、時間を引数として渡すのはやり過ぎのようです。

あなたは正しい、そしてモックを使用すると、コードをテスト可能および時間を渡すことを避けることができます(しゃれの意図は未定義)。コード例:

def time_of_day():
    return datetime.datetime.utcnow().strftime('%H:%M:%S')

ここで、うるう秒の間に何が起こるかをテストするとします。あなたが言うように、これを過剰な方法でテストするには、(本番)コードを変更する必要があります:

def time_of_day(now=None):
    now = now if now is not None else datetime.datetime.utcnow()
    return now.strftime('%H:%M:%S')

Pythonサポートされるうるう秒 の場合、テストコードは次のようになります。

def test_handle_leap_second(self):
    actual = time_of_day(
        now=datetime.datetime(year=2015, month=6, day=30, hour=23, minute=59, second=60)
    expected = '23:59:60'
    self.assertEquals(actual, expected)

これをテストすることはできますが、コードは必要以上に複雑であり、テストstillは、ほとんどの製品コードが使用するコードブランチを確実に実行できません(つまり、 nowの値)。 mockを使用してこれを回避します。元の製品コードから始めます。

def time_of_day():
    return datetime.datetime.utcnow().strftime('%H:%M:%S')

テストコード:

@unittest.patch('datetime.datetime.utcnow')
def test_handle_leap_second(self, utcnow_mock):
    utcnow_mock.return_value = datetime.datetime(
        year=2015, month=6, day=30, hour=23, minute=59, second=60)
    actual = time_of_day()
    expected = '23:59:60'
    self.assertEquals(actual, expected)

これにはいくつかの利点があります。

  • 依存関係のtime_of_dayを独立してテストしています。
  • 同じコードパスを製品コードとしてテストしています。
  • 量産コードは可能な限り単純です。

余談ですが、将来のモックフレームワークがこのようなことを容易にすることが期待されます。たとえば、モックされた関数を文字列として参照する必要があるため、time_of_dayが別のソースを使用し始めたときに、IDEで自動的に変更することはできません。

5
l0b0

よく書かれたコードの品質は変更に対してロバストです。つまり、要件の変更が発生した場合、コードの変更は比例するはずです。これは理想的です(常に達成されるわけではありません)が、テスト可能なコードを記述することで、この目標に近づくことができます。

なぜそれが私たちを近づけるのに役立つのですか?本番環境では、私たちのコードは、他のすべてのコードの統合との相互作用を含む、本番環境内で動作します。単体テストでは、この環境の多くを一掃します。テストは変更であるため、コードは変更に対して堅牢です。ユニットをさまざまな方法で使用しており、本番で使用する場合とは異なる入力(モック、実際には渡されない可能性のある不良入力など)を使用しています。

これにより、システムで変更が発生した日のコードが準備されます。時間の計算に、タイムゾーンに基づいて異なる時間をかける必要があるとしましょう。これで、その時間を渡すことができ、コードに変更を加える必要がなくなります。時間を渡さずに現在の時間を使用したい場合は、デフォルトの引数を使用できます。私たちのコードは変更に対してロバストです。

4
cbojar

私の経験から、プログラムの構築時に行う最も重要で最も広範囲にわたる決定の1つは、コードを単位に分解する方法です(ここで「単位」は最も広い意味で使用されます)。クラスベースのOO言語を使用している場合、プログラムの実装に使用されるすべての内部メカニズムをいくつかのクラスに分割する必要があります。次に、各クラスのコードをいくつかのクラスに分割する必要があります。メソッドの数。一部の言語では、コードを関数に分割する方法を選択します。または、SOAを実行する場合は、ビルドするサービスの数と実行するサービスを決定する必要があります各サービスに。

選択した内訳は、プロセス全体に多大な影響を与えます。適切な選択を行うと、コードの記述が容易になり、バグが少なくなります(テストとデバッグを開始する前でも)。変更やメンテナンスが簡単になります。おもしろいことに、いったん良い内訳が見つかれば、通常は悪いものよりもtestのほうが簡単です。

これはなぜですか?すべての理由を理解して説明できるとは思いません。しかし、1つの理由は、適切な内訳は常に、実装のユニットに適度な「粒度」を選択することを意味することです。単一のクラス/メソッド/関数/モジュール/などにあまりにも多くの機能とロジックを詰め込みたくない。これにより、コードが読みやすくなり、書きやすくなりますが、テストも容易になります。

それだけではありません。優れた内部設計とは、各実装ユニットの予想される動作(入力/出力など)を明確かつ正確に定義できることを意味します。これはテストにとって重要です。優れた設計とは、通常、実装の各ユニットが他のユニットに対して適度な数の依存関係を持つことを意味します。これにより、他の人がコードを読みやすく理解しやすくなるだけでなく、テストも容易になります。理由は続きます。たぶん、他の人が私ができないより多くの理由を説明できるかもしれません。

あなたの質問の例に関して言えば、「良いコード設計」がすべての外部依存関係(システムクロックへの依存関係など)は常に「注入」されるべきであると言っていることと同じではないと思います。それは良い考えかもしれませんが、ここで説明することとは別の問題であり、その長所と短所については詳しく説明しません。

ちなみに、現在の時刻を返すシステム関数を直接呼び出したり、ファイルシステムを操作したりしても、コードを単体で単体テストできないわけではありません。コツは、システム関数の戻り値を偽造できるようにする標準ライブラリの特別なバージョンを使用することです。他の人がこのテクニックについて言及したことは一度もありませんが、多くの言語や開発プラットフォームで行うのはかなり簡単です。 (うまくいけば、言語ランタイムはオープンソースであり、構築も簡単です。コードの実行にリンク手順が含まれている場合は、リンク先のライブラリを簡単に制御することもできます。)

要約すると、テスト可能なコードは必ずしも「良い」コードではありませんが、「良い」コードは通常テスト可能です。

4
Alex D

[〜#〜] solid [〜#〜] の原則を使用している場合、特にこれを [〜#〜] kiss [〜#で拡張すると、 〜][〜#〜] dry [〜#〜] 、および [〜#〜] yagni [〜#〜]

私にとって欠けている点の1つは、メソッドの複雑さのポイントです。単純なゲッター/セッターメソッドですか?次に、テストフレームワークを満たすテストを作成するだけでは、時間の無駄になります。

データを操作するより複雑なメソッドであり、内部ロジックを変更する必要がある場合でも確実に機能するようにしたい場合は、テストメソッドの呼び出しとして最適です。多くの場合、数日/数週間/数か月後にコードの一部を変更する必要があり、テストケースを作成できて本当に嬉しかったです。最初にメソッドを開発したとき、私はテストメソッドを使用してテストしましたが、うまくいくと確信していました。変更後、私の主要なテストコードはまだ機能しています。そのため、私の変更によって本番環境で古いコードが壊れることはないと確信しました。

テストを記述するもう1つの側面は、メソッドの使用方法を他の開発者に示すことです。多くの場合、開発者はメソッドの使用方法と戻り値の例を検索します。

ちょうど私の 2セント

1
BtD