TDDを使用したシンプルだが実例

私は、多くのプログラマーと同様に、 TDDプラクティスについて多くのことを聞いて読んでいます。 私は商用プロジェクトでの私自身の経験から、単体テストでの優れたコードカバレッジの利点とその欠如の危険性を知っていますが、さまざまな理由でTDDを純粋な形で使用することはできませんでした。 先日ゲームプロジェクトを書き始めたので、試してみる良い機会だと思いました。 結局のところ、通常のアプローチと比較した違いは、最も単純なクラスを実装する場合でも感じることができます。 この例を段階的に記述し、最後に私が見た結果を説明します。 このトピックは、TDDに興味がある人にとって役立つと思います。 より経験豊富な同僚から、コメントと批判を聞きたいです。

理論については説明しませんが、独立して簡単に見つけることができます。 この例はJavaで記述されており、TestNGは単体テストフレームワークとして使用されます。

挑戦する


ユニットの基本クラスを開発することから始めました-ユニット。 基本的なレベルでは、他のユニットにできる健康とダメージの供給がユニットに必要です。

それはもっと簡単に思えるかもしれません:

public class Unit { private int health; private int damage; public int getHealth() { return health; } public int setHealth(int health) { this.health = health; } public int getDamage() { return damage; } public int setDamage(int damage) { this.damage = damage; } } 


実装は非常に単純です。 確かに、このクラスの使用を開始するときは、より便利なメソッド、コンストラクターなどを追加する必要があります。 しかし、これまでのところ、何が必要で何が必要かわからないので、すぐに書きすぎたくありません。

そのため、これが従来の「額」方式であることが判明しました。 次に、TDDを介して同じクラスを実装してみましょう。

TDDを適用


実際には、上記の実装を作成しませんでした。最初はUnitクラスは存在しません。 テストクラスを作成することから始めます。

 @Test public class UnitTest { } 


ユニットクラスの要件について考え始めます。 最初に頭に浮かぶのは、ユニットを作成し、そのヘルスとダメージを設定できるといいということです。 だから書く。

 @Test public class UnitTest { @Test public void youCreateAUnitGivenItsHealthAndDamage() { new Unit(100, 25); } } 


もちろん、テストはコンパイルすらしません。合格するようにテストを行います。

 public class Unit { public Unit(int health, int damage) { } } 


リファクタリングするものはまだありません。 私たちは次のテストを書いています-ユニットの現在の状態を調べたいです。

 @Test public class UnitTest { @Test public void youCreateAUnitGivenItsHealthAndDamage() { new Unit(100, 25); } @Test public void youCheckUnitHealthWithGetter() { Unit unit = new Unit(100, 25); assertEquals(100, unit.getHealth()); } } 


コンパイルエラーが原因でテストがクラッシュします。UnitクラスにはgetHealthメソッドがありません。 テストに合格するようにコードを修正します。

 public class Unit { private int health; public Unit(int health, int damage) { this.health = health; } public int getHealth() { return health; } } 


再びリファクタリングするものは何もありません。 私たちはさらに考えます-おそらくユニットがダメージを受けることができればいいでしょう。

 @Test public class UnitTest { @Test public void youCreateAUnitGivenItsHealthAndDamage() { new Unit(100, 25); } @Test public void youCheckUnitHealthWithGetter() { Unit unit = new Unit(100, 25); assertEquals(100, unit.getHealth()); } @Test public void unitCanTakeDamage() { Unit unit = new Unit(100, 25); unit.takeDamage(25); } } 


テストに合格するようにコードを修正します。

 public class Unit { private int health; public Unit(int health, int damage) { this.health = health; } public int getHealth() { return health; } public void takeDamage(int damage) { } } 


そうです、受けたダメージはユニットの健康状態から差し引く必要があります。 これについては別のテストを作成します。

 @Test public class UnitTest { @Test public void youCreateAUnitGivenItsHealthAndDamage() { new Unit(100, 25); } @Test public void youCheckUnitHealthWithGetter() { Unit unit = new Unit(100, 25); assertEquals(100, unit.getHealth()); } @Test public void unitCanTakeDamage() { Unit unit = new Unit(100, 25); unit.takeDamage(25); } @Test public void damageTakenReducesUnitHealth() { Unit unit = new Unit(100, 25); unit.takeDamage(25); assertEquals(75, unit.getHealth()); } } 


クラスの動作が原因でクラッシュする最初のテスト。 正解。

 public class Unit { private int health; public Unit(int health, int damage) { this.health = health; } public int getHealth() { return health; } public void takeDamage(int damage) { health -= damage; } } 


ここで、すでに少しリファクタリングできます。 ここではそのようにしておくことができますが、私はクラスの終わりにゲッターに慣れています。

 public class Unit { private int health; public Unit(int health, int damage) { this.health = health; } public void takeDamage(int damage) { health -= damage; } public int getHealth() { return health; } } 


先に進みます。 私たちの部隊はすでに健康保護区を持っており、損傷する可能性があります。 他のユニットにダメージを与える方法を彼に教えます!

 @Test public class UnitTest { @Test public void youCreateAUnitGivenItsHealthAndDamage() { new Unit(100, 25); } @Test public void youCheckUnitHealthWithGetter() { Unit unit = new Unit(100, 25); assertEquals(100, unit.getHealth()); } @Test public void unitCanTakeDamage() { Unit unit = new Unit(100, 25); unit.takeDamage(25); } @Test public void damageTakenReducesUnitHealth() { Unit unit = new Unit(100, 25); unit.takeDamage(25); assertEquals(75, unit.getHealth()); } @Test public void unitCanDealDamageToAnotherUnit() { Unit damageDealer = new Unit(100, 25); Unit damageTaker = new Unit(100, 25); damageDealer.dealDamage(damageTaker); } } 


ユニットクラスを完成させています。

 public class Unit { private int health; public Unit(int health, int damage) { this.health = health; } public void takeDamage(int damage) { health -= damage; } public void dealDamage(Unit damageTaker) { } public int getHealth() { return health; } } 


ユニットが別のユニットにダメージを与えた場合、そのヘルスが低下することは明らかです。

 @Test public class UnitTest { @Test public void youCreateAUnitGivenItsHealthAndDamage() { new Unit(100, 25); } @Test public void youCheckUnitHealthWithGetter() { Unit unit = new Unit(100, 25); assertEquals(100, unit.getHealth()); } @Test public void unitCanTakeDamage() { Unit unit = new Unit(100, 25); unit.takeDamage(25); } @Test public void damageTakenReducesUnitHealth() { Unit unit = new Unit(100, 25); unit.takeDamage(25); assertEquals(75, unit.getHealth()); } @Test public void unitCanDealDamageToAnotherUnit() { Unit damageDealer = new Unit(100, 25); Unit damageTaker = new Unit(100, 25); damageDealer.dealDamage(damageTaker); } @Test public void unitThatDamageDealtToTakesDamageDealerUnitDamage() { Unit damageDealer = new Unit(100, 25); Unit damageTaker = new Unit(100, 25); damageDealer.dealDamage(damageTaker); assertEquals(75, damageTaker.getHealth()); } } 


新たに書かれたテストが落ちます-ユニットクラスを修正します。

 public class Unit { private int health; private int damage; public Unit(int health, int damage) { this.health = health; this.damage = damage; } public void takeDamage(int damage) { health -= damage; } public void dealDamage(Unit damageTaker) { damageTaker.takeDamage(damage); } public int getHealth() { return health; } } 


少し輝かせましょう:ダメージ変数はfinalにすることができます。クラス変数と混同しないように、takeDamageメソッドのパラメーターの名前を変更すると良いでしょう。

 public class Unit { private int health; private final int damage; public Unit(int health, int damage) { this.health = health; this.damage = damage; } public void takeDamage(int incomingDamage) { health -= incomingDamage; } public void dealDamage(Unit damageTaker) { damageTaker.takeDamage(damage); } public int getHealth() { return health; } } 


次に、ヘルスがゼロを下回らないテストを作成する必要があります。ゼロになっている場合、ユニットは死んでいると言うことができる必要があります。 余分なボリュームを追加しないために、ここで停止します。 この例を理解すれば十分であり、いくつかの結論を引き出すことができると思います。

結論


  1. 「正面」を実装する場合よりも最も単純なクラスを実装するのに数倍時間がかかりました。これは、TDDのマネージャーや非技術系プログラマーをしばしば怖がらせるものです。
  2. 最初の単純な実装と、TDDで取得した最後の実装を比較できます。 主な違いは、最後の実装が実際にオブジェクト指向であり、ユニットを独立したオブジェクトとして操作し、その状態について尋ね、特定のアクションを実行するように求めることができることです。 このクラスで動作するコードも、よりオブジェクト指向です。
  3. クラス自体に加えて、完全なテストセットを受け取りました。 私の経験から、テストで完全にカバーされたコードよりも開発者にとって大きなメリットを想像するのは難しいことを知っています。 同じ経験から、コードの後に​​テストが記述されている場合、完全なカバレッジを提供するのは難しい場合があります-何かのテストを書くのを忘れたり、テストするのが簡単すぎるように見えたりします テスト自体は、多くの場合、複雑で扱いにくいものです。 1つのテストは、テストされたコードのいくつかの側面をテストしようとしています。 ここでは、簡単で理解しやすいテストのセットを入手しました。これは、メンテナンスがはるかに簡単になります。
  4. ライブコードのドキュメントを入手しました! 作成者のアイデア、目的、クラスの動作を理解するには、誰でもテスト内のメソッドの名前を読むだけです。 コードを理解し、この情報を入手すれば、桁違いに簡単になり、何が何であるかを説明するために同僚の注意をそらす必要はありません。
  5. 前の段落はすでによく知られていますが、私は1つの新しい結論を出しました。TDDを使用して開発するとき、クラスから得たいもの、その動作、およびユースケースについてよりよく考えます。 すでに開発されたコンポーネントを十分に理解すると、より複雑なコンポーネントを簡単に作成できます。
  6. この例には関係ありませんが、ここに別のポイントを追加したかったのです。 テストを通じて開発する場合、タスクの複雑さをより適切に評価します。すでに何が機能していて、何が完了するのかを常に把握しています。 テストがなければ、すべてがすでに書かれていると感じることがよくありますが、デバッグと修正にはまだかなりの時間が必要であることがわかります。


この例は非常に単純であり、これに誤りがないことを確認してください。 このトピックでは、2つのフィールドのクラスを記述する方法については説明しませんが、TDDのメリットをそのような基本的な例でも確認できます。

ご清聴ありがとうございました。 高度なプログラミング技術を学び、あなたの仕事を楽しんでください!

Source: https://habr.com/ru/post/J151639/


All Articles