Pokazywanie postów oznaczonych etykietą TDD. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą TDD. Pokaż wszystkie posty

poniedziałek, 5 grudnia 2011

TDD cz.2

Drugą część zaczniemy od kilku słów na temat testów integracyjnych. Później powiem coś na temat wzorców testowania jednostkowego. A na koniec opiszę pare zagadnień, które z różnych powodów nie zostały opisane wcześniej

I. Testy integracyjne

Ogólnie rzecz biorąc testowanie integracyjne wykonywane jest w celu wykrycia błędów w interfejsach i interakcjach pomiędzy modułami.Testy jednostkowe natomiast testują metody wybranej klasy, a klasy z nią współpracujące mockują. Testem jednostkowym nie jest więc test, który:
  • Komunikuje się z bazą danych
  • Komunikuje się po przez sieć
  • Pracuje z systemem plików
  • Nie może być uruchomiony równolegle z innymi testami
  • Musisz dokonać zmian w środowisku uruchomieniowym, aby zadziałał
Najczęściej spotykanym obszarem wykorzystania testów integracyjnych jest dostęp do danych. Testując integracyjnie dostęp do danych należy pamiętać o kilku ważnych rzeczach:
  • Testowana baza danych zazwyczaj nie jest idealnym odzwierciedleniem bazy produkcyjnej
  • Tworząc bazę testową należy wziąć pod uwagę następujące czynniki:
    • Jak bardzo ustawienia bazy testowej są zbliżone do bazy produkcyjnej
    • Na ile łatwo jest współdzielić konfigurację w zespole
    • Na ile łatwy jest dostęp do bazy
  • Zawsze czyść bazę danych przed testem, a nie po jego wykonaniu
  • Stwórz klasę wspierającą testowanie, która będzie zajmowała się czyszczeniem bazy danych
W celu odseparowania warstwy dostępu do danych używamy wzorca DAO. Możemy wtedy mockować dostęp do danych.

II. Wzorce testowania jednostkowego

1) Wzorce asercji

Wzorce te zostały zebrane i opisane przez Gerarda Meszaros http://xunitpatterns.com/

Wzorce asercji można podzielić na:

  • Asercja stanu końcowego – sprawdza stan końcowy
  • Asercja pomocnicza - programista upewnia się, że obiekt, który tworzy, ma odpowiedni stan
  • Asercja delta – badana jest różnica między stanem początkowym a końcowym
  • Własna asercja
  • Asercja interakcji

Programowa realizacja tych wzorców przejawia się w metodach klasy Assert. Metody te zostały opisane w podpunkcie IV.2

http://premium-hands.blogspot.com/2011/11/normal-0-21-false-false-false-pl-x-none.html

My skupimy się tylko na dwóch ostatnich typach.

Własna asercja (Custom Assertion) – jeśli używasz wielokrotnie tej samej asercji lub asercja jest złożona warto wyodrębnić metodę – własną asercję. Może się to wydawać oczywiste, ale warto o tym wspomnieć. Asercja własna jest najczęściej wynikiem refaktoryzacji kodu. Przypomnijmy, refaktoryzacji nie podlega tylko właściwy kod aplikacji, ale także kod testowy.

Asercja interakcji (Interaction Assertion) – zamiast badać stan bada się interakcję obiektów testowanych. Do tego celu głównie używa się Mock Objects. Tym sposobem możemy sprawdzić wywołanie konkretnej metody, parametry z jakimi została wywołana, a także kolejność wywołań. Testowanie interakcji stosuje się dla testów złożonych, gdzie ze względu na trudność realizacji testowanie stanu jest bardzo trudne.

2. Wzorce otoczenia testu

Otoczenie testu (test fixture) – otoczenie testu to warunki początkowe wspólne dla kilku testów. Otoczenie testu nadaje kontekst dla testów, które do niego przynależą. Jest realizowane po przez pola klasy testu i inicjację w metodzie oznaczonej atrybutem [SetUp] TODO: Poszukać atrybutu dla VS TeamTest. Otoczenie testu minimalizuje duplikację, wyodrębnia cechy wspólne wielu testów w formie pól i metody SetUp. Dobrze przemyślane rozwiązanie spowoduje, iż metody testowe będą się składać z prostych wywołań i asercji. Warto także wspomnieć, iż atrybut opisujący klasę testową w NUnit to [TestFixture].

Metoda fabryki (Parameterized Creation Method) – zamiast budować stan obiektów za każdym razem wypełniając wiele pól, warto wyodrębnić metodę pomocniczą, która pomoże stworzyć wymagane obiekty

  • Często podczas testowania należy tworzyć wiele pomocniczych obiektów
  • Stwórz metodę pomocniczą – metodę fabryki, która zajmie się szczegółami przygotowania obiektów do testu, które nie mają większego znaczenia
  • Czasami wystarczy wykorzystać odpowiedni konstruktor, jeśli istnieje lub stworzyć nowy

Klasy pomocnicze

  • Dodatkowo przydają się również pomocnicze klasy, które upraszczają tworzenie obiektów, np. generator unikalnych identyfikatorów (np. generator imion i nazwisk)

Matka obiektów

  • Klasa fabryki stworzona na potrzeby testów
  • Zawiera metody kreacyjne
  • Zawiera metody zmieniające stan obiektów (np. zamówione – złożone, zaakceptowane oczekujące zrealizowane)
  • Zbiera metody pomocnicze ułatwiające tworzenie obiektów na potrzeby testów
  • Może być to jedna klasa lub kilka klas
  • Klasy te mogą powstawać w efekcie refaktoryzacji testów

Automatyczne porządki (Automated teardown)

  • Sprzątanie po teście w przypadku złożonych danych jest nietrywialnie – łatwo zapomnieć o stworzonych obiektach
  • Tworzy się rejestr obiektów, do którego trafiają wszystkie obiekty tworzone w [SetUp], które należy usunąć po zakończeniu testu [TearDown]

3. Wzorce klas testowych:

Autopodstawienie (Self-Shunt)

  • Klasa testowa implementuje interfejs, który ma stanowić imitację (Test double)
  • Pozwala uniknąć tworzenia dodatkowych klas
  • Zaciemnia konstrukcję
  • Przydatne w prostych przypadkach

Uprzywilejowany dostęp (Privileged Access)

  • Bezpośrednie testowanie metod prywatnych jest w NUnit niemożliwe
  • Można skorzystać z refleksji do wywołania metod prywatnych
  • Nie powinno się nadużywać tej techniki
  • Zbyt wiele metod prywatnych, które należy przetestować, może sugerować refaktoryzację Wydzielenie klasy.

Dodatkowy konstruktor (Extra Constructor)

  • Jeśli kod, który testujesz ma zależności zaszyte wewnątrz klasy, trudno będzie go testować w izolacji
  • Stwórz dodatkowy konstruktor, który pozwoli dostarczać dane z zewnątrz
  • W teście użyj nowego konstruktora

III. Elementy testowanego kodu

1. Kompozycja zamiast dziedziczenia

Dziedziczenie jest jednym ze sposobów rozdzielenia odpowiedzialności między klasami. Umożliwia dodanie lub zmianę zachowania w klasie. Wadą jest to, że przeciążane metody mają tendencję do dużej zależności od klasy, w której się znajdują. Oznacza to, że zazwyczaj obciążone są pewnymi założeniami (np. metoda intensywnie korzysta z wewnętrznego stanu obiektu). Klasy dziedziczące mają tendencję także do rozrastania się. Często okazuje się że pewne elementy z klas nadrzędnych nie są potrzebne w klasach podrzędnych. Klasy są ze sobą mocno powiązane dziedziczeniem co utrudnia używanie test double.

Dzięki kompozycji klasa deleguje założone zadania do innych komponentów. Następuje lepsze rozłożenie odpowiedzialności. W testach możemy używać test doubles w miejscu klas zależnych. Poniważ klasy są mniejsze łatwiej testować je jednostkowo.

2. Unikanie elementów statycznych

Elementy statyczne mają szereg wad, które mogą utrudniać testowanie:

  • Elementy statyczne mają charakter globalny (są związane z klasą a nie z obiektem)
  • Elementy statyczne nie podlegają dziedziczeniu
  • Nie można zastosować do nich test double
  • Testowanie w pełni jednostkowe jest niemożliwe
  • Zależność jest zaszyta wewnątrz metody
3. Architektura Warstwowa

  • Architektura warstwowa sprzyja testowaniu
  • Powinny być wydzielone przynajmniej 3 warstwy
    • Interfejs użytkownika
    • Logika biznesowa
    • Dostęp do danych
  • Klasy wizualne interfejsu nie powinny zawierać żadnej logiki przetwarzania. Logikę UI należy przenieść do obiektów pomocniczych
IV. Różne różności

W ostatniej części tego wpisu chciałem wspomnieć o kilku rzeczach, na które nie znalazło się miejsce w poprzednich rozdziałach.

1. Złote zasady testów automatycznych

  • Automatyczne – wykonują się bez udziału człowieka
  • Zupełne – obejmują wszystkie istotne przypadki testowe
  • Powtarzalne – mogą być uruchamiane wielokrotnie
  • Niezależne – nie powinny zależeć od zmian w środowisku zewnętrznym testu
  • Profesjonalnie – kod testu powinien być napisany równie starannie, jak kod produkcyjny
2. Strategie wyboru testów

a) Ogół – szczegół
b) Znane – nieznane
c) Ścieżka pozytywna – ścieżka negatywna

Ad. a

- Ogół

Najpierw testy wysokiego poziomu – tworzonych jest wiele testów bez początkowego wchodzenia w szczegóły.

Zalety:

  • Większe zrozumienie całościowe systemu

- Szczegół

Dany test jest rozwijany tak długo, aż nie osiągnie się ostatecznego, pełnego rozwiązania.

Zalety:

  • Szybka eliminacja ryzyka, gdyż rozpoznawane są szczegóły problemu


Ad.b Znane - nieznane

- Znane

Najpierw wybierane są te testy, które sprawdzają typowe elementy funkcjonalności, które są łatwo przewidywalne,

Zalety:

  • Jednoznaczne ścieżka prowadząca dająca szybkie efekty

- Nieznane

Najpierw wybierane są te testy, które obejmują mniej typowe przypadki

Zalety:

  • Wczesna analiza i potencjalna eliminacja ryzyka – zostaną szybko wykryte te elementy których nie da się wykonać lub będzie to nieopłacalne

Note:

Nie warto stosować kiedy nietypowe przypadki są bardzo mało prawdopodobne lub nie mają wpływu na system.

Ad.c Ścieżka pozytywna ścieżka negatywna

- Ścieżka pozytywna

Testowane są scenariusze typowego, zakończonego powodzeniem działania metody lub funkcji.

Zalety:

  • Testowane jest to, co przydarza się najczęściej

- Ścieżka negatywna

Testowane są scenariusze sytuacji wyjątkowych, na które system powinien być przygotowany

Zalety:

  • Minimalizowane jest ryzyko wynikające z błędów użytkownika lub programisty

Note:

Zazwyczaj najpierw warto wybrać testowanie z użyciem ścieżki pozytywnej, a następnie ścieżki negatywnej

Jak to połączyć w praktyce?

  1. Zacznij od prostego i oczywistego testu (sunny day scenario)
    - proste podstawienia, typowy scenariusz
  2. Testuj każdą funkcję z uwagą.
    - Zastanów się czy dana funkcja jest istotna z punktu widzenia systemu i jeśli tak, to, dlaczego
  3. Najpierw testuj ogólnie, a następnie szczegółowo
    - W ten sposób odnajdziesz najbardziej newralgiczne punkty systemu
  4. Po przetestowaniu podstawowego scenariusza, wybierz trudne ścieżki (nieznane lub negatywne)




niedziela, 4 grudnia 2011

Scrum i Team Foundation Server cz.6 - Proces TDD

W kilku ostatnich wpisach przedstawiłem czym jest metodyka Scrum, po co i jak ją stosować przy użyciu Team Foundation Server. W teorii moglibyśmy już rozpocząć projekt i z powodzeniem go prowadzić. Możliwe, że więcej wiedzy nie było by Wam szybko potrzebne, ale podejrzewam, że całkiem szybko zaczęły się pojawiać pytania o zarządzanie i przepływ zadań, np.:
-  Który rodzaj zadania jest  z czym powiązany?
- Czy jak dodamy buga to mamy dodać do niego też taska?
- do czego służy test case a do czego impediment?
- itd. Itp.
Takie pytania są nieuchronne, każdy Scrum Master będzie musiał je wysłuchiwać po 100 razy. Można jednak ich częstotliwość zmniejszyć poprzez spisanie dokumentu, zawierającego przykładowy przepływ pomiędzy zadaniami oraz zasady tworzenia dokumentów. Dzięki niemu będziemy mogli w krytycznej sytuacji bez większych wyrzutów sumienia odpowiedzieć „zajrzyj sobie do dokumentacji”.

W Scrum centralnym elementem opisu biznesowego przypadku jest Historia Użytkownika (User Story) zwana również „elementem rejestru produktu” (Product Backlog Item – PBI). Opisuje ona wartość biznesową, która ma zostać dodana do projektu np. ”Jako użytkownik gry One Card Master chcę, zobaczyć informację o aktualnej liście uczestników gry, abym wiedział z kim gram”.
Schemat można było by opisać (tak jak przedstawiono we wpisie) przez:
JAKO <Osoba, rola>
CHCĘ <Funkcjonalność, czynność>
ABY <Uzasadnienie biznesowe>
W teorii pisanie historii użytkownika wydaje się sprawą zupełnie prostą, ale w praktyce okazuje się, że jest zupełnie inaczej. Same zasady tworzenia historii użytkownika są materiałem na osobny wpis.
No ale wracając do tematu procesu. Tak jak wspomniałem we wpisie Scrum jest mocno powiązany z metodyką TDD. Podążając z jej zasadami, będzie nam dużo łatwiej prowadzić projekt, oraz zarządzać przepływem. We wspomnianym wpisie powiedziałem, że każdy PBI powinien mieć wyraźnie i dokładnie opisane kryteria akceptacji – czyli warunki, które muszą zostać spełnione, żeby zadanie zostało zaakceptowane.

Same historie użytkownika opisują jedynie ogólne aspekty realizacji biznesowego problemu. Aby kompleksowo zamodelować proces wytwarzania funkcjonalności konieczne jest dodanie do nich konkretnych elementów tj:
Źródło: Crispin Parker's Blog

- Task – relacja „Implemented-By” – czyli opisuje wszystko to co jest konieczne do tego, żeby od strony programistycznej zaimplementować historię użytkownika
- Acceptance Test – relacja „Tested-By” – opisuje testy akceptacyjne, czyli to w jaki sposób historia użytkownika będzie testowana
- Bug Report – relacja „Failed – By” – opisuje co poszło nie tak przy testach akceptacyjnych
- Impediment – relacja „Impeded-By” – przeszkoda, Opisuje problemy, które wystąpiły przy implementacji historii użytkownika, może to być np. sytuacja, że przy tworzeniu algorytmu nie wzięliśmy pod uwagę jakiegoś aspektu, którego zaimplementowanie wymaga dodatkowej analizy

Co z sytuacjami gdy odnajdziemy inne błędy, które nie zostały ujęte w testy akceptacji? Dorzucamy do rejestru produktu (ewentualnie rejestru spritu jeżeli jest on kluczowy dla jego oddania) element typu Bug.  Ma on identyczną strukturę zależności jak historia użytkownika. Powinien również posiadać zadanie, opis testów, które będą dokonywane przy weryfikacji tego buga, może zawierać Impediment oraz raport o błędzie, który mówi o tym, że rozwiązanie błędu nie zostało zaakceptowane.

Paczkę koniecznej wiedzy teoretycznej już uzyskaliśmy, możemy teraz przejść do przykładu. Będzie on pokazany na Team Foundation Server 11 DP oraz Visual Studio 11 DP. Załóżmy, że tworzymy znaną z innych wpisów grę One Card Master. Mamy już zaimplementowaną część kodu odpowiadającego za zarządzanie graczami. Dostępne już są klasy:

- informacje o graczu:

public class Player
{
    public string Name { get; set; }
}

- zarządzajanie graczami, pozwalające na ich dodawanie

public interface IPlayersManager
{
    IList<Player> GetPlayers();

    void AddPlayer(Player player);
}

public class PlayersManager : IPlayersManager
{
    private readonly IList<Player> _playersList = new Listt<Player>();
        
    public void AddPlayer(Player player)
    {
        _playersList.Add(player);
    }

    public IList<Player> GetPlayers()
    {
        return _playersList;
    }
}

- dostępne są również klasy pozwalające na drukowanie informacji na ekranie

public interface IPrinter
{
    void Print(string text);
}

public class ConsolePrinter : IPrinter
{
    public void Print(string text)
    {
        Console.WriteLine(text);
    }
}

Klient zażyczył sobie, że musimy dodać funkcjonalność wyświetlania aktualnej listę graczy. 
Załóżmy, że znajdujemy się na etapie planowania sprintu. Pierwszym krokiem, który powinniśmy zrobić jest dodanie nowej historii użytkownika. Dokonujemy tego poprzez kliknięcie menu jak na poniższym obrazku.


Historia użytkownika

Ukaże nam się okno definicji historii użytkownika:


Nazywamy naszą historię użytkownika "Wyświetlanie aktualnej listy graczy" podajemy jego opis:

”Jako użytkownik gry One Card Master chcę, zobaczyć informację o aktualnej liście uczestników gry, abym wiedział z kim gram”

Podajemy kryteria akceptacji:

1. Po dołączeniu nowego gracza system powinien wyświetlić zaktualizowaną listę użytkowników.
2. W obecnej wersji powinien wyświetlać informacje na konsoli.
3. Informacja o użytkowniku powinna być zapisana w formacie "{Lp}. Nazwa: {Nazwa}".

Przypadek testowy

Zapisujemy historię użytkownika, ale nie zamykamy okna tylko przechodzimy do zakładki "Test cases" i naciskamy przycisk "New".


Dodajemy w ten sposób test akceptacji. Nazywamy go "Test wyświetlania aktualnej listy graczy" i naciskamy "OK". Otworzy się nam widok przypadku testowego. Zapisujemy go. Dostaniemy teraz pełen dostęp do opcji tworzenia testu historii użytkownika.
Jak łatwo zauważyć cały czas postępujemy tutaj zgodnie z metodyką TDD:
- najpierw opisaliśmy co chcemy (historia użytkownika),
- następnie co musi być spełnione, żeby uznać funkcjonalność za poprawnie działającą (kryteria akceptacji),
- teraz napiszemy jak będziemy to testować.


Naciskamy przycisk "Edit with Microsoft Test Manager". Po tej akcji przejdziemy do zewnętrznego programu przygotowanego przez Microsoft specjalnie do planowania, zarządzania oraz przeprowadzania testów (postaram się go przedstawić w osobnym wpisie). Nie wgłębiając się specjalnie w szczegóły - posłuży on nam do zdefiniowania kroków naszego testu.


Dodajemy trzy kroki (poprzez uzupełnienie odpowiednich pól w tabelce "Steps"):

"1. Dodajemy gracza "Jan Kowalski" poprzez metodę AddPlayer klasy PlayersManager - Wyświetlona informacja: "01. Nazwa: Jan Kowalski"
2. Dodajemy gracza "Krzysztof Krawczyk" poprzez metodę AddPlayer klasy PlayersManager - Wyświetlone informacje: "01. Nazwa: Jan Kowalski", "02. Nazwa: Krzysztof Krawczyk"
3. Dodajemy gracza "Eustachy Janicki" poprzez metodę AddPlayer klasy PlayersManager - Wyświetlone informacje: "01. Nazwa: Jan Kowalski", "02. Nazwa: Krzysztof Krawczyk", "03. Eustachy Janicki" "

Przechodzimy do zakładki Summary i przeklejamy tam tekst z kryteriów akceptacji, zapisujemy zmiany i zamykamy Test Managera. Po odświeżeniu widoku Test Case'a powinny pojawić się na nim wpisane przez nas dane.

Zadanie

Wracamy ponownie do naszej historii użytkownika. Dodamy teraz zadanie developerskie poprzez przejście do zakładki Tasks i naciśnięcie przycisku "New". Nazywamy go "Implementacja wyświetlania aktualnej listy graczy".



Wypełniamy pola zgodnie z powyższym obrazkiem (opis jako: "Należy stworzyć taki mechanizm, który po dodaniu gracza poprzez metodę AddPlayer z klasy PlayerManager automatycznie wyświetli na ekranie aktualną listę graczy.") i przypisujemy go do osoby, która będzie go realizowała (np. do siebie) poprzez "Assigned To".
Gdy zaczniemy realizację zadania ustawiamy jego status (State) na "In Progress", oraz status PBI na "Approved".

Przeszkoda

Załóżmy, że okazało się, że nie wiemy jak coś zrobić. Specyfikacja jest niedokładna, mamy problem z wymyśleniem odpowiedniego rozwiązania,  albo problemy techniczne z komputerem.

Załóżmy, że nie mamy pojęcia jak ugryź kompletnie to zadanie, musimy przeprowadzić konsultacje. Przechodzimy wtedy do naszej historii użytkownika, do zakładki "Links" i naciskamy przycisk "New".


Uzupełniamy "Link Type" jako "Child" (bo Impediment będzie dzieckiem naszej historii użytkownika) oraz "Work Item Type" jako Impediment. Nazywamy go "Problem z metodą automatycznego wyświetlania aktualnej listy graczy" i naciskamy OK.


Pojawi się widok Impedimentu, dopisujemy opis (np. "Mam problem z wymyśleniem metody automatycznego wyświetlania aktualnej listy graczy, potrzebuję konsultacji z kimś bardziej doświadczonym."). Zapisujemy go i wracamy do widoku PBI. Musimy na nim zaznaczyć, że prace nad nim zostały wstrzymane. Dokonujemy tego poprzez oznaczenie jego pola "Blocked" na "Yes".

Załóżmy, że udało nam się ustalić, że powinniśmy rozwiązać problem
automatycznego wyświetlania listy gracz przy pomocy wzorca obserwatora. Przechodzimy do utworzonej wcześniej Przeszkody, wpisujemy nasze rozwiązanie w zakładkę "Resolution" i zmieniamy status na zamknięty ("Closed").

Możemy teraz przystąpić do programowania.

Implementacja

Implementację zaczynamy oczywiście od napisania testu akceptacyjnego (przykład będzie mocno uproszczony bo nie o to tutaj chodzi - więcej na temat testów jednostkowych możecie znaleźć we wpisach tutaj i tu). Postępujemy zgodnie z tym co zawarliśmy w testach akceptacyjnych oraz Impedimencie. Opis wzorca obserwator można znaleźć przykładowo tutaj.
Test może wyglądać następująco:

[TestMethod]
public void AddPlayersPrintValidInformations()
{
    var mocks = new MockRepository();

    var playersManagers = new PlayersManager();
    var printer = mocks.StrictMock<Player>();
    var playersObserver = new PlayersObserver(printer);

    playersObserver.Observe(playersManagers);
            
    playersManagers.Attach(playersObserver);
            
    //Dodanie pierwszego gracza
    Expect.Call(()=> printer.Print("1: Jan Kowalski"));
            
    //Dodanie drugiego gracza
    Expect.Call(() => printer.Print("1: Jan Kowalski"));
    Expect.Call(() => printer.Print("2: Krzysztof Krawczyk"));
            
    //Dodanie trzeciego gracza
    Expect.Call(() => printer.Print("1: Jan Kowalski"));
    Expect.Call(() => printer.Print("2: Krzysztof Krawczyk"));
    Expect.Call(() => printer.Print("3: Eustachy Janicki"));

    mocks.ReplayAll();

    //Dodanie pierwszego gracza
    playersManagers.AddPlayer(new Player { Name = "Jan Kowalski" });
            
    //Dodanie drugiego gracza
    playersManagers.AddPlayer(new Player { Name = "Krzysztof Krawczyk" });
            
    //Dodanie trzeciego gracza
    playersManagers.AddPlayer(new Player { Name = "Eustachy Janicki" });

    mocks.VerifyAll();
}

Oczywiście przy pisząc test postępujemy zgodnie z metodologią TDD, po kolei uzupełniając definicje klas i metod. Przykładowa ich implementacja mogła by wyglądać:

public class PlayersObserver : IPlayersObserver
{
    private readonly IPrinter _printer;
    private IPlayersManager _playersManager;

    public PlayersObserver(IPrinter printer)
    {
        _printer = printer;
    }

    public void Observe(IPlayersManager playersManager)
    {
        _playersManager = playersManager;
    }


    public void Update()
    {
        var players = _playersManager.GetPlayers();

        var i = 0;

        foreach (var player in players)
        {
            _printer.Print(
                String.Format(
                    "{0}: {1}", ++i, player.Name));
        }
    }

    public void OnPrintState(string info)
    {
        Console.WriteLine(info);
    }
}

public class PlayersManager : IPlayersManager
{
    private readonly IList<IPlayersObserver> _observers 
        = new List<IPlayersObserver>();

    private readonly IList<Player> _playersList = new List<Player>();

    public void Attach(IPlayersObserver observer)
    {
        _observers.Add(observer);
    }

    public void Detach(IPlayersObserver observer)
    {
        _observers.Remove(observer);
    }

    public void AddPlayer(Player player)
    {
        _playersList.Add(player);
        foreach(var observer in _observers)
        {
            observer.Update();
        }
    }

    public IList<Player> GetPlayers()
    {
        return _playersList;
    }
}

W tym momencie kończy się proces implementacji. Nasze testy przechodzą. Możemy zrobić Check in naszych zmian. Przechodzimy do Team Explorera, naciskamy "Pending Changes".


Pierwszą rzeczą jest dodanie komentarza (nie zapominajmy do tym!). Kolejną powiązanie naszych zmian z zadaniami. Naciskamy "Add Work Item by ID" i dodajemy zarówno nasze zadanie jak i PBI. Przy wiązaniu zmian mamy dwie opcje:
- Associate - po prostu "doklejamy" informację o tym, że zmiana dotyczy danego Work Itema (WI)
- Resolve - robi to samo co Associate i dodatkowo zmienia status WI na rozwiązany
Ponieważ uznajemy nasze zadanie za skończone zaznaczamy je jako Resolve. Decyzję o zakończeniu historii użytkownika zgodnie z metodyką Scrum podejmuje Właściciel Produktu, dlatego też w przypadku PBI zaznaczamy tylko Associate.
Gdy nasze zmiany wejdą z sukcesem pozostaje nam tylko jedna rzecz do zrobienia - podpięcie naszego testu akceptacyjnego do zdefiniowanego Test Case'a. Otwieramy informacje o nim i przechodzimy do zakładki "Associated automation".


Wybieramy nasz test jednostkowy poprzez przycisk koło pola "Automated test name".
Poprawiamy wartość effort przy zadaniu oraz PBI i jeżeli wszystko zrobiliśmy poprawnie zakończyliśmy tym samym nasz proces implementacji.

Testy

Po zakończeniu implementacji (zwykle na koniec Sprintu) Właściciel Produktu testuje czy wszystkie historie użytkownika zostały zrealizowane poprawnie.
W naszym przypadku okazuje się, że wystąpił błąd. W opisie testu akceptacji było napisane, że:

"Dodajemy gracza "Jan Kowalski" poprzez metodę AddPlayer klasy PlayersManager - Wyświetlona informacja: "01. Nazwa: Jan Kowalski"
 
My w trakcie implementacji nie zauważyliśmy, że numery mniejsze od 10 mają mieć zero na początku. Proces zgłaszania i rozwiązania wygląda następująco:
1. Właściciel produktu dodaje teraz błąd powiązany z naszą historią użytkownika (w sposób analogiczny jak podpinaliśmy impediment).
2. My, po analizie błędu oceniamy ile czasu zajmie jego poprawienie, uaktualniamy effort dla PBI oraz buga
3. Dodajemy zadanie-dziecko do błędu (analogicznie jak zadanie do historii użytkownika)
4. Gdy rozwiążemy błąd i wrzucamy nasze zmiany spinamy je zarówno z zadaniem,błędem jak i historią użytkownika.
5. Gdy Właściciel Produktu zaakceptuje rozwiązanie zamyka błąd i oznacza historię użytkownika za zrealizowaną.

Podsumowanie

Mam nieodparte wrażenie, że ten artykuł jest z jednej strony za krótki, z drugiej, że za długi. Dlaczego? Za krótki gdyż temat jest tak rozległy, że można by było napisać książkę i to by było mało. Za długi, bo nie wiem czy ilość szczegółów nie zamaże obrazu całości.
Mam jednak nadzieję, że udało mi się jednak przekazać ideę oraz że lektura mojego artykułu pozwoli Wam zrozumieć jak można zamodelować proces tworzenia funkcjonalności w TFS zgodnie z TDD i metodyką Scrum.

piątek, 2 grudnia 2011

TDD cz.1


Może na wstępie kilka słów o TDD. TDD to technika tworzenia oprogramowania zaliczana do metodyk zwinnych. Od tradycyjnego sposobu wytwarzania oprogramowania odróżnia ją przede wszystkim cykl programowania.


Tradycyjny cykl programowania

Taki sposób tworzenia oprogramowania obciążony jest kilkoma wadami:

  • Testowanie
    • Testerzy testują kod nie znając go zbyt dobrze
    • Brak zaangażowania testerów
    • ­­­­­Na czas testowania kod jest zamrożony
    • Naprawa błędu po zakończeniu implementacji jest zazwyczaj 2-krotnie wyższa niż w jej trakcie
  • Utrzymanie
    • Testy „po” sprawdzają wybiórczo kod
    • Testy nie przyczyniają się do zwiększenia jakości kodu, a jedynie sprawdzają, jak działa napisany kod
Ponieważ w podejściu TDD ważniejsze jest "co", a nie "jak" cykl programowania jest odwrócony.

Cykl programowania TDD

Przepis na TDD jest prosty:

  1. Najpierw napisz test, który nie przechodzi.
  2. Stwórz najprostsze możliwe rozwiązanie
  3. Refaktoryzuj
  4. Wróć do kroku 1

Najpierw napisz test

  • W TDD jest to ważne narzędzie – tutaj powstają pierwsze decyzje projektowe.
  • Programista musi przemyśleć jak kod zostanie użyty
  • Test wymusi odnalezienie tego, co rzeczywiście jest potrzebne w systemie
  • Testy stają się dokumentacją

Stwórz najprostsze możliwe rozwiązanie

  • Napisz dokładnie to, co jest wymagane przez test.
  • Nie dodawaj fajerwerków do kodu
  • Kod musi przejść stworzone testy – tyle wystarczy.

Refaktoryzuj

  • Przyjrzyj się swojemu rozwiązaniu
  • Zastanów się, co należy zrobić lepiej
  • Zmieniaj to, co rzeczywiście pomoże lepiej zrealizować wymagania
  • Popraw brzydkie fragmenty kodu

Ważnym pojęciem w TDD jest programowanie przyrostowe. Metoda najmniejszych kroków. Programowanie przyrostowe odnosi się zarówno do kodu programu jak i testów, które stają się coraz pełniejsze wraz z każdą iteracją.

W danym momencie dokonuj tylko jednej zmiany w systemie (możliwie najmniejszej)

Taka technika programowania posiada kilka zalet:

  • W każdym momencie masz w pełni działający kod
  • Małe zmiany powodują, że łatwiej nad mini zapanować
  • Małe zmiany powodują, że łatwiej znaleźć błędy, gdyż powodów błędów jest mniej.
  • Pracowanie na mniejszych fragmentach kodu daje większą kontrolę programiście
  • Projekt powstaje ewolucyjnie – programista może czerpać doświadczenie z tego, co napisał do tej pory.

W dalszej części tego dokumentu zauważycie, że metody przyrostowe wykorzystywane są na każdym kroku w TDD. Zaczynamy od rzeczy najprostszych i najprostszych rozwiązań

Przyczyną opracowania tej techniki programowania są ograniczenia pola świadomości:

  • Ludzki świadomy umysł jest w stanie przetwarzać 5-7 informacji w danym momencie
  • Dzieląc problem na mniejsze części ułatwiasz sobie pracę
  • Mniej szczegółów musisz zapamiętać
  • Jesteś bardziej świadomy konsekwencji wprowadzonych zmian

Test-Driven Development – jak to się robi

Pierwszą rzeczą, jaką należy zrobić przystępując od tworzenia projektu jest spis funkcjonalności. Możemy tutaj posłużyć się szablonem User Story, który pozwala na opisanie funkcjonalności w sposób najbardziej ogólny:

Jako <<osoba, rola>>,

chcę <<funkcjonalność, czynność>>,

aby <<uzasadnienie biznesowe&gt>;

As <<osoba, rola&gt>;,

I want <<funkcjonalność, czynność>>,

so that <<uzasadnienie biznesowe>>

Tutaj zahaczamy trochę o Behaviour-Driven Development. Jest to popularny pomysł na sterowanie procesem pisania kodu. Skupia się na wymaganiach użytkownika i interesariuszy. Określa formę zapisu wymagań, które są przekształcane w testy.

W BDD osią rozwoju systemu jest zachowanie systemu. Testy powinny przede wszystkim opisywać (i testować) zachowanie systemu. Formułowanie zachowania staje się sposobem definiowania wymagań i zarazem ich dokumentacją.

Tutaj znajdziecie ogólny opis BDD http://www.araneo.pl/blog/behaviour-driven-development

Posiadając już taką listę User Story „Musisz odpowiedzieć sobie na jedno zajebiście, ale to zajebiście ważne pytanie: co lubisz w życiu robić? Jaka jest następna najważniejsza rzecz, którą system powinien robić? A potem zacznij to robić.”

Jednym słowem należy wybrać najważniejszą z punktu widzenia użytkownika funkcjonalność i stworzyć dla niej kryteria akceptacyjne. Kryteria akceptacyjne formułowane są jako scenariusze. Możemy skorzystać tutaj z szablonu:

Zakładając że <<opis kontekstu>>

Gdy <<opis zdarzenia w systemie>>

Wtedy <<spodziewany rezultat>>

Diven <<opis kontekstu>>

when <<opis zdarzenia w systemie>>

then <<spodziewany rezultat>>

Kryteria akceptacyjne powinny być wykonywalne. Aby kryteria akceptacyjne miały największą wartość użytkową powinny mieć odzwierciedlenie w kodzie. Istnieje wiele frameworków wspierających BDD

  • JBehave (Java)
  • NBehave (.NET)
  • Spec (Ruby)
  • i wiele innych …
BDD wspiera tworzenie specyfikacji poprzez przykład (specification by example). Wymagania funkcjonale mogą być definiowane w formie kryteriów akceptacyjnych, które stają się jednocześnie dokumentacją.

Teraz przyszedł czas na testy akceptacyjne.

Ogólny schemat testowania akceptacyjnego

Testy akceptacyjne testują funkcjonalność z punktu widzenia użytkownika. Skupiają się na perspektywie użytkownika końcowego. Symulują one interakcję z systemem zewnętrznym (np. z użytkownikiem, usługą sieciową):

  • poprzez interakcję z użytkownikiem
  • wysyłając komunikaty symulujące system zewnętrzny
  • wywołując usługi sieciowe
  • parsując generowane raporty

Testy akceptacyjne odpowiadają za tzw. jakość zewnętrzną, czyli w jakim stopniu system zaspokaja potrzeby klienta i użytkowników. Pozwalają nam także sterować testami jednostkowymi, które to z kolei odpowiadają, za jakość wewnętrzną, czyli w jakim stopniu system zaspokaja potrzeby programistów i administratorów. (Testy integracyjne są gdzieś pośrodku J)

To jak już tyle wiemy to można już coś zrobić.

Od czego zacząć?

  • Zbuduj infrastrukturę, aby zautomatyzować proces
    build – deploy – test
  • Zwiększasz ilość uzyskiwanej informacji zwrotnej
  • Dzięki temu od razu będziesz mógł weryfikować budowanie i wdrażanie systemu
  • Szkielet – implementacja bardzo wąskiego wycinka systemu, który można zbudować, wdrożyć i przetestować
  • Zobacz jak system działa najszybciej jak to tylko możliwe
  • Jeśli istnieje system zewnętrzny, którego trudno użyć, zbuduj jego namiastkę

Przydatne jest także stworzenie zgrubnego projektu, czyli wysokopoziomowej struktury aplikacji lub części nad którą pracujesz. Przemyśl główne komponenty i ich powiązania. Jak komponenty będą się komunikować. Na stworzenie zgrubnego projektu przeznacz co najwyżej kilkanaście minut – przydatny jest flipcharta lub tablicy.


Dobre praktyki

  • Rozpocznij implementację funkcjonalności od testu akceptacyjnego
    • Wyrażone w języku użytkownika
    • Pozwala zrozumieć system przed przystąpieniem do implementacji
  • Oddziel testy, które wskazują postęp od testów regresyjnych
    • Testy regresyjne zawsze muszą przechodzić
    • Testy wskazujące postęp mogą nie działać w czasie rozwoju
  • Rozpocznij od najprostszego przypadku
    • Podstawowy scenariusz pozytywny
    • Implementacja powinna być prosta, ale nie zdegenerowana (zbyt prosta)
  • Pisz testy, które miałbyś ochotę przeczytać
    • Czytelny test wspiera nas w odnajdywaniu błędów i oszczędza nasz czas
  • Obserwuj, w jaki sposób test nie przechodzi
    • Zmodyfikuj komunikaty o błędach, aby były proste i jednoznaczne
  • Testuj zachowanie a nie metody
    • Przykładowo test testMyMethod nie mówi, jaki jest cel testu
    • Maksymalne pokrycie kodu nie jest dobrym celem
    • Skup się na funkcjach, które metoda realizuje
  • Jeśli trudno napisać test
    • Nie zastanawiaj się jak go napisać
    • A raczej co sprawia, że trudno jest przetestować
    • Najczęściej kiepski kod trudno jest przetestować
      • Przeanalizuj jakość kodu i go zrefaktoryzuj
  • Praktykuj ciągłe doskonalenie
    • Bezustannie analizuj swoje techniki testowania
    • Odnajduj słabe punkty swoich strategii i zmieniaj je
    • Szukaj balansu pomiędzy testami jednostkowymi a testami integracyjnymi

Na początku tego dokumentu wspominałem, że podejście przyrostowe można napotkać na każdym kroku TDD. Implementując daną funkcjonalność zaczynamy od najprostszego przypadku (testu akceptacyjnego), a następnie go rozwijamy. Przykładowo nasza wspaniała aplikacja karciana będzie umożliwiać użytkownikowi wyświetlenie listy gier, jakie rozegrał.

Jako Użytkownik

Chcę zobaczyć listę gier,

Aby móc przeglądać historię rozegranych partii.

To by było nasze User Story J. Dodatkowymi wymaganiami byłoby, że listę można sortować, a wszystkie przegrane oznaczone są na czerwono.

Przykładowa lista testów w inkrementacjach mogłaby wyglądać tak:

  • Pusta lista wyników (dla użytkownika bez rozegranych partii)
  • Lista wyników z kilkoma grami
  • Lista wyników z wyróżnionymi porażkami
  • Lista wyników z sortowaniem

Cały proces testowania zaczyna się i kończy na teście akceptacyjnym. Jakby to powiedzieli mądrzy ludzie, test akceptacyjny jest klamrą dopinającą cały proces implementacji funkcjonalności. Pisząc test akceptacyjny, zauważamy braki w naszym projekcie. (Brak metod, klas itd. Przypomnijmy, że na początku posiadamy jedynie szkielet aplikacji.) Co robimy, jeżeli zauważymy brak danej metody? Piszemy dla niej test (oczywiście nie zapominając o specyfikacji i podejściu przyrostowym). Potem implementacja, znowu może nam czegoś brakować i znowu testy i znowu implementacja i tak w kółko, aż dojdziemy do końca.

Wspominałem wcześniej, że testy akceptacyjne sterują testami jednostkowymi. Zaczynami od testów najwyższego poziomu. W trakcie ich powstawania powstają testy niżej poziomowe. Po ich implementacji wracamy z powrotem do testu wysokopoziomowego. Może to trochę słabo opisałem, ale mam nadzieję, że wszyscy wiedzą, co chciałem powiedzieć.

 
Design by Free WordPress Themes | Bloggerized by Lasantha - Premium Blogger Themes | Online Project management