Database
 sql >> база данни >  >> RDS >> Database

Правила за внедряване на TDD в стар проект

Статията „Плъзгаща се отговорност на шаблона на хранилището“ повдигна няколко въпроса, на които е много трудно да се отговори. Имаме ли нужда от хранилище, ако пълното пренебрегване на техническите подробности е невъзможно? Колко сложно трябва да бъде хранилището, за да може добавянето му да се счита за полезно? Отговорът на тези въпроси варира в зависимост от акцента, поставен в развитието на системите. Вероятно най-трудният въпрос е следният:имате ли нужда от хранилище? Проблемът с „течащата абстракция“ и нарастващата сложност на кодирането с повишаване на нивото на абстракция не позволяват да се намери решение, което да задоволи и двете страни на оградата. Например при отчитането дизайнът на намеренията води до създаването на голям брой методи за всеки филтър и сортиране, а общото решение създава големи разходи за кодиране.

За да имам пълна картина, разгледах проблема с абстракциите от гледна точка на тяхното приложение в наследен код. В този случай хранилището ни представлява интерес само като инструмент за получаване на качествен код без грешки. Разбира се, този модел не е единственото необходимо за прилагането на TDD практиките. След като изядох бушел сол по време на разработването на няколко големи проекта и наблюдавах какво работи и какво не, разработих за себе си няколко правила, които ми помагат да следвам практиките на TDD. Отворен съм за конструктивна критика и други методи за прилагане на TDD.

Предговор

Някои може да забележат, че не е възможно да се приложи TDD в стар проект. Има мнение, че различните видове интеграционни тестове (UI-тестове, от край до край) са по-подходящи за тях, тъй като е твърде трудно да се разбере старият код. Също така можете да чуете, че писането на тестове преди действителното кодиране води само до загуба на време, защото може да не знаем как ще работи кодът. Трябваше да работя по няколко проекта, където бях ограничен само до интеграционни тестове, вярвайки, че единичните тестове не са показателни. В същото време бяха написани много тестове, пуснаха много услуги и т. н. В резултат на това само един човек можеше да ги разбере, който всъщност ги е написал.

По време на практиката си успях да работя по няколко много големи проекта, където имаше много наследен код. Някои от тях включваха тестове, а други не (имаше само намерение да ги реализират). Участвах в два големи проекта, в които по някакъв начин се опитах да приложа TDD подхода. В началния етап TDD се възприемаше като разработка на Test First. В крайна сметка разликите между това опростено разбиране и сегашното възприятие, наречено накратко BDD, станаха по-ясни. Който и език да се използва, основните точки, аз ги наричам правила, остават сходни. Някой може да намери паралели между правилата и други принципи за писане на добър код.

Правило 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);
    }
}

Такъв уведомител работи самостоятелно, може да бъде тестван и промените, направени в стария код, са минимални. Точно това казва второто правило.

Правило 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:Добавете само тестван код

Както писах по-рано, трябва да сведете до минимум промените в стария код. За да направите това, старият и новият/променен код могат да бъдат разделени. Новият код може да бъде поставен в методи, които могат да бъдат проверени с помощта на единични тестове. Този подход ще помогне за намаляване на свързаните рискове. Има две техники, които са описани в книгата „Ефективна работа с наследен код“ (връзка към книгата по-долу).

Метод/клас Sprout – тази техника ви позволява да вградите много сигурен нов код в стар. Начинът, по който добавих уведомителя, е пример за този подход.

Метод на обвиване – малко по-сложен, но същността е същата. Не винаги работи, а само в случаите, когато се извиква нов код преди/след стар. При възлагане на отговорности две извиквания на метода ApplyTaxes бяха заменени с едно извикване. За това беше необходимо да се промени вторият метод, така че логиката да не се счупи много и да може да се провери. Ето как изглеждаше класът преди промените.

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:„Прекъснете“ скрити зависимости

Това е правилото за най-голямото зло в стар код:използването на нов оператор вътре в метода на един обект за създаване на други обекти, хранилища или други сложни обекти. Защо е лошо? Най-простото обяснение е, че това прави частите на системата силно свързани и помага за намаляване на тяхната кохерентност. Още по-кратко:води до нарушаване на принципа „ниско свързване, висока кохезия“. Ако погледнете от другата страна, тогава този код е твърде труден за извличане в отделен, независим инструмент. Да се ​​отървете от такива скрити зависимости наведнъж е много трудоемко. Но това може да стане постепенно.

Първо, трябва да прехвърлите инициализацията на всички зависимости към конструктора. По-специално, това се отнася за новите оператори и създаване на класове. Ако имате 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 е нарушен. Това е първата причина, поради която не си струва да го правите. Второто е, че по този начин проверявате не поведението на целия модул, а как модулът го реализира. Вътрешната реализация може да се промени независимо от поведението на модула. Следователно в този случай получавате крехки тестове и е необходимо повече време, отколкото е необходимо, за да ги поддържате.

За да избегнете необходимостта от тестване на частни методи, представете класовете си като набор от атомарни инструменти и не знаете как се изпълняват. Очаквате някакво поведение, което тествате. Това отношение важи и за класовете в контекста на събранието. Класовете, които са достъпни за клиенти (от други асембли), ще бъдат публични, а тези, които извършват вътрешна работа – частни. Въпреки това, има разлика от методите. Вътрешните класове могат да бъдат сложни, така че могат да бъдат трансформирани във вътрешни и също така тествани.

Пример

Например, за да тествам едно условие в частния метод на класа EuropeTaxes, няма да напиша тест за този метод. Очаквам данъците да се прилагат по определен начин, така че тестът ще отрази точно това поведение. В теста ръчно преброих какъв трябва да бъде резултатът, приех го като стандарт и очаквах същия резултат от класа.

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 изисква постоянна практика и удоволствието идва, докато учите. Постепенно ще има повече писмени модулни тестове, ще започнете да усещате „здравето“ на вашата система и ще започнете да оценявате простотата на писане на код, описвайки изискванията в първия етап. Има TDD проучвания, проведени върху истински големи проекти в Microsoft и IBM, които показват намаляване на грешките в производствените системи от 40% на 80% (вижте връзките по-долу).

Допълнително четене

  1. Книга „Ефективна работа с наследения код“ от Майкъл Федърс
  2. TDD, когато е до врата в наследения код
  3. Разрушаване на скрити зависимости
  4. Жизненият цикъл на наследения код
  5. Трябва ли да тествате частни методи на модул в клас?
  6. Вътрешни елементи за тестване на модули
  7. 5 често срещани погрешни схващания относно TDD и единичните тестове
  8. Закон на Деметра

  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Пренаписване на заявки за подобряване на производителността

  2. Подобрена поддръжка за възстановяване на паралелни статистически данни

  3. Прагове за оптимизиране – групиране и агрегиране на данни, част 1

  4. Как стартират паралелните планове – част 5

  5. Гъвкави и управляеми проекти на спецификациите (BOM).