sql >> データベース >  >> RDS >> Database

古いプロジェクトにTDDを実装するためのルール

    「リポジトリパターンのスライディング責任」という記事は、答えるのが非常に難しいいくつかの質問を提起しました。技術的な詳細を完全に無視することが不可能な場合、リポジトリが必要ですか?追加する価値があると見なすには、リポジトリをどの程度複雑にする必要がありますか?これらの質問に対する答えは、システムの開発に重点が置かれていることによって異なります。おそらく最も難しい質問は次のとおりです。リポジトリも必要ですか? 「流れるような抽象化」の問題と、抽象化のレベルの増加に伴うコーディングの複雑さの増大により、フェンスの両側を満足させるソリューションを見つけることができません。たとえば、レポートでは、意図の設計により、フィルターと並べ替えごとに多数のメソッドが作成され、一般的なソリューションではコーディングのオーバーヘッドが大きくなります。

    全体像を把握するために、レガシーコードでのアプリケーションの観点から抽象化の問題を調べました。この場合、リポジトリは、高品質でバグのないコードを取得するためのツールとしてのみ関心があります。もちろん、このパターンだけがTDDプラクティスの適用に必要なものではありません。いくつかの大規模なプロジェクトの開発中に大量の塩を食べ、何が機能し、何が機能しないかを観察した後、TDDの実践に従うのに役立ついくつかのルールを自分で開発しました。私は建設的な批判やTDDを実装する他の方法を受け入れています。

    序文

    古いプロジェクトではTDDを適用できないことに気付く人もいるかもしれません。古いコードを理解するのは非常に難しいため、さまざまなタイプの統合テスト(UIテスト、エンドツーエンド)の方が適しているという意見があります。また、コードがどのように機能するかわからない場合があるため、実際のコーディングの前にテストを作成すると、時間のロスにつながるだけであると聞くことができます。私はいくつかのプロジェクトに取り組む必要がありましたが、単体テストは指標ではないと信じて、統合テストのみに限定されていました。同時に、多くのテストが作成され、多くのサービスが実行されました。その結果、実際にテストを作成したのは1人だけでした。

    練習中、私はいくつかの非常に大規模なプロジェクトに取り組むことができました。そこでは、多くのレガシーコードがありました。それらのいくつかはテストを特徴としており、他はそうではありませんでした(それらを実装する意図だけがありました)。私は2つの大きなプロジェクトに参加しましたが、そこではどういうわけかTDDアプローチを適用しようとしました。初期の段階では、TDDはテストファースト開発として認識されていました。最終的に、この単純化された理解と、略してBDDと呼ばれる現在の認識との違いが明らかになりました。どちらの言語を使用する場合でも、主なポイントは、私がそれらをルールと呼んでいますが、同じままです。誰かがルールと良いコードを書く他の原則との類似点を見つけることができます。

    ルール1:ボトムアップ(裏返し)の使用

    このルールは、作業中のプロジェクトに新しいコードを埋め込む際の分析とソフトウェア設計の方法を指します。

    新しいプロジェクトを設計するとき、システム全体を描くのは絶対に自然なことです。この段階で、コンポーネントのセットとアーキテクチャの将来の柔軟性の両方を制御します。したがって、互いに簡単かつ直感的に統合できるモジュールを作成できます。このようなトップダウンアプローチにより、将来のアーキテクチャの優れた先行設計を実行し、必要なガイドラインを記述し、最終的に必要なものの全体像を把握することができます。しばらくすると、プロジェクトはいわゆるレガシーコードに変わります。そして、楽しみが始まります。

    多数のモジュールとそれらの間の依存関係を持つ既存のプロジェクトに新しい機能を埋め込む必要がある段階では、適切な設計を行うためにそれらすべてを頭の中に置くことは非常に難しい場合があります。この問題のもう1つの側面は、このタスクを実行するために必要な作業量です。したがって、この場合、ボトムアップアプローチがより効果的です。つまり、最初に必要なタスクを解決する完全なモジュールを作成し、次にそれを既存のシステムに組み込み、必要な変更のみを行います。この場合、このモジュールは機能の完全なユニットであるため、このモジュールの品質を保証できます。

    アプローチはそれほど単純ではないことに注意してください。たとえば、古いシステムで新しい機能を設計する場合、好むと好まざるとにかかわらず、両方のアプローチを使用します。最初の分析では、システムを評価してから、モジュールレベルまで下げ、実装してから、システム全体のレベルに戻る必要があります。私の意見では、ここでの主なことは、新しいモジュールが完全な機能であり、別個のツールとして独立している必要があることを忘れないことです。このアプローチに厳密に従うほど、古いコードに加えられる変更は少なくなります。

    ルール2:変更されたコードのみをテストする

    古いプロジェクトで作業する場合、メソッド/クラスの考えられるすべてのシナリオのテストを作成する必要はまったくありません。さらに、シナリオがたくさんある可能性があるため、シナリオによってはまったく気付かない場合があります。プロジェクトはすでに生産中であり、顧客は満足しているので、リラックスすることができます。一般に、このシステムでは変更のみが問題を引き起こします。したがって、それらだけをテストする必要があります。

    選択したアイテムのカートを作成してデータベースに保存するオンラインストアモジュールがあります。特定の実装については気にしません。完了–これはレガシーコードです。ここで、新しい動作を導入する必要があります。カートのコストが$ 1000を超えた場合に、経理部門に通知を送信します。これが私たちが見るコードです。変更を導入する方法は?

    public class EuropeShop : Shop
    {
        public override void CreateSale()
        {
            var items = LoadSelectedItemsFromDb();
            var taxes = new EuropeTaxes();
            var saleItems = items.Select(item => taxes.ApplyTaxes(item)).ToList();
            var cart = new Cart();
            cart.Add(saleItems);
            taxes.ApplyTaxes(cart);
            SaveToDb(cart);
        }
    }

    最初のルールによると、変更は最小限でアトミックでなければなりません。データの読み込みには関心がなく、税金の計算やデータベースへの保存については気にしません。しかし、私たちは計算されたカートに興味があります。必要なことを実行するモジュールがあれば、それは必要なタスクを実行します。それが私たちがこれを行う理由です。

    public class EuropeShop : Shop
    {
        public override void CreateSale()
        {
            var items = LoadSelectedItemsFromDb();
            var taxes = new EuropeTaxes();
            var saleItems = items.Select(item => taxes.ApplyTaxes(item)).ToList();
            var cart = new Cart();
            cart.Add(saleItems);
            taxes.ApplyTaxes(cart);
    
            // NEW FEATURE
            new EuropeShopNotifier().Send(cart);
    
            SaveToDb(cart);
        }
    }

    このような通知機能はそれ自体で動作し、テストでき、古いコードに加えられた変更は最小限です。これはまさに2番目のルールが言っていることです。

    ルール3:要件のみをテストします

    単体テストによるテストを必要とする多くのシナリオから自分を解放するために、モジュールから実際に必要なものを考えてください。モジュールの要件として想像できる条件の最小セットを最初に記述します。最小セットはセットであり、新しいセットを追加すると、モジュールの動作はあまり変化せず、削除するとモジュールは機能しません。この場合、BDDアプローチは大いに役立ちます。

    また、モジュールのクライアントである他のクラスがモジュールとどのように相互作用するか想像してみてください。モジュールを構成するために10行のコードを記述する必要がありますか?システムの各部分間の通信は単純であるほど、優れています。したがって、古いコードから特定の何かを担当するモジュールを選択することをお勧めします。この場合、SOLIDが役立ちます。

    次に、上記のすべてがコードでどのように役立つかを見てみましょう。まず、カートの作成に間接的にのみ関連付けられているすべてのモジュールを選択します。これがモジュールの責任の分散方法です。

    public class EuropeShop : Shop
    {
        public override void CreateSale()
        {
            // 1) load from DB
            var items = LoadSelectedItemsFromDb();
    
            // 2) Tax-object creates SaleItem and
            // 4) goes through items and apply taxes
            var taxes = new EuropeTaxes();
            var saleItems = items.Select(item => taxes.ApplyTaxes(item)).ToList();
    
            // 3) creates a cart and 4) applies taxes
            var cart = new Cart();
            cart.Add(saleItems);
            taxes.ApplyTaxes(cart);
    
            new EuropeShopNotifier().Send(cart);
    
            // 4) store to DB
            SaveToDb(cart);
        }
    }

    このようにして、それらを区別することができます。もちろん、このような変更は、大規模なシステムでは一度に行うことはできませんが、徐々に行うことはできます。たとえば、変更が税モジュールに関連する場合、システムの他の部分が税モジュールにどのように依存するかを単純化できます。これは、高い依存関係を取り除き、将来的に自己完結型のツールとして使用するのに役立ちます。

    public class EuropeShop : Shop
    {
        public override void CreateSale()
        {
            // 1) extracted to a repository
            var itemsRepository = new ItemsRepository();
            var items = itemsRepository.LoadSelectedItems();
    			
            // 2) extracted to a mapper
            var saleItems = items.ConvertToSaleItems();
    			
            // 3) still creates a cart
            var cart = new Cart();
            cart.Add(saleItems);
    			
            // 4) all routines to apply taxes are extracted to the Tax-object
            new EuropeTaxes().ApplyTaxes(cart);
    			
            new EuropeShopNotifier().Send(cart);
    			
            // 5) extracted to a repository
            itemsRepository.Save(cart);
        }
    }

    テストに関しては、これらのシナリオで十分です。これまでのところ、それらの実装には関心がありません。

    public class EuropeTaxesTests
    {
        public void Should_not_fail_for_null() { }
    
        public void Should_apply_taxes_to_items() { }
    
        public void Should_apply_taxes_to_whole_cart() { }
    
        public void Should_apply_taxes_to_whole_cart_and_change_items() { }
    }
    
    public class EuropeShopNotifierTests
    {
        public void Should_not_send_when_less_or_equals_to_1000() { }
    
        public void Should_send_when_greater_than_1000() { }
    
        public void Should_raise_exception_when_cannot_send() { }
    }

    ルール4:テスト済みのコードのみを追加する

    以前に書いたように、古いコードへの変更を最小限に抑える必要があります。これを行うには、古いコードと新しい/変更されたコードを分割できます。新しいコードは、単体テストを使用してチェックできるメソッドに配置できます。このアプローチは、関連するリスクを軽減するのに役立ちます。 「レガシーコードを効果的に使用する」という本(以下の本へのリンク)で説明されている2つのテクニックがあります。

    Sproutメソッド/クラス–この手法を使用すると、非常に安全な新しいコードを古いコードに埋め込むことができます。通知機能を追加した方法は、このアプローチの例です。

    ラップ方法–もう少し複雑ですが、本質は同じです。常に機能するとは限りませんが、古いコードの前後に新しいコードが呼び出された場合にのみ機能します。責任を割り当てるときに、ApplyTaxesメソッドの2つの呼び出しが1つの呼び出しに置き換えられました。このため、ロジックが大きく壊れないように、2番目の方法を変更してチェックできるようにする必要がありました。変更前のクラスはこのようでした。

    public class EuropeTaxes : Taxes
    {
        internal override SaleItem ApplyTaxes(Item item)
        {
            var saleItem = new SaleItem(item)
            {
                SalePrice = item.Price*1.2m
            };
            return saleItem;
        }
    
        internal override void ApplyTaxes(Cart cart)
        {
            if (cart.TotalSalePrice <= 300m) return;
            var exclusion = 30m/cart.SaleItems.Count;
            foreach (var item in cart.SaleItems)
                if (item.SalePrice - exclusion > 100m)
                    item.SalePrice -= exclusion;
        }
    }
    

    そして、ここでそれがどのように見えるか。カートの要素を操作するロジックは少し変更されましたが、一般的にはすべて同じままでした。この場合、古いメソッドは最初に新しいApplyToItemsを呼び出し、次に以前のバージョンを呼び出します。これがこのテクニックの本質です。

    public class EuropeTaxes : Taxes
    {
        internal override void ApplyTaxes(Cart cart)
        {
            ApplyToItems(cart);
            ApplyToCart(cart);
        }
    
        private void ApplyToItems(Cart cart)
        {
            foreach (var item in cart.SaleItems)
                item.SalePrice = item.Price*1.2m;
        }
    
        private void ApplyToCart(Cart cart)
        {
            if (cart.TotalSalePrice <= 300m) return;
            var exclusion = 30m / cart.SaleItems.Count;
            foreach (var item in cart.SaleItems)
                if (item.SalePrice - exclusion > 100m)
                    item.SalePrice -= exclusion;
        }
    }

    ルール5:隠された依存関係を「破る」

    これは、古いコードの最大の悪についてのルールです。新しいの使用です。 1つのオブジェクトのメソッド内の演算子を使用して、他のオブジェクト、リポジトリ、または他の複雑なオブジェクトを作成します。なぜそれが悪いのですか?最も簡単な説明は、これによりシステムの各部分が高度に接続され、それらの一貫性が低下するのに役立つということです。さらに短い:「低結合度、高凝集度」の原則に違反します。反対側を見ると、このコードを別の独立したツールに抽出するのは非常に困難です。このような隠れた依存関係を一度に取り除くのは非常に面倒です。しかし、これは徐々に行うことができます。

    まず、すべての依存関係の初期化をコンストラクターに転送する必要があります。特に、これは newに適用されます 演算子とクラスの作成。クラスのインスタンスを取得するServiceLocatorがある場合は、コンストラクターからも削除する必要があります。コンストラクターでは、必要なすべてのインターフェイスをコンストラクターから引き出すことができます。

    次に、外部オブジェクト/リポジトリのインスタンスを格納する変数は、抽象型である必要があり、インターフェイスが優れている必要があります。開発者により多くの機能を提供するため、インターフェースは優れています。結果として、これにより、モジュールからアトミックツールを作成できるようになります。

    第三に、大きなメソッドシートを残さないでください。これは、メソッドがその名前で指定されている以上のことを行うことを明確に示しています。また、デメテルの法則であるSOLIDに違反している可能性があることも示しています。

    次に、カートを作成するコードがどのように変更されたかを見てみましょう。カートを作成するコードブロックのみが変更されていません。残りは外部クラスに配置され、任意の実装で置き換えることができます。現在、EuropeShopクラスは、コンストラクターで明示的に表される特定のものを必要とするアトミックツールの形式を取ります。コードがわかりやすくなります。

    public class EuropeShop : Shop
    {
        private readonly IItemsRepository _itemsRepository;
        private readonly Taxes.Taxes _europeTaxes;
        private readonly INotifier _europeShopNotifier;
    
        public EuropeShop()
        {
            _itemsRepository = new ItemsRepository();
            _europeTaxes = new EuropeTaxes();
            _europeShopNotifier = new EuropeShopNotifier();
        }
    
        public override void CreateSale()
        {
            var items = _itemsRepository.LoadSelectedItems();
            var saleItems = items.ConvertToSaleItems();
    
            var cart = new Cart();
            cart.Add(saleItems);
    
            _europeTaxes.ApplyTaxes(cart);
            _europeShopNotifier.Send(cart);
            _itemsRepository.Save(cart);
        }
    }SCRIPT

    ルール6:大きなテストが少ないほど良い

    大きなテストは、ユーザースクリプトをテストしようとするさまざまな統合テストです。間違いなく重要ですが、コードの奥深くで一部のIFのロジックをチェックするのは非常にコストがかかります。このテストの作成には、機能自体の作成と同じくらいの時間がかかります。それらをサポートすることは、変更が難しい別のレガシーコードのようなものです。しかし、これらは単なるテストです!

    どのテストが必要かを理解し、この理解を明確に順守する必要があります。統合チェックが必要な場合は、ポジティブおよびネガティブな相互作用シナリオを含む、最小限のテストセットを作成します。アルゴリズムをテストする必要がある場合は、最小限の単体テストのセットを作成してください。

    ルール7:プライベートメソッドをテストしないでください

    プライベートメソッドは複雑すぎるか、パブリックメソッドから呼び出されないコードが含まれている可能性があります。あなたが考えることができる他の理由は、「悪い」コードまたはデザインの特徴であることがわかると確信しています。ほとんどの場合、プライベートメソッドのコードの一部を別のメソッド/クラスにする必要があります。 SOLIDの第一原理に違反していないか確認してください。これが、そうする価値がない最初の理由です。 2つ目は、この方法で、モジュール全体の動作ではなく、モジュールがそれをどのように実装するかを確認することです。内部実装は、モジュールの動作に関係なく変更できます。したがって、この場合、脆弱なテストが発生し、それらをサポートするために必要以上に時間がかかります。

    プライベートメソッドをテストする必要をなくすために、クラスをアトミックツールのセットとして提示すると、それらがどのように実装されているかがわかりません。テストしている動作を期待します。この態度は、アセンブリのコンテキストでのクラスにも適用されます。 (他のアセンブリから)クライアントが利用できるクラスはパブリックになり、内部作業を実行するクラスはプライベートになります。ただし、方法とは異なります。内部クラスは複雑になる可能性があるため、内部クラスに変換してテストすることもできます。

    たとえば、EuropeTaxesクラスのプライベートメソッドで1つの条件をテストするために、このメソッドのテストは作成しません。税金が一定の方法で適用されることを期待しているので、テストはまさにこの振る舞いを反映します。テストでは、結果がどうあるべきかを手動で数え、それを標準として採用し、クラスから同じ結果を期待しました。

    public class EuropeTaxes : Taxes
    {
        // code skipped
    
        private void ApplyToCart(Cart cart)
        {
            if (cart.TotalSalePrice <= 300m) return; // <<< I WANT TO TEST THIS CONDIFTION
            var exclusion = 30m / cart.SaleItems.Count;
            foreach (var item in cart.SaleItems)
                if (item.SalePrice - exclusion > 100m)
                    item.SalePrice -= exclusion;
        }
    }
    
    // test suite
    public class EuropeTaxesTests
    {
        // code skipped
    
        [Fact]
        public void Should_apply_taxes_to_cart_greater_300()
        {
            #region arrange
            // list of items which will create a cart greater 300
            var saleItems = new List<Item>(new[]{new Item {Price = 83.34m},
                new Item {Price = 83.34m},new Item {Price = 83.34m}})
                .ConvertToSaleItems();
            var cart = new Cart();
            cart.Add(saleItems);
    
            const decimal expected = 83.34m*3*1.2m;
            #endregion
    
            // act
            new EuropeTaxes().ApplyTaxes(cart);
    
            // assert
            Assert.Equal(expected, cart.TotalSalePrice);
        }
    }

    ルール8:メソッドのアルゴリズムをテストしないでください

    特定のメソッドの呼び出し回数を確認したり、呼び出し自体を確認したりする人もいます。つまり、メソッドの内部動作を確認する人もいます。プライベートのものをテストするのと同じくらい悪いです。違いは、そのようなチェックのアプリケーション層だけです。このアプローチでも、多くの脆弱なテストが行​​われるため、TDDを適切に受けない人もいます。

    続きを読む…

    ルール9:テストなしでレガシーコードを変更しないでください

    これは、この道をたどりたいというチームの願望を反映しているため、最も重要なルールです。この方向に進みたいという願望がなければ、上記のすべてに特別な意味はありません。開発者がTDDを使用したくない場合(その意味を理解していない、利点がわからないなど)、その真の利点は、それがどれほど困難で非効率的であるかを絶えず議論することによって曖昧になります。

    TDDを使用する場合は、これについてチームと話し合い、Definition of Doneに追加して、適用します。最初は、すべてが新しい場合と同じように、難しいでしょう。他の芸術と同様に、TDDには絶え間ない練習が必要であり、学ぶにつれて喜びが生まれます。徐々に、より多くの単体テストが作成され、システムの「健全性」を感じ始め、最初の段階で要件を説明するコードの記述の単純さを理解し始めます。 MicrosoftとIBMの実際の大規模プロジェクトで実施されたTDD調査では、実稼働システムのバグが40%から80%に減少したことが示されています(以下のリンクを参照)。

    参考資料

    1. MichaelFeathersによる「レガシーコードを効果的に使用する」の本
    2. レガシーコードで首に達するときのTDD
    3. 隠れた依存関係を破る
    4. レガシーコードのライフサイクル
    5. クラスでプライベートメソッドを単体テストする必要がありますか?
    6. ユニットテストの内部
    7. TDDと単体テストに関する5つの一般的な誤解
    8. デメテルの法則

    1. コマンドプロンプトからSQLServerデータベースを復元するプロセス全体

    2. Postgresのテーブル列のデフォルト値を取得しますか?

    3. Oracleで主キーを持つ重複行を見つける11の方法

    4. Oracleで結果を制限する方法