Czy powinno to być „Arrange-Assert-Act-Assert”?


94

Jeśli chodzi o klasyczny wzorzec testowy Arrange-Act-Assert , często dodaję kontr-twierdzenie, które poprzedza Act. W ten sposób wiem, że przemijające stwierdzenie naprawdę mija w wyniku działania.

Myślę o tym jako analogicznym do czerwonego w czerwono-zielonym-refaktorze, gdzie tylko wtedy, gdy widzę czerwony pasek podczas moich testów, wiem, że zielony pasek oznacza, że ​​napisałem kod, który robi różnicę. Jeśli napiszę pozytywny test, każdy kod go spełni; podobnie, w odniesieniu do Arrange-Assert-Act-Assert, jeśli moje pierwsze stwierdzenie nie powiedzie się, wiem, że jakakolwiek ustawa przeszłaby ostateczną weryfikację - tak, że w rzeczywistości nie weryfikowała niczego na temat ustawy.

Czy twoje testy są zgodne z tym schematem? Dlaczego lub dlaczego nie?

Zaktualizuj wyjaśnienie: początkowe stwierdzenie jest zasadniczo przeciwieństwem ostatecznego stwierdzenia. To nie jest twierdzenie, że Arrange zadziałało; to twierdzenie, że Act jeszcze nie zadziałał.

Odpowiedzi:


121

Nie jest to najczęstsza czynność, ale wciąż jest na tyle powszechna, że ​​ma własną nazwę. Ta technika nazywa się Guard Assertion . Możesz znaleźć szczegółowy opis tego na stronie 490 w doskonałej książce xUnit Test Patterns autorstwa Gerarda Meszarosa (wysoce zalecane).

Zwykle sam nie używam tego wzorca, ponieważ uważam, że bardziej poprawne jest napisanie konkretnego testu, który weryfikuje wszelkie warunki wstępne, które czuję potrzebę spełnienia. Taki test powinien zawsze kończyć się niepowodzeniem, jeśli warunek wstępny zawodzi, a to oznacza, że ​​nie potrzebuję go osadzonego we wszystkich innych testach. Daje to lepszą izolację obaw, ponieważ jeden przypadek testowy weryfikuje tylko jedną rzecz.

Może istnieć wiele warunków wstępnych, które muszą zostać spełnione dla danego przypadku testowego, więc możesz potrzebować więcej niż jednego potwierdzenia ochrony. Zamiast powtarzać je we wszystkich testach, posiadanie jednego (i tylko jednego) testu dla każdego warunku wstępnego sprawia, że ​​kod testu jest łatwiejszy w utrzymaniu, ponieważ w ten sposób będziesz mieć mniej powtórzeń.


+1, bardzo dobra odpowiedź. Ostatnia część jest szczególnie ważna, ponieważ pokazuje, że możesz strzec rzeczy jako osobny test jednostkowy.
murrekatt

3
Generalnie robiłem to w ten sposób, ale jest problem z przeprowadzeniem oddzielnego testu w celu zapewnienia warunków wstępnych (szczególnie w przypadku dużej bazy kodu ze zmieniającymi się wymaganiami) - test warunków wstępnych zostanie z czasem zmodyfikowany i nie będzie zsynchronizowany z `` głównym '' test, który zakłada te warunki wstępne. Zatem wszystkie warunki wstępne mogą być dobre i zielone, ale te warunki wstępne nie są spełnione w głównym teście, który teraz zawsze pokazuje kolor zielony i dobry. Ale gdyby warunki wstępne były w głównym teście, zawiodłyby. Czy spotkałeś się z tym problemem i znalazłeś fajne rozwiązanie?
nchaud

2
Jeśli często zmieniasz swoje testy, możesz mieć inne problemy , ponieważ spowoduje to, że testy będą mniej wiarygodne. Nawet w obliczu zmieniających się wymagań rozważ projektowanie kodu w sposób umożliwiający tylko dodawanie .
Mark Seemann

@MarkSeemann Masz rację, że musimy zminimalizować liczbę powtórzeń, ale z drugiej strony może być wiele rzeczy, które mogą wpłynąć na Arrange dla konkretnego testu, chociaż sam test Arrange by przeszedł. Na przykład czyszczenie w teście Arrange lub po innym teście było błędne i Arrange nie byłoby takie samo jak w teście Arrange.
Rekshino


8

Oto przykład.

public void testEncompass() throws Exception {
    Range range = new Range(0, 5);
    assertFalse(range.includes(7));
    range.encompass(7);
    assertTrue(range.includes(7));
}

Możliwe, że napisałem Range.includes() aby po prostu zwrócić prawdę. Nie zrobiłem tego, ale mogę sobie wyobrazić, że mogłem. Albo mogłem napisać to źle na wiele innych sposobów. Miałbym nadzieję i spodziewałbym się, że z TDD właściwie to zrobiłem - to includes()po prostu działa - ale może nie udało mi się. Zatem pierwsze stwierdzenie jest testem poczytalności, aby upewnić się, że drugie stwierdzenie jest naprawdę znaczące.

Przeczytaj sam, assertTrue(range.includes(7));mówi: „Zapewnij, że zmodyfikowany zakres obejmuje 7”. Czytane w kontekście pierwszego stwierdzenia, mówi ono: „potwierdź, że wywołanie metody encompass () powoduje uwzględnienie 7. A ponieważ encompass jest jednostką, którą testujemy, myślę, że ma to jakąś (małą) wartość.

Przyjmuję własną odpowiedź; wielu innych źle zinterpretowało moje pytanie jako dotyczące testowania konfiguracji. Myślę, że to jest nieco inne.


Dzięki, że przyszedłeś z przykładem, Carl. Cóż, w czerwonej części cyklu TDD, dopóki encompass () naprawdę coś zrobi; pierwsze twierdzenie jest bezcelowe, jest tylko powieleniem drugiego. Na zielono zaczyna być przydatny. Podczas refaktoryzacji nabiera sensu. Fajnie byłoby mieć framework UT, który robi to automatycznie.
filant

Załóżmy, że jesteś TDD tą klasą Range, czy nie będzie kolejnego nieudanego testu testującego Range ctora, kiedy go złamiesz?
filant

1
@philippe: Nie jestem pewien, czy rozumiem pytanie. Konstruktor Range i include () mają własne testy jednostkowe. Czy mógłbyś to rozwinąć, proszę?
Carl Manaster

Aby pierwsze potwierdzenie assertFalse (range.includes (7)) zakończyło się niepowodzeniem, musisz mieć defekt w konstruktorze zakresu. Chciałem więc zapytać, czy testy dla konstruktora Range nie zepsują się w tym samym czasie, co to stwierdzenie. A co z twierdzeniem po ustawie o innej wartości: np. AssertFalse (zakres obejmuje (6))?
filant

1
Moim zdaniem konstrukcja zakresu jest poprzedzona funkcjami takimi jak include (). Tak więc, chociaż zgadzam się, tylko wadliwy konstruktor (lub wadliwy include ()) spowodowałby niepowodzenie tego pierwszego stwierdzenia, test konstruktora nie obejmowałby wywołania funkcji include (). Tak, wszystkie funkcje aż do pierwszego stwierdzenia zostały już przetestowane. Ale to początkowe negatywne stwierdzenie przekazuje coś i, moim zdaniem, coś pożytecznego. Nawet jeśli każde takie stwierdzenie przejdzie, gdy zostanie pierwotnie napisane.
Carl Manaster

7

Arrange-Assert-Act-AssertTest można zawsze refactored do dwóch testów:

1. Arrange-Assert

i

2. Arrange-Act-Assert

Pierwszy test będzie dotyczył tylko tego, co zostało ustawione w fazie aranżacji, a drugi test będzie dotyczył tylko tego, co wydarzyło się w fazie działania.

Ma to tę zaletę, że daje bardziej precyzyjne informacje zwrotne na temat tego, czy faza aranżacji, czy aktu się nie powiodła, gdy była w oryginale Arrange-Assert-Act-Assert są one ze sobą powiązane i musiałbyś zgłębić i zbadać dokładnie, które asercja zawiodła i dlaczego zawiodła, aby wiedzieć, to był układ lub akt, który zawiódł.

Spełnia również cel testowania jednostkowego lepiej, ponieważ dzielisz test na mniejsze niezależne jednostki.

Na koniec pamiętaj, że za każdym razem, gdy zobaczysz podobne sekcje aranżacji w innym teście, powinieneś spróbować wyciągnąć je do wspólnych metod pomocniczych, aby testy były bardziej SUCHE i łatwiejsze do utrzymania w przyszłości.


3

Teraz to robię. AAAA innego rodzaju

Arrange - setup
Act - what is being tested
Assemble - what is optionally needed to perform the assert
Assert - the actual assertions

Przykład testu aktualizacji:

Arrange: 
    New object as NewObject
    Set properties of NewObject
    Save the NewObject
    Read the object as ReadObject

Act: 
    Change the ReadObject
    Save the ReadObject

Assemble: 
    Read the object as ReadUpdated

Assert: 
    Compare ReadUpdated with ReadObject properties

Powodem jest to, że ACT nie zawiera odczytu ReadUpdated, ponieważ nie jest on częścią aktu. Akt tylko zmienia i ratuje. Tak naprawdę, ARRANGE ReadUpdated dla potwierdzenia, wzywam ASSEMBLE w celu potwierdzenia. Ma to na celu uniknięcie pomyłki w sekcji ARRANGE

ASSERT powinien zawierać tylko potwierdzenia. To pozostawia ASSEMBLE między ACT i ASSERT, który tworzy asercję.

Wreszcie, jeśli nie uda ci się zorganizować, twoje testy nie są poprawne, ponieważ powinieneś mieć inne testy, aby zapobiec / znaleźć te trywialne błędy. Ponieważ dla scenariusza, który przedstawiam, powinny już istnieć inne testy sprawdzające ODCZYT i TWORZENIE. Jeśli utworzysz „Guard Assertion”, możesz łamać DRY i tworzyć konserwację.


1

Rzucanie asercji „kontroli poczytalności” w celu sprawdzenia stanu przed wykonaniem testowanej czynności jest starą techniką. Zwykle piszę je jako rusztowanie testowe, aby udowodnić sobie, że test robi to, czego oczekuję, i usuwam je później, aby uniknąć zaśmiecania testów rusztowaniem testowym. Czasami pozostawienie rusztowania na miejscu pomaga testowi służyć jako narracja.


1

Czytałem już o tej technice - być może przy okazji - od ciebie - ale jej nie używam; głównie dlatego, że jestem przyzwyczajony do formularza potrójnego A dla moich testów jednostkowych.

Teraz jestem ciekawy i mam kilka pytań: jak piszesz swój test, czy sprawiasz, że to twierdzenie zawodzi, po cyklu czerwony-zielony-czerwony-zielony-refaktor, czy też dodajesz je później?

Czy czasami zawodzisz, być może po refaktoryzacji kodu? Co ci to mówi? Może mógłbyś podzielić się przykładem, w którym to pomogło. Dzięki.


Zazwyczaj nie zmuszam początkowego asercji do niepowodzenia - w końcu nie powinno zawieść, tak jak powinno to być w przypadku asercji TDD, zanim zostanie napisana jej metoda. I nie pisać, kiedy to piszę, przed , tylko w normalnym toku pisania testu, nie później. Szczerze mówiąc, nie pamiętam, żeby to zawodziło - może to sugeruje, że to strata czasu. Spróbuję podać przykład, ale w tej chwili nie mam go na myśli. Dzięki za pytania; są pomocne.
Carl Manaster

1

Robiłem to już wcześniej, badając test, który się nie powiódł.

Po mocnym drapaniu się w głowę stwierdziłem, że przyczyną jest nieprawidłowe działanie metod wywoływanych podczas „Arrange”. Niepowodzenie testu było mylące. Dodałem Assert po aranżacji. To spowodowało, że test zakończył się niepowodzeniem w miejscu, które uwydatniło rzeczywisty problem.

Myślę, że występuje tu również zapach kodu, jeśli część testu Arrange jest zbyt długa i skomplikowana.


Drobna uwaga: uważam, że zbyt skomplikowana aranżacja ma raczej charakter zapachu projektu niż zapachu kodu - czasami projekt jest taki, że tylko skomplikowana aranżacja pozwoli Ci przetestować urządzenie. Wspominam o tym, ponieważ ta sytuacja wymaga głębszego rozwiązania niż zwykły zapach kodu.
Carl Manaster,

1

Ogólnie bardzo lubię „Organizuj, działaj, zgłaszaj” i używam go jako osobistego standardu. Jednak jedyną rzeczą, o której mi nie przypomina, jest uporządkowanie tego, co zaaranżowałem, kiedy stwierdzenia zostaną wykonane. W większości przypadków nie powoduje to zbytniej irytacji, ponieważ większość rzeczy automatycznie znika poprzez zbieranie śmieci itp. Jeśli jednak ustanowiłeś połączenia z zasobami zewnętrznymi, prawdopodobnie będziesz chciał je zamknąć, gdy skończysz z twoimi twierdzeniami lub wielu z was ma serwer lub drogie zasoby, które gdzieś trzymają połączenia lub ważne zasoby, które powinien być w stanie przekazać komuś innemu. Jest to szczególnie ważne, jeśli jesteś jednym z tych programistów, którzy nie używają TearDown ani TestFixtureTearDowndo czyszczenia po jednym lub kilku testach. Oczywiście „Rozmieść, działaj, zgłoś” nie odpowiada za niepowodzenie w zamknięciu tego, co otwieram; Wspominam o tym „gotcha” tylko dlatego, że nie znalazłem jeszcze dobrego synonimu słowa „A-word” dla słowa „dispose” do polecenia! Jakieś sugestie?


1
@carlmanaster, jesteś dla mnie wystarczająco blisko! Wklejam to w moim następnym TestFixture, aby wypróbować go dla rozmiaru. To jak to małe przypomnienie, aby robić to, czego powinna cię nauczyć mama: „Jeśli je otworzysz, zamknij je! Jeśli coś zepsujesz, posprzątaj!” Może ktoś inny mógłby to poprawić, ale przynajmniej zaczyna się od „a!” Dzięki za Twoją sugestię!
John Tobler,

1
@carlmanaster, wypróbowałem "Annul". To lepsze niż „porzucenie” i to w pewnym sensie działa, ale wciąż szukam innego słowa „A”, które utkwiło mi w głowie tak doskonale, jak „Zorganizuj, działaj, potwierdź”. Może „unicestwić ?!”
John Tobler,

1
Więc teraz mam „Rozmieść, Załóż, Działaj, Zapewnij, Zniszcz”. Hmmm! Zbyt wiele rzeczy komplikuję, co? Może lepiej po prostu KISS się i wrócę do „Rozmieść, działaj i potwierdzaj!”
John Tobler,

1
Może użyć R do resetowania? Wiem, że to nie jest szóstka, ale brzmi jak pirat mówiący: Aaargh! i Reset rymów z Assert: o
Marcel Valdez Orozco

1

Zapoznaj się z wpisem Wikipedii dotyczącym projektowania według umowy . Święta trójca Arrange-Act-Assert jest próbą zakodowania niektórych z tych samych koncepcji i dotyczy udowodnienia poprawności programu. Z artykułu:

The notion of a contract extends down to the method/procedure level; the
contract for each method will normally contain the following pieces of
information:

    Acceptable and unacceptable input values or types, and their meanings
    Return values or types, and their meanings
    Error and exception condition values or types that can occur, and their meanings
    Side effects
    Preconditions
    Postconditions
    Invariants
    (more rarely) Performance guarantees, e.g. for time or space used

Istnieje kompromis między wysiłkiem włożonym w jego ustawienie a wartością dodaną. AAA to przydatne przypomnienie o wymaganych minimalnych krokach, ale nie powinno zniechęcać nikogo do tworzenia dodatkowych kroków.


0

Zależy od twojego środowiska / języka testowego, ale zazwyczaj jeśli coś w części aranżacji zawiedzie, zostanie zgłoszony wyjątek i test zakończy się niepowodzeniem, wyświetlając go zamiast uruchamiać część Act. Więc nie, zwykle nie używam drugiej części Assert.

Ponadto w przypadku, gdy twoja część aranżacji jest dość złożona i nie zawsze rzuca wyjątek, możesz rozważyć zawinięcie jej w jakąś metodę i napisanie własnego testu, aby mieć pewność, że się nie powiedzie (bez rzucanie wyjątku).


0

Nie używam tego wzorca, bo myślę, że robię coś takiego:

Arrange
Assert-Not
Act
Assert

Może to być bezcelowe, ponieważ przypuszczalnie wiesz, że część aranżacji działa poprawnie, co oznacza, że ​​wszystko, co znajduje się w części aranżacji, musi również zostać przetestowane lub być na tyle proste, aby nie wymagało testów.

Na przykładzie odpowiedzi:

public void testEncompass() throws Exception {
    Range range = new Range(0, 5);
    assertFalse(range.includes(7)); // <-- Pointless and against DRY if there 
                                    // are unit tests for Range(int, int)
    range.encompass(7);
    assertTrue(range.includes(7));
}

Obawiam się, że tak naprawdę nie rozumiesz mojego pytania. Początkowe potwierdzenie nie dotyczy testowania Arrange; chodzi po prostu o zapewnienie, że ustawa doprowadzi do stwierdzenia stanu na końcu.
Carl Manaster

Chodzi mi o to, że cokolwiek umieścisz w części Assert-Not, jest już implikowane w części Arrange, ponieważ kod w części Arrange jest dokładnie testowany i już wiesz, co robi.
Marcel Valdez Orozco

Ale uważam, że część Assert-Not ma wartość, ponieważ mówisz: biorąc pod uwagę, że część Arrange pozostawia `` świat '' w `` tym stanie '', wtedy mój `` akt '' pozostawi `` świat '' w tym `` nowym stanie '' ; a jeśli implementacja kodu, od którego zależy część Arrange, ulegnie zmianie, to test też się zepsuje. Ale znowu, może to być przeciwko DRY, ponieważ (powinieneś) mieć również testy dla dowolnego kodu, na którym polegasz w części Arrange.
Marcel Valdez Orozco

Może w projektach, w których jest kilka zespołów (lub duży zespół) pracujących nad tym samym projektem, taka klauzula byłaby całkiem przydatna, w przeciwnym razie uważam ją za niepotrzebną i zbędną.
Marcel Valdez Orozco

Prawdopodobnie taka klauzula byłaby lepsza w testach integracyjnych, testach systemowych lub testach akceptacyjnych, gdzie część aranżacyjna zwykle zależy od więcej niż jednego komponentu, a jest więcej czynników, które mogą spowodować nieoczekiwaną zmianę początkowego stanu „świata”. Ale nie widzę na to miejsca w testach jednostkowych.
Marcel Valdez Orozco

0

Jeśli naprawdę chcesz przetestować wszystko w przykładzie, wypróbuj więcej testów ... na przykład:

public void testIncludes7() throws Exception {
    Range range = new Range(0, 5);
    assertFalse(range.includes(7));
}

public void testIncludes5() throws Exception {
    Range range = new Range(0, 5);
    assertTrue(range.includes(5));
}

public void testIncludes0() throws Exception {
    Range range = new Range(0, 5);
    assertTrue(range.includes(0));
}

public void testEncompassInc7() throws Exception {
    Range range = new Range(0, 5);
    range.encompass(7);
    assertTrue(range.includes(7));
}

public void testEncompassInc5() throws Exception {
    Range range = new Range(0, 5);
    range.encompass(7);
    assertTrue(range.includes(5));
}

public void testEncompassInc0() throws Exception {
    Range range = new Range(0, 5);
    range.encompass(7);
    assertTrue(range.includes(0));
}

Ponieważ w przeciwnym razie brakuje tak wielu możliwości błędu ... np. Po uwzględnieniu zakres obejmuje tylko 7, itd ... Istnieją również testy długości zakresu (aby upewnić się, że nie zawiera on również przypadkowej wartości), i kolejny zestaw testów w całości do próby objęcia 5 w zakresie ... czego byśmy się spodziewali - wyjątku w encompass, czy zakresu niezmienionego?

W każdym razie chodzi o to, że jeśli w akcie są jakieś założenia, które chcesz sprawdzić, poddaj je własnemu testowi, tak?


0

Używam:

1. Setup
2. Act
3. Assert 
4. Teardown

Ponieważ czysta konfiguracja jest bardzo ważna.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.