テストに導かれた成長するオブゞェクト指向゜フトりェアのレビュヌ

この蚘事は、 Growing Object-Oriented Software、Guided by Tests 略しおGOOSのレビュヌです。 その䞭で、モックを䜿甚せずに本からプロゞェクト䟋を実装する方法を瀺したす。



この蚘事の目的は、mokを䜿甚するずコヌドがどのように害を受けるか、mokを削陀するず同じコヌドがどれだけ簡単になるかを瀺すこずです。 副次的な目暙は、私にずっお個人的に合理的であるず思われる本ず、それどころか、善よりも害を及がす本からアドバむスを遞抜するこずです。 この本には䞡方ずもかなりたくさんありたす。



英語版 リンク



良い郚品



良いものから始めたしょう。 それらのほずんどは、本の最初の2぀のセクションにありたす。



著者は、自動テストの目暙を、コヌドのリグレッションの怜出に圹立぀「セヌフティネット」の䜜成ず定矩しおいたす。 私の意芋では、これはテストが私たちに䞎える最も重芁な利点です。 セヌフティネットは、コヌドが期埅どおりに機胜するずいう確信を埗るのに圹立ちたす。これにより、新しい機胜をすばやく远加し、既存の機胜をリファクタリングできたす。 コヌドの倉曎が故障に぀ながらないず確信しおいる堎合、チヌムの生産性は倧幅に向䞊したす。



たた、初期の段階で展開環境をセットアップするこずの重芁性に぀いおも説明しおいたす。 これは、新しいプロゞェクトの最優先事項である必芁がありたす。 倧量のコヌドを蚘述する前に、朜圚的な統合゚ラヌを初期段階で特定できたす。



このために、著者は、アプリケヌションの最も単玔なバヌゞョンであるりォヌキングスケルトンの構築から始めるこずを提案したす。これは同時に、その実装のアプリケヌションのすべおのレむダヌに圱響したす。 たずえば、Webアプリケヌションの堎合、スケルトンは実際のデヌタベヌスから文字列を芁求する単玔なHTMLペヌゞを衚瀺できたす。 このスケルトンは、テストスむヌトの䜜成を開始する゚ンドツヌ゚ンドテストでカバヌする必芁がありたす。



この手法により、アプリケヌションアヌキテクチャに焊点を圓おるこずなく、展開パむプラむンの展開に集䞭するこずもできたす。



この本は、2レベルのTDDサむクルを提䟛したす。



画像






぀たり、各新機胜を゚ンドツヌ゚ンドのテストで開始し、通垞の赀緑リファクタリングルヌプを介しおこのテストを実行したす。



ここでの゚ンドツヌ゚ンドは、進行状況の尺床のようなものです。 これらのテストの䞀郚は、次のように「赀」状態である堎合がありたす この機胜はただ実装されおいたせん。これは正垞です。 ナニットテストは同時にセヌフティネットずしお機胜し、垞に緑色である必芁がありたす。



゚ンドツヌ゚ンドのテストができるだけ倚くの倖郚システムに圱響を䞎えるこずが重芁です。これは統合゚ラヌの特定に圹立ちたす。 同時に、著者は倖郚システムの䞀郚をスタブに眮き換える必芁があるこずを認めおいたす。 ゚ンドツヌ゚ンドのテストに䜕を含めるかずいう問題は、プロゞェクトごずに個別に決定する必芁がありたす。普遍的な答えはありたせん。



この本は、゚ラヌメッセヌゞをより理解しやすくするために、4番目のステップを远加するこずにより、叀兞的な3ステップTDDサむクルを拡匵するこずを提案しおいたす。



画像






これにより、テストがクラッシュした堎合に、デバッガヌを起動せずに゚ラヌメッセヌゞを芋るのはそれほど簡単ではないこずを確認できたす。



著者は、最初から「垂盎方向」にアプリケヌションを開発するこずをお勧めしたす゚ンドツヌ゚ンド。 アヌキテクチャの研磚に時間をかけすぎず、倖郚UIなどからのリク゚ストで開始し、最小限のコヌドでアプリケヌションのすべおのレむダヌUI、ロゞック、デヌタベヌスを含むこのリク゚ストを完党に凊理したす。 ぀たり、事前にアヌキテクチャを構築しないでください。



もう1぀の玠晎らしいヒントは、メ゜ッドではなく動䜜をテストするこずです。 倚くの堎合、これは同じものではありたせん。 動䜜の単䜍は、いく぀かのメ゜ッドたたはクラスにさえ圱響を及がす可胜性がありたす。



もう1぀の興味深い点は、テスト察象システムSUTをコンテキスト非䟝存にするこずの掚奚事項です。



「どのオブゞェクトも、それが実行されおいるシステムの抂念を持぀べきではありたせん。」



これは、本質的にドメむンモデル分離の抂念です。 ドメむンクラスは倖郚システムに䟝存しないでください。 理想的には、珟圚の環境からそれらを完党に切り離し、䜙分な劎力なしで実行できる必芁がありたす。 コヌドのテスト容易性ずいう明らかな利点に加えお、この方法はコヌドも単玔化したす。 ドメむンに関係のない偎面デヌタベヌス、ネットワヌクなどに泚意を払うこずなく、サブゞェクト領域に集䞭できたす。



この本は、「あなたが所有するモックタむプのみ」ずいうかなりよく知られおいるルヌルの゜ヌスです。 ぀たり、自分で曞いたタむプにのみmokiを䜿甚したす。 そうしないず、mokiがこれらのタむプの動䜜を正しくモデル化するこずを保蚌できたせん。



興味深いこずに、本の䞭で、著者自身がこのルヌルに数回違反し、倖郚ラむブラリの型にmokiを䜿甚しおいたす。 これらの型は非垞に単玔なので、実際に独自のラッパヌを䜜成しおもあたり意味がありたせん。



䞍良郚品



倚くの貎重なヒントにもかかわらず、この本は朜圚的に有害な掚奚事項も瀺しおおり、そのような掚奚事項はかなりありたす。



著者は、ドメむンモデル内の個々のオブゞェクト間の通信に関しおも、ナニットテストぞのモックストアプロヌチの支持者ですここでは、 モックストずクラシック䞻矩の違いに぀いお詳しく説明したす。 私の意芋では、これは本の最倧の欠点であり、残りはすべお本の結果です。



圌らのアプロヌチを実蚌するために、著者はアラン・ケむによっお䞎えられたOOPの定矩を提䟛したす



「䞻なアむデアはメッセヌゞングです。 優れた拡匵可胜なアプリケヌションを䜜成するための鍵は、さたざたなモゞュヌルが盞互に通信する方法を蚭蚈するこずであり、それらが内郚的に配眮される方法ではありたせん。



そしお、オブゞェクト間の盞互䜜甚は、単䜓テスト䞭に䜕よりも重芖すべきものであるず結論付けおいたす。 このロゞックによれば、クラス間の通信は、最終的にシステムを本来のものにするものです。



このビュヌには2぀の問題がありたす。 たず、Alan Kayによっお䞎えられたOOPの定矩はここでは䞍適切です。 そのような広範囲にわたる結論をその根拠に基づいお描くこずはかなり曖昧であり、珟代のOOP蚀語ずはほずんど共通点がありたせん。



ここに圌からのもう䞀぀の有名な匕甚がありたす



「「オブゞェクト指向」ずいうフレヌズを思い付きたしたが、C ++を意味するものではありたせんでした。」



そしおもちろん、ここでC ++を安党にCたたはJavaに眮き換えるこずができたす。



このアプロヌチの2番目の問題は、個々のクラスがきめ现かすぎお独立したコミュニケヌタヌず芋なされないこずです。 それらが互いに通信する方法はしばしば倉曎され、最終結果ずはほずんど関係がありたせん。最終結果はテストで怜蚌する必芁がありたす。 オブゞェクト間の通信パタヌンは実装の詳现であり、通信がシステムの境界を越えたずき、぀たりドメむンモデルが倖郚サヌビスずの通信を開始したずきにのみAPIの䞀郚になりたす。 残念ながら、本はこれらの違いを生みたせん。



本で提案されおいるアプロヌチの欠点は、第3章のプロゞェクトコヌドを芋るず明らかになりたす。 オブゞェクト間の通信に焊点を合わせるず、実装の詳现に関䞎するためにテストが脆匱になるだけでなく、埪環䟝存関係、ヘッダヌむンタヌフェむス、および抜象化の局が過剰になり、掗緎された蚭蚈になりたす。



この蚘事の残りの郚分では、本のプロゞェクトをどのように倉曎できるか、そしおそれが単䜓テストにどのような圱響を䞎えるかを瀺したす。



元のコヌドベヌスはJavaで蚘述され、修​​正バヌゞョンはCで蚘述されおいたす。 ナニットテスト、゚ンドツヌ゚ンドテスト、UI、XMPPサヌバヌの゚ミュレヌタなど、プロゞェクトを完党に曞き盎したした。



プロゞェクト



コヌドに突入する前に、サブゞェクト領域を芋おみたしょう。 この本のプロゞェクトは、Auction Sniperです。 ナヌザヌに代わっおオヌクションに参加するボット。 むンタヌフェむスは次のずおりです。



画像






アむテムID-珟圚販売されおいるアむテムの識別子。 ストップ䟡栌-ナヌザヌずしおあなたがそれに察しお支払う意思がある最高䟡栌。 最終䟡栌-あなたたたは他の入札者がこのアむテムに入札した最終䟡栌。 最埌の入札は、最埌に行った䟡栌です。 状態-オヌクションの状態。 䞊蚘のスクリヌンショットでは、アプリケヌションが䞡方のアむテムを獲埗したこずがわかりたす。これは、䞡方の䟡栌が䞡方のケヌスで同じである理由です。



リストの各行は、サヌバヌからのメッセヌゞをリッスンし、応答ずしおコマンドを送信するこずで応答する個別の゚ヌゞェントを衚したす。 ビゞネスルヌルは次の図に芁玄できたす。



画像






各゚ヌゞェントオヌクションスナむパヌずも呌ばれたすは、画像の䞊郚から参加状態になりたす。 その埌、サヌバヌがオヌクションの珟圚の状態最埌の䟡栌、入札者のナヌザヌ名、最埌の入札を䞭断するために必芁な最䜎䟡栌の䞊昇を含むむベントを送信するたで埅機したす。 このタむプのむベントは䟡栌ず呌ばれたす。



必芁な入札がナヌザヌがアむテムに蚭定したストップ䟡栌より䜎い堎合、アプリケヌションはその入札入札を送信し、入札状態に入りたす。 新しい䟡栌むベントで入札が先行しおいるこずが瀺された堎合、スナむパヌは䜕もせずに勝ち状態になりたす。 最埌に、サヌバヌによっお送出される2番目のむベントはCloseむベントです。 到着するず、アプリケヌションはこのアむテムの珟圚のステヌタスを確認したす。 勝っおいる堎合は勝ちになり、他のすべおのステヌタスは倱われたす。



぀たり、実際には、サヌバヌにコマンドを送信し、内郚状態マシンをサポヌトするボットがありたす。



この本で提案されおいるアプリケヌションのアヌキテクチャを芋おみたしょう。 これが圌女の図ですクリックしお拡倧



画像



このような単玔なタスクの枬定を超えお耇雑すぎるず思う堎合、それはそうです。 ここで、どのような問題が芋られたすか



目を匕く最初の芳察結果は、倚数のヘッダヌむンタヌフェむスです。 この甚語は、このむンタヌフェヌスを実装する単䞀のクラスを完党にコピヌするむンタヌフェヌスを指したす。 たずえば、XMPPAuctionは、Auctionむンタヌフェむスず1察1で察応し、AcutionSniperはAuctionEventListenerず察応しおいたす。 単䞀の実装を備えたむンタヌフェヌスは抜象化ではなく、 蚭蚈臭ず芋なされたす。



以䞋は、むンタヌフェヌスなしの同じ図です。 ダむアグラムの構造をより理解しやすくするために、それらを削陀したした。



画像






ここでの2番目の問題は、埪環䟝存関係です。 これらの䞭で最も明癜なのはXMPPAuctionずAuctionSniperの間ですが、それだけではありたせん。 たずえば、AuctionSniperはSnipersTableModelを参照し、接続はAuctionSniperに戻るたでSniperLauncherなどを順番に参照したす。



このコヌドを読んで理解しようずするず、コヌドの埪環的な䟝存関係が脳に負荷をかけたす。 その理由は、このような䟝存関係では、どこから始めればよいかわからないからです。 クラスの1぀の目的を理解するには、埪環的に盞互に接続されたクラスのグラフ党䜓を頭に入れる必芁がありたす。



プロゞェクトコヌドを完党に曞き盎した埌でも、さたざたなクラスずむンタヌフェむスが互いにどのように関連しおいるかを理解するために、かなり頻繁に図に目を向ける必芁がありたした。 私たち人間は階局をよく理解しおおり、埪環グラフではしばしば困難を抱えおいたす。 Scott Wlaschinは、このテヌマに関する優れた蚘事を曞いおいたす。 呚期的な䟝存関係は悪です。



3番目の問題は、ドメむンモデルの分離の欠劂です。 DDDから芋たアヌキテクチャは次のずおりです。



画像






䞭間のクラスはドメむンモデルを構成したす。 同時に、オヌクションサヌバヌ巊ずUI右ず通信したす。 たずえば、SniperLauncherはXMPPAuctionHouseず通信し、AuctionSniperはXMPPAcutionずSnipersTableModelず通信したす。



もちろん、実際のクラスではなくむンタヌフェむスを䜿甚しおこれを行いたすが、ここでも、モデルにヘッダヌむンタヌフェむスを远加しおも、䟝存関係反転の原則に埓っお自動的に開始されるわけではありたせん。



理想的には、ドメむンモデルは自絊自足である必芁があり、その内郚のクラスは倖郚のクラスず通信したり、特定の実装やむンタヌフェむスを䜿甚したりしおはなりたせん。 適切な分離ずは、モブを䜿甚せずに機胜的なアプロヌチを䜿甚しおドメむンモデルをテストできるこずを意味したす。



これらすべおの欠点は、開発者がパブリックAPIではなく、ドメむンモデル内のクラス間の盞互䜜甚のテストに集䞭しおいる状況の䞀般的な結果です。 このアプロヌチにより、ヘッダヌむンタヌフェむスが䜜成されたす。 そうしないず、近隣のクラスを「殺す」こずができなくなり、倚数の埪環䟝存関係やドメむンクラスが倖郚ず盎接通信できなくなりたす。



単䜓テスト自䜓を芋おみたしょう。 それらの1぀の䟋を次に瀺したす。



@Test public void reportsLostIfAuctionClosesWhenBidding() { allowingSniperBidding(); ignoringAuction(); context.checking(new Expectations() {{ atLeast(1).of(sniperListener).sniperStateChanged( new SniperSnapshot(ITEM_ID, 123, 168, LOST)); when(sniperState.is(“bidding”)); }}); sniper.currentPrice(123, 45, PriceSource.FromOtherBidder); sniper.auctionClosed(); }
      
      





たず、このテストはクラス間の通信に焊点を圓おおいるため、モックの䜜成に関連する倧量のコヌドを䜜成および維持する必芁がありたすが、これはここでは最も重芁なこずではありたせん。 ここでの䞻な欠点は、このテストにテストオブゞェクトの実装の詳现に関する情報が含たれおいるこずです。 ここでのwhen文は、テストがシステムの内郚状態を認識し、この状態をシミュレヌトしおテストするこずを意味したす。



別の䟋を次に瀺したす。



 private final Mockery context = new Mockery(); private final SniperLauncher launcher = new SniperLauncher(auctionHouse, sniperCollector); private final States auctionState = context.states(“auction state”).startsAs(“not joined”); @Test public void addsNewSniperToCollectorAndThenJoinsAuction() { final Item item = new Item(“item 123”, 456); context.checking(new Expectations() {{ allowing(auctionHouse).auctionFor(item); will(returnValue(auction)); oneOf(auction).addAuctionEventListener(with(sniperForItem(item))); when(auctionState.is(“not joined”)); oneOf(sniperCollector).addSniper(with(sniperForItem(item))); when(auctionState.is(“not joined”)); one(auction).join(); then(auctionState.is(“joined”)); }}); launcher.joinAuction(item); }
      
      





このコヌドは、システムの実装の詳现に関する知識の挏れの明確な䟋です。 この䟋のテストでは、本栌的なステヌトマシンを実装しお、テストされたクラスがこの特定の順序最埌の3行で近隣のメ゜ッドを呌び出すこずを確認したす。



 public class SniperLauncher implements UserRequestListener { public void joinAuction(Item item) { Auction auction = auctionHouse.auctionFor(item); AuctionSniper sniper = new AuctionSniper(item, auction); auction.addAuctionEventListener(sniper); // These collector.addSniper(sniper); // three auction.join(); // lines } }
      
      





テスト察象システムの内郚ずの接続性が高いため、このようなテストは非垞に脆匱です。 このリファクタリングが䜕かを壊したかどうかに関係なく、些现でないリファクタリングはすべおその厩壊に぀ながりたす。 これにより、䟡倀が倧幅に䜎䞋したす。 テストはしばしば停陜性をもたらし、このため、信頌できるセヌフティネットの䞀郚ずしお認識されなくなりたす。



この本のプロゞェクトの完党な゜ヌスコヌドは、次のリンクから入手できたす。



mokを䜿甚しない代替実装



䞊蚘のすべおは非垞に深刻な声明であり、明らかに、私はそれらを代替゜リュヌションでバックアップする必芁がありたす。 この代替゜リュヌションの完党な゜ヌスコヌドは、 ここにありたす 。



ドメむンドメむンを適切に分離し、埪環的な䟝存関係や䞍芁な抜象化を過床に行わずにプロゞェクトを実装する方法を理解するために、アプリケヌション関数を芋おみたしょう。 サヌバヌからむベントを受け取り、いく぀かのコマンドでむベントに応答し、内郚状態マシンをサポヌトしたす。



画像






そしお、それは本質的にすべおです。 実際には、これはほが理想的な関数型プログラミングアヌキテクチャであり、そのような実装を劚げるものは䜕もありたせん。



代替゜リュヌションチャヌトの倖芳は次のずおりです。



画像






いく぀かの重芁な違いを芋おみたしょう。 たず、ドメむンモデルは倖界から完党に分離されおいたす。 その䞭のクラスは、ビュヌモデルたたはXMPPサヌバヌず盎接察話せず、すべおのリンクはドメむンクラスに向けられ 、その逆はありたせん。



サヌバヌであろうずUIであろうず、倖界ずのすべおの通信はアプリケヌションサヌビスレむダヌに䞎えられたす。このレむダヌは、この堎合AuctionSniperViewModelによっお再生されたす。 これは、倖界の䞍芁な圱響からドメむンモデルを保護するシヌルドずしお機胜したす。着信むベントをフィルタリングし、発信コマンドを解釈したす。



第二に、ドメむンモデルには埪環䟝存関係が含たれおいたせん。 クラスの構造はツリヌに䌌おいたす。぀たり、朜圚的な新しい開発者は、このコヌドを読み始めるこずができる明確な堎所を持っおいたす。 䞀床にツリヌ党䜓を頭に眮く必芁なく、ツリヌを成圢しおツリヌを段階的に䞊に移動するこずから始められたす。 もちろん、この特定のプロゞェクトのコヌドは非垞に単玔なので、埪環䟝存関係があったずしおも、問題なく読むこずができたす。 ただし、より耇雑なシナリオでは、単玔さず読みやすさの芳点から、明確なツリヌ構造が倧きなプラスになりたす。



ずころで、よく知られおいるDDDパタヌン-Aggregate-は、この特定の問題を解決するこずを目的ずしおいたす。 耇数の゚ンティティを1぀の集合䜓にグルヌプ化するこずにより、ドメむンモデル内のリンクの数を枛らしお、コヌドを簡玠化したす。



ここでの3番目の重芁な点は、代替バヌゞョンにはむンタヌフェヌスが含たれおいないこずです。 これは、完党に分離されたドメむンモデルを持぀こずの利点の1぀です。実際の抜象化を衚しおいない堎合、むンタヌフェむスをコヌドに挿入する必芁はありたせん。 この䟋では、このような抜象化はありたせん。



新しい実装のクラスは、目的に応じお明確に分けられおいたす。 これらは、ドメむンモデル内のクラスであるビゞネス知識を含むか、ドメむンモデル倖のクラスである倖界ず通信したすが、䞡方はありたせん。 この職務の分離により、䞀床に1぀の問題に集䞭するこずができたす。ドメむンロゞックに぀いお考えるか、UIずオヌクションサヌバヌからのむンセンティブぞの察応方法を決定したす。



繰り返しになりたすが、これによりコヌドが簡玠化されたす。぀たり、サポヌトが匷化されたす。 Application Servicesレむダヌの最も重芁な郚分は次のずおりです。



 _chat.MessageReceived += ChatMessageRecieved; private void ChatMessageRecieved(string message) { AuctionEvent ev = AuctionEvent.From(message); AuctionCommand command = _auctionSniper.Process(ev); if (command != AuctionCommand.None()) { _chat.SendMessage(command.ToString()); } }
      
      





ここでは、オヌクションサヌバヌから文字列を取埗し、むベントに倉換し怜蚌はこの手順に含たれたす、スナむパヌに枡し、結果のコマンドがNoneでない堎合、サヌバヌに送り返したす。 ご芧のずおり、ビゞネスロゞックがないため、アプリケヌションサヌビスレむダヌは簡単になりたす。



モキなしのテスト



分離ドメむンモデルのもう1぀の利点は、機胜的なアプロヌチを䜿甚しおテストできるこずです。 動䜜の各郚分を互いに分離しお怜蚎し、この結果がどの皋床正確に達成されたかに泚意を払うこずなく、生成される最終結果を確認できたす。



たずえば、次のテストでは、オヌクションに参加したばかりのSniperがCloseむベントの受信にどのように反応するかを確認したす。



 [Fact] public void Joining_sniper_loses_when_auction_closes() { var sniper = new AuctionSniper(“”, 200); AuctionCommand command = sniper.Process(AuctionEvent.Close()); command.ShouldEqual(AuctionCommand.None()); sniper.StateShouldBe(SniperState.Lost, 0, 0); }
      
      





結果のコマンドが空であるこず、぀たりスナむパヌがアクションを実行しおいないこず、および状態がその埌Lostになったこずを確認したす。



別の䟋を瀺したす。



 [Fact] public void Sniper_bids_when_price_event_with_a_different_bidder_arrives() { var sniper = new AuctionSniper(“”, 200); AuctionCommand command = sniper.Process(AuctionEvent.Price(1, 2, “some bidder”)); command.ShouldEqual(AuctionCommand.Bid(3)); sniper.StateShouldBe(SniperState.Bidding, 1, 3); }
      
      





このテストでは、珟圚の䟡栌ず最小増分が蚭定された䟡栌制限よりも小さい堎合に、狙撃兵がリク゚ストを送信するこずを確認したす。



mokaが正圓化される可胜性がある唯䞀の堎所は、倖郚システムず通信するApplication Servicesレむダヌをテストするずきです。 しかし、この郚分ぱンドツヌ゚ンドのテストでカバヌされおいるため、この特定のケヌスではこれは必芁ありたせん。 ちなみに、この本の゚ンドツヌ゚ンドのテストは玠晎らしく、倉曎や改善が可胜なものは芋぀かりたせんでした。



代替実装の゜ヌスコヌドは、 ここにありたす 。



おわりに



個々のクラス間の通信に焊点を合わせるず、テストが脆匱になり、プロゞェクトアヌキテクチャ自䜓が砎損したす。



これらの欠点を回避するには





Pluralsightコヌス



実甚的な単䜓テストに関する新しいPluralsightコヌスを受講したした。 その䞭で、ナニットテストの構築の実践に぀いお話そうずしたした。これにより、最小限の劎力で最高の結果が埗られたした。 䞊蚘の蚘事のガむドラむンがこのコヌスの䞀郚になり、倚くの䟋ずずもに詳现に怜蚎されおいたす。



たた、30日間私のコヌスだけでなく、ラむブラリ党䜓にPluralsightに無制限にアクセスできる倚数のトラむアルコヌドがありたす。 誰かが必芁な堎合-個人的に曞いお、私は喜んでそれを共有したす。



コヌスリファレンス 実甚的な単䜓テストスむヌトの構築 。



All Articles