前回の授業では、「なぜリファクタリングをするのか、何をリファクタリングするのか、いつリファクタリングを行うのか、どのようにリファクタリングを行うのか」について概説し、リファクタリングの重要性を強調しました。開発の。
私の知る限り、多くのプログラマーは依然としてリファクタリングの慣行に同意しています.プロジェクトの悪いコードに直面して、彼らもリファクタリングしたいと思っていますが、リファクタリング後に問題が発生することを心配しており、彼らの努力は感謝されません. 実際、リファクタリングしたいコードが他の同僚によって開発されたものであり、そのコードに特に精通していない場合、リファクタリングによってバグが発生するリスクは依然として非常に高くなります。
では、リファクタリングがうまくいかないようにするにはどうすればよいでしょうか。さまざまな設計原則、アイデア、およびパターンに習熟している必要があり、リファクタリングされたビジネスとコードを十分に理解している必要もあります。これらの個人の能力要因に加えて、リファクタリングがうまくいかないことを確認するための最も実装可能で効果的な方法は、単体テストです。リファクタリングが完了した後、新しいコードがまだ単体テストに合格できる場合は、コードの元のロジックの正確性が破壊されておらず、外部から見える元の動作が変更されていないことを意味します。前のレッスン定義のリファクタリング。
今日は単体テストについて学びます。本日の内容は主に以下の内容です。
- 単体テストとは
- 単体テストを作成する理由
- 単体テストの書き方
- チームで単体テストを実装する方法は?
それでは早速、今日から学習を始めましょう!
単体テストとは
単体テストは、R&D エンジニア自身が作成し、記述したコードの正確性をテストします。私たちはしばしばそれを統合テストと比較します。統合テスト (統合テスト) と比較して、単体テストはテストの粒度が小さくなります。統合テストのテスト対象は、システム全体または特定の機能モジュールであり、ユーザー登録およびログイン機能が正常であるかどうかのテストなど、エンド ツー エンド (エンド ツー エンド) テストです。単体テストのテスト対象はクラスや関数であり、クラスや関数が期待通りのロジックで実行されるかどうかをテストするために使用されます。これはコードレベルのテストです。
理論を比較するために、例を挙げて説明します。
public class Text {
private String content;
public Text(String content) {
this.content = content;
}
/**
* 将字符串转化成数字,忽略字符串中的首尾空格;
* 如果字符串中包含除首尾空格之外的非数字字符,则返回 null。
*/
public Integer toNumber() {
if (content == null || content.isEmpty()) {
return null;
}
//... 省略代码实现...
return null;
}
}
Text クラスの toNumber() 関数の正確性をテストしたい場合、単体テストをどのように記述すればよいでしょうか?
実際、単体テストの作成自体には高度な技術は必要ありません。プログラマーの思慮深さのレベルをテストして、さまざまな正常および異常な状況をカバーするテスト ケースを設計し、予想される状況または予期しない状況下でコードが正しく実行されることを確認できるかどうかを確認することが重要です。
テストの包括性を確保するために、toNumber() 関数の次のテスト ケースを設計する必要があります。
- 文字列に数字のみが含まれる場合: "123"、toNumber() 関数は対応する整数: 123 を出力します。
- 文字列が空または null の場合、toNumber() 関数は null を返します。
- 文字列の先頭と末尾にスペースが含まれている場合: "123"、"123"、"123"、toNumber() は対応する整数: 123 を返します。
- 文字列の先頭と末尾に複数のスペース (" 123 ") が含まれている場合、toNumber() は対応する整数 123 を返します。
- 文字列に数字以外の文字が含まれている場合: "123a4"、"123 4"、toNumber() は null を返します。
テスト ケースを設計したら、あとはコードに変換するだけです。コードに変換するプロセスは非常に簡単です。以下にコードを貼り付けました。参照してください (ここではテスト フレームワークを使用していないことに注意してください)。
public class Assert {
public static void assertEquals(Integer expectedValue, Integer actualValue) {
if (actualValue != expectedValue) {
String message = String.format(
"Test failed, expected: %d, actual: %d.", expectedValue, actualValue);
System.out.println(message);
} else {
System.out.println("Test succeeded.");
}
}
public static boolean assertNull(Integer actualValue) {
boolean isNull = actualValue == null;
if (isNull) {
System.out.println("Test succeeded.");
} else {
System.out.println("Test failed, the value is not null:" + actualValue);
}
return isNull;
}
}
public class TestCaseRunner {
public static void main(String[] args) {
System.out.println("Run testToNumber()");
new TextTest().testToNumber();
System.out.println("Run testToNumber_nullorEmpty()");
new TextTest().testToNumber_nullorEmpty();
System.out.println("Run testToNumber_containsLeadingAndTrailingSpaces()");
new TextTest().testToNumber_containsLeadingAndTrailingSpaces();
System.out.println("Run testToNumber_containsMultiLeadingAndTrailingSpaces()");
new TextTest().testToNumber_containsMultiLeadingAndTrailingSpaces();
System.out.println("Run testToNumber_containsInvalidCharaters()");
new TextTest().testToNumber_containsInvalidCharaters();
}
}
public class TextTest {
public void testToNumber() {
Text text = new Text("123");
Assert.assertEquals(123, text.toNumber());
}
public void testToNumber_nullorEmpty() {
Text text1 = new Text(null);
Assert.assertNull(text1.toNumber());
Text text2 = new Text("");
Assert.assertNull(text2.toNumber());
}
public void testToNumber_containsLeadingAndTrailingSpaces() {
Text text1 = new Text(" 123");
Assert.assertEquals(123, text1.toNumber());
Text text2 = new Text("123 ");
Assert.assertEquals(123, text2.toNumber());
Text text3 = new Text(" 123 ");
Assert.assertEquals(123, text3.toNumber());
}
public void testToNumber_containsMultiLeadingAndTrailingSpaces() {
Text text1 = new Text(" 123");
Assert.assertEquals(123, text1.toNumber());
Text text2 = new Text("123 ");
Assert.assertEquals(123, text2.toNumber());
Text text3 = new Text(" 123 ");
Assert.assertEquals(123, text3.toNumber());
}
public void testToNumber_containsInvalidCharaters() {
Text text1 = new Text("123a4");
Assert.assertNull(text1.toNumber());
Text text2 = new Text("123 4");
Assert.assertNull(text2.toNumber());
}
}
単体テストを作成する理由
単体テストは、リファクタリングを効果的にエスコートするだけでなく、コードの品質を保証する 2 つの最も効果的な手段の 1 つでもあります (もう 1 つはコード レビューです)。Google で働いていたとき、私は多くの単体テスト コードを書きました. 私の開発経験と合わせて、単体テストの次の利点をまとめました. 少し「後退」しているように聞こえるかもしれませんが、単体テストを真剣に作成した場合は、非常に共鳴するはずです。
1. 単体テストは、コード内のバグを効果的に見つけるのに役立ちます
バグのないコードを書けるかどうかは、エンジニアのコーディング能力を判断する重要な基準の 1 つであり、多くの大手企業、特に FLAG のような外資系企業へのインタビューでも注目されています。私のように 10 年以上コードを書いていて、かなり厳密で明確なロジックを持っている人でさえ、単体テストを通じて、コード内に多くの不完全な考慮事項があることに気付くことがよくあります。
Google を辞めた後、私が働いていた多くの企業は「速く、ラフで、激しい」開発モデルを採用しており、単体テストの要件がまったくありませんでしたが、私は提出したすべてのコードに対して完全な単体を書くことを主張しました。 . おかげで、私が書いたコードにはほとんどバグがありません。これにより、低レベルのバグを修正するための時間を大幅に節約でき、他のより有意義なことに時間を割くことができるため、仕事で多くの人から認められています。単体テストの作成に固執することは、コードの品質を確保するための「キラー」であり、他の人とのギャップを開くのに役立つ「小さな秘密」でもあると言えます。
2.単体テストを書くと、コード設計の問題を見つけるのに役立ちます
前述したように、コードのテスト容易性は、コードの品質を判断するための重要な基準です。コードの一部について、単体テストを作成するのが難しい場合、または単体テストを作成するのが非常に難しい場合は、完成させるために単体テスト フレームワークの非常に高度な機能に依存する必要があります。たとえば、依存性注入が使用されていない、静的関数、グローバル変数、高度に結合されたコードの広範な使用など、コード設計は十分に合理的ではありません。
3. 単体テストは統合テストを強力に補完するものです
プログラム動作のバグは、除数が空と判定されない、ネットワークのタイムアウトなど、一部の境界条件や異常な状況で発生することがよくあります。異常な状態のほとんどは、テスト環境でシミュレートするのがより困難です。単体テストでは、次のレッスンで説明するモック メソッドを使用して、モックのオブジェクトを制御し、これらの異常な状況でコードのパフォーマンスをテストするためにシミュレートする必要がある例外を返すことができます。
さらに、一部の複雑なシステムでは、統合テストでは包括的にカバーできません。多くの場合、複雑なシステムには多くのモジュールがあります。各モジュールにはさまざまな入力、出力、および異常な状態があり、これらを組み合わせると、システム全体でシミュレートするテスト シナリオと設計するテスト ケースが無数にあります. テスト チームがどれほど強力であっても、すべてを網羅することはできません.
単体テストは統合テストを完全に置き換えることはできませんが、各クラスと各機能が期待どおりに実行され、潜在的なバグが少ないことを確認できれば、組み立てられたシステムで問題が発生する可能性はそれに応じて減少します。
4. 単体テストを作成するプロセスは、それ自体がコードのリファクタリングのプロセスです
前回のレッスンでは、開発の一環として継続的なリファクタリングを実行する必要があると述べました。そのため、単体テストを作成することは、実際には継続的なリファクタリングを実装する効果的な方法です。コードを設計して実装するとき、すべての問題について明確に考えるのは困難です。単体テストを書くことは、コードのセルフ コード レビューに相当します. このプロセス中に、いくつかの設計上の問題 (テストできないコード設計など) やコードの記述上の問題 (境界条件の不適切な処理など) を見つけることができます。ターゲットを絞った方法でリファクタリングします。
5. 単体テストを読むと、コードにすぐに慣れることができます
コードを読む最も効果的な方法は、まずビジネスの背景とデザインのアイデアを理解してからコードを読むことです。しかし、私の知る限り、プログラマーはドキュメントやコメントを書くのがあまり好きではなく、ほとんどのプログラマーによって書かれたコードは「自明」であることが難しいです。ドキュメントやコメントがない場合は、単体テストが代わりになります。単体テスト ケースは実際にはユーザー ケースであり、コードの機能とその使用方法を反映しています。単体テストでは、コードが実装する機能、考慮する必要がある特別な状況、および処理する必要がある境界条件を知るために、コードを深く読む必要はありません。
6. 単体テストは、現場で実装できる TDD の改善されたソリューションです
テスト駆動開発 (略して TDD) は、よく言及されるものの実装されることはめったにない開発モデルです。その中核となる指針となるイデオロギーは、コードの前にテスト ケースを作成することです。しかし、プログラマーがこの開発モードを完全に受け入れて慣れることは非常に難しく、コードを書く前にテストケースを書くどころか、単体テストを書くのも怠惰なプログラマーが多いのです。
個人的には単体テストはTDDの改良にすぎないと思っており、まずコードを書き、次に単体テストを書き、最後に単体テストに基づいて問題を報告し、それから戻ってコードをリファクタリングします。この開発プロセスは受け入れやすく、実装しやすく、TDD の利点を考慮に入れています。
単体テストの書き方
単体テストとは何かについて説明する際に、toNumber() 関数の単体テストを作成する例を挙げました。その例によれば、単体テストを作成することは、コードのさまざまな入力、例外、および境界条件をカバーするテスト ケースを設計し、これらのテスト ケースをコードに変換するプロセスであると結論付けることができます。
テストケースをコードに変換するとき、単体テストフレームワークを使用してテストコードの記述を簡素化できます。たとえば、Java のよく知られた単体テスト フレームワークには、Junit、TestNG、Spring Test などがあります。これらのフレームワークは、共通の実行プロセス (テスト ケースを実行する TestCaseRunner など) やツール ライブラリ (さまざまな Assert 判定関数など) などを提供します。それらを使用すると、テスト コードを記述するときに、テスト ケース自体の記述に集中するだけで済みます。
toNumber() 関数のテスト ケースについては、Junit 単体テスト フレームワークを使用して再実装します。具体的なコードは次のとおりです。テストフレームワークを使用せずに以前の実装と比較して、大幅に簡略化されているかどうかを確認できますか?
import org.junit.Assert;
import org.junit.Test;
public class TextTest {
@Test
public void testToNumber() {
Text text = new Text("123");
Assert.assertEquals(new Integer(123), text.toNumber());
}
@Test
public void testToNumber_nullorEmpty() {
Text text1 = new Text(null);
Assert.assertNull(text1.toNumber());
Text text2 = new Text("");
Assert.assertNull(text2.toNumber());
}
@Test
public void testToNumber_containsLeadingAndTrailingSpaces() {
Text text1 = new Text(" 123");
Assert.assertEquals(new Integer(123), text1.toNumber());
Text text2 = new Text("123 ");
Assert.assertEquals(new Integer(123), text2.toNumber());
Text text3 = new Text(" 123 ");
Assert.assertEquals(new Integer(123), text3.toNumber());
}
@Test
public void testToNumber_containsMultiLeadingAndTrailingSpaces() {
Text text1 = new Text(" 123");
Assert.assertEquals(new Integer(123), text1.toNumber());
Text text2 = new Text("123 ");
Assert.assertEquals(new Integer(123), text2.toNumber());
Text text3 = new Text(" 123 ");
Assert.assertEquals(new Integer(123), text3.toNumber());
}
@Test
public void testToNumber_containsInvalidCharaters() {
Text text1 = new Text("123a4");
Assert.assertNull(text1.toNumber());
Text text2 = new Text("123 4");
Assert.assertNull(text2.toNumber());
}
}
これらの単体テスト フレームワークの使用方法については、ほとんどの場合、非常に詳細な公式ドキュメントが提供されており、自分で確認できます。これらのことを理解して習得するのはそれほど難しくないので、これはコラムの焦点ではありません。単体テストの書き方について、私の経験をまとめてお伝えしたいと思います。具体的には以下の点を盛り込みます。
1. 単体テストを書くのは本当に時間がかかりますか?
単体テストコードの量は、テストするコード自体の 1 ~ 2 倍になる場合がありますが、作成プロセスは非常に面倒ですが、それほど時間はかかりません。結局、あまり多くのコード設計の問題を考慮する必要はなく、テスト コードは比較的簡単に実装できます。異なるテストケース間のコードの違いはそれほど大きくないかもしれません。単純にコピーして貼り付けて変更してください。
2. 単体テストのコード品質に関する要件はありますか?
結局、単体テストは生産ラインでは実行されず、各クラスのテスト コードは比較的独立しており、基本的に相互に依存しません。したがって、テスト対象のコードと比較して、単体テスト コードの品質に対する要件をいくらか下げることができます。ネーミングがイレギュラーで、コードが少し重複していますが問題ありません。
3. カバレッジが十分に高い限り、単体テストは実施されていますか?
ユニットテストカバレッジは比較的数値化しやすい指標であり、ユニットテストが適切に書かれているかどうかの判断基準としてよく使われます。JaCoCo、Cobertura、Emma、Clover など、カバレッジ統計専用の市販ツールが多数あります。カバレッジを計算するには多くの方法があります.単純なものはステートメント カバレッジであり、より高度なものは条件付きカバレッジ、決定カバレッジ、およびパス カバレッジです。
カバレッジの計算方法がどれだけ進歩したとしても、ユニットテストの品質を測定するための唯一の基準としてカバレッジを使用することは合理的ではありません。実際には、テスト ケースが考えられるすべてのケース、特にいくつかのコーナー ケースをカバーしているかどうかを確認することがより重要です。簡単な例で説明しましょう。
次のコードのように、cal(10.0, 2.0) のように 100% のカバレッジを達成するために必要なテスト ケースは 1 つだけですが、それはテストが十分に包括的であることを意味するものではありません。 0 、コードが期待どおりに実行されるかどうか。
public double cal(double a, double b) {
if (b != 0) {
return a / b;
}
}
実際には、単体テストのカバレッジ率に過度に注意を払うと、開発者はカバレッジ率を向上させるために不要なテスト コードを大量に作成することになります.たとえば、get メソッドと set メソッドは非常に単純であり、テストする必要はありません。 . 過去の経験から、単体テストのカバー率が 60 ~ 70% の場合、プロジェクトはオンラインになることができます。プロジェクトのコード品質に対する要件が比較的高い場合は、単体テスト カバレッジの要件を適切に引き上げることができます。
4. 単体テストを作成するには、コードの実装ロジックを理解する必要がありますか?
単体テストは、テストされた関数の特定の実装ロジックに依存せず、テストされた関数が実装する関数のみを気にします。カバレッジを追求するためにコードを 1 行ずつ読んでから、実装ロジックの単体テストを記述してはなりません。そうしないと、コードがリファクタリングされると、コードの外部動作は変更されないままコードの実装ロジックが変更され、元の単体テストの実行に失敗し、リファクタリングを保護できなくなります。また、元のコードにも違反します。単体テストを書く意図。
5. 単体テスト フレームワークの選択方法は?
単体テストを書くこと自体はそれほど複雑な技術を必要とせず、ほとんどの単体テスト フレームワークが要件を満たすことができます。社内では、少なくともチームには統一されたユニット テスト フレームワークが必要です。記述したコードが選択した単体テスト フレームワークでテストできない場合は、コードが適切に記述されておらず、コードのテスト容易性が十分でないことが原因である可能性があります。現時点では、別のより高度な単体テスト フレームワークを探すのではなく、テストを容易にするためにコードをリファクタリングする必要があります。
単体テストの実装が難しいのはなぜですか?
単体テストはリファクタリングがうまくいかないことを確認するための有効な手段であると多くの本で言及されていますが、単体テストの重要性を認識している人もたくさんいます。しかし、健全で高品質な単体テストを備えているプロジェクトはいくつあるでしょうか? 私の知る限り、BAT のような企業のプロジェクトを含めて、本当にごくわずかしかありません。信じられない場合は、多くの主要な国内メーカーのオープン ソース プロジェクトを見ることができます.単体テストがまったくないプロジェクトが多く、多くのプロジェクトの単体テストは非常に不完全です.彼らはテストするだけです.ロジックが正しく実行されているかどうか。したがって、単体テストを 100% 実装することは、言うは易く行うは難しです。
単体テストを書くことは、まさに忍耐の試練です。一般に、単体テスト コードの量は、テストされるコードの量よりも多く、場合によっては数倍になります。多くの人は、単体テストを書くのは面倒で、課題が少ないのでやりたがらないと考えがちです。単体テストの実装を最初に開始したとき、より真剣に取り組み、よりうまく実行できた多くのチームやプロジェクトがあります。しかし、開発タスクが厳しくなると、単体テストの要件が緩和されます. 壊れたウィンドウ効果が発生すると、誰もが書くのをやめます. これは非常に一般的です.
もう1つの状況は、歴史的な問題により、元のコードは単体テストを記述しておらず、コードはすでに10万行以上積み重なっており、単体テストを1つずつ補足することは不可能です. この場合、最初に新しく書かれたコードに単体テストが必要であることを確認する必要があります。次に、特定のクラスを変更するたびに、単体テストがない場合は、ちなみに追加しますが、これにはエンジニアが持っている必要があります十分に強いマスター意識(所有権)があれば、結局のところ、リーダーの監督だけでは多くのことを適所に実装することは困難です。
さらに、テスト チームで単体テストを作成するのは時間の無駄であり、不必要だと考える人もいます。プログラマーの業界はインテリジェンス集約型のはずですが、現在、一部の大規模工場を含め、多くの企業が労働集約型にしています。開発プロセスでは、単体テストもコード レビュー プロセスもありません。あったとしても、それがすることは満足のいくものではありません。コードを書いて直接提出し、それをブラックボックステスターに投げて激しくテストし、問題が検出された場合は開発チームにフィードバックされて修正されます.問題が検出されない場合は、オンラインのままにしてから修復しました。
このような開発モードでは、チームは単体テストを書く必要がないと感じることがよくありますが、単体テストをうまく書き、コード レビューを適切に行い、コードの品質に注意を払えば、実際に投資を削減できます。ブラックボックステストが大部分。私がいた頃は、多くのプロジェクトに参加するテスト チームはほとんどなく、コードの正確性は開発チームによって完全に保証され、オンライン バグはほとんどありませんでした。
キーレビュー
では、本日の内容は以上です。マスターする必要がある主要なコンテンツを要約して確認しましょう。
1.単体テストとは?
ユニットテストはコードレベルでのテストで、R&D自身が書いたコードのロジックの正しさをテストするためにR&D自身が書いたものです。単体テストとはその名の通り、結合テストとは異なる「単体」をテストすることであり、この「単体」は通常、モジュールやシステムではなく、クラスや関数です。
2. 単体テストを作成する理由
単体テストを作成するプロセスは、それ自体がコード レビューとリファクタリングのプロセスであり、コードのバグやコード設計の問題を効果的に見つけることができます。さらに、単体テストは統合テストを強力に補完するものであり、コードにすばやく慣れるのに役立ち、現場で実装できる TDD の改善ソリューションです。
3. 単体テストの書き方
単体テストを書くことは、さまざまな入力、例外、およびエッジ ケースをカバーするコードのさまざまなテスト ケースを設計し、それらをコードに変換することです。単体テストの記述を簡素化するために、いくつかのテスト フレームワークを使用できます。さらに、単体テストでは、次の正しい認識を確立する必要があります。
- 単元テストの作成は、退屈ではありますが、それほど時間はかかりません。
- 単体テスト コードの品質要件を少し下げることができます。
- 単体テストの品質を測定する唯一の基準としてカバレッジを使用するのは不合理です。
- 単体テストは、テスト対象のコードの特定の実装ロジックに依存しません。
- 単体テスト フレームワークをテストできません。その主な理由は、コードがテストできないためです。
4. 単体テストの実装が難しいのはなぜですか?
一方では単体テストを書くのは面倒で、技術的な課題も大きくないため、多くのプログラマーはそれを書きたがりません;他方では、国内の研究開発は「速く、ラフで、激しい」傾向があり、開発が順調に進んでいるため、単体テストを実行するのは簡単です。結局、重要な問題は、チームが単体テストの正しい理解を確立しておらず、単体テストが不要であると感じていることであり、監督だけではうまく実装することは困難です。
クラスディスカッション
今日のクラスディスカッションには、次の 2 つがあります。
- 参加したプロジェクトの単体テストを作成しましたか? 単体テストは適切ですか? 単体テストを作成する過程で、どのような問題に遭遇しましたか? それを解決する方法は?
- 面接では、特にいくつかの境界条件の処理について、候補者が問題を包括的に考慮しているかどうかを調べるために、コードを書いた後にいくつかのテストケースをリストするよう候補者によく求めます。そこで、今日のクラス ディスカッションのもう 1 つのトピックは次のとおりです。二分探索のバリアント アルゴリズムを記述し、インクリメントされた配列内で特定の値以上の最初の要素を見つけ、コードの完全な単体テスト ケースを設計します。
メッセージエリアに答えを書き留めて、クラスメートとコミュニケーションを取り、共有してください。何かを得た場合は、この記事を友達と共有してください。