Czy w jednym teście jednostkowym jest w porządku wiele stwierdzeń?

396

W komentarzu do tego wspaniałego postu Roy Osherove wspomniał o projekcie OAPT , który ma na celu uruchomienie każdego potwierdzenia w jednym teście.

Na stronie głównej projektu napisano:

Właściwe testy jednostkowe powinny zakończyć się niepowodzeniem z dokładnie jednego powodu, dlatego powinieneś używać jednego assert na test jednostkowy.

A także Roy napisał w komentarzach:

Moją wytyczną jest zazwyczaj to, że testujesz jeden logiczny POJĘCIE na test. możesz mieć wiele twierdzeń na tym samym obiekcie . zwykle będą to te same testowane koncepcje.

Myślę, że w niektórych przypadkach potrzebnych jest wiele stwierdzeń (np. Zabezpieczenie ), ale ogólnie staram się tego unikać. Jaka jest Twoja opinia? Podaj przykład z prawdziwego świata, w którym naprawdę wiele twierdzeń jest naprawdę potrzebnych .

Restuta
źródło
2
Jak robisz kpiny bez wielu stwierdzeń? Każde oczekiwanie na próbę jest samo w sobie twierdzeniem, w tym każdą kolejnością narzucanych połączeń.
Christopher Creutzig
15
Widziałem nadużywaną w przeszłości filozofię „jednego twierdzenia na metodę”. Aby to umożliwić, stary współpracownik użył funkcyjnego mechanizmu dziedziczenia. Doprowadziło to do wielu podklas (jedna na gałąź) i wielu testów, które wykonały ten sam proces ustawiania / usuwania tylko w celu sprawdzenia różnych wyników. Był powolny, trudny do odczytania i poważny problem z konserwacją. Nigdy nie przekonałem go, by wrócił do bardziej klasycznego podejścia. Książka Gerarda Meszarosa szczegółowo omawia ten temat.
Travis Parks,
3
Myślę, że ogólną zasadą jest, abyś próbował zminimalizować liczbę stwierdzeń na test. Jednak dopóki test wystarczająco zawęzi problem do określonego miejsca w kodzie, jest to test przydatny.
ConditionRacer
2
Widziałem przypadki, w których zastosowano wiele asertów zamiast RowTest(MbUnit) / TestCase(NUnit) w celu przetestowania różnych zachowań na krawędzi. Użyj odpowiednich narzędzi do pracy! (Niestety, MSTest nie wydaje się jeszcze mieć możliwości testowania w rzędzie.)
GalacticCowboy
@GalacticCowboy Możesz uzyskać podobną funkcjonalność RowTesti TestCasekorzystanie ze źródeł danych testowych . Używam prostego pliku CSV z wielkim sukcesem.
julealgon

Odpowiedzi:

236

Nie uważam, że to musi być zła rzecz , ale uważam, że powinniśmy dążyć do tego, aby w naszych testach były tylko pojedyncze stwierdzenia . Oznacza to, że piszesz o wiele więcej testów, a nasze testy kończą testowaniem tylko jednej rzeczy na raz.

Powiedziawszy to, powiedziałbym, że może połowa moich testów faktycznie ma tylko jedno stwierdzenie. Myślę, że staje się zapachem kodu (testu?) Tylko wtedy, gdy masz około pięciu lub więcej stwierdzeń w teście.

Jak rozwiązujesz wiele twierdzeń?

Jaco Pretorius
źródło
9
Podoba mi się ta odpowiedź - tzn. Jest OK, ale ogólnie nie jest dobra (-:
Murph,
5
Co? Dlaczego chcesz to zrobić? wykonanie metody jest dokładnie takie samo?
jgauffin
197
Pojedynczy test na jednostkę to świetny sposób na sprawdzenie zdolności czytelnika do przewijania w górę i w dół.
Tom
3
Jakieś uzasadnienie tego? Jak na razie, ta obecna odpowiedź mówi tylko, co powinna być, ale nie dlaczego.
Steven Jeuris,
37
Zdecydowanie się nie zgadzam. Odpowiedź nie wymienia żadnej zalety posiadania pojedynczego potwierdzenia, a oczywistą wadą jest to, że trzeba kopiować i wklejać testy tylko dlatego, że tak mówi odpowiedź w Internecie. Jeśli musisz przetestować wiele pól wyniku lub wiele wyników pojedynczej operacji, absolutnie powinieneś potwierdzić je wszystkie w niezależnych asercjach, ponieważ daje to o wiele więcej użytecznych informacji niż testowanie ich w dużym obiekcie blob-asert. W dobrym teście testujesz pojedynczą operację, a nie pojedynczy wynik operacji.
Peter
295

Testy powinny zakończyć się niepowodzeniem tylko z jednego powodu, ale nie zawsze oznacza to, że powinna istnieć tylko jedna Assertinstrukcja. IMHO ważniejsze jest trzymanie się wzorca „ aranżuj, działaj, potwierdzaj ”.

Kluczem jest to, że masz tylko jedną akcję, a następnie sprawdzasz wyniki tej akcji za pomocą asercji. Ale jest to „Ułóż, Działaj, Potwierdź, Koniec testu ”. Jeśli masz ochotę kontynuować testowanie, wykonując kolejną czynność, a potem więcej twierdzeń, uczyń to osobnym testem.

Cieszę się, że widzę wiele instrukcji asercji, które stanowią część testowania tej samej akcji. na przykład

[Test]
public void ValueIsInRange()
{
  int value = GetValueToTest();

  Assert.That(value, Is.GreaterThan(10), "value is too small");
  Assert.That(value, Is.LessThan(100), "value is too large");
} 

lub

[Test]
public void ListContainsOneValue()
{
  var list = GetListOf(1);

  Assert.That(list, Is.Not.Null, "List is null");
  Assert.That(list.Count, Is.EqualTo(1), "Should have one item in list");
  Assert.That(list[0], Is.Not.Null, "Item is null");
} 

Państwo mogli połączyć je w jeden dochodzić, ale to już zupełnie inna sprawa od twierdząc, że pan powinien lub koniecznością . Nie ma poprawy od ich połączenia.

np. pierwszy może być

Assert.IsTrue((10 < value) && (value < 100), "Value out of range"); 

Ale to nie jest lepsze - komunikat błędu jest mniej konkretny i nie ma innych zalet. Jestem pewien, że możesz pomyśleć o innych przykładach, w których połączenie dwóch lub trzech (lub więcej) twierdzeń w jeden duży warunek boolowski utrudnia czytanie, trudniej je zmienić i trudniej jest ustalić, dlaczego się nie udało. Dlaczego robi to tylko ze względu na regułę?

Uwaga : Kod, który tu piszę, to C # z NUnit, ale zasady będą obowiązywać w innych językach i ramach. Składnia może być również bardzo podobna.

Anthony
źródło
32
Kluczem jest to, że masz tylko jedną akcję, a następnie sprawdzasz wyniki tej akcji za pomocą asercji.
Amitābha,
1
Myślę, że to również interesujące, aby mieć więcej niż jednego Assert, jeśli Arrange jest czasochłonne.
Rekshino
1
@ Rekshino, jeśli arrange jest czasochłonne, możemy udostępnić kod arrange, na przykład poprzez umieszczenie kodu arrange w procedurze inicjalizacji testu.
Shaun Luttin
2
Więc jeśli porównuję do odpowiedzi Jaco, „tylko jeden zapewnią” staje się „tylko jedną grupą twierdzeń”, co ma dla mnie większy sens.
Walfrat,
2
To dobra odpowiedź, ale nie zgadzam się, że jedno stwierdzenie nie jest lepsze. Przykład złego twierdzenia nie jest lepszy, ale to nie znaczy, że jedno stwierdzenie nie byłoby lepsze, gdyby zostało wykonane poprawnie. Wiele bibliotek zezwala na niestandardowe asercje / dopasowywania, więc coś można stworzyć, jeśli jeszcze nie jest obecne. Na przykład Assert.IsBetween(10, 100, value), moim zdaniem , wydruki Expected 8 to be between 10 and 100 lepsze niż dwa osobne stwierdzenia. Z pewnością możesz argumentować, że nie jest to konieczne, ale zwykle warto zastanowić się, czy łatwo jest zredukować do jednego twierdzenia przed zrobieniem ich całego zestawu.
Thor84no
85

Nigdy nie myślałem, że więcej niż jeden twierdzenie było złe.

Ciągle to robię:

public void ToPredicateTest()
{
    ResultField rf = new ResultField(ResultFieldType.Measurement, "name", 100);
    Predicate<ResultField> p = (new ConditionBuilder()).LessThanConst(400)
                                                       .Or()
                                                       .OpenParenthesis()
                                                       .GreaterThanConst(500)
                                                       .And()
                                                       .LessThanConst(1000)
                                                       .And().Not()
                                                       .EqualsConst(666)
                                                       .CloseParenthesis()
                                                       .ToPredicate();
    Assert.IsTrue(p(ResultField.FillResult(rf, 399)));
    Assert.IsTrue(p(ResultField.FillResult(rf, 567)));
    Assert.IsFalse(p(ResultField.FillResult(rf, 400)));
    Assert.IsFalse(p(ResultField.FillResult(rf, 666)));
    Assert.IsFalse(p(ResultField.FillResult(rf, 1001)));

    Predicate<ResultField> p2 = (new ConditionBuilder()).EqualsConst(true).ToPredicate();

    Assert.IsTrue(p2(new ResultField(ResultFieldType.Confirmation, "Is True", true)));
    Assert.IsFalse(p2(new ResultField(ResultFieldType.Confirmation, "Is False", false)));
}

Tutaj używam wielu stwierdzeń, aby upewnić się, że złożone warunki można przekształcić w oczekiwany predykat.

Testuję tylko jedną jednostkę ( ToPredicatemetodę), ale obejmuję wszystko, co mogę wymyślić w teście.

Matt Ellen
źródło
46
Wiele twierdzeń jest złych z powodu wykrycia błędu. Jeśli nie powiodło się twoje pierwsze Assert.IsTrue, inne twierdzenia nie zostaną wykonane i nie otrzymasz od nich żadnych informacji. Z drugiej strony, jeśli miałbyś 5 testów zamiast 1 z 5 twierdzeniami, możesz dostać coś pożytecznego
Sly
6
Czy uważasz, że nadal źle jest, jeśli wszystkie twierdzenia testują ten sam rodzaj funkcjonalności? Podobnie jak powyżej, w przykładzie testowane są warunki warunkowe, a jeśli którykolwiek z nich się nie powiedzie, należy to naprawić. Czy ma dla Ciebie znaczenie to, że możesz przegapić 2 ostatnie stwierdzenia, jeśli wcześniejsze zawiodą?
cringe
106
Rozwiązuję swoje problemy pojedynczo. Tak więc fakt, że test może się nie powieść więcej niż raz, nie przeszkadza mi. Gdybym je podzielił, pojawiłyby się te same błędy, ale naraz. Łatwiej jest mi naprawić wszystko na raz. Przyznaję, że w tym przypadku dwa ostatnie twierdzenia można by prawdopodobnie przeformułować na własne testy.
Matt Ellen,
19
Twoja sprawa jest bardzo reprezentatywna, dlatego NUnit ma dodatkowy atrybut TestCase - nunit.org/?p=testCase&r=2.5
Restuta
10
środowisko testowe google C ++ ma ASSERT () i EXPECT (). ASSERT () zatrzymuje się w przypadku niepowodzenia, podczas gdy EXPECT () kontynuuje. Jest to bardzo przydatne, gdy chcesz sprawdzić więcej niż jedną rzecz w teście.
ratkok
21

Kiedy używam testów jednostkowych do sprawdzania zachowania na wysokim poziomie, absolutnie umieszczam wiele twierdzeń w jednym teście. Oto test, którego faktycznie używam dla jakiegoś awaryjnego kodu powiadomienia. Kod uruchamiany przed testem wprowadza system w stan, w którym uruchomienie głównego procesora powoduje wysłanie alarmu.

@Test
public void testAlarmSent() {
    assertAllUnitsAvailable();
    assertNewAlarmMessages(0);

    pulseMainProcessor();

    assertAllUnitsAlerting();
    assertAllNotificationsSent();
    assertAllNotificationsUnclosed();
    assertNewAlarmMessages(1);
}

Przedstawia warunki, które muszą istnieć na każdym etapie procesu, aby mieć pewność, że kod zachowuje się tak, jak się spodziewam. Jeśli jedno stwierdzenie nie powiedzie się, nie obchodzi mnie, że pozostałe nawet nie uciekną; ponieważ stan systemu nie jest już aktualny, te kolejne stwierdzenia nie powiedziały mi nic cennego. * Jeśli się assertAllUnitsAlerting()nie powiedzie, to nie będę wiedział, co zrobić z assertAllNotificationSent()sukcesem LUB porażką, dopóki nie ustalę, co spowodowało poprzedni błąd i poprawiłem to.

(* - W porządku, mogą być przydatne w debugowaniu problemu. Ale już otrzymano najważniejsze informacje, że test się nie powiódł).

BlairHippo
źródło
Gdy to zrobisz, powinieneś lepiej korzystać ze środowisk testowych z testami zależnymi, jest to lepsze (np. Testng obsługuje tę funkcję)
Kemoda,
8
Piszę również takie testy, abyś był pewny, co robi kod i zmiany stanu, nie sądzę, że jest to test jednostkowy, ale test integracyjny.
mdma,
Jakie są twoje opinie na temat przefaktoryzowania go w assertAlarmStatus (int numberOfAlarmMessages) ;?
Borjab,
1
Twoje twierdzenia byłyby bardzo miłymi nazwami testowymi.
Bjorn Tipling
Lepiej pozwolić testowi działać, nawet przy nieprawidłowych danych wejściowych. Daje ci w ten sposób więcej informacji (a zwłaszcza jeśli nadal mija, gdy się tego nie spodziewałeś).
CurtainDog,
8

Kolejny powód, dla którego uważam, że wielokrotne twierdzenie w jednej metodzie nie jest złą rzeczą, opisano w następującym kodzie:

class Service {
    Result process();
}

class Result {
    Inner inner;
}

class Inner {
    int number;
}

W moim teście chcę po prostu przetestować, który service.process()zwraca poprawną liczbę w Innerinstancjach klasy.

Zamiast testować ...

@Test
public void test() {
    Result res = service.process();
    if ( res != null && res.getInner() != null ) Assert.assertEquals( ..., res.getInner() );
}

robię

@Test
public void test() {
    Result res = service.process();
    Assert.notNull(res);
    Assert.notNull(res.getInner());
    Assert.assertEquals( ..., res.getInner() );
}
Betlista
źródło
2
I to dobrze, że nie powinieneś mieć żadnej logiki warunkowej w swoich testach. To sprawia, że ​​test jest bardziej złożony i mniej czytelny. I sądzę, że Roy nakreślił to w swoim blogu, że wielokrotne twierdzenie o jednym obiekcie jest w porządku przez większość czasu. Tak więc ci, których masz, są tylko strażnikami i można je mieć.
Restuta
2
Twoje Assert.notNullsą zbędne, twój test zakończy się niepowodzeniem z NPE, jeśli są zerowe.
sara
1
Twój pierwszy przykład (z if) przejdzie, jeśli resjestnull
sara
1
@kai notNull's są zbędne, zgadzam się, ale uważam, że łatwiej jest mieć aser (i jeśli nie jestem leniwy również z odpowiednią wiadomością) zamiast wyjątku ...
Betlista
1
Cóż, nieudane twierdzenie również rzuca wyjątek, aw obu przypadkach otrzymujesz bezpośredni link do dokładnego wiersza, który rzucił go wraz z towarzyszącym śladem stosu, więc osobiście wolałbym nie zaśmiecać testu warunkami, które i tak zostaną sprawdzone. Wolałbym oneliner à la Assert.assertEquals(..., service.process().getInner());, możliwy z wyodrębnionymi zmiennymi, jeśli linia będzie „za długa”
sara
6

Myślę, że istnieje wiele przypadków, w których pisanie wielu twierdzeń jest prawidłowe w ramach reguły, że test powinien zakończyć się niepowodzeniem tylko z jednego powodu.

Na przykład wyobraź sobie funkcję, która analizuje ciąg daty:

function testParseValidDateYMD() {
    var date = Date.parse("2016-01-02");

    Assert.That(date.Year).Equals(2016);
    Assert.That(date.Month).Equals(1);
    Assert.That(date.Day).Equals(0);
}

Jeśli test się nie powiedzie, to z jednego powodu parsowanie jest niepoprawne. Jeśli argumentujesz, że test może się nie powieść z trzech różnych powodów, IMHO byłby zbyt drobiazgowy w swojej definicji „jednego powodu”.

Pete
źródło
3

Nie znam żadnej sytuacji, w której dobrze byłoby mieć wiele twierdzeń w samej metodzie [Test]. Głównym powodem, dla którego ludzie lubią mieć wiele asercji, jest to, że próbują mieć jedną klasę [TestFixture] dla każdej testowanej klasy. Zamiast tego możesz podzielić swoje testy na więcej klas [TestFixture]. Pozwala to zobaczyć wiele sposobów, w których kod mógł nie zareagować w oczekiwany sposób, zamiast tylko tych, w których pierwsze potwierdzenie nie powiodło się. W ten sposób uzyskuje się testowanie co najmniej jednego katalogu na klasę z dużą ilością klas [TestFixture]. Każda klasa [TestFixture] zostałaby nazwana na podstawie określonego stanu testowanego obiektu. Metoda [SetUp] wprowadzi obiekt do stanu opisanego przez nazwę klasy. Następnie masz wiele metod [Testowych], z których każda potwierdza różne rzeczy, których oczekiwałbyś, że będą prawdziwe, biorąc pod uwagę obecny stan obiektu. Każda metoda [Test] jest nazwana na cześć tego, co twierdzi, z tym wyjątkiem, że może być nazwana na podstawie koncepcji zamiast po prostu odczytu kodu w języku angielskim. Następnie każda implementacja metody [Test] potrzebuje tylko jednego wiersza kodu, w którym coś potwierdza. Kolejną zaletą tego podejścia jest to, że testy są bardzo czytelne, ponieważ staje się całkiem jasne, co testujesz i czego oczekujesz, patrząc na nazwy klas i metod. Skaluje się to również lepiej, gdy zaczniesz zdawać sobie sprawę ze wszystkich małych przypadków krawędziowych, które chcesz przetestować, i gdy znajdziesz błędy. z wyjątkiem tego, że może być nazwany na podstawie koncepcji zamiast po prostu angielskiego odczytu kodu. Następnie każda implementacja metody [Test] potrzebuje tylko jednego wiersza kodu, w którym coś potwierdza. Kolejną zaletą tego podejścia jest to, że testy są bardzo czytelne, ponieważ staje się całkiem jasne, co testujesz i czego oczekujesz, patrząc na nazwy klas i metod. Skaluje się to również lepiej, gdy zaczniesz zdawać sobie sprawę ze wszystkich małych przypadków krawędziowych, które chcesz przetestować, i gdy znajdziesz błędy. z wyjątkiem tego, że może być nazwany na podstawie koncepcji zamiast po prostu angielskiego odczytu kodu. Następnie każda implementacja metody [Test] potrzebuje tylko jednego wiersza kodu, w którym coś potwierdza. Kolejną zaletą tego podejścia jest to, że testy są bardzo czytelne, ponieważ staje się całkiem jasne, co testujesz i czego oczekujesz, patrząc na nazwy klas i metod. Skaluje się to również lepiej, gdy zaczniesz zdawać sobie sprawę ze wszystkich małych przypadków krawędziowych, które chcesz przetestować, i gdy znajdziesz błędy. i czego oczekujesz, patrząc na nazwy klas i metod. Skaluje się to również lepiej, gdy zaczniesz zdawać sobie sprawę ze wszystkich małych przypadków krawędziowych, które chcesz przetestować, i gdy znajdziesz błędy. i czego oczekujesz, patrząc na nazwy klas i metod. Skaluje się to również lepiej, gdy zaczniesz zdawać sobie sprawę ze wszystkich małych przypadków krawędziowych, które chcesz przetestować, i gdy znajdziesz błędy.

Zwykle oznacza to, że ostatni wiersz kodu w metodzie [SetUp] powinien przechowywać wartość właściwości lub wartość zwracaną w zmiennej zmiennej prywatnej [TestFixture]. Następnie możesz twierdzić wiele różnych rzeczy na temat tej zmiennej instancji z różnych metod [Test]. Można również wysunąć twierdzenia na temat tego, jakie różne właściwości testowanego obiektu są ustawione na teraz, gdy jest on w pożądanym stanie.

Czasami po drodze musisz wprowadzić asercje, gdy testujesz obiekt w pożądanym stanie, aby upewnić się, że nie zepsułeś się przed wprowadzeniem obiektu w pożądany stan. W takim przypadku te dodatkowe twierdzenia powinny pojawić się w metodzie [SetUp]. Jeśli coś pójdzie nie tak w metodzie [SetUp], będzie jasne, że coś było nie tak z testem, zanim obiekt osiągnął pożądany stan, który zamierzałeś przetestować.

Innym problemem, na który możesz natknąć się, jest testowanie wyjątku, którego oczekiwano. Może to skusić Cię do nieprzestrzegania powyższego modelu. Jednak nadal można to osiągnąć, wychwytując wyjątek w metodzie [SetUp] i przechowując go w zmiennej instancji. Pozwoli ci to na stwierdzenie różnych rzeczy na temat wyjątku, każdy we własnej metodzie [Test]. Następnie możesz również potwierdzić inne rzeczy dotyczące testowanego obiektu, aby upewnić się, że nie wystąpiły niezamierzone skutki uboczne zgłoszonego wyjątku.

Przykład (byłoby to podzielone na wiele plików):

namespace Tests.AcctTests
{
    [TestFixture]
    public class no_events
    {
        private Acct _acct;

        [SetUp]
        public void SetUp() {
            _acct = new Acct();
        }

        [Test]
        public void balance_0() {
            Assert.That(_acct.Balance, Is.EqualTo(0m));
        }
    }

    [TestFixture]
    public class try_withdraw_0
    {
        private Acct _acct;
        private List<string> _problems;

        [SetUp]
        public void SetUp() {
            _acct = new Acct();
            Assert.That(_acct.Balance, Is.EqualTo(0));
            _problems = _acct.Withdraw(0m);
        }

        [Test]
        public void has_problem() {
            Assert.That(_problems, Is.EquivalentTo(new string[] { "Withdraw amount must be greater than zero." }));
        }

        [Test]
        public void balance_not_changed() {
            Assert.That(_acct.Balance, Is.EqualTo(0m));
        }
    }

    [TestFixture]
    public class try_withdraw_negative
    {
        private Acct _acct;
        private List<string> _problems;

        [SetUp]
        public void SetUp() {
            _acct = new Acct();
            Assert.That(_acct.Balance, Is.EqualTo(0));
            _problems = _acct.Withdraw(-0.01m);
        }

        [Test]
        public void has_problem() {
            Assert.That(_problems, Is.EquivalentTo(new string[] { "Withdraw amount must be greater than zero." }));
        }

        [Test]
        public void balance_not_changed() {
            Assert.That(_acct.Balance, Is.EqualTo(0m));
        }
    }
}
INTPnerd
źródło
Jak radzisz sobie z danymi wejściowymi TestCase w tym przypadku?
Simon Gillbee
Tak więc w mojej obecnej organizacji mamy ponad 20 000 testów jednostkowych bardzo podobnych do tego, co pokazujesz. To jest koszmar. Znaczna część kodu konfiguracji testu jest kopiowana / wklejana, co skutkuje niepoprawną konfiguracją testu i niepoprawnymi testami, które przechodzą. Dla każdej [Test]metody ta klasa jest ponownie tworzona i [SetUp]metoda jest wykonywana ponownie. To zabija .NET Garbage Collector i powoduje, że testy przebiegają bardzo wolno: ponad 5 minut lokalnie, ponad 20 minut na serwerze kompilacji. Testy 20K powinny trwać około 2-3 minut. W ogóle nie polecałbym tego stylu testowania, szczególnie w przypadku dużych pakietów testowych.
fourpastmidnight
@fourpastmidnight większość tego, co powiedziałeś, wydaje się słuszną krytyką, ale chodzi o to, aby skopiować i wkleić kod instalacyjny, a zatem być w błędzie, to nie jest problem struktury, ale nieodpowiedzialnych programistów (co może być wynikiem nieodpowiedzialnych menedżerów lub negatywnego środowiska bardziej niż złych programistów). Jeśli ludzie po prostu kopiują i wklejają kod i mają nadzieję, że jest on poprawny i nie zawracają sobie głowy zrozumieniem kodu, w jakimkolwiek kontekście z dowolnego powodu, muszą zostać przeszkoleni, aby tego nie robić, lub pozwolić im odejść, jeśli nie mogą przeszkolony. Jest to sprzeczne z każdą dobrą zasadą programowania.
still_dreaming_1
Ale ogólnie zgadzam się, że to szalona przesada, która spowoduje wiele wzdęć / bagażu / powielania, które doprowadzą do różnego rodzaju problemów. Byłem szalony i polecam takie rzeczy. Mam nadzieję, że każdego dnia będę mógł powiedzieć o sobie dzień wcześniej, ponieważ oznacza to, że nigdy nie przestanę szukać lepszych sposobów robienia rzeczy.
still_dreaming_1
@ still_dreaming_1 wrt „nieodpowiedzialni programiści”: Zgadzam się, że takie zachowanie jest poważnym problemem. Jednak tego rodzaju struktura testowa naprawdę zachęca do takiego zachowania, na lepsze lub gorsze. Pomijając złe praktyki programistyczne, moim głównym zastrzeżeniem do tej formy jest to, że naprawdę zabija ona wydajność testu. Nie ma nic gorszego niż wolno działający zestaw testów. Wolno działający pakiet testowy oznacza, że ​​ludzie nie będą uruchamiać testów lokalnie, a nawet pokusą ich pominięcia w kompilacjach pośrednich - znowu, problem ludzi, ale tak się dzieje - czego można uniknąć, zapewniając szybkie uruchamianie testów z.
fourpastmidnight
2

Posiadanie wielu asercji w tym samym teście stanowi problem tylko wtedy, gdy test się nie powiedzie. Następnie może być konieczne debugowanie testu lub przeanalizowanie wyjątku, aby dowiedzieć się, które stwierdzenie nie powiedzie się. Z jednym stwierdzeniem w każdym teście zwykle łatwiej jest wskazać, co jest nie tak.

Nie mogę wymyślić scenariusza, w którym naprawdę potrzebnych jest wiele twierdzeń , ponieważ zawsze można je przepisać jako wiele warunków w tym samym stwierdzeniu. Może być jednak preferowane, jeśli na przykład masz kilka kroków w celu weryfikacji danych pośrednich między krokami, zamiast ryzykować, że późniejsze kroki ulegną awarii z powodu złych danych wejściowych.

Guffa
źródło
1
Jeśli łączysz wiele warunków w jedną asercję, wtedy w przypadku niepowodzenia wiesz tylko, że jeden nie powiódł się. Dzięki wielu twierdzeniom wiesz konkretnie o niektórych z nich (tych aż do awarii). Rozważ sprawdzenie, czy zwrócona tablica zawiera jedną wartość: sprawdź, czy nie jest ona pusta, czy ma dokładnie jeden element, a następnie wartość tego elementu. (W zależności od platformy) samo sprawdzenie wartości może natychmiast dać zerowe odwołanie (mniej pomocne niż niepowodzenie asercji zerowej) i nie sprawdza długości tablicy.
Richard
@Richard: Uzyskanie wyniku, a następnie wyodrębnienie czegoś z tego wyniku byłoby procesem składającym się z kilku etapów, więc omówiłem to w drugim akapicie odpowiedzi.
Guffa
2
Ogólna zasada: jeśli masz wiele asercji w teście, każdy powinien mieć inną wiadomość. Więc nie masz tego problemu.
Anthony
A użycie testera jakości, takiego jak NCrunch, pokaże dokładnie, w której linii test się nie powiódł, zarówno w kodzie testowym, jak i w testowanym kodzie.
fourpastmidnight
2

Jeśli test się nie powiedzie, nie będziesz wiedział, czy poniższe twierdzenia również się złamią. Często oznacza to, że brakuje cennych informacji, aby ustalić źródło problemu. Moim rozwiązaniem jest użycie jednego potwierdzenia, ale z kilkoma wartościami:

String actual = "val1="+val1+"\nval2="+val2;
assertEquals(
    "val1=5\n" +
    "val2=hello"
    , actual
);

To pozwala mi zobaczyć wszystkie nieudane twierdzenia na raz. Używam kilku wierszy, ponieważ większość IDE wyświetli różnice w łańcuchach w oknie dialogowym porównania obok siebie.

Aaron Digulla
źródło
2

Jeśli masz kilka stwierdzeń w jednej funkcji testowej, spodziewam się, że będą one bezpośrednio związane z przeprowadzanym testem. Na przykład,

@Test
test_Is_Date_segments_correct {

   // It is okay if you have multiple asserts checking dd, mm, yyyy, hh, mm, ss, etc. 
   // But you would not have any assert statement checking if it is string or number,
   // that is a different test and may be with multiple or single assert statement.
}

Posiadanie wielu testów (nawet jeśli uważasz, że to prawdopodobnie przesada) nie jest złą rzeczą. Można argumentować, że ważniejsze i najważniejsze testy są ważniejsze. W związku z tym, gdy twierdzisz, upewnij się, że twoje instrukcje potwierdzające są poprawnie umieszczone, zamiast martwić się zbyt wieloma wielokrotnymi twierdzeniami. Jeśli potrzebujesz więcej niż jednego, użyj więcej niż jednego.

hagubear
źródło
1

Celem testu jednostkowego jest dostarczenie jak największej ilości informacji na temat tego, co się nie powiedzie, ale również pomoc w dokładnym wskazaniu najbardziej podstawowych problemów. Jeśli wiesz logicznie, że jedno twierdzenie się nie powiedzie, biorąc pod uwagę, że drugie nie powiedzie się lub innymi słowy, istnieje zależność między testem, wówczas sensowne jest rzutowanie ich jako wielu twierdzeń w ramach jednego testu. Ma to tę zaletę, że nie zaśmieca wyników testu oczywistymi błędami, które można by wyeliminować, gdybyśmy wyrzucili pierwsze stwierdzenie w ramach jednego testu. W przypadku, gdy ta relacja nie istnieje, preferowane byłoby oczywiście rozdzielenie tych twierdzeń na poszczególne testy, ponieważ w przeciwnym razie znalezienie tych awarii wymagałoby wielu iteracji testów, aby rozwiązać wszystkie problemy.

Jeśli następnie projektujesz jednostki / klasy w taki sposób, że trzeba by napisać zbyt złożone testy, zmniejsza to obciążenie podczas testowania i prawdopodobnie promuje lepszy projekt.

jpierson
źródło
1

Tak, można mieć wiele twierdzeń, o ile test zakończony niepowodzeniem dostarczy wystarczających informacji, aby zdiagnozować awarię. Będzie to zależeć od tego, co testujesz i jakie są tryby awarii.

Właściwe testy jednostkowe powinny zakończyć się niepowodzeniem z dokładnie jednego powodu, dlatego powinieneś używać jednego assert na test jednostkowy.

Nigdy nie uważałem takich sformułowań za pomocne (że klasa powinna mieć jeden powód do zmiany, jest przykładem takiego nieprzydatnego powiedzenia). Rozważmy twierdzenie, że dwa łańcuchy są równe, jest to semantycznie równoważne stwierdzenie, że długość dwóch łańcuchów jest taka sama, a każdy znak w odpowiednim indeksie jest równy.

Możemy uogólnić i powiedzieć, że każdy system wielu asercji może zostać przepisany jako jedno twierdzenie, a każde pojedyncze stwierdzenie może zostać rozłożone na zbiór mniejszych stwierdzeń.

Skup się więc na klarowności kodu i klarowności wyników testu i pozwól, aby kierowało liczbą używanych twierdzeń, a nie odwrotnie.

CurtainDog
źródło
0

Odpowiedź jest bardzo prosta - jeśli testujesz funkcję, która zmienia więcej niż jeden atrybut, tego samego obiektu lub nawet dwóch różnych obiektów, a poprawność funkcji zależy od wyników wszystkich tych zmian, to chcesz potwierdzić że każda z tych zmian została poprawnie wykonana!

Rozumiem logiczną koncepcję, ale odwrotny wniosek mówi, że żadna funkcja nie może nigdy zmienić więcej niż jednego obiektu. Ale z mojego doświadczenia nie jest to możliwe we wszystkich przypadkach.

Przyjmij logiczną koncepcję transakcji bankowej - w większości przypadków wycofanie kwoty z jednego konta bankowego MUSI obejmować dodanie tej kwoty do innego konta. NIGDY nie chcesz rozdzielać tych dwóch rzeczy, tworzą one jednostkę atomową. Możesz chcieć zrobić dwie funkcje (wycofanie / addMoney), a tym samym napisać dwa różne testy jednostkowe - dodatkowo. Ale te dwie czynności muszą odbywać się w ramach jednej transakcji, a Ty chcesz również upewnić się, że transakcja działa. W takim przypadku po prostu nie wystarczy upewnić się, że poszczególne kroki zakończyły się powodzeniem. W teście musisz sprawdzić oba konta bankowe.

Mogą istnieć bardziej złożone przykłady, których nie przetestowałbyś w teście jednostkowym, a przede wszystkim w teście integracji lub testu akceptacji. Ale te granice są płynne, IMHO! Decyzja nie jest tak łatwa, zależy od okoliczności i może osobistych preferencji. Wypłata pieniędzy z jednego konta i dodanie go do innego konta jest nadal bardzo prostą funkcją i zdecydowanie kandydatem do testów jednostkowych.

cslotty
źródło
-1

To pytanie jest związane z klasycznym problemem równoważenia problemów ze spaghetti i kodami lasagna.

Posiadanie wielu twierdzeń może łatwo wpaść w problem spaghetti, w którym nie masz pojęcia, o co chodzi w teście, ale posiadanie pojedynczego potwierdzenia na test może sprawić, że twoje testowanie będzie równie nieczytelne, gdy będziesz mieć wiele testów w dużej lasagnie, co sprawi, że ustalenie, który test robi to, co niemożliwe. .

Istnieją pewne wyjątki, ale w tym przypadku odpowiedzią jest trzymanie wahadła w środku.

gsf
źródło
-3

W ogóle nie zgadzam się z „niepowodzeniem tylko z jednego powodu”. Co ważniejsze, testy są krótkie i brzmią wyraźnie imo.

Nie zawsze jest to jednak możliwe do osiągnięcia, a gdy test jest skomplikowany, (długa) nazwa opisowa i testowanie mniejszej liczby rzeczy ma większy sens.

Johan Larsson
źródło