回答:
これに対する多くの素晴らしい回答も私の質問にあります:「TDDの始まり-課題?ソリューション?推奨事項?」
私のブログの投稿(私の質問に一部影響を受けたもの)もご覧になることをお勧めします。それについて、いくつかの良いフィードバックを得ました。つまり:
どこから始めればよいかわかりませんか?
- 新たにスタート。新しいコードを書くときだけテストを書くことを考えてください。これは、古いコードを再加工することも、まったく新しい機能にすることもできます。
- 簡単に始めましょう。TDD風であるだけでなく、テストフレームワークに頭を悩ませることもありません。Debug.Assertは正常に動作します。出発点として使用してください。プロジェクトを台無しにしたり、依存関係を作成したりすることはありません。
- ポジティブから始めましょう。あなたはあなたの技術を改善しようとしています、それについて気分が良いです。私は停滞していて自分たちをより良くするために新しいことを試みないことに満足している多くの開発者を見てきました。あなたは正しいことをしています。これを覚えておけば、あきらめないようにするのに役立ちます。
- 挑戦の準備を始めてください。テストを始めるのはかなり難しいです。挑戦を期待しますが、覚えておいてください–挑戦は克服することができます。
期待することだけをテストする
発生する可能性のあるすべての問題を把握し、それをテストして修正するために常にそこに座っていたため、最初に本当の問題を抱えていました。これは頭痛への簡単な方法です。テストは実際のYAGNIプロセスでなければなりません。問題があることがわかっている場合は、テストを作成します。それ以外の場合は、気にしないでください。
1つのものだけをテストする
各テストケースでテストできるのは1つだけです。テストケース名に「and」が含まれていることに気付いた場合は、何か問題があります。
これが「ゲッターとセッター」から先に進むことができることを願っています:)
言語ではなくコードをテストします。
次のような単体テスト:
Integer i = new Integer(7);
assert (i.instanceOf(integer));
コンパイラーを作成していて、instanceof
メソッドが機能しない可能性がゼロでない場合にのみ役立ちます。
強制的に言語に依存できるものはテストしないでください。あなたの場合、私はあなたのauthenticateメソッドとsaveメソッドに焦点を当てます-そして、それらのフィールドのいずれかまたはすべてでnull値を正常に処理できることを確認するテストを書きます。
これは私を単体テストに連れて行ってくれて、とても幸せになりました
ユニットテストを開始しました。長い間、それを始めるのは良いことだと思っていましたが、どのように始めればいいのか、そして何をテストすればよいのかわかりませんでした。
次に、会計プログラムの重要なコードを書き直す必要がありました。この部分は、多くの異なるシナリオが含まれていたため、非常に複雑でした。私が話している部分は、すでに会計システムに入力されている販売および/または購入の請求書を支払う方法です。
支払い方法が非常に多かったので、コーディングを開始する方法がわかりませんでした。請求書は$ 100になる可能性がありますが、顧客は$ 99のみを転送しました。顧客に販売請求書を送信したが、その顧客からも購入した可能性があります。だからあなたは彼を300ドルで売ったが、あなたは100ドルで買った。残高を決済するために顧客に200ドルを支払うことを期待できます。また、500ドルで販売したが、顧客が250ドルしか支払わない場合はどうでしょうか。
だから私は多くの可能性を解決するために非常に複雑な問題を持っていました。
ここでユニットテストが役に立ちました。
私は(テストコード内に)販売と購入の両方の請求書のリストを作成するメソッドを書き始めました。次に、実際の支払いを作成するための2番目のメソッドを作成しました。通常、ユーザーはユーザーインターフェイスを介してその情報を入力します。
次に、最初のTestMethodを作成し、支払い割引なしで1つの請求書の非常に単純な支払いをテストしました。銀行支払いがデータベースに保存されるときに、システム内のすべてのアクションが発生します。ご覧のとおり、請求書を作成し、支払い(銀行取引)を作成して、取引をディスクに保存しました。私の主張では、銀行取引とリンクされた請求書に最終的に正しい数字が何であるべきかを入れました。取引後、お支払い回数、お支払い金額、割引額、請求書の残高を確認します。
テストの実行後、データベースに移動して、期待したものがそこにあったかどうかを再確認しました。
後テストを書いた、支払い方法(BankHeaderクラスの一部)のコーディングを開始しました。コーディングでは、最初のテストに合格するためのコードに悩まされました。他のもっと複雑なシナリオについてはまだ考えていませんでした。
最初のテストを実行し、テストに合格するまで小さなバグを修正しました。
次に、2番目のテストの作成を開始しました。今回は支払い割引を扱います。テストを書いた後、割引をサポートするように支払い方法を変更しました。
支払い割引で正しさをテストすると同時に、単純な支払いもテストしました。もちろん、両方のテストに合格するはずです。
次に、より複雑なシナリオにたどり着きました。
1)新しいシナリオを考える
2)そのシナリオのテストを書く
3)その単一のテストを実行して、合格するかどうかを確認します
4)うまくいかなかった場合、合格するまでコードをデバッグして変更します。
5)コードを変更しながら、すべてのテストを実行し続けました
これが、非常に複雑な支払い方法を作成する方法です。ユニットテストを行わなかったため、コーディングを開始する方法がわかりませんでした。テストでは、単純な方法から始めて、より単純なシナリオでも機能することを確認しながら、段階的に拡張することができます。
ユニットテストを使用することで、コーディングの数日(または数週間)を節約でき、多かれ少なかれ私の方法の正確さを保証していると確信しています。
後で新しいシナリオを考えた場合、それをテストに追加して、機能しているかどうかを確認できます。そうでない場合はコードを変更できますが、それでも他のシナリオが正しく機能していることを確認してください。これにより、メンテナンスおよびバグ修正フェーズの日数を節約できます。
はい、テスト済みのコードであっても、ユーザーが思いもよらないことをしたり、実行できないようにしたりすると、バグが残る可能性があります
以下は、支払い方法をテストするために作成したテストの一部です。
public class TestPayments
{
InvoiceDiaryHeader invoiceHeader = null;
InvoiceDiaryDetail invoiceDetail = null;
BankCashDiaryHeader bankHeader = null;
BankCashDiaryDetail bankDetail = null;
public InvoiceDiaryHeader CreateSales(string amountIncVat, bool sales, int invoiceNumber, string date)
{
......
......
}
public BankCashDiaryHeader CreateMultiplePayments(IList<InvoiceDiaryHeader> invoices, int headerNumber, decimal amount, decimal discount)
{
......
......
......
}
[TestMethod]
public void TestSingleSalesPaymentNoDiscount()
{
IList<InvoiceDiaryHeader> list = new List<InvoiceDiaryHeader>();
list.Add(CreateSales("119", true, 1, "01-09-2008"));
bankHeader = CreateMultiplePayments(list, 1, 119.00M, 0);
bankHeader.Save();
Assert.AreEqual(1, bankHeader.BankCashDetails.Count);
Assert.AreEqual(1, bankHeader.BankCashDetails[0].Payments.Count);
Assert.AreEqual(119M, bankHeader.BankCashDetails[0].Payments[0].PaymentAmount);
Assert.AreEqual(0M, bankHeader.BankCashDetails[0].Payments[0].PaymentDiscount);
Assert.AreEqual(0, bankHeader.BankCashDetails[0].Payments[0].InvoiceHeader.Balance);
}
[TestMethod]
public void TestSingleSalesPaymentDiscount()
{
IList<InvoiceDiaryHeader> list = new List<InvoiceDiaryHeader>();
list.Add(CreateSales("119", true, 2, "01-09-2008"));
bankHeader = CreateMultiplePayments(list, 2, 118.00M, 1M);
bankHeader.Save();
Assert.AreEqual(1, bankHeader.BankCashDetails.Count);
Assert.AreEqual(1, bankHeader.BankCashDetails[0].Payments.Count);
Assert.AreEqual(118M, bankHeader.BankCashDetails[0].Payments[0].PaymentAmount);
Assert.AreEqual(1M, bankHeader.BankCashDetails[0].Payments[0].PaymentDiscount);
Assert.AreEqual(0, bankHeader.BankCashDetails[0].Payments[0].InvoiceHeader.Balance);
}
[TestMethod]
[ExpectedException(typeof(ApplicationException))]
public void TestDuplicateInvoiceNumber()
{
IList<InvoiceDiaryHeader> list = new List<InvoiceDiaryHeader>();
list.Add(CreateSales("100", true, 2, "01-09-2008"));
list.Add(CreateSales("200", true, 2, "01-09-2008"));
bankHeader = CreateMultiplePayments(list, 3, 300, 0);
bankHeader.Save();
Assert.Fail("expected an ApplicationException");
}
[TestMethod]
public void TestMultipleSalesPaymentWithPaymentDiscount()
{
IList<InvoiceDiaryHeader> list = new List<InvoiceDiaryHeader>();
list.Add(CreateSales("119", true, 11, "01-09-2008"));
list.Add(CreateSales("400", true, 12, "02-09-2008"));
list.Add(CreateSales("600", true, 13, "03-09-2008"));
list.Add(CreateSales("25,40", true, 14, "04-09-2008"));
bankHeader = CreateMultiplePayments(list, 5, 1144.00M, 0.40M);
bankHeader.Save();
Assert.AreEqual(1, bankHeader.BankCashDetails.Count);
Assert.AreEqual(4, bankHeader.BankCashDetails[0].Payments.Count);
Assert.AreEqual(118.60M, bankHeader.BankCashDetails[0].Payments[0].PaymentAmount);
Assert.AreEqual(400, bankHeader.BankCashDetails[0].Payments[1].PaymentAmount);
Assert.AreEqual(600, bankHeader.BankCashDetails[0].Payments[2].PaymentAmount);
Assert.AreEqual(25.40M, bankHeader.BankCashDetails[0].Payments[3].PaymentAmount);
Assert.AreEqual(0.40M, bankHeader.BankCashDetails[0].Payments[0].PaymentDiscount);
Assert.AreEqual(0, bankHeader.BankCashDetails[0].Payments[1].PaymentDiscount);
Assert.AreEqual(0, bankHeader.BankCashDetails[0].Payments[2].PaymentDiscount);
Assert.AreEqual(0, bankHeader.BankCashDetails[0].Payments[3].PaymentDiscount);
Assert.AreEqual(0, bankHeader.BankCashDetails[0].Payments[0].InvoiceHeader.Balance);
Assert.AreEqual(0, bankHeader.BankCashDetails[0].Payments[1].InvoiceHeader.Balance);
Assert.AreEqual(0, bankHeader.BankCashDetails[0].Payments[2].InvoiceHeader.Balance);
Assert.AreEqual(0, bankHeader.BankCashDetails[0].Payments[3].InvoiceHeader.Balance);
}
[TestMethod]
public void TestSettlement()
{
IList<InvoiceDiaryHeader> list = new List<InvoiceDiaryHeader>();
list.Add(CreateSales("300", true, 43, "01-09-2008")); //Sales
list.Add(CreateSales("100", false, 6453, "02-09-2008")); //Purchase
bankHeader = CreateMultiplePayments(list, 22, 200, 0);
bankHeader.Save();
Assert.AreEqual(1, bankHeader.BankCashDetails.Count);
Assert.AreEqual(2, bankHeader.BankCashDetails[0].Payments.Count);
Assert.AreEqual(300, bankHeader.BankCashDetails[0].Payments[0].PaymentAmount);
Assert.AreEqual(-100, bankHeader.BankCashDetails[0].Payments[1].PaymentAmount);
Assert.AreEqual(0, bankHeader.BankCashDetails[0].Payments[0].InvoiceHeader.Balance);
Assert.AreEqual(0, bankHeader.BankCashDetails[0].Payments[1].InvoiceHeader.Balance);
}
Assert.AreEqual(0, bankHeader.BankCashDetails[0].Payments[3].InvoiceHeader.Balance);
それらが本当に取るに足らないものであれば、テストを気にしないでください。たとえば、このように実装されている場合。
public class User
{
public string Username { get; set; }
public string Password { get; set; }
}
逆に、(getter / setterでパスワードを暗号化および復号化するなど)巧妙な処理を行っている場合は、テストを行ってください。
この質問は、テストされるメソッドとテストされないメソッドにどこで線を引くかという質問のようです。
値を割り当てるためのセッターとゲッターは、一貫性と将来の成長を念頭に置いて作成されており、しばらくすると、セッター/ゲッターがより複雑な操作に進化する可能性があると予測しています。一貫性と将来の成長のためにも、これらのメソッドの単体テストを実施することは理にかなっています。
コードの信頼性は、特に追加機能を追加するための変更が行われている間は、第一の目標です。私はテスト方法論にセッター/ゲッターを含めたことで解雇された人を誰も知らないが、私が知っていた、または思い出せる簡単なセット/ゲットラッパーであるメソッドをテストしたいと思っていた人が確かにいるが、それはなかった長いケース。
チームの別のメンバーが、set / getメソッドを拡張して、テストが必要になったがテストを作成しなかったロジックを含めるようにした可能性があります。しかし今、コードはこれらのメソッドを呼び出しており、それらが変更されたことや、詳細なテストが必要であることを認識していません。また、開発およびQAで行うテストは欠陥を引き起こしませんが、リリース初日の実際のビジネスデータではそれをトリガーします。
2人のチームメイトは、セット/ゲットが失敗してユニットテストでカバーされないロジックを含むようにモーフィングされたときに、誰がボールを落としてユニットテストに失敗したかについて議論します。最初にset / getsを作成したチームメイトは、単純なset / getsに対してテストが初日から実装されていれば、このクリーンから抜け出すのが簡単になります。
私の意見では、ユニットテストですべてのメソッドをカバーする数分の「無駄な」時間は、たとえ些細なものであっても、道のりの頭痛やお金の損失/ビジネスの評判、そして誰かの仕事の損失を節約できるかもしれません。
そして、単純なメソッドを単体テストでラップしたという事実は、ジュニアチームメイトが単純なメソッドを重要なメソッドに変更し、テストを更新するように促すときに、欠陥が含まれていたので誰も問題を抱えていないように見えるかもしれません。生産に達することから。
私たちがコーディングする方法、および私たちのコードから見ることができる規律は、他の人を助けることができます。
ゲッターやセッターのように、プライベートフィールドを設定する以外に特別な動作がないコードは本当に簡単です。3.0では、C#には、コンパイラーがプライベートフィールドを処理する構文糖も含まれているため、プログラムする必要はありません。
私は通常、クラスに期待する動作を検証する非常に単純なテストを多数記述します。2つの数値を加算するような単純なものであっても。簡単なテストを書くことと、コードのいくつかの行を書くことの間で多くのことを切り替えます。これは、自分が思いもよらないことを壊してしまうことを恐れずに、コードを変更できるからです。
すべてをテストする必要があります。現在、ゲッターとセッターがありますが、ある日、検証などを行うために、多少変更する可能性があります。今日作成するテストは明日使用され、すべてが通常どおり機能し続けることを確認します。テストを作成するときは、「今のところそれは取るに足らないこと」などの考慮事項を忘れてください。アジャイルまたはテスト駆動のコンテキストでは、将来のリファクタリングを想定してテストする必要があります。また、非常に長い文字列やその他の「悪い」コンテンツなど、本当に奇妙な値を入力してみましたか?まああなたは...あなたのコードが将来どれほどひどく悪用される可能性があると思い込まないでください。
一般的に、広範囲にわたるユーザーテストを書くことは片側にあり、疲れることに気づきます。一方、アプリケーションの動作に関する貴重な洞察を常に提供し、簡単な(そして誤った)仮定(たとえば、ユーザー名の長さが常に1000文字未満になる)を捨てるのに役立ちます。
ゲッターとセッターのためのユニットテストを書くのに害はありません。現時点では、内部でフィールドの取得/設定を行っているだけかもしれませんが、将来的には検証ロジックや、テストする必要のあるプロパティ間の依存関係が存在する可能性があります。あなたがそれについて考えている間に今それを書いて、その時が来たらそれを改造することを覚えていることは今より簡単です。
壊れる可能性があると思ったら、テストを書いてください。私は通常、setter / getterをテストしませんが、姓と名を連結するUser.Nameの1つを作成すると言うと、誰かが姓と名の順序を変更した場合、少なくとも彼は知っています。彼はテストされた何かを変えました。
クラスのすべてのメソッドの実行をUTでカバーし、メソッドの戻り値を確認する必要があります。これには、特にメンバー(プロパティ)が複雑なクラスであり、初期化中に大量のメモリを割り当てる必要がある場合に、ゲッターとセッターが含まれます。たとえば、非常に大きな文字列(またはギリシャ記号の文字)を使用してセッターを呼び出し、結果が正しいことを確認します(切り捨てられていない、エンコーディングが適切など)。
適用される単純な整数の場合-整数の代わりにlongを渡すとどうなりますか?それがあなたがUTを書く理由です:)
理想的には、クラスを作成しているときに単体テストを実行することになります。これは、テスト駆動開発を使用するときに行う方法です。各関数ポイントを実装するときにテストを追加し、エッジケースもテストでカバーするようにします。
後でテストを書くのはもっと大変ですが、実行可能です。
これが私があなたの立場でやることです:
これにより、リグレッションに対する適切なバッファーとして機能するユニットテストの優れたワーキングセットが得られます。
このアプローチの唯一の問題は、コードを設計する必要があることですでテストにする必要があることです。早い段階でカップリングのミスをした場合、高いカバレッジを簡単に得ることができません。
これが、コードを書く前にテストを書くことが本当に重要である理由です。疎結合のコードを書くことを強制します。
get / setであっても、実装方法によっては奇妙な結果になる可能性があるため、メソッドとして扱う必要があります。
これらの各テストでは、プロパティのパラメーターのセットを指定し、受け入れ可能なプロパティと受け入れられないプロパティの両方を定義して、呼び出しが期待どおりに戻る/失敗するようにします。
また、SQLインジェクションの例として、セキュリティの問題点を認識し、これらをテストする必要があります。
そのため、プロパティのテストについて心配する必要があります。
GUIインターフェースの外部でテスト可能なコードを書いているすべてのテストを作成します。
通常、別の層またはビジネスロジックレイヤー内に配置するビジネスロジックを持つ、私が作成するロジック。
次に、何かを行うためのテストを書くのは簡単です。
最初のパスでは、「ビジネスロジックレイヤー」の各パブリックメソッドの単体テストを記述します。
私がこのようなクラスを持っていた場合:
public class AccountService
{
public void DebitAccount(int accountNumber, double amount)
{
}
public void CreditAccount(int accountNumber, double amount)
{
}
public void CloseAccount(int accountNumber)
{
}
}
これらのアクションを実行する必要があることを知っているコードを記述する前に最初に行うことは、単体テストの記述を開始することです。
[TestFixture]
public class AccountServiceTests
{
[Test]
public void DebitAccountTest()
{
}
[Test]
public void CreditAccountTest()
{
}
[Test]
public void CloseAccountTest()
{
}
}
テストを記述して、何かを行うために記述したコードを検証します。物事のコレクションを繰り返し処理し、それぞれについて何かを変更する場合は、同じことを行うテストと実際に発生したアサートを記述します。
実行できるアプローチは他にもたくさんあります。つまり、Behavoir Driven Development(BDD)はもっと複雑で、ユニットテストのスキルから始めるのに最適な場所ではありません。
ですから、話の教訓は、あなたが心配するかもしれないことを何でもすることを何でもテストし、ユニットテストがサイズの小さい特定のものをテストし続けることです、多くのテストが良いです。
ビジネスロジックをユーザーインターフェースレイヤーの外側に置いて、テストを簡単に記述できるようにしてください。
TestDriven.NetまたはReSharperをお勧めします。どちらもVisual Studioに簡単に統合できます。
AuthenticateメソッドとSaveメソッドに複数のテストを記述することをお勧めします。成功例(すべてのパラメーターが提供され、すべてのスペルが正しいなど)に加えて、さまざまな失敗例(パラメーターが正しくないか欠落している、該当する場合はデータベース接続が利用できないなど)についてテストすることをお勧めします。参考として、NUnitを使用したC#の実用的なユニットテストをお勧めします。
他の人が述べたように、ゲッターとセッターに条件付きロジックがない限り、ゲッターとセッターの単体テストはやりすぎです。
コードでテストが必要な場所を正確に推測することは可能ですが、私は通常、この推測を裏付けるメトリックが必要だと思います。私の見解では、ユニットテストはコードカバレッジメトリックと連動しています。
たくさんのテストが含まれているが、カバレッジが小さいコードは十分にテストされていません。とはいえ、100%カバレッジのコードで境界とエラーのケースをテストしていないのも、あまり良くありません。
高いカバレッジ(90%以上)と変動する入力データのバランスが必要です。
「ガベージイン」をテストすることを忘れないでください!
また、単体テストは、失敗をチェックしない限り、単体テストではありません。アサートがない、または既知の例外でマークされている単体テストは、実行時にコードが停止しないことをテストするだけです。
テストは常に失敗または予期しない/望ましくないデータを報告するようにテストを設計する必要があります。
ソフトウェア開発者がテスト駆動開発を行うときに忘れてしまうことの1つは、私たちの行動の背後にある目的です。実稼働コードがすでに配置された後にユニットテストが記述されている場合、テストの値は非常に低くなります(完全に失われるわけではありません)。
ユニットテストの真の精神では、これらのテストは、主にコードを「テスト」するためのものではありません。または、90%-100%のコードカバレッジを向上させます。これらはすべて、最初にテストを記述することの利点です。大きな見返りは、TDDの自然なプロセスにより、プロダクションコードの終わりが大幅に改善されることです。
この考えをよりよく伝えるために、以下の資料が参考になるかもしれません。
より多くの単体テストを作成することが、より高品質の製品を得るのに役立つと私たちが感じた場合、テスト駆動開発の貨物カルトに苦しんでいる可能性があります。