Czy używanie testów jednostkowych do opowiadania historii to dobry pomysł?

13

Mam więc moduł uwierzytelniania, który napisałem jakiś czas temu. Teraz widzę błędy na mojej drodze i piszę dla nich testy jednostkowe. Podczas pisania testów jednostkowych trudno mi wymyślić dobre nazwiska i dobre obszary do przetestowania. Na przykład mam takie rzeczy

  • Wymaga Logowania_ powinien być_redirect_when_not_logged_in
  • Wymaga Logowania_powinna_przejść_przejście_w momencie_logowania_w
  • Login_should_work_when_given_proper_credentials

Osobiście uważam, że jest to trochę brzydkie, chociaż wydaje się „właściwe”. Mam również problemy z rozróżnieniem testów, po prostu skanując je (muszę przynajmniej dwukrotnie przeczytać nazwę metody, aby wiedzieć, co się nie udało)

Pomyślałem więc, że może zamiast pisać testy, które czysto testują funkcjonalność, może napisz zestaw testów obejmujących scenariusze.

Na przykład jest to testowy odcinek, który wymyśliłem:

public class Authentication_Bill
{
    public void Bill_has_no_account() 
    { //assert username "bill" not in UserStore
    }
    public void Bill_attempts_to_post_comment_but_is_redirected_to_login()
    { //Calls RequiredLogin and should redirect to login page
    }
    public void Bill_creates_account()
    { //pretend the login page doubled as registration and he made an account. Add the account here
    }
    public void Bill_logs_in_with_new_account()
    { //Login("bill", "password"). Assert not redirected to login page
    }
    public void Bill_can_now_post_comment()
    { //Calls RequiredLogin, but should not kill request or redirect to login page
    }
}

Czy to słyszy się wzór? Widziałem historie akceptacji i takie tam, ale jest to zupełnie inna sytuacja. Duża różnica polega na tym, że wymyślam scenariusze „wymuszenia” testów. Zamiast ręcznie próbować wymyślić możliwe interakcje, które będę musiał przetestować. Wiem też, że zachęca to do testów jednostkowych, które nie testują dokładnie jednej metody i klasy. Myślę, że to jest OK. Wiem też, że spowoduje to problemy przynajmniej dla niektórych środowisk testowych, ponieważ zwykle zakładają, że testy są od siebie niezależne i kolejność nie ma znaczenia (gdzie to by było w tym przypadku).

W każdym razie, czy jest to w ogóle wskazany wzór? Czy byłoby to idealne dopasowanie do testów integracyjnych mojego interfejsu API, a nie jako testów „jednostkowych”? To tylko osobisty projekt, więc jestem otwarty na eksperymenty, które mogą się nie udać.

Earlz
źródło
4
Granice między testami jednostkowymi, integracyjnymi i funkcjonalnymi są niewyraźne. Gdybym musiał wybrać nazwę swojego kodu pośredniczącego, byłby on funkcjonalny.
yannis
Myślę, że to kwestia gustu. Osobiście używam nazwy wszystkiego, co testuję z _testdołączonym i używam komentarzy, aby zanotować oczekiwane wyniki. Jeśli jest to projekt osobisty, znajdź styl, w którym czujesz się komfortowo i trzymaj się tego.
Pan Lister
1
Napisałem odpowiedź ze szczegółami na temat bardziej tradycyjnego sposobu pisania testów jednostkowych przy użyciu wzoru Arrange / Act / Assert, ale znajomy odniósł wiele sukcesów, używając github.com/cucumber/cucumber/wiki/Gherkin , który jest używane do specyfikacji i afaik może generować testy ogórków.
StuperUser
Chociaż nie użyłbym metody, którą pokazałeś za pomocą nunit lub podobnego narzędzia, nspec wspiera budowanie kontekstu i testowanie w bardziej opartej na fabule naturze: nspec.org
Mike
1
zmień „Bill” na „User” i gotowe
Steven A. Lowe

Odpowiedzi:

15

Tak, dobrym pomysłem jest podanie nazw testów przykładowych scenariuszy, które testujesz. Używanie narzędzia do testowania jednostek nie tylko do testów jednostkowych może być w porządku, wiele osób robi to z sukcesem (ja też).

Ale nie, zdecydowanie nie jest dobrym pomysłem pisanie testów w sposób, w którym liczy się kolejność wykonywania testów. Na przykład NUnit pozwala użytkownikowi wybrać interaktywnie, który test chce on / ona zostać wykonany, więc to nie będzie działać w zamierzony sposób.

Można tego łatwo uniknąć, oddzielając główną część testową każdego testu (w tym „aser”) od części, które ustawiają system w prawidłowym stanie początkowym. Korzystając z powyższego przykładu: napisz metody zakładania konta, logowania i zamieszczania komentarzy - bez żadnego potwierdzenia. Następnie ponownie zastosuj te metody w różnych testach. Będziesz także musiał dodać kod do [Setup]metody urządzeń testowych, aby upewnić się, że system jest w prawidłowo zdefiniowanym stanie początkowym (na przykład brak kont w bazie danych, nikt do tej pory nie był podłączony itp.).

EDYCJA: Oczywiście wydaje się, że jest to sprzeczne z „historią” natury twoich testów, ale jeśli nadasz metodom pomocniczym sensowne nazwy, znajdziesz swoje historie w ramach każdego testu.

Powinno to wyglądać tak:

[TestFixture]
public class Authentication_Bill
{
    [Setup]
    public void Init()
    {  // bring the system in a predefined state, with noone logged in so far
    }

    [Test]
    public void Test_if_Bill_can_create_account()
    {
         CreateAccountForBill();
         // assert that the account was created properly 
    }

    [Test]
    public void Test_if_Bill_can_post_comment_after_login()
    { 
         // here is the "story" now
         CreateAccountForBill();
         LoginWithBillsAccount();
         AddCommentForBill();
        //  assert that the right things happened
    }

    private void CreateAccountForBill()
    {
        // ...
    }
    // ...
}
Doktor Brown
źródło
Chciałbym pójść dalej i powiedzieć, że używanie narzędzia xUnit do uruchamiania testów funkcjonalnych jest w porządku, o ile nie pomylisz oprzyrządowania z rodzajem testu i trzymasz te testy oddzielnie od rzeczywistych testów jednostkowych, aby deweloperzy mogli nadal uruchamiaj testy jednostkowe w czasie zatwierdzania. Prawdopodobnie będą one znacznie wolniejsze niż testy jednostkowe.
bdsl
4

Problem z opowiadaniem historii za pomocą testów jednostkowych polega na tym, że nie jest jasne, że testy jednostkowe powinny być ustawione i przeprowadzane całkowicie niezależnie od siebie.

Dobry test jednostkowy powinien być całkowicie odizolowany od wszystkich innych kodów zależnych, jest to najmniejsza jednostka kodu, którą można przetestować.

Daje to tę korzyść, że oprócz potwierdzenia działania kodu, jeśli test się nie powiedzie, otrzymasz bezpłatną diagnozę dokładnie tego, gdzie kod jest nieprawidłowy. Jeśli test nie jest izolowany, musisz sprawdzić, od czego zależy, aby dowiedzieć się dokładnie, co poszło nie tak, i przegapić główną zaletę testów jednostkowych. Posiadanie kolejności wykonania może również wywoływać wiele fałszywych negatywów, jeśli test się nie powiedzie, kolejne testy mogą się nie powieść pomimo kodu, który test działa idealnie.

Dobry artykuł bardziej szczegółowo to klasyk na temat brudnych testów hybrydowych .

Aby klasy, metody i wyniki były czytelne, wielki test Art of Unit wykorzystuje konwencję nazewnictwa

Klasa testowa:

ClassUnderTestTests

Metody testowe:

MethodUnderTest_Condition_ExpectedResult

Aby skopiować przykład @Doc Brown, zamiast korzystać z [Setup], który uruchamia się przed każdym testem, piszę metody pomocnicze do budowania izolowanych obiektów do testowania.

[TestFixture]
public class AuthenticationTests
{
    private Authentication GetAuthenticationUnderTest()
    {
        // create an isolated Authentication object ready for test
    }

    [Test]
    public void CreateAccount_WithValidCredentials_CreatesAccount()
    {
         //Arrange
         Authentication codeUnderTest = GetAuthenticationUnderTest();
         //Act
         Account result = codeUnderTest.CreateAccount("some", "valid", "data");
         //Assert
         //some assert
    }

    [Test]
    public void CreateAccount_WithInvalidCredentials_ThrowsException()
    {
         //Arrange
         Authentication codeUnderTest = GetAuthenticationUnderTest();
         Exception result;
         //Act
         try
         {
             codeUnderTest.CreateAccount("some", "invalid", "data");
         }
         catch(Exception e)
         {
             result = e;
         }
         //Assert
         //some assert
    }
}

Testy zakończone niepowodzeniem mają sensowną nazwę, która daje pewną narrację o tym, która metoda zawiodła, warunek i oczekiwany wynik.

Tak zawsze pisałem testy jednostkowe, ale przyjaciel odniósł duży sukces z Gerkinem .

StuperUser
źródło
1
Chociaż uważam, że to dobry post, nie zgadzam się z tym, co mówi linkowany artykuł na temat testu „hybrydowego”. Posiadanie „małych” testów integracyjnych (dodatkowo, nie alternatywnie do czystych testów jednostkowych, oczywiście) może być bardzo pomocne, nawet jeśli nie są w stanie powiedzieć dokładnie, która metoda zawiera zły kod. Jeśli te testy są możliwe do utrzymania, zależy od tego, jak czysty jest kod dla tych testów, nie są one same w sobie „brudne”. I myślę, że cel tych testów może być bardzo jasny (jak w przykładzie PO).
Doc Brown
3

To, co opisujesz, bardziej przypomina mi Behaviour Driven Design (BDD) niż testowanie jednostkowe. Spójrz na SpecFlow, która jest technologią .NET BDD opartą na DSL Gherkin .

Potężne rzeczy, które każdy człowiek może czytać / pisać bez wiedzy o kodowaniu. Nasz zespół testowy odnosi wielki sukces, wykorzystując go w naszych pakietach testów integracji.

Jeśli chodzi o konwencje dotyczące testów jednostkowych, odpowiedź @ DocBrown wydaje się solidna.

Drew Marsh
źródło
Aby uzyskać informacje, BDD jest dokładnie jak TDD, zmienia się tylko styl pisania. Przykład: TDD = assert(value === expected)BDD = value.should.equals(expected)+ opisujesz cechy warstw, które rozwiązują problem „niezależności testu jednostkowego”. To świetny styl!
Offirmo,