Najlepsze praktyki programowania sterowanego testami przy użyciu języka C # i RhinoMocks [zamknięte]

86

Aby pomóc mojemu zespołowi w pisaniu testowalnego kodu, stworzyłem prostą listę najlepszych rozwiązań ułatwiających testowanie naszej bazy kodu C #. (Niektóre z punktów odnoszą się do ograniczeń Rhino Mocks, frameworka do mockowania dla C #, ale reguły mogą mieć również zastosowanie bardziej ogólnie.) Czy ktoś ma jakieś najlepsze praktyki, których przestrzega?

Aby zmaksymalizować testowalność kodu, przestrzegaj następujących zasad:

  1. Najpierw napisz test, a potem kod. Przyczyna: gwarantuje to, że piszesz testowalny kod i że każda linia kodu otrzyma testy napisane dla niego.

  2. Projektuj klasy przy użyciu iniekcji zależności. Powód: nie możesz kpić ani testować tego, czego nie można zobaczyć.

  3. Oddziel kod interfejsu użytkownika od jego zachowania za pomocą Model-View-Controller lub Model-View-Presenter. Przyczyna: umożliwia przetestowanie logiki biznesowej, podczas gdy części, których nie można przetestować (interfejs użytkownika), są zminimalizowane.

  4. Nie pisz statycznych metod ani klas. Powód: metody statyczne są trudne lub niemożliwe do wyodrębnienia, a Rhino Mocks nie jest w stanie z nich kpić.

  5. Programuj interfejsy, a nie klasy. Powód: użycie interfejsów wyjaśnia relacje między obiektami. Interfejs powinien definiować usługę, której obiekt potrzebuje ze swojego środowiska. Ponadto interfejsy można łatwo mockować przy użyciu Rhino Mocków i innych frameworków.

  6. Izoluj zależności zewnętrzne. Przyczyna: nie można przetestować nierozwiązanych zależności zewnętrznych.

  7. Oznacz jako wirtualne metody, które zamierzasz kpić. Przyczyna: Rhino Mocks nie jest w stanie naśladować metod niewirtualnych.

Kevin Albrecht
źródło
To jest przydatna lista. Obecnie używamy NUnit i Rhino.Mocks i dobrze jest przedstawić te kryteria członkom zespołu, którzy są mniej zaznajomieni z tą stroną testów jednostkowych.
Chris Ballard,

Odpowiedzi:

58

Zdecydowanie dobra lista. Oto kilka uwag na ten temat:

Najpierw napisz test, a potem kod.

Zgadzam się, na wysokim poziomie. Ale ja byłbym bardziej szczegółowy: „Najpierw napisz test, a następnie napisz kod wystarczający do zaliczenia testu i powtórz”. W przeciwnym razie bałbym się, że moje testy jednostkowe będą bardziej przypominały testy integracyjne lub akceptacyjne.

Projektuj klasy przy użyciu iniekcji zależności.

Zgoda. Kiedy obiekt tworzy własne zależności, nie masz nad nimi kontroli. Inversion of Control / Dependency Injection daje ci tę kontrolę, pozwalając ci odizolować testowany obiekt za pomocą mocków / stubów / itp. W ten sposób testujesz obiekty w izolacji.

Oddziel kod interfejsu użytkownika od jego zachowania za pomocą Model-View-Controller lub Model-View-Presenter.

Zgoda. Zwróć uwagę, że nawet prezenter / kontroler można przetestować za pomocą DI / IoC, przekazując mu skrótowy / symulowany widok i model. Sprawdź Presenter First TDD, aby uzyskać więcej informacji.

Nie pisz statycznych metod ani klas.

Nie jestem pewien, czy zgadzam się z tym. Możliwe jest testowanie jednostkowe metody / klasy statycznej bez używania makiet. Być może jest to jedna z tych specyficznych zasad Rhino Mock, o których wspomniałeś.

Programuj interfejsy, a nie klasy.

Zgadzam się, ale z nieco innego powodu. Interfejsy zapewniają twórcom oprogramowania dużą elastyczność - wykraczają poza tylko obsługę różnych struktur obiektów pozorowanych. Na przykład nie jest możliwe prawidłowe wsparcie DI bez interfejsów.

Izoluj zależności zewnętrzne.

Zgoda. Ukryj zależności zewnętrzne za własną fasadą lub adapterem (w razie potrzeby) z interfejsem. Pozwoli to odizolować oprogramowanie od zewnętrznych zależności, czy to usługi sieciowej, kolejki, bazy danych czy czegoś innego. Jest to szczególnie ważne, gdy Twój zespół nie kontroluje zależności (czyli zewnętrznej).

Oznacz jako wirtualne metody, które zamierzasz kpić.

To jest ograniczenie Rhino Mocks. W środowisku, które preferuje ręcznie kodowane kody pośredniczące zamiast makiety frameworka obiektowego, nie byłoby to konieczne.

I kilka nowych punktów do rozważenia:

Użyj kreatywnych wzorców projektowych. Pomoże to w przypadku DI, ale umożliwia również wyodrębnienie tego kodu i przetestowanie go niezależnie od innej logiki.

Pisz testy używając techniki Arrange / Act / Assert Billa Wake'a . Ta technika bardzo jasno określa, jaka konfiguracja jest konieczna, co faktycznie jest testowane i czego się oczekuje.

Nie bój się toczyć własnych prób. Często okaże się, że używanie makietowych struktur obiektowych sprawia, że ​​testy są niezwykle trudne do odczytania. Wprowadzając własne, będziesz mieć pełną kontrolę nad swoimi makietami / kodami pośrednimi i będziesz w stanie zapewnić czytelność testów. (Wróć do poprzedniego punktu.)

Unikaj pokusy refaktoryzacji powielania testów jednostkowych do abstrakcyjnych klas bazowych lub metod konfiguracji / porzucenia. Spowoduje to ukrycie kodu konfiguracyjnego / czyszczącego przed programistą próbującym usprawnić test jednostkowy. W tym przypadku przejrzystość każdego pojedynczego testu jest ważniejsza niż refaktoryzacja duplikacji.

Wdrażaj ciągłą integrację. Wpisz swój kod na każdym „zielonym pasku”. Twórz oprogramowanie i uruchamiaj pełny zestaw testów jednostkowych przy każdym zameldowaniu. (Jasne, nie jest to praktyka kodowania sama w sobie; ale jest to niesamowite narzędzie do utrzymywania oprogramowania w czystości i pełnej integracji).

aridlehoover
źródło
3
Zwykle stwierdzam, że jeśli test jest trudny do odczytania, nie jest to wina frameworka, ale kodu, który testuje. Jeśli konfiguracja SUT jest skomplikowana, być może należałoby podzielić ją na więcej koncepcji.
Steve Freeman
10

Jeśli pracujesz z .Net 3.5, możesz zajrzeć do biblioteki symulującej Moq - używa ona drzew wyrażeń i lambd, aby usunąć nieintuicyjny idiom rekord-odpowiedź z większości innych bibliotek fałszywych.

Zapoznaj się z tym przewodnikiem Szybki start, aby zobaczyć, o ile bardziej intuicyjne stają się przypadki testowe, oto prosty przykład:

// ShouldExpectMethodCallWithVariable
int value = 5;
var mock = new Mock<IFoo>();

mock.Expect(x => x.Duplicate(value)).Returns(() => value * 2);

Assert.AreEqual(value * 2, mock.Object.Duplicate(value));
zadam
źródło
5
Myślę, że nowa wersja Rhino Mocks też działa w ten sposób
George Mauer,
3

To jest bardzo pomocny post!

Dodam, że zawsze ważne jest, aby zrozumieć kontekst i testowany system (SUT). Przestrzeganie zasad TDD co do litery jest znacznie łatwiejsze, gdy piszesz nowy kod w środowisku, w którym istniejący kod jest zgodny z tymi samymi podmiotami. Ale kiedy piszesz nowy kod w środowisku innym niż TDD, okazuje się, że wysiłki związane z TDD mogą szybko przekroczyć Twoje szacunki i oczekiwania.

Dla niektórych z was, którzy żyją w całkowicie akademickim świecie, terminy i dostawa mogą nie być ważne, ale w środowisku, w którym oprogramowanie to pieniądz, efektywne wykorzystanie wysiłku TDD ma kluczowe znaczenie.

TDD w dużym stopniu podlega prawu malejącego zwrotu krańcowego . Krótko mówiąc, twoje wysiłki w kierunku TDD są coraz bardziej wartościowe, dopóki nie osiągniesz punktu maksymalnego zwrotu, po którym kolejny czas zainwestowany w TDD ma coraz mniejszą wartość.

Wydaje mi się, że podstawową wartością TDD są granice (czarna skrzynka), a także okazjonalne testowanie białych skrzynek krytycznych obszarów systemu.


źródło
2

Prawdziwym powodem programowania w oparciu o interfejsy nie jest ułatwienie życia Rhino, ale wyjaśnienie relacji między obiektami w kodzie. Interfejs powinien definiować usługę, której obiekt potrzebuje ze swojego środowiska. Klasa zapewnia określoną implementację tej usługi. Przeczytaj książkę Rebecca Wirfs-Brock „Projektowanie obiektów” o rolach, obowiązkach i współpracownikach.

Steve Freeman
źródło
Zgoda ... Zaktualizuję moje pytanie, aby to odzwierciedlić.
Kevin Albrecht
1

Dobra lista. Jedną z rzeczy, które możesz chcieć ustalić - a nie mogę dać ci wielu rad, ponieważ dopiero zaczynam o tym myśleć - jest sytuacja, gdy klasa powinna znajdować się w innej bibliotece, przestrzeni nazw, zagnieżdżonych przestrzeniach nazw. Możesz nawet zechcieć wcześniej ustalić listę bibliotek i przestrzeni nazw oraz upoważnić zespół do spotkania i podjęcia decyzji o połączeniu dwóch / dodaniu nowej.

Och, pomyślałem o czymś, co robię, a Ty też możesz chcieć. Generalnie mam bibliotekę testów jednostkowych z ustawieniem testowym na zasady klas, w których każdy test trafia do odpowiedniej przestrzeni nazw. Mam też zwykle inną bibliotekę testów (testy integracyjne?), Która jest bardziej w stylu BDD . To pozwala mi na pisanie testów, aby określić, co powinna robić metoda, a także co powinna robić aplikacja.

George Mauer
źródło
Robię również podobną sekcję testowania w stylu BDD (oprócz kodu testów jednostkowych) w osobistym projekcie.
Kevin Albrecht,
0

Oto kolejny, o którym pomyślałem, że lubię robić.

Jeśli planujesz uruchamiać testy z poziomu Gui do testów jednostkowych, a nie z TestDriven.Net lub NAnt, łatwiej było ustawić typ projektu testów jednostkowych na aplikację konsolową, a nie bibliotekę. Pozwala to na ręczne uruchamianie testów i przechodzenie przez nie w trybie debugowania (który wspomniany wcześniej TestDriven.Net może faktycznie zrobić za Ciebie).

Poza tym zawsze lubię mieć otwarty projekt Playground do testowania fragmentów kodu i pomysłów, których nie znam. Nie należy tego sprawdzać w kontroli źródła. Co więcej, powinien znajdować się w oddzielnym repozytorium kontroli źródła tylko na komputerze programisty.

George Mauer
źródło