sql >> Databasteknik >  >> RDS >> Database

Regler för implementering av TDD i gamla projekt

Artikeln "Sliding Responsibility of the Repository Pattern" väckte flera frågor som är mycket svåra att besvara. Behöver vi ett förråd om fullständigt åsidosättande av tekniska detaljer är omöjligt? Hur komplext måste förvaret vara för att det ska kunna anses lönsamt? Svaret på dessa frågor varierar beroende på vilken vikt som läggs vid utvecklingen av system. Den förmodligen svåraste frågan är följande:behöver du ens ett förråd? Problemet med "flytande abstraktion" och den växande komplexiteten i kodning med en ökning av abstraktionsnivån tillåter inte att hitta en lösning som skulle tillfredsställa båda sidor av stängslet. Till exempel, i rapportering leder avsiktsdesign till skapandet av ett stort antal metoder för varje filter och sortering, och en generisk lösning skapar en stor kodningsoverhead.

För att få en fullständig bild tittade jag på problemet med abstraktioner i termer av deras tillämpning i en äldre kod. Ett arkiv, i det här fallet, är av intresse för oss endast som ett verktyg för att erhålla kvalitet och felfri kod. Naturligtvis är detta mönster inte det enda som är nödvändigt för tillämpningen av TDD-praxis. Efter att ha ätit en skäppa salt under utvecklingen av flera stora projekt och sett vad som fungerar och vad som inte fungerar, utvecklade jag några regler för mig själv som hjälper mig att följa TDD-praxis. Jag är öppen för konstruktiv kritik och andra metoder för att implementera TDD.

Förord

Vissa kanske märker att det inte går att tillämpa TDD i ett gammalt projekt. Det finns en uppfattning om att olika typer av integrationstester (UI-tester, end-to-end) är mer lämpade för dem eftersom det är för svårt att förstå den gamla koden. Du kan också höra att att skriva tester innan själva kodningen bara leder till tidsförlust, eftersom vi kanske inte vet hur koden kommer att fungera. Jag var tvungen att arbeta med flera projekt, där jag bara var begränsad till integrationstester, och trodde att enhetstester inte är vägledande. Samtidigt skrevs många tester, de körde många tjänster etc. Som ett resultat kunde bara en person förstå dem, som faktiskt skrev dem.

Under min praktik hann jag arbeta med flera mycket stora projekt, där det fanns mycket äldre kod. Vissa av dem innehöll tester, och de andra gjorde det inte (det fanns bara en avsikt att implementera dem). Jag deltog i två stora projekt, där jag på något sätt försökte tillämpa TDD-metoden. I det inledande skedet uppfattades TDD som en Test First-utveckling. Så småningom blev skillnaderna mellan denna förenklade förståelse och den nuvarande uppfattningen, kort kallad BDD, tydligare. Vilket språk som än används förblir huvudpunkterna, jag kallar dem regler, likartade. Någon kan hitta paralleller mellan reglerna och andra principer för att skriva bra kod.

Regel 1:Använd Bottom-Up (Inside-Out)

Denna regel hänvisar snarare till analysmetoden och mjukvarudesign när nya kodbitar bäddas in i ett fungerande projekt.

När du designar ett nytt projekt är det helt naturligt att föreställa sig ett helt system. I detta skede kontrollerar du både uppsättningen av komponenter och arkitekturens framtida flexibilitet. Därför kan du skriva moduler som enkelt och intuitivt kan integreras med varandra. En sådan Top-Down-metod gör att du kan utföra en bra förhandsdesign av den framtida arkitekturen, beskriva de nödvändiga ledstjärnorna och få en komplett bild av vad du i slutändan vill ha. Efter ett tag förvandlas projektet till det som kallas legacy code. Och sedan börjar det roliga.

I det skede när det är nödvändigt att bädda in en ny funktionalitet i ett befintligt projekt med en massa moduler och beroenden mellan dem, kan det vara mycket svårt att sätta in dem alla i huvudet för att göra rätt design. Den andra sidan av detta problem är mängden arbete som krävs för att utföra denna uppgift. Därför kommer bottom-up-metoden att vara mer effektiv i det här fallet. Med andra ord, först skapar du en komplett modul som löser den nödvändiga uppgiften, och sedan bygger du in den i det befintliga systemet och gör bara de nödvändiga ändringarna. I det här fallet kan du garantera kvaliteten på denna modul, eftersom den är en komplett enhet av det funktionella.

Det bör noteras att det inte är så enkelt med tillvägagångssätten. Till exempel, när du designar en ny funktionalitet i ett gammalt system kommer du att, om du gillar det eller inte, använda båda metoderna. Under den inledande analysen behöver du fortfarande utvärdera systemet, sedan sänka det till modulnivå, implementera det och sedan gå tillbaka till hela systemets nivå. Enligt min mening är huvudsaken här att inte glömma att den nya modulen ska vara en komplett funktionalitet och vara oberoende, som ett separat verktyg. Ju striktare du kommer att följa detta tillvägagångssätt, desto färre ändringar kommer att göras i den gamla koden.

Regel 2:Testa endast den modifierade koden

När man arbetar med ett gammalt projekt, finns det absolut inget behov av att skriva tester för alla möjliga scenarier av metoden/klassen. Dessutom kanske du inte är medveten om vissa scenarier alls, eftersom det kan finnas gott om dem. Projektet är redan i produktion, kunden är nöjd, så du kan koppla av. I allmänhet är det bara dina ändringar som orsakar problem i det här systemet. Därför bör endast de testas.

Exempel

Det finns en onlinebutiksmodul som skapar en varukorg med utvalda varor och lagrar den i en databas. Vi bryr oss inte om den specifika implementeringen. Klart som gjort – det här är den äldre koden. Nu måste vi introducera ett nytt beteende här:skicka ett meddelande till redovisningsavdelningen ifall varukorgskostnaden överstiger $1000. Här är koden vi ser. Hur inför man förändringen?

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);
    }
}

Enligt den första regeln måste förändringarna vara minimala och atomära. Vi är inte intresserade av dataladdning, vi bryr oss inte om skatteberäkningen och sparandet till databasen. Men vi är intresserade av den uträknade vagnen. Om det fanns en modul som gör vad som krävs, då skulle den utföra den nödvändiga uppgiften. Det är därför vi gör det här.

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);
    }
}

En sådan anmälare fungerar på egen hand, kan testas och ändringarna i den gamla koden är minimala. Detta är precis vad den andra regeln säger.

Regel 3:Vi testar endast krav

För att avlasta dig själv från antalet scenarier som kräver testning med enhetstester, tänk på vad du faktiskt behöver från en modul. Skriv först för den minsta uppsättning villkor som du kan tänka dig som krav för modulen. Minsta uppsättning är uppsättningen, som när den kompletteras med en ny, ändras inte modulens beteende mycket, och när den tas bort fungerar modulen inte. BDD-metoden hjälper mycket i det här fallet.

Föreställ dig också hur andra klasser som är klienter till din modul kommer att interagera med den. Behöver du skriva 10 rader kod för att konfigurera din modul? Ju enklare kommunikationen är mellan delarna av systemet, desto bättre. Därför är det bättre att välja moduler som ansvarar för något specifikt från den gamla koden. SOLID kommer till hjälp i det här fallet.

Exempel

Låt oss nu se hur allt som beskrivs ovan kommer att hjälpa oss med koden. Välj först alla moduler som endast är indirekt associerade med skapandet av vagnen. Så är ansvaret för modulerna fördelat.

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);
    }
}

På så sätt kan de särskiljas. Sådana förändringar kan naturligtvis inte göras på en gång i ett stort system, men de kan göras gradvis. När ändringar till exempel avser en skattemodul kan du förenkla hur andra delar av systemet är beroende av den. Detta kan hjälpa till att bli av med höga beroenden och använda det i framtiden som ett självständigt verktyg.

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);
    }
}

När det gäller testerna kommer dessa scenarier att vara tillräckliga. Än så länge intresserar inte implementeringen oss.

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() { }
}

Regel 4:Lägg endast till testad kod

Som jag skrev tidigare bör du minimera ändringar i den gamla koden. För att göra detta kan den gamla och nya/modifierade koden delas upp. Den nya koden kan placeras i metoder som kan kontrolleras med hjälp av enhetstester. Detta tillvägagångssätt kommer att bidra till att minska de associerade riskerna. Det finns två tekniker som har beskrivits i boken "Working Effectively with Legacy Code" (länk till boken nedan).

Sprout metod/klass – denna teknik låter dig bädda in en mycket säker ny kod i en gammal. Sättet jag lade till anmälaren på är ett exempel på detta tillvägagångssätt.

Wrap-metoden – lite mer komplicerad, men essensen är densamma. Det fungerar inte alltid, utan bara i de fall en ny kod anropas före/efter en gammal. Vid tilldelning av ansvar ersattes två anrop av ApplyTaxes-metoden med ett anrop. För detta var det nödvändigt att ändra den andra metoden så att logiken inte går sönder mycket och den kan kontrolleras. Så såg klassen ut före förändringarna.

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;
    }
}

Och här hur det ser ut efteråt. Logiken i att arbeta med elementen i vagnen förändrades lite, men i allmänhet förblev allt detsamma. I det här fallet anropar den gamla metoden först en ny ApplyToItems och sedan dess tidigare version. Detta är kärnan i denna teknik.

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;
    }
}

Regel 5:Bryt dolda beroenden

Detta är regeln om det största onda i en gammal kod:användningen av den nya operatör inuti metoden för ett objekt för att skapa andra objekt, arkiv eller andra komplexa objekt. Varför är det dåligt? Den enklaste förklaringen är att detta gör delarna i systemet starkt sammankopplade och bidrar till att minska deras koherens. Ännu kortare:leder till brott mot principen "låg koppling, hög sammanhållning". Om du tittar på den andra sidan är den här koden för svår att extrahera till ett separat, oberoende verktyg. Att bli av med sådana dolda beroenden på en gång är mycket mödosamt. Men detta kan göras gradvis.

Först måste du överföra initieringen av alla beroenden till konstruktorn. I synnerhet gäller detta den nya operatörer och skapande av klasser. Om du har ServiceLocator för att få instanser av klasser, bör du också ta bort den till konstruktorn, där du kan dra ut alla nödvändiga gränssnitt från den.

För det andra måste variabler som lagrar instansen av ett externt objekt/lager ha en abstrakt typ och bättre ett gränssnitt. Gränssnittet är bättre eftersom det ger fler möjligheter till en utvecklare. Som ett resultat kommer detta att göra det möjligt att göra ett atomverktyg av en modul.

För det tredje, lämna inte stora metodblad. Detta visar tydligt att metoden gör mer än vad den anges i namnet. Det är också en indikation på ett möjligt brott mot SOLID, Demeterlagen.

Exempel

Låt oss nu se hur koden som skapar kundvagnen har ändrats. Endast kodblocket som skapar vagnen förblev oförändrat. Resten placerades i externa klasser och kan ersättas med valfri implementering. Nu tar EuropeShop-klassen formen av ett atomverktyg som behöver vissa saker som är explicit representerade i konstruktören. Koden blir lättare att uppfatta.

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

Regel 6:Ju färre stora tester, desto bättre

Stora tester är olika integrationstester som försöker testa användarskript. Utan tvekan är de viktiga, men att kontrollera logiken hos vissa IF i djupet av koden är mycket dyrt. Att skriva detta test tar lika lång tid, om inte mer, som att skriva själva funktionaliteten. Att stödja dem är som en annan äldre kod, som är svår att ändra. Men det här är bara tester!

Det är nödvändigt att förstå vilka tester som behövs och tydligt följa denna förståelse. Om du behöver en integrationskontroll, skriv ett minimum av tester, inklusive positiva och negativa interaktionsscenarier. Om du behöver testa algoritmen, skriv en minimal uppsättning enhetstester.

Regel 7:Testa inte privata metoder

En privat metod kan vara för komplex eller innehålla kod som inte anropas från offentliga metoder. Jag är säker på att någon annan anledning du kan komma på kommer att visa sig vara en egenskap hos en "dålig" kod eller design. Troligtvis bör en del av koden från den privata metoden göras till en separat metod/klass. Kontrollera om den första principen för SOLID bryts. Detta är den första anledningen till att det inte är värt att göra det. Det andra är att du på detta sätt inte kontrollerar hela modulens beteende, utan hur modulen implementerar den. Den interna implementeringen kan ändras oberoende av modulens beteende. Därför får du i det här fallet ömtåliga tester, och det tar mer tid än nödvändigt att stödja dem.

För att undvika behovet av att testa privata metoder, presentera dina klasser som en uppsättning atomverktyg och du vet inte hur de implementeras. Du förväntar dig något beteende som du testar. Denna inställning gäller även klasser i samband med församlingen. Klasser som är tillgängliga för kunder (från andra församlingar) kommer att vara offentliga och de som utför internt arbete - privata. Även om det finns en skillnad från metoder. Interna klasser kan vara komplexa, så de kan omvandlas till interna och även testas.

Exempel

Till exempel, för att testa ett villkor i den privata metoden i klassen EuropeTaxes, kommer jag inte att skriva ett test för denna metod. Jag förväntar mig att skatter kommer att tillämpas på ett visst sätt, så testet kommer att spegla just detta beteende. I testet räknade jag manuellt vad som skulle bli resultatet, tog det som standard och förväntade mig samma resultat från klassen.

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);
    }
}

Regel 8:Testa inte algoritmen för metoder

Vissa människor kontrollerar antalet samtal för vissa metoder, verifierar själva samtalet, etc., med andra ord, kontrollerar det interna arbetet med metoder. Det är lika illa som att testa de privata. Skillnaden ligger bara i appliceringsskiktet för en sådan kontroll. Detta tillvägagångssätt ger återigen många ömtåliga tester, vilket gör att vissa människor inte tar TDD ordentligt.

Läs mer...

Regel 9:Ändra inte äldre kod utan tester

Detta är den viktigaste regeln eftersom den återspeglar lagets önskan att följa denna väg. Utan önskan att gå i denna riktning har allt som har sagts ovan ingen speciell betydelse. För om en utvecklare inte vill använda TDD (inte förstår dess innebörd, inte ser fördelarna, etc.), så kommer dess verkliga fördel att suddas ut av ständig diskussion om hur svårt och ineffektivt det är.

Om du ska använda TDD, diskutera detta med ditt team, lägg till det i Definition of Done och tillämpa det. Till en början blir det jobbigt, som med allt nytt. Som alla konstverk kräver TDD konstant övning, och njutning kommer när du lär dig. Gradvis kommer det att bli fler skriftliga enhetstester, du kommer att börja känna "hälsan" i ditt system och börja uppskatta enkelheten i att skriva kod och beskriva kraven i det första steget. Det finns TDD-studier utförda på riktigt stora projekt i Microsoft och IBM, som visar en minskning av buggar i produktionssystem från 40 % till 80 % (se länkarna nedan).

Mer läsning

  1. Bok "Working Effectively with Legacy Code" av Michael Feathers
  2. TDD upp till halsen i Legacy Code
  3. Att bryta dolda beroenden
  4. Livscykeln för äldre kod
  5. Ska du enhetstesta privata metoder på en klass?
  6. Enhetstestning internt
  7. 5 vanliga missuppfattningar om TDD och enhetstester
  8. Demeterlagen

  1. pghoard Alternatives - PostgreSQL Backup Management med ClusterControl

  2. Steg för steg uppgraderingsprocess till R12.2 Uppgradering del -3

  3. Hur man tar bort en kolumn i SQL Server med T-SQL

  4. Flera index vs index med flera kolumner