Database
 sql >> Baza danych >  >> RDS >> Database

Zasady wdrażania TDD w starym projekcie

Artykuł „Sliding Responsibility of the Repository Pattern” postawił kilka pytań, na które bardzo trudno odpowiedzieć. Czy potrzebujemy repozytorium, jeśli całkowite pominięcie szczegółów technicznych jest niemożliwe? Jak skomplikowane musi być repozytorium, aby jego dodanie można było uznać za wartościowe? Odpowiedzi na te pytania różnią się w zależności od nacisku kładzionego na rozwój systemów. Prawdopodobnie najtrudniejsze pytanie brzmi:czy potrzebujesz repozytorium? Problem „płynącej abstrakcji” i rosnąca złożoność kodowania wraz ze wzrostem poziomu abstrakcji nie pozwalają na znalezienie rozwiązania, które zadowoliłoby obie strony ogrodzenia. Na przykład, w raportowaniu, projektowanie intencji prowadzi do stworzenia dużej liczby metod dla każdego filtra i sortowania, a ogólne rozwiązanie powoduje duże narzuty na kodowanie.

Aby mieć pełny obraz, przyjrzałem się problemowi abstrakcji pod kątem ich zastosowania w kodzie odziedziczonym. Repozytorium w tym przypadku interesuje nas jedynie jako narzędzie do uzyskania jakościowego i bezbłędnego kodu. Oczywiście ten wzorzec nie jest jedyną rzeczą niezbędną do zastosowania praktyk TDD. Po zjedzeniu buszla soli podczas opracowywania kilku dużych projektów i obserwowaniu, co działa, a co nie, opracowałem dla siebie kilka zasad, które pomagają mi postępować zgodnie z praktykami TDD. Jestem otwarty na konstruktywną krytykę i inne metody wdrażania TDD.

Przedmowa

Niektórzy mogą zauważyć, że nie jest możliwe zastosowanie TDD w starym projekcie. Istnieje opinia, że ​​różne rodzaje testów integracyjnych (testy UI, end-to-end) są dla nich bardziej odpowiednie, ponieważ zbyt trudno jest zrozumieć stary kod. Słychać też, że pisanie testów przed właściwym kodowaniem to tylko strata czasu, ponieważ możemy nie wiedzieć, jak kod będzie działał. Musiałem pracować nad kilkoma projektami, gdzie ograniczałem się tylko do testów integracyjnych, wierząc, że testy jednostkowe nie mają charakteru orientacyjnego. W tym samym czasie napisano wiele testów, prowadzili wiele usług itp. W rezultacie tylko jedna osoba mogła je zrozumieć, która w rzeczywistości je napisała.

Podczas mojej praktyki udało mi się pracować nad kilkoma bardzo dużymi projektami, w których było dużo starszego kodu. Niektóre z nich zawierały testy, a inne nie (był tylko zamiar ich wdrożenia). Brałem udział w dwóch dużych projektach, w których jakoś próbowałem zastosować podejście TDD. Na początkowym etapie TDD było postrzegane jako rozwój Test First. W końcu różnice między tym uproszczonym rozumieniem a obecnym postrzeganiem, zwanym krótko BDD, stały się wyraźniejsze. Niezależnie od użytego języka główne punkty, które nazywam regułami, pozostają podobne. Ktoś może znaleźć paralele między regułami i innymi zasadami pisania dobrego kodu.

Zasada 1:Korzystanie z dołu do góry (od wewnątrz)

Ta zasada odnosi się raczej do metody analizy i projektowania oprogramowania podczas osadzania nowych fragmentów kodu w działającym projekcie.

Kiedy projektujesz nowy projekt, zupełnie naturalne jest zobrazowanie całego systemu. Na tym etapie kontrolujesz zarówno zestaw komponentów, jak i przyszłą elastyczność architektury. Dzięki temu można pisać moduły, które można łatwo i intuicyjnie ze sobą zintegrować. Takie podejście odgórne pozwala na wykonanie dobrego wstępnego projektu przyszłej architektury, opisanie niezbędnych wytycznych i uzyskanie pełnego obrazu tego, czego ostatecznie chcesz. Po pewnym czasie projekt zamienia się w tak zwany starszy kod. I wtedy zaczyna się zabawa.

Na etapie, kiedy konieczne jest osadzenie nowej funkcjonalności w istniejącym projekcie z mnóstwem modułów i zależności między nimi, umieszczenie ich wszystkich w głowie może być bardzo trudne, aby wykonać właściwy projekt. Drugą stroną tego problemu jest ilość pracy potrzebnej do wykonania tego zadania. Dlatego podejście oddolne będzie w tym przypadku skuteczniejsze. Innymi słowy, najpierw tworzysz kompletny moduł, który rozwiązuje niezbędne zadanie, a następnie wbudowujesz go w istniejący system, wprowadzając tylko niezbędne zmiany. W takim przypadku możesz zagwarantować jakość tego modułu, ponieważ jest to kompletna jednostka funkcjonalności.

Należy zauważyć, że z podejściami nie jest to takie proste. Na przykład, projektując nową funkcjonalność w starym systemie, będziesz, czy ci się to podoba, czy nie, użyjesz obu podejść. Podczas wstępnej analizy trzeba jeszcze ocenić system, następnie obniżyć go do poziomu modułu, wdrożyć, a następnie wrócić na poziom całego systemu. Moim zdaniem najważniejsze jest, aby nie zapominać, że nowy moduł powinien być pełną funkcjonalnością i być niezależnym, jako osobne narzędzie. Im ściślej będziesz stosować się do tego podejścia, tym mniej zmian zostanie wprowadzonych w starym kodzie.

Zasada 2:Testuj tylko zmodyfikowany kod

Pracując ze starym projektem, nie ma absolutnie potrzeby pisania testów dla wszystkich możliwych scenariuszy metody/klasy. Co więcej, możesz w ogóle nie być świadomy niektórych scenariuszy, ponieważ może ich być wiele. Projekt już w produkcji, klient zadowolony, więc możesz odpocząć. Ogólnie tylko Twoje zmiany powodują problemy w tym systemie. Dlatego tylko one powinny być testowane.

Przykład

Istnieje moduł sklepu internetowego, który tworzy koszyk wybranych pozycji i przechowuje go w bazie danych. Nie dbamy o konkretną realizację. Zrobione jak zrobione — to jest stary kod. Teraz musimy wprowadzić tutaj nowe zachowanie:wyślij powiadomienie do działu księgowości w przypadku, gdy koszt koszyka przekroczy 1000 zł. Oto kod, który widzimy. Jak wprowadzić zmianę?

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

Zgodnie z pierwszą zasadą zmiany muszą być minimalne i atomowe. Nie interesuje nas ładowanie danych, nie zależy nam na naliczaniu podatku i zapisywaniu do bazy danych. Ale nas interesuje wyliczony koszyk. Gdyby istniał moduł, który robi to, co jest wymagane, wykonałby niezbędne zadanie. Dlatego to robimy.

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

Taki powiadamiacz działa samodzielnie, można go przetestować, a zmiany wprowadzone w starym kodzie są minimalne. Dokładnie to mówi druga zasada.

Zasada 3:Testujemy tylko wymagania

Aby uwolnić się od wielu scenariuszy, które wymagają testowania za pomocą testów jednostkowych, zastanów się, czego faktycznie potrzebujesz od modułu. Napisz najpierw minimalny zestaw warunków, które możesz sobie wyobrazić jako wymagania dla modułu. Zestaw minimalny to zestaw, który po uzupełnieniu o nowy, zachowanie modułu niewiele się zmienia, a po wyjęciu moduł nie działa. Podejście BDD bardzo pomaga w tym przypadku.

Wyobraź sobie również, jak będą z nim współdziałać inne klasy, które są klientami Twojego modułu. Czy potrzebujesz napisać 10 linijek kodu, aby skonfigurować swój moduł? Im prostsza komunikacja między częściami systemu, tym lepiej. Dlatego lepiej wybrać moduły odpowiedzialne za coś konkretnego ze starego kodu. SOLID pomoże w tym przypadku.

Przykład

Zobaczmy teraz, jak wszystko opisane powyżej pomoże nam z kodem. Najpierw wybierz wszystkie moduły, które są tylko pośrednio związane z tworzeniem koszyka. W ten sposób rozkłada się odpowiedzialność za moduły.

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

W ten sposób można je odróżnić. Oczywiście takich zmian nie można dokonać od razu w dużym systemie, ale można je wprowadzać stopniowo. Na przykład, gdy zmiany dotyczą modułu podatkowego, możesz uprościć uzależnienie od niego innych części systemu. Może to pomóc w pozbyciu się wysokich zależności i wykorzystaniu go w przyszłości jako samodzielnego narzędzia.

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

Jeśli chodzi o testy, te scenariusze będą wystarczające. Jak na razie ich realizacja nas nie interesuje.

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

Zasada 4:Dodaj tylko przetestowany kod

Jak pisałem wcześniej, powinieneś zminimalizować zmiany w starym kodzie. Aby to zrobić, stary i nowy/zmodyfikowany kod można podzielić. Nowy kod można umieścić w metodach, które można sprawdzić za pomocą testów jednostkowych. Takie podejście pomoże zmniejszyć związane z tym ryzyko. Istnieją dwie techniki, które zostały opisane w książce „Efektywna praca z starszym kodem” (link do książki poniżej).

Metoda/klasa Sprout – ta technika pozwala na osadzenie bardzo bezpiecznego nowego kodu w starym. Sposób, w jaki dodałem powiadomienie, jest przykładem tego podejścia.

Metoda owijania – nieco bardziej skomplikowana, ale istota jest taka sama. Nie zawsze działa, ale tylko w przypadkach, gdy nowy kod jest wywoływany przed/po starym. Podczas przypisywania odpowiedzialności dwa wywołania metody ApplyTaxes zostały zastąpione jednym wywołaniem. W tym celu konieczna była zmiana drugiej metody, aby logika nie psuła się zbytnio i można było to sprawdzić. Tak wyglądała klasa przed zmianami.

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

A oto jak to wygląda. Logika pracy z elementami wózka trochę się zmieniła, ale ogólnie wszystko pozostało takie samo. W takim przypadku stara metoda wywołuje najpierw nową metodę ApplyToItems, a następnie jej poprzednią wersję. To jest istota tej techniki.

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

Zasada 5:„Przerwij” ukryte zależności

Oto zasada dotycząca największego zła starego kodu:użycie nowego operator wewnątrz metody jednego obiektu do tworzenia innych obiektów, repozytoriów lub innych złożonych obiektów. Dlaczego jest tak źle? Najprostszym wyjaśnieniem jest to, że sprawia to, że części systemu są silnie połączone i pomaga zmniejszyć ich spójność. Jeszcze krótsze:prowadzi do naruszenia zasady „niskie sprzężenie, wysoka kohezja”. Jeśli spojrzysz na drugą stronę, to ten kod jest zbyt trudny do wyodrębnienia w osobne, niezależne narzędzie. Pozbycie się tak ukrytych zależności od razu jest bardzo pracochłonne. Ale można to zrobić stopniowo.

Najpierw musisz przenieść inicjalizację wszystkich zależności do konstruktora. W szczególności dotyczy to nowego operatorów i tworzenie klas. Jeśli masz ServiceLocator do pobierania instancji klas, powinieneś również usunąć go do konstruktora, skąd możesz wyciągnąć z niego wszystkie niezbędne interfejsy.

Po drugie, zmienne przechowujące instancję zewnętrznego obiektu/repozytorium muszą mieć typ abstrakcyjny, a lepiej interfejs. Interfejs jest lepszy, ponieważ zapewnia programiście więcej możliwości. W rezultacie pozwoli to na zrobienie narzędzia atomowego z modułu.

Po trzecie, nie zostawiaj dużych arkuszy metod. To wyraźnie pokazuje, że metoda robi więcej, niż jest to określone w jej nazwie. Wskazuje to również na możliwe naruszenie SOLID, prawa Demeter.

Przykład

Zobaczmy teraz, jak zmienił się kod, który tworzy koszyk. Tylko blok kodu, który tworzy koszyk, pozostał niezmieniony. Reszta została umieszczona w klasach zewnętrznych i może być podstawiona dowolną implementacją. Teraz klasa EuropeShop przybiera formę narzędzia atomowego, które wymaga pewnych rzeczy, które są jawnie przedstawione w konstruktorze. Kod staje się łatwiejszy do zrozumienia.

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

Zasada 6:im mniej dużych testów, tym lepiej

Duże testy to różne testy integracyjne, które próbują przetestować skrypty użytkownika. Niewątpliwie są one ważne, ale sprawdzenie logiki jakiegoś JEŻELI w głąb kodu jest bardzo kosztowne. Napisanie tego testu zajmuje tyle samo czasu, jeśli nie więcej, jak napisanie samej funkcjonalności. Wspieranie ich jest jak kolejny przestarzały kod, który trudno zmienić. Ale to tylko testy!

Konieczne jest zrozumienie, które testy są potrzebne i jasne przestrzeganie tego zrozumienia. Jeśli potrzebujesz sprawdzenia integracji, napisz minimalny zestaw testów, w tym scenariusze pozytywnych i negatywnych interakcji. Jeśli chcesz przetestować algorytm, napisz minimalny zestaw testów jednostkowych.

Zasada 7:Nie testuj metod prywatnych

Metoda prywatna może być zbyt złożona lub zawierać kod, który nie jest wywoływany z metod publicznych. Jestem pewien, że każdy inny powód, o którym możesz pomyśleć, okaże się cechą „złego” kodu lub projektu. Najprawdopodobniej część kodu z metody prywatnej powinna być zrobiona osobną metodą/klasą. Sprawdź, czy nie została naruszona pierwsza zasada SOLID. To pierwszy powód, dla którego nie warto tego robić. Po drugie, w ten sposób sprawdzasz nie zachowanie całego modułu, ale sposób, w jaki moduł je implementuje. Implementacja wewnętrzna może się zmieniać niezależnie od zachowania modułu. Dlatego w tym przypadku otrzymujesz delikatne testy, a ich wsparcie zajmuje więcej czasu niż to konieczne.

Aby uniknąć konieczności testowania metod prywatnych, przedstaw swoje klasy jako zestaw narzędzi atomowych i nie wiesz, jak są zaimplementowane. Oczekujesz zachowania, które testujesz. Taka postawa dotyczy również zajęć w kontekście zgromadzenia. Klasy, które są dostępne dla klientów (z innych zestawów) będą publiczne, a te, które wykonują pracę wewnętrzną – prywatne. Chociaż istnieje różnica w stosunku do metod. Klasy wewnętrzne mogą być złożone, więc można je przekształcić w wewnętrzne, a także przetestować.

Przykład

Na przykład, aby przetestować jeden warunek w prywatnej metodzie klasy EuropeTaxes, nie napiszę testu dla tej metody. Spodziewam się, że podatki będą stosowane w określony sposób, więc test będzie odzwierciedlał właśnie to zachowanie. W teście ręcznie policzyłem, jaki powinien być wynik, przyjąłem go jako standard i spodziewałem się tego samego wyniku od klasy.

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

Zasada 8:Nie testuj algorytmu metod

Niektórzy sprawdzają liczbę wywołań poszczególnych metod, weryfikują samo wywołanie itp., czyli sprawdzają wewnętrzną pracę metod. To tak samo złe, jak testowanie prywatnych. Różnica polega tylko na warstwie aplikacyjnej takiego czeku. To podejście ponownie daje wiele delikatnych testów, przez co niektórzy ludzie nie przyjmują prawidłowo TDD.

Czytaj więcej…

Zasada 9:Nie modyfikuj starszego kodu bez testów

To najważniejsza zasada, ponieważ odzwierciedla chęć zespołu do podążania tą ścieżką. Bez chęci podążania w tym kierunku wszystko, co zostało powiedziane powyżej, nie ma specjalnego znaczenia. Ponieważ jeśli programista nie chce korzystać z TDD (nie rozumie jego znaczenia, nie widzi korzyści itp.), to jego realna korzyść zostanie zamazana przez ciągłą dyskusję o tym, jak trudne i nieefektywne jest to.

Jeśli zamierzasz korzystać z TDD, porozmawiaj o tym ze swoim zespołem, dodaj to do Definicji ukończenia i zastosuj. Na początku będzie ciężko, jak ze wszystkim nowym. Jak każda sztuka, TDD wymaga ciągłej praktyki, a przyjemność przychodzi, gdy się uczysz. Stopniowo będzie więcej pisanych testów jednostkowych, zaczniesz odczuwać „zdrowie” swojego systemu i zaczniesz doceniać prostotę pisania kodu, opisującego wymagania w pierwszym etapie. Przeprowadzono badania TDD na naprawdę dużych projektach w firmach Microsoft i IBM, pokazujące zmniejszenie liczby błędów w systemach produkcyjnych z 40% do 80% (patrz linki poniżej).

Dalsze czytanie

  1. Książka „Efektywna praca ze starszym kodem” autorstwa Michaela Feathersa
  2. TDD, gdy po szyję w Legacy Code
  3. Przełamywanie ukrytych zależności
  4. Cykl życia starszego kodu
  5. Czy powinieneś testować jednostki prywatne w klasie?
  6. Wewnętrzne testy jednostkowe
  7. 5 typowych nieporozumień dotyczących TDD i testów jednostkowych
  8. Prawo Demeter

  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Czy Twój sterownik ODBC obsługuje źródła danych użytkownika?

  2. OGRANICZENIE KLUCZA OBCEGO SQL:Kompletny, łatwy przewodnik dla początkujących

  3. Podstawy sys.dm_exec_requests

  4. Błędy połączenia z bazą danych lub uwierzytelniania z ruchomym typem

  5. Nie twórz na ślepo tych brakujących indeksów!