Wątek przeniesiony 2015-05-28 18:19 z C# i .NET przez somekind.

Testy jednostkowe - rady i wskazówki

Odpowiedz Nowy wątek
2015-05-20 12:16
0

Mniej-więcej rok temu napisałem (o czym zresztą był temat na 4p) niewielkiego CMSa. Projekt ten jest po wielu poprawkach, głównie dzięki Waszym sugestiom, stąd sam kod jest chyba w całkiem dobrym stanie. Brakuje jednak jednego elementu - testów.

Projekt na githubie: https://github.com/AlfaLeporis/blog/tree/master/Blog

Przymierzam się do napisania nowego CMSa z większą ilością funkcji tym razem już z testami, stąd pytanie do Was - jak powinny wyglądać testy jednostkowe dla wyżej podanego projektu z linku? Chodzi mi o jakieś rady/wskazówki co powinienem testować/mockować itp.

Pozdrawiam : )

Pozostało 580 znaków

2015-05-20 16:28
2

asp.net mvc znam bardzo pobieznie wiec dam ci 3 bardzo ogolne i proste rady:
1) testuj tylko nieprywatne skladowe
2) pisz male testy z nazwami tlumaczacymi wejscie i spodziewany rezultat
3) powinienes mockowac zaleznosci, zwlaszcza te z side effectami (baza danych, pliki, timery itp)

Pozostało 580 znaków

2015-05-24 11:50
0

Dzięki, przyda się :) Mam jeszcze pytanie dotyczące stricte ASP.NET MVC. Otóż załóżmy że mamy sobie jakiś kontroler z metodą Home, do której przypisane jest sporo różnych filtrów (przykładowo filtr który dodaje do ViewData jakieś informacje). Jak w takim razie testować wspomnianą metodę? Bez filtrów jest to proste, ale co zrobić w takim przypadku? Ew. jakiś link w którym to jest wyjaśnione bo nie znalazłem szczerze mówiąc niczego na ten temat.

Pozostało 580 znaków

2015-05-24 13:59
3

Filtry testuje się niezależnie od kontrolerów.

Niemniej ja uważam, że kontrolery nie powinny zawierać logiki, więc nie ma sensu ich testować.


"HUMAN BEINGS MAKE LIFE SO INTERESTING. DO YOU KNOW, THAT IN A UNIVERSE SO FULL OF WONDERS, THEY HAVE MANAGED TO INVENT BOREDOM."
Pokaż pozostałe 5 komentarzy
Nie znam książek o testowaniu, nie pomogę. :) Ogólnie o OOP zawsze polecam Head First: Design Patterns I OOA&D. - somekind 2015-05-24 14:44
Dzięki, zapoznam się. :) - tdudzik 2015-05-24 14:46
@somekind, co Ty gadasz, API też się przykrywa testami, choćby po to, żeby zagwarantować, że metoda API zawoła właściwą metodę serwisu i zwróci jej wynik, no chyba że API generuje automat. - ŁF 2015-05-28 12:50
Kwestia podejścia, dla mnie test metody, która tylko woła inną metodę, nie ma większego sensu. - somekind 2015-05-28 13:00
Aż do momentu, kiedy jakiś młodszy programista dotknie tego kodu, a review "tak prostej rzeczy" będzie robił inny młodszy programista. - ŁF 2015-05-28 13:03

Pozostało 580 znaków

2015-05-27 12:53
0

Mam prośbę, zerknijcie jak wyglądają poniższe testy i napiszcie czy jest ok/co poprawić/czy w ogóle mam złe podejście do tematu:

http://4programmers.net/Pastebin/4134

Dla wyjaśnienia, testowany jest tutaj pewien serwis do odczytywania konfiguracji (zwykłe pary key-value). Do mockowania użyłem Moq, a FakeDbSet to DbSet in memory. Ale to na pewno wiecie : ]

edytowany 1x, ostatnio: AlfaLeporis, 2015-05-27 12:54

Pozostało 580 znaków

2015-05-27 14:12
Mały Kaczor Donald
1

Dobra książka o testach to ta stąd:
http://practicalunittesting.com/
Co do jej dostępności to musisz pytać googla :-) ale na pewno widziałem na Amazonie, nie wiem jak w księgarniach czy z innych źródeł.

Pozostało 580 znaków

2015-05-27 15:05
0

Zmieniłbym u Ciebie nazewnictwo testów. Wywaliłbym prefix Test (od oznaczania testów masz atrybuty) i słówko Should (niepotrzebne wydłużenie i tak zwykle długich nazw metod testowych). Nazwa powinna też konkretniej opisywać test. Czyli na przykład zamiast: TestSetValueWithBadKeyShouldReturnException użyj: SetValueForNonExistingKeyThrowsException.

Pozostało 580 znaków

2015-05-27 16:18
2

Czytelne nazwy metod testujących pisze się np. tak: MetodaTestowana__SprawdzaneWarunki__OczekiwanyWynik
Dla Twojego przykładu:

GetValueByKeyName__KeyNotExists__ThrowsKeyNotFoundException
GetValueByKeyName__KeyExists__ReturnsValidValue
Exist__KeyNotExists__ReturnsFalse
Exist__KeyExists__ReturnsTrue
AddPair__KeyAlreadyExists__ThrowsDuplicateKeyException

I jeszcze ogólne uwagi:

  1. ExpectedException może być niebezpieczny, bo może pomyłkowo złapać wyjątek, który nie będzie wynikiem testowanej metody lecz po prostu nastąpi w metodzie testującej. W efekcie wynik testu będzie fałszywie pozytywny.
  2. Testy wielu metod w jednej klasie powodują, że klasa testująca rozrasta się do gigantycznych rozmiarów. Warto zachować umiar, i jeśli klasa robi się długa, podzielić ją na zasadzie jedna klasa testująca dla jednej metody testowanej. Tak jest czytelniej, zgodnie z SRP, a poza tym setup testu robi się krótszy. Wówczas należy też pominąć pierwszą część nazwy metody testującej z przykładów powyżej.

"HUMAN BEINGS MAKE LIFE SO INTERESTING. DO YOU KNOW, THAT IN A UNIVERSE SO FULL OF WONDERS, THEY HAVE MANAGED TO INVENT BOREDOM."

Pozostało 580 znaków

2015-05-28 12:48
ŁF
0

Nietestowanie prywatnych metod - całkowicie się z tym nie zgadzam. Testy jednostkowe powinny testować każdą metodę, sprawdzać czy dla prawidłowych danych zwraca prawidłowe wyniki i czy dla danych nieprawidłowych zachowuje się przewidywalnie (rzuca określone wyjątki/zwraca określoną wartość). Jeśli nie będziesz testować prywatnych metod (a w praktyce internal), to nie będziesz w stanie napisać testów jednostkowych, tylko testy integracyjne.

Podwójne znaki podkreślenia w nazwach - wygląda to paskudnie. Jeśli dwa są lepsze niż jeden, to czemu nie trzy albo cztery? Wygląda jak powrót do notacji węgierskiej.
U nas używamy nieco innej składni - jeśli testowane są przypadki typowego, poprawnego działania, to metoda testująca nazywa się tak samo jak metoda testowana. Jeśli testujemy zachowanie dla nieprawidłowych danych, to nazwę stanowi nazwa testowanej metody + podkreślenie + nazwa case'a, np. SendPasswordRetrievalEmailToUser_UserNotFound. Jak widać trochę podobnie do tego, co zaproponował somekind, ale bez podwójnych podkreśleń (fuu!) i bez zawierania w nazwie tego, co ma się zadziać. To ostatnie jest spowodowane DRY - jeśli ktoś robiąc zmianę w testowanej metodzie, a potem w teście, zapomni zmienić nazwę testu, to potem będą głupoty - nazwa testu będzie sugerować jedno, jego działo będzie robić drugie. A jak wiadomo, jeśli coś złego może się przydarzyć, to na pewno się przydarzy ;-)
BTW "KeyNotExists" to nie po angielsku.

Nietestowanie kontrolerów to podobny żart jak nietestowanie prywatnych metod. Przecież to nadal kod pisany przez programistę i potem potencjalnie modyfikowany przez kolejnych programistów. Z tego względu MUSI być przykryty testem, chociażby tylko po to, żeby sprawdzić że mock został wywołany i jego wynik został wepchnięty do modelu.

Patrząc na kod testów @AlfaLeporis:
Jeśli używałbyś NUnita, to warto zapoznać się z atrybutami SetUp, TearDown, TestFixtureSetUp i TestFixtureTearDown. Potem zamiast w każdym teście wywoływać var service = GetServiceWithData(); (DRY!) robisz sobie pole private Service sut; (sut od Service Under Tests) i w Setup inicjalizujesz sobie je na nowo przed każdym testem, tak samo robisz z mockami. Z kolei w Teardown walidujesz wszystkie mocki.
U nas wygląda to tak (część kodu wycięta) - NUnit + Moq:

    public class UserControllerTests
    {
        private Mock<IUserService> userServiceMock;
        private Mock<ILicenceContextService> licenceContextServiceMock;
        ...

        private Mock<UserController> cutPartialMock;
        private UserController cut; // cut, bo Controller Under Tests

        #region Setup/Teardown
        [SetUp]
        public void TestSetUp()
        {
            userServiceMock = new Mock<IUserService>();
            licenceContextServiceMock = new Mock<ILicenceContextService>(MockBehavior.Strict);
            ...

            cut = new UserController(userServiceMock.Object, licenceContextServiceMock.Object, ...);
            cutPartialMock = new Mock<UserController>(MockBehavior.Loose, userServiceMock.Object, licenceContextServiceMock.Object, ...) { CallBase = true };
        }

        [TearDown]
        public void TestTearDown()
        {
            cutPartialMock.VerifyAll();
            userServiceMock.VerifyAll();
            licenceContextServiceMock.VerifyAll();
            ...
        }
        #endregion

// i jakiś przykładowy test, od razu na partial mocku żeby było więcej wiedzy
        [TestCase(true, true, null)]
        [TestCase(false, true, ErrorMessages.NoSuchUser)]
        [TestCase(true, false, ErrorMessages.CantCancelPendingAccountDelete)]
        public void CancelDeleteUserAccount(bool userExist, bool isDeleteCanceled, string expectedMessage)
        {
            var user = new User { Id = 128123, Email = "[email protected]", DeleteProcedureHash = "abc" };

            userServiceMock.Setup(x => x.GetUserById(user.Id)).Returns(userExist ? user : null);

            if (userExist)
                userAccountServiceMock.Setup(x => x.CancelDelete(user.Email, user.DeleteProcedureHash)).Returns(isDeleteCanceled);

            var result = cut.CancelDeleteUserAccount((int)user.Id);

            var data = (JsonStatusModel)result.Data;
            Assert.AreEqual(expectedMessage._(), data.statusMessage);
        }

Jeszcze jedna sprawa - ze względu na bazę danych niektóre testy muszą być integracyjne z dwóch powodów. Po pierwsze EF nie pozwala na mockowanie, więc na najniższym poziomie - tam gdzie testuje się metody pracujące z bazą - trzeba pracować z "żywą" bazą danych. Po drugie ze względu na indeksy i klucze obce (albo i triggery), a także ze względu na współbieżność i transakcje czasem może się okazać, że jednostkowo/na mockach cały proces działa fajnie, a mimo to w praktyce się wywala. Z tego powodu czasem warto dodać czasem też test integracyjny sprawdzający cały proces (acz testowanie zachowania transakcji - UpdateException, OptimisticConcurrencyException - to rzecz koszmarnie trudna do automatycznego odtworzenia).


edytowany 3x, ostatnio: ŁF, 2015-05-28 13:13

Pozostało 580 znaków

2015-05-28 13:59
2

Metody prywatne testują się przy okazji testów metod publicznych, a jeśli realizują tak skomplikowaną logikę, że wymagają jakichś dodatkowych przypadków testowych, to znaczy, że klasa łamie SRP, i taką metodę trzeba wyciągnąć do innej klasy, uczynić publiczną i normalnie przetestować.

Podwójne znaki podkreślenia w nazwach - wygląda to paskudnie. Jeśli dwa są lepsze niż jeden, to czemu nie trzy albo cztery?

Dla mnie właściwie bez różnicy, czy są dwa czy jeden. To kwestia konwencji, a celem jest oddzielenie poszczególnych części nazwy metody.

Jak widać trochę podobnie do tego, co zaproponował somekind, ale bez podwójnych podkreśleń (fuu!) i bez zawierania w nazwie tego, co ma się zadziać. To ostatnie jest spowodowane DRY - jeśli ktoś robiąc zmianę w testowanej metodzie, a potem w teście, zapomni zmienić nazwę testu, to potem będą głupoty - nazwa testu będzie sugerować jedno, jego działo będzie robić drugie.

Oczekiwany wynik w nazwie metody jest dość istotny, bo informuje o celu testu. Gdy test się wywali nie trzeba później tracić czasu na domyślanie się, jaki miał być wynik. W praktyce jest to dodatkowa forma dokumentacji kodu.
Problem utrzymania kodu testowego to inna sprawa, no i jeśli ktoś tego nie robi, to jest leniwy, a nie konwencja jest zła.

Nietestowanie kontrolerów to podobny żart jak nietestowanie prywatnych metod. Przecież to nadal kod pisany przez programistę i potem potencjalnie modyfikowany przez kolejnych programistów. Z tego względu MUSI być przykryty testem, chociażby tylko po to, żeby sprawdzić że mock został wywołany i jego wynik został wepchnięty do modelu.

Jeśli ktoś ma nadmiar czasu, albo płacą mu od sztuki testu, to pewno ma to sens.
Ja wychodzę z założenia, że od testowania GUI są testy GUI, czy to automatyczne, czy też manualne. A kontroler to właśnie GUI - nie powinien robić nic poza wywołaniem niższej warstwy i przekierowaniem do widoku, jeśli w kontrolerze zawarta jest jakakolwiek inna logika, to mamy fat controller i łamiemy MVC.

http://www.maciejaniserowicz.[...]estowac-a-czego-nie-testowac/


"HUMAN BEINGS MAKE LIFE SO INTERESTING. DO YOU KNOW, THAT IN A UNIVERSE SO FULL OF WONDERS, THEY HAVE MANAGED TO INVENT BOREDOM."
Btw. odskakując od tematu - jeżeli w kontrolerze mam wywołanie metody serwisu + konwersja zwróconego domain object na view model (np. za pomocą ValueInjecter) to już jest to fat controller czy jeszcze mieści się to w granicach rozsądku? - AlfaLeporis 2015-05-28 18:29
A dlaczego serwis nie zwraca viewmodelu? - somekind 2015-05-28 19:23
Z tego co czytałem to kontrolery operują na viewModelach, serwisy na domain objects, a repozytoria na właściwych modelach (które są umieszczone w DbSetach). Chyba że coś źle zrozumiałem... - AlfaLeporis 2015-05-28 21:08
Nie tyle źle, co dość powierzchownie. Serwisy to w ogóle szeroki temat, bo są różne rodzaje serwisów: biznesowe, aplikacyjne, infrastrukturalne. Tak samo są różne rodzaje modeli i encji. Owszem, kontroler operuje na ViewModelu, ale ten ViewModel może dostać z niższej warstwy, np. od serwisu aplikacyjnego. Serwis aplikacyjny to takie coś, co np. gada z repozytoriami i serwisami biznesowymi, (czyli tymi, które operują na encjach domenowych), konwertuje obiekty domenowe/bazodanowe na jakieś DTO/VM, ogólnie organizuje cały proces łącząc infrastrukturę z biznesem. - somekind 2015-05-29 10:51

Pozostało 580 znaków

2015-05-28 15:13
1

tak, pewnie jeszcze jak nie ma 100% pokrycia, to w ogole projekt jest do wyrzucenia ;)
testowanie kazdej metody (na dodatek prywatnej) sprawia ze lamie sie enkapsulacje i zasade DRY o ktorej wspominasz.
enkapsulacje - bo upubliczniasz 'flaki' i tworzysz zaleznosci ktorych nie powinno byc.
DRY - bo wielokrotnie testujesz te same metody (zwlaszcza gdy jest sporo malych metod pomocniczych).

podkreslenia to nie jest zadna notacja wegierska a jedynie poprawienie czytelnosci, w teamie stosujemy format NazwaMetody_opis_wejscia_opis_spodziewanego_rezultatu i calkiem niezle sie to sprawdza.
testujac tylko elementy publicznego interfejsu, bardzo rzadko zdarza sie ze zmienia sie nazwy, jesli juz to dochodza nowe albo cala rzecz wylatuje (przynajmniej w projektach w ktorych pracuje).

Nie wypowiem sie o [nie]testowaniu kontrolerow bo nie znam sie na tyle zeby dyskutowac, ale mysle ze troche przesadzasz twierdzac ze cos jest 'zartem', to ze masz takie a nie inne podejscie to nie znaczy ze to jest fakt i jedynie sluszna praktyka.

Mysle ze nie zawsze warto uzywac SetUp/TearDown, czasem lepiej wrzucic ta jedna linijke na poczatku metody, zwlaszcza gdy testowane metody maja rozne wymagania co do zaleznosci.
TestFixtureXXX czesto prowadzi do tego ze testy traca niezaleznosc miedzy soba wiec raczej staram sie unikac.

Napisalabym ze unit testy z zewnetrzna baza to zart, ale sie powstrzymam ;)

Pozostało 580 znaków

Odpowiedz
Liczba odpowiedzi na stronę

1 użytkowników online, w tym zalogowanych: 0, gości: 1, botów: 0