Wprowadzenie do Test Driven Development na przykładach

Dec 23 2022
Istnieje świetna książka napisana przez Kenta Becka, Test-Driven Development by Example. Ta książka jest bardzo polecana w branży, ponieważ mimo wszystkich swoich wad (i powoli tracących na aktualności) daje ponadczasowy zarys rozwoju opartego na testach.

Istnieje świetna książka napisana przez Kenta Becka, Test-Driven Development by Example. Ta książka jest bardzo polecana w branży, ponieważ mimo wszystkich swoich wad (i powoli tracących na aktualności) daje ponadczasowy zarys rozwoju opartego na testach. Test Driven Development (TDD) to coś, co uwielbiam, ale w świecie, w którym kluczowe punkty można przedstawić w artykule, polecanie książki (niezależnie od tego, jak świetna) stało się ostatnio trudniejsze. Jeśli kiedykolwiek zastanawiałeś się, jak programować przy użyciu TDD, dlaczego istnieje i dlaczego jest tak potężny, ten artykuł ma na celu zbadanie niektórych powodów.

Rozwój oparty na testach to dyscyplina inżynierii oprogramowania. Jest to praktyka pisania testu dla danego zestawu kodu, następnie pisania kodu produkcyjnego dla tego testu i wreszcie czyszczenia (refaktoryzacji) kodu. Ten cykl jest podstawą rozwoju opartego na testach, czerwony (testy napisane), zielony (testy zaliczone), a następnie czysty (refaktoryzacja w stanie zielonym). Ten cykl pokazuje moc TDD — nie ma potrzeby testowania kodu tak samo, jak Twoi użytkownicy (lub QA). Testy powinny być szybkie, a możliwość ich uruchomienia powinna być łatwo dostępna — wystarczy nacisnąć przycisk, a nie tylko zapewnić kontrolę jakości.

Testuj, rozwijaj, czyść lub czerwony, zielony, czysty to cykl dla TDD

Napiszmy test dla pewnego kodu teoretycznego. Przykłady w tym artykule są w języku C#, jednak programiści Java powinni być w stanie je łatwo zrozumieć. Jeśli chcesz wypróbować to na bieżąco, to jest pusty szablon projektu, a to jest ostateczny projekt . Jeśli korzystasz z języka Java, jest to pusty projekt, aw końcowym repozytorium projektu znajduje się wersja Java tego samouczka.

[TestClass]
public class AccountTests
{
    [TestMethod]
    public void Balance_ReturnsZero_WhenANewAccountIsCreatedTest()
    {
        // Arrange
        var newAccount = new Account();

        // Act
        int balance = newAccount.Balance;

        // Assert
        Assert.AreEqual(0, balance);
    }
}

public class Account
{
    public int Balance;
}

Dlaczego ten „najmniejszy kod” jest dobrą zasadą? Celem w TDD jest 100% pokrycie kodu, inżynier śpi dobrze, im bardziej wierzy, że napisany przez niego kod nie jest tajemnicą (tajemniczy kod oznacza, że ​​zawiera błędy i nie jest w pełni przetestowany). 100% pokrycia nie jest możliwe z różnych powodów, ale dążenie do jak największego zbliżenia się do tego jest skutecznym celem osiągnięcia pozytywów TDD. Kiedy ktoś pisze więcej kodu niż jest to potrzebne, ostatecznie pisze ścieżki rozgałęziające (lub dane zależne), co oznacza zmianę zachowania. Każde oczekiwane zachowanie danego modułu powinno mieć test ; dlatego jeśli ktoś pisze minimalny kod, aby osiągnąć zielony, ciężar spoczywa na testach, aby udowodnić, że zachowanie jest błędne .

[TestMethod]
public void Overdraft_ReturnsZero_WhenANewAccountIsCreatedTest()
{
    // Arrange
    var newAccount = new Account();

    // Act
    int overdraft = newAccount.Overdraft;

    // Assert
    Assert.AreEqual(0, overdraft);
}

[TestMethod]
public void Balance_ReturnsZero_WhenOverdraftAndBalanceAreZeroAnd100IsTakenTest()
{
    // Arrange
    var newAccount = new Account();

    // Act
    newAccount.Balance -= 100;
    int balance = newAccount.Balance;

    // Assert
    Assert.AreEqual(0, balance);
}

Jaka jest najmniejsza ilość kodu, którą można napisać, aby te testy przeszły pomyślnie? Być może czek w rachunku bieżącym wynosi zero i zwrot w naturze lub saldo zwraca tylko zero. To jest kod, który zdecydowałem się wybrać, który wyjaśnia, dlaczego pisanie najmniejszego kodu jest skuteczne.

public class Account
{
    public int Balance 
    {
        get => 0; 
        set { } 
    }

    public int Overdraft;
}

[TestMethod]
public void Balance_Returns100_When100IsAddedToABlankBalanceTest()
{
    // Arrange
    var newAccount = new Account();

    // Act
    newAccount.Balance += 100;
    int balance = newAccount.Balance;

    // Assert
    Assert.AreEqual(100, balance);
}

public class Account
{
    public int Balance 
    {
        get => balance; 
        set 
        {
            if(value > 0)
            {
                balance = value;
            }
        }
    }
    private int balance;

    public int Overdraft;
}

[TestMethod]
public void Balance_ReturnsMinus100_WhenBalanceIsZeroAndOverdraftIs100And100IsTakenFromBalanceTest()
{
    int expectedValue = -100;

    // Arrange
    var newAccount = new Account();
    newAccount.Overdraft = 100;

    // Act
    newAccount.Balance -= 100;
    int balance = newAccount.Balance;

    // Assert
    Assert.AreEqual(expectedValue, balance);
}

// Production code
public class Account
{
    public int Balance 
    {
        get => balance; 
        set 
        {
            if(value >= -Overdraft)
            {
                balance = value;
            }
        }
    }
    private int balance;

    public int Overdraft;
}

Powyżej nie ma zbyt wiele kodu, ale wystarczy, że nie lubię tego, co powinno zostać naprawione. Należy unikać logiki w ramach ustawionej właściwości (ogólnie dopuszczalne są tylko optymalizacje ze zmienioną właściwością). Podczas gdy testy przekazują kod w ramach operacji Set, ten kod prawdopodobnie powinien znaleźć się w metodzie.

Podczas gdy testy przechodzą pomyślnie (zielone), programiści są pewni, że żadne zmiany nie wpłyną na zachowanie. Po każdej zmianie, która ma zmienić zachowanie, należy przeprowadzić testy (przynajmniej dla modułu). Jeśli coś zepsuje zachowanie, programista będzie o tym wiedział, gdy stan zmieni się na czerwony i co najważniejsze, może poinformować go, co zrobił źle.

public class Account
{
    public int Balance 
    {
        get => balance; 
        set 
        {
            balance = ReturnHighestSafeBalance(balance, value);
        }
    }
    private int balance;

    public int Overdraft;

    private int ReturnHighestSafeBalance(int existingValue, int newValue)
    {
        int safeValue = existingValue;
        if (newValue >= -Overdraft)
        {
            safeValue = newValue;
        }

        return newValue;
    }
}

Ten przepływ jest podstawą rozwoju opartego na testach, jednak to tylko zarysowanie tego, co można osiągnąć.

Istnieje kilka korzyści, które można zaobserwować z niewielkiej ilości kodu napisanego w tym artykule. Testy napisane dla danego modułu są formą dokumentacji, uczą każdego programistę nie tylko jakie są oczekiwane wyniki, ale także najkrótszą drogę do osiągnięcia tych wyników. Podczas współpracy nad dużą bazą kodu testy upewnij się, że wszelkie zmiany lub nowe funkcje nie psują istniejącej funkcjonalności (regresja) (prawdopodobne są również testy regresji podczas naprawiania błędów).

W przyszłych artykułach mam nadzieję zbadać, jak testować bez pełnego kodu — po raz pierwszy przy użyciu frameworków Mocking. W ramkach Mocking jest również możliwość testowania logiki bez konkretnego testowania danej jednostki - która jak wszystko ma swoje wady. Na przykład należy unikać zdegenerowanych obiektów, które z łatwością można zbadać w ich własnym artykule. Na razie mam nadzieję, że spróbujesz programowania opartego na testach, a zwłaszcza w C# lub Javie!

Pliki do pobrania

  • Projekt końcowy (C# i Java)
  • TDD autorstwa Agile Alliance ( link )
  • Co to jest Test Driven Development (TDD): Podejście i korzyści autorstwa Jasha Unadkata ( link )
  • Co to jest programowanie sterowane testami? z TestDriven.io ( link )
  • Rozwój oparty na testach: na przykładzie Kenta Becka (2003)
  • Nauka programowania opartego na testach: przewodnik poligloty po pisaniu przejrzystego kodu autorstwa Saleema Siddiqui (2021)
  • Czysty kod: podręcznik zwinnego wytwarzania oprogramowania (2008)
  • Obraz testowy kodu: Obraz autorstwa upklyak na Freepik
  • 23.12.2022: Dodano wersję Java projektu.