Czy naprawdę potrzebuję ram testów jednostkowych?

19

Obecnie w mojej pracy mamy duży pakiet testów jednostkowych dla naszej aplikacji C ++. Nie używamy jednak ram testów jednostkowych. Po prostu używają makra C, które w zasadzie otacza aser i cout. Coś jak:

VERIFY(cond) if (!(cond)) {std::cout << "unit test failed at " << __FILE__ << "," << __LINE__; asserst(false)}

Następnie po prostu tworzymy funkcje dla każdego z naszych testów, takie jak

void CheckBehaviorYWhenXHappens()
{
    // a bunch of code to run the test
    //
    VERIFY(blah != blah2);
    // more VERIFY's as needed
}

Nasz serwer CI odbiera komunikat „test jednostkowy nie powiódł się” i kompilacja kończy się niepowodzeniem, wysyłając wiadomość e-mail do programistów.

A jeśli mamy zduplikowany kod instalacyjny, po prostu refaktoryzujemy go tak, jak każdy inny zduplikowany kod, który mielibyśmy w produkcji. Opakowujemy go za funkcje pomocnicze, niektóre klasy testowe zawijają konfigurację często używanych scenariuszy.

Wiem, że istnieją frameworki, takie jak CppUnit i test jednostek podwyższających. Zastanawiam się, jaką wartość to dodają? Czy tęsknię za tym, co przynoszą na stół? Czy jest coś przydatnego, co mogę na nich zyskać? Waham się przed dodaniem zależności, chyba że wnosi ona prawdziwą wartość, zwłaszcza że wydaje się, że to, co mamy, jest bardzo proste i działa dobrze.

Doug T.
źródło

Odpowiedzi:

8

Jak już powiedzieli inni, masz już własne, proste, domowe ramy.

Wydaje się, że jest to banalne. Istnieją jednak inne cechy środowiska testów jednostkowych, które nie są tak łatwe do wdrożenia, ponieważ wymagają zaawansowanej znajomości języka. Funkcje, których zwykle wymagam od środowiska testowego i nie są tak łatwe do homebrew, to:

  • Automatyczne zbieranie przypadków testowych. Zdefiniowanie nowej metody testowej powinno wystarczyć do jej wykonania. JUnit automatycznie zbiera wszystkie metody, których nazwy zaczynają się od test, NUnit ma [Test]adnotację, Boost.Test używa makr BOOST_AUTO_TEST_CASEi BOOST_FIXTURE_TEST_CASE.

    Jest to głównie wygoda, ale każda wygoda, jaką można uzyskać, zwiększa szansę, że programiści faktycznie napiszą testy, które powinni, i że podłączą je poprawnie. Jeśli masz długie instrukcje, ktoś teraz tęskni za nimi i być może niektóre testy nie zostaną uruchomione i nikt tego nie zauważy.

  • Możliwość uruchamiania wybranych przypadków testowych bez modyfikowania kodu i ponownej kompilacji. Każda przyzwoita struktura testów jednostkowych pozwala określić, które testy mają być uruchamiane w wierszu polecenia. Jeśli chcesz debugować testy jednostkowe (jest to najważniejszy punkt w nich dla wielu programistów), musisz mieć możliwość wybrania tylko niektórych do uruchomienia, bez konieczności poprawiania kodu w dowolnym miejscu.

    Powiedzmy, że właśnie otrzymałeś raport o błędzie # 4211 i można go odtworzyć za pomocą testu jednostkowego. Więc piszesz jeden, ale nie musisz mówić biegaczowi, aby uruchomił tylko ten test, abyś mógł debugować, co tam właściwie jest nie tak.

  • Możliwość oznaczania testów oczekiwanych awarii dla każdego przypadku testowego bez modyfikowania samych kontroli. W rzeczywistości zmieniliśmy szkielety w pracy, aby uzyskać ten.

    Każdy zestaw testowy o odpowiedniej wielkości będzie miał testy, które zawodzą, ponieważ funkcje, które testują, nie zostały jeszcze zaimplementowane, nie zostały jeszcze zakończone, nikt nie miał czasu, aby je naprawić lub coś w tym rodzaju. Bez możliwości oznaczania testów jako oczekiwanych awarii, nie zauważysz kolejnej awarii, gdy są one regularnie, więc testy przestają spełniać swoje główne zadanie.

Jan Hudec
źródło
dzięki, myślę, że to najlepsza odpowiedź. W tej chwili moje makro działa, ale nie mogę wykonać żadnej z wymienionych funkcji.
Doug T.
1
@Jan Hudec „To przede wszystkim wygoda, ale każda wygoda, jaką można uzyskać, zwiększa szansę, że programiści faktycznie napiszą testy, które powinni, i że podłączą je poprawnie.”; Wszystkie frameworki testowe są (1) niepraktyczne w instalacji, często zawierają bardziej przestarzałe lub niewyczerpujące instrukcje instalacji niż aktualne aktualne instrukcje; (2) jeśli zaangażujesz się bezpośrednio w środowisko testowe, bez interfejsu pośrodku, jesteś z nim w związku małżeńskim, przełączanie ram nie zawsze jest łatwe.
Dmitry
@Jan Hudec Jeśli spodziewamy się, że więcej osób napisze testy jednostkowe, musimy mieć więcej wyników w Google dla „Co to jest test jednostkowy”, niż „Co to jest test jednostkowy”. Nie ma sensu przeprowadzać testów jednostkowych, jeśli nie masz pojęcia, co jest niezależne od jakichkolwiek testów ramowych lub definicji testów jednostkowych. Nie możesz przeprowadzić Testowania Jednostek, chyba że dobrze rozumiesz, czym jest Test Jednostkowy, ponieważ w przeciwnym razie nie ma sensu przeprowadzać Testowania Jednostkowego.
Dmitry
Nie kupuję tego argumentu dotyczącego wygody. Pisanie kodu testowego jest bardzo trudne, jeśli opuścisz banalny świat przykładów. Wszystkie te makiety, konfiguracje, biblioteki, zewnętrzne programy serwerów makiet itp. Wszystkie wymagają znajomości środowiska testowego od wewnątrz.
Lothar
@Lothar, tak, to jest dużo pracy i dużo do nauczenia się, ale wciąż muszę pisać prosty szablon, ponieważ brakuje ci kilku przydatnych narzędzi, czyni pracę znacznie mniej przyjemną i robi zauważalną różnicę w wydajności.
Jan Hudec
27

Wygląda na to, że już używasz frameworka, domowej roboty.

Jaka jest wartość dodana bardziej popularnych ram? Powiedziałbym, że wartość, którą dodają, polega na tym, że kiedy trzeba wymieniać kod z osobami spoza firmy, można to zrobić, ponieważ jest on oparty na znanym i powszechnie używanym frameworku .

Z drugiej strony, domowe frameworki zmuszają cię do tego, abyś nigdy nie dzielił się swoim kodem, lub do dostarczenia samego frameworka, co może stać się kłopotliwe z powodu rozwoju samego frameworka.

Jeśli podasz swój kod takiemu koledze, jak jest, bez wyjaśnień i bez ram testów jednostkowych, nie będzie w stanie go skompilować.

Drugą wadą domowych frameworków jest kompatybilność . Popularne frameworki testów jednostkowych zapewniają zgodność z różnymi IDE, systemami kontroli wersji itp. W tej chwili może to nie być dla Ciebie bardzo ważne, ale co się stanie, jeśli pewnego dnia będziesz musiał coś zmienić na serwerze CI lub przeprowadzić migrację do nowego IDE lub nowego VCS? Czy na nowo wymyślisz koło?

Wreszcie, większe frameworki zapewniają więcej funkcji, które mogą być potrzebne do zaimplementowania we własnym frameworku jednego dnia. Assert.AreEqual(expected, actual)nie zawsze wystarcza. Co jeśli potrzebujesz:

  • zmierzyć precyzję?

    Assert.AreEqual(3.1415926535897932384626433832795, actual, 25)
    
  • anulować test, jeśli działa zbyt długo? Ponowne wprowadzenie limitu czasu może nie być proste, nawet w językach, które ułatwiają programowanie asynchroniczne.

  • przetestować metodę, która oczekuje na zgłoszenie wyjątku?

  • masz bardziej elegancki kod?

    Assert.Verify(a == null);
    

    jest w porządku, ale czy nie jest bardziej wyrazisty zamiar napisania następnego wiersza?

    Assert.IsNull(a);
    
Arseni Mourzenko
źródło
„Framework”, którego używamy, znajduje się w bardzo małym pliku nagłówkowym i jest zgodny z semantyką assert. Więc nie martwię się o wymienione wady.
Doug T.
4
Uważam, że zapewnia najbardziej trywialną część frameworka testowego. Runner, który zbiera i uruchamia przypadki testowe i sprawdza wyniki, jest nietrywialną ważną częścią.
Jan Hudec
@ Jan Nie do końca podążam. Mój biegacz jest główną rutyną wspólną dla każdego programu w C ++. Czy moduł biegający do testów jednostkowych robi coś bardziej zaawansowanego i przydatnego?
Doug T.
1
Twój framework pozwala tylko na semantykę sprawdzania i uruchamiania testów w głównej metodzie ... jak dotąd. Poczekaj, aż będziesz musiał zgrupować swoje twierdzenia w wielu scenariuszach, scenariuszach powiązanych z grupą razem na podstawie zainicjowanych danych itp.
James Kingsbery
@DougT .: Tak, przyzwoity biegacz testów jednostkowych robi kilka bardziej wyrafinowanych użytecznych rzeczy. Zobacz moją pełną odpowiedź.
Jan Hudec
4

Jak już powiedzieli inni, masz już własne, domowe ramy.

Jedyny powód, dla którego widzę użycie innego frameworka testowego, byłby z „powszechnej wiedzy” branży. Nowi programiści nie musieliby uczyć się twojego domu (choć wygląda to bardzo prosto).

Ponadto inne ramy testowe mogą mieć więcej funkcji, z których możesz skorzystać.

ozz
źródło
1
Zgoda. Jeśli nie masz ograniczeń w obecnej strategii testowania, nie widzę powodu, by to zmieniać. Dobry framework prawdopodobnie zapewniłby lepsze możliwości organizacji i raportowania, ale musiałbyś uzasadnić dodatkowe prace wymagane do integracji z bazą kodu (w tym z systemem kompilacji).
TMN
3

Masz już framework, nawet jeśli jest prosty.

Głównymi zaletami większego frameworka, jaki widzę, jest możliwość posiadania wielu różnych twierdzeń (takich jak twierdzenie podwyżek), logiczna kolejność testów jednostkowych oraz możliwość uruchamiania tylko podzbioru testów jednostkowych w czas. Wzorzec testów xUnit jest całkiem przyjemny do naśladowania, jeśli możesz - na przykład jeden setUP () i tearDown (). Oczywiście, to blokuje cię we wspomnianym frameworku. Pamiętaj, że niektóre frameworki mają lepszą integrację próbną niż inne - na przykład próbną wersję Google i test.

Jak długo zajmie Ci refaktoryzacja wszystkich testów jednostkowych do nowej struktury? Dni lub kilka tygodni może warto, ale więcej może nie tyle.

Sardathrion - Przywróć Monikę
źródło
2

Z mojego punktu widzenia oboje macie przewagę i znajdujecie się w „niekorzystnej sytuacji” (sic).

Zaletą jest to, że masz system, w którym czujesz się komfortowo i który działa dla Ciebie. Cieszysz się, że potwierdza to ważność twojego produktu i prawdopodobnie nie znajdziesz wartości biznesowej, próbując zmienić wszystkie swoje testy na coś, co korzysta z innego frameworka. Jeśli możesz przefakturować kod, a testy wykryją zmiany - lub jeszcze lepiej, jeśli możesz zmodyfikować testy, a istniejący kod nie przejdzie testów do czasu, aż zostanie zrefaktoryzowany, oznacza to, że masz wszystkie podstawy. Jednak...

Jedną z zalet posiadania dobrze zaprojektowanego API do testowania jednostek jest to, że w większości współczesnych IDE jest dużo natywnego wsparcia. Nie wpłynie to na hard-core VI i emacs użytkowników, którzy szydzą z użytkowników Visual Studio tam, ale dla tych, którzy używają dobrego IDE, masz możliwość debugowania testów i wykonania ich w obrębie samo IDE. Jest to dobre, jednak istnieje jeszcze większa zaleta w zależności od używanego frameworka, czyli w języku używanym do testowania kodu.

Kiedy mówię język , nie mówię o języku programowania, ale mówię o bogatym zestawie słów owiniętych płynną składnią, dzięki której kod testowy jest czytany jak historia. W szczególności stałem się zwolennikiem korzystania z ram BDD . Moim ulubionym API DotNet BDD jest StoryQ, ale istnieje kilka innych o tym samym podstawowym celu, którym jest wyciągnięcie pojęcia z dokumentu wymagań i zapisanie go w kodzie w podobny sposób, jak jest napisany w specyfikacji. Naprawdę dobre interfejsy API idą jednak jeszcze dalej, przechwytując każdą pojedynczą instrukcję w teście i wskazując, czy instrukcja wykonała się pomyślnie, czy nie. Jest to niezwykle przydatne, ponieważ możesz zobaczyć cały test wykonany bez powrotu wcześniej, co oznacza, że ​​twoje wysiłki debugowania stają się niezwykle wydajne, ponieważ musisz tylko skupić uwagę na częściach testu, które zakończyły się niepowodzeniem, bez konieczności dekodowania całego połączenia sekwencja. Inną miłą rzeczą jest to, że wynik testu pokazuje wszystkie te informacje,

Jako przykład tego, o czym mówię, porównaj następujące:

Korzystanie z zapewnień:

Assert(variable_A == expected_value_1); // if this fails...
Assert(variable_B == expected_value_2); // ...this will not execute
Assert(variable_C == expected_value_3); // ...and nor will this!

Korzystając z płynnego interfejsu API BDD: (Wyobraź sobie, że kursywa to w zasadzie wskaźniki metod)

WithScenario("Test Scenario")
    .Given(*AConfiguration*) // each method
    .When(*MyMethodToTestIsCalledWith*, variable_A, variable_B, variable_C) // in the
    .Then(*ExpectVariableAEquals*, expected_value_1) // Scenario will
        .And(*ExpectVariableBEquals*, expected_value_2) // indicate if it has
        .And(*ExpectVariableCEquals*, expected_value_3) // passed or failed execution.
    .Execute();

Teraz przyznano, że składnia BDD jest dłuższa i bardziej sformułowana, a te przykłady są strasznie wymyślone, jednak w bardzo złożonych sytuacjach testowych, w których wiele rzeczy zmienia się w systemie w wyniku danego zachowania systemu, składnia BDD oferuje jasne opis tego, co testujesz i jak zdefiniowano konfigurację testu. Możesz pokazać ten kod programistom, a oni natychmiast zrozumieją, co się dzieje. Ponadto, jeśli „zmienna_A” nie przejdzie testu w obu przypadkach, przykład Asserts nie wykona się po pierwszym stwierdzeniu, dopóki problem nie zostanie rozwiązany, podczas gdy interfejs BDD API wykona kolejno każdą metodę wywoływaną w łańcuchu i wskaże, która z nich poszczególne części oświadczenia były błędne.

Osobiście uważam, że to podejście działa znacznie lepiej niż bardziej tradycyjne frameworki xUnit w tym sensie, że językiem testowania jest ten sam język, w którym klienci będą mówić o swoich logicznych wymaganiach. Mimo to udało mi się wykorzystać frameworki xUnit w podobnym stylu, bez potrzeby wymyślania pełnego testowego interfejsu API, aby wesprzeć moje wysiłki, a podczas gdy twierdzenia nadal skutecznie same się zwieją, czytają bardziej czysto. Na przykład:

Korzystanie z Nunit :

[Test]
void TestMyMethod()
{
    const int theExpectedValue = someValue;

    GivenASetupToTestMyMethod();

    var theActualValue = WhenIExecuteMyMethodToTest();

    Assert.That(theActualValue, Is.EqualTo(theExpectedValue)); // nice, but it's not BDD
}

Jeśli zdecydujesz się na skorzystanie z interfejsu API do testowania jednostkowego, radzę eksperymentować z dużą liczbą różnych interfejsów API przez chwilę, a także zachować ostrożność i podejście do swojego podejścia. Chociaż ja osobiście opowiadam się za BDD, twoje potrzeby biznesowe mogą wymagać czegoś innego w zależności od okoliczności twojego zespołu. Kluczem jest jednak uniknięcie wtórnego zgadywania istniejącego systemu. Zawsze możesz wesprzeć swoje istniejące testy kilkoma testami wykorzystującymi inny interfejs API, jeśli to konieczne, ale z pewnością nie poleciłbym ogromnego przepisywania testów, aby wszystko było takie samo. Ponieważ przestarzały kod przestaje być używany, możesz łatwo zastąpić go i jego testy nowym kodem, a także przetestować przy użyciu alternatywnego interfejsu API, i to bez konieczności inwestowania w duży wysiłek, który niekoniecznie przyniesie rzeczywistą wartość biznesową. Jeśli chodzi o używanie interfejsu API do testowania jednostkowego,

S.Robins
źródło
1

To, co masz, jest proste i wykonuje pracę. Jeśli to działa, świetnie. Nie potrzebujesz głównego szkieletu testów jednostkowych, a ja wahałbym się zająć przeniesieniem istniejącej biblioteki testów jednostkowych do nowego szkieletu. Myślę, że największą wartością platform testowania jednostkowego jest zmniejszenie bariery wejścia; dopiero zaczynasz pisać testy, ponieważ framework jest już na miejscu. Przekroczyłeś już ten punkt, więc nie dostaniesz tej korzyści.

Inną zaletą korzystania z głównego nurtu - i jest to niewielka korzyść, IMO - jest to, że nowi programiści mogą już być na bieżąco z dowolnym używanym frameworkiem, a zatem będą wymagali mniej szkolenia. W praktyce przy prostym podejściu, takim jak to, co opisałeś, nie powinno to być wielkim problemem.

Ponadto większość platform głównego nurtu ma pewne funkcje, które może posiadać lub nie. Te funkcje zmniejszają kod hydrauliczny i przyspieszają i ułatwiają pisanie przypadków testowych:

  • Automatyczne uruchamianie przypadków testowych za pomocą konwencji nazewnictwa, adnotacji / atrybutów itp.
  • Różne bardziej szczegółowe stwierdzenia, dzięki czemu nie musisz pisać logiki warunkowej dla wszystkich swoich twierdzeń ani wychwytywać wyjątków w celu potwierdzenia ich typu.
  • Kategoryzacja przypadków testowych, dzięki czemu można łatwo uruchamiać ich podzbiory.
Adam Jaskiewicz
źródło