Prośba o wytłumaczenie testów jednostkowych na przykładzie metody w asp.net i Moq

0

Witam,
chciałbym poprosić bardziej doświadczonych o pomoc w poprawnym zrozumieniu idei testów jednostkowych (unit tests) ponieważ momentami wydają mi się one pozbawione sensu :/

Mamy przykładową metodę w serwisie:

public string GetFile(long fileId)
{
    using (var db = new ApplicationDbContext())
    {
        try
        {
            string source = db.File.FirstOrDefault(x => x.id_file == fileId)?.file_path;
            string destination = System.Web.HttpContext.Current.Server.MapPath("~/Temporary");

            if (Directory.Exists(destination) && File.Exists(source))
            {
                string randomName = Path.GetRandomFileName().Replace(".","") + ".jpg";
                File.Copy(source, destination + "\\" + randomName);

                return "http://" + System.Web.HttpContext.Current.Request.UrlReferrer.Host + ":" + 
                        System.Web.HttpContext.Current.Request.UrlReferrer.Port + "/Temporary/" + randomName;
            }
            else
            {
                throw new Exception("Nie można znaleźć pliku źródłowego ("+source+") i/lub folderu docelowego ("+destination+").");
            }
        }
        catch (Exception ex)
        {
            return "";
        }
    }
}

Metoda ta pobiera ścieżkę do pliku na podstawie jego id, kopiuje do folderu tymczasowego i zwraca url do tejże kopii.

Teraz chcę napisać test do tej metody, ale ... co ja mogę na dobrą sprawę tutaj przetestować? Napisałem coś takiego:

    [TestClass]
    public class FileServiceTest
    {
        private Mock<IFileService> _service;

        [TestInitialize]
        public void Initialize()
        {
            _service = new Mock<IFileService>();
        }

        [TestMethod]
        public void GettingFileTempWithSuccess()
        {
            //Act
            string path = _service.Object.GetFile(It.IsAny<long>());
            //Assert
            Assert.AreNotEqual("", path);
        }
    }

Ten test sprawdzi czy dla dowolnej wartości typu long moja metoda zwróci mi jakąś ścieżkę. Ale co ten test mi daje skoro inaczej być nie może (tzn. może np. nie istnieć plik, albo folder Temporary, ale tego moja metoda testowa już nie sprawdzi), a jeżeli kiedyś zmienię metodę i np. będę przekazywał wartości double (wiem, że w tym przypadku to nie możliwe, ale dla innych funkcji już jak najbardziej) to i tak kod mi się wysypie podczas próby zbuildowania projektu, bo wtedy próbowałbym przyrównać w 7 linii mojej metody wartość long z tablicy bazy danych do parametru double - tak czy siak zostałbym poinformowany o tym błędzie i bym go musiał poprawić. Po co mi zatem moja metoda testowa? Gdzie tu jest ta wartość dodana, dla której warto poświęcić czas i testy mimo wszystko napisać?

Dziękuję za czas poświęcony na przeczytanie mojego posta i ewentualne podpowiedzi :)

1

Jak pracujesz w grupie, może jakiś młotek zmienić Ci coś w funkcji i sypać exception w pewnym przypadku i takowy test podczas budowania powie Ci że twoja metoda już nie funkcjonuje tak jak kiedyś. Ale to jedna z wielu zalet.

catch (Exception ex) { return ""; } to jest karygodne nigdy tak nie rób!
wyjątek trzeba obsłużyć lub przekazać dalej

0

I przy okazji posta poprzednika, jeśli nie obsługujesz wyjątku w tym miejscu, to rzucasz go dalej tak:

 catch(Exception ex)
{
  throw;
}

a nigdy tak:

 catch(Exception ex)
{
  throw ex; //źle
}
0
hzmzp napisał(a):

catch (Exception ex) { return ""; } to jest karygodne nigdy tak nie rób!
wyjątek trzeba obsłużyć lub przekazać dalej

Wiem, wiem, uprościłem kod żeby nie przeklejać niepotrzebnych linii, normalnie wyjątek jest dodawany do loga żeby móc sprawdzić co poszło nie tak, a sam kontroler jeśli uzyska pustą ścieżkę do pliku z serwisu to zwróci komunikat z informacją użytkownikowi.

hzmzp napisał(a):

Jak pracujesz w grupie, może jakiś młotek zmienić Ci coś w funkcji i sypać exception w pewnym przypadku i takowy test podczas budowania powie Ci że twoja metoda już nie funkcjonuje tak jak kiedyś. Ale to jedna z wielu zalet.

Czyli na dobrą sprawę test ma na celu tylko ogólnie poinformować, że coś się sypie w danej metodzie, ale już co i dlaczego to mam się sam dowiedzieć debugując krok po kroku kod programu. Dobrze rozumiem?

1

@boogi

testujesz czy kod zostal wykonany (kazde rozgalezienie). Dodatkowo testujesz czy dany kod robi to co chcesz

W twoim kodzie ciezko cos przetestowac bo masz za duzo zaleznosci

ApplicationDbContext
tutaj masz jedna zaleznosc, ja powinienes wlasnie z mockowac. Przeciez nie pracujesz na prawdziwych danych przy testach jednostkowych
System.Web.HttpContext.Current.Server.MapPath
to tez musisz zmockowac jezeli chcesz przetestowac, bo po prostu to Ci sie wykrzaczty

catch powinienes w ogole usunac stad jezeli wyrzucasz wyjatek i piszesz testy jeden taki ktory wyrzuci wyjatek drugi taki co przejdzie (to wlasnie rozgalezienie kodu)

i jezeli zmockujesz dane jakies, to mozesz sprawdzic czy poprawnie filename zostal stworzony

np takie cos

            //arrange
            var services = new SomeService();
            services.FileCompressorService = fileCompressorMock.Object; // to jest spowodowane architektura, normalnie powinno sie podac przez konstruktor

            var creationDate = DateTime.UtcNow;

            //Act
            filename = services.GetFileName(indonesianTaxReportConfiguration);

            //Assert
            filename.Should().BeEquivalentTo($"TCode1_{creationDate.ToString("yyyyMMdd")}000000.zip");

no i niestety odpisze post pod spodem, bo zapewne nie moge komentowac zgodnie z @furious programming, wiec niech to jakis mod pozniej zlaczy, bo chce by ta wiadomosc przeczytal autor, a edycja w takim wypadku nie zadziala (bo jezeli autor przeczytal moj post to juz nie przeczyta edycji)

sciagnij sobie NBuildera do budowania danych. Bardzo fajny tool

0
fasadin napisał(a):

W twoim kodzie ciezko cos przetestowac bo masz za duzo zaleznosci

ApplicationDbContext
tutaj masz jedna zaleznosc, ja powinienes wlasnie z mockowac. Przeciez nie pracujesz na prawdziwych danych przy testach jednostkowych

To jest moja baza danych, w jaki sposób ją właściwie użyć? Mogę zmockować:
var mockedDB = new Mock<ApplicationDbContext>();
i potem w tej atrapie dobrać się do dowolnej tabeli (mockedDB.Object.File - File to jedna z tabel w mojej db) ale samo to wystarczy, że w wywołaniu
_service.Object.GetFile(It.IsAny<long>());
użyte zostanie właśnie stworzona wcześniej atrapa bazy danych? Dlaczego w takim razie teraz w ogóle testy przechodzą, skoro - jak rozumiem - używam "prawdziwej" db a ona jest pusta?

0

przeciez zawsze mozesz debugowac i zobaczyc co i dlaczego sie dzieje. (jak Ci sie nie chce to odpowiedz masz na dole)

Mockowac mozesz tylko interfejsy. Nie mozesz mockowac prawdziwych obiektow

Nie mozesz dobrac sie do tabeli, bo ona nie istnieje (zapewne). Mock tylko "klonuje" zachowanie obiektu, ale mozesz zdefiniowac, ze funkcja (nie wiem jak z property, musisz popatrzec)
Object.File
zwroci Ci jakis obiekt ktory zbudujesz w tescie

tworzysz nowa instancje przez co zwracasz http + null + null ... i test Ci przechodzi bo tekst nie jest pusty. Jest http (to jest przypuszczenie, moze byc bledne w zaleznosci co masz tam zaimplementowane)

1
boogi napisał(a):

Witam,

A skąd ten protekcjonalizm?

Ten test sprawdzi czy dla dowolnej wartości typu long moja metoda zwróci mi jakąś ścieżkę.

Nieprawda, niczego nie sprawdzi, bo testujesz mocka, a nie swój kod.

Ale co ten test mi daje skoro inaczej być nie może (tzn. może np. nie istnieć plik, albo folder Temporary, ale tego moja metoda testowa już nie sprawdzi)

Bo Twoja metoda jest ściśle związana z bazą danych oraz systemem plików, więc po prostu nie nadaje się do testowania jednostkowego. Testować jednostkowo można kod, na który ma się wpływ, i który da się odseparować od zewnętrznych zależności.
Aby umożliwić testowanie jednostkowe kodu, który ma zewnętrzne zależności stosuje się mocki (jeśli nie masz zewnętrznych zależności, to mocków nie potrzebujesz). Taki mock udaje, że jest bazą danych, usługą sieci web albo systemem plików, i zwraca wyniki, które każesz mu zwrócić. Dopiero wtedy możesz przetestować, czy Twoja rzeczywista metoda operująca na tych udawanych obiektach zachowuje się poprawnie dla różnych zwracanych przez nie wyników, i czy sama zwraca to, czego od niej oczekujesz. Po to pisze się testy - aby sprawdzić, czy wynik działania metody jest zgodny z założeniami.
Ale, aby móc użyć mocków, wszystkie zależności muszą być wstrzykiwane do klasy z zewnątrz (najlepiej przez konstruktor), a nie tworzone w ciele metody - bo takich rzeczy nie da się zamockować. (A przynajmniej nie w rozsądny sposób.)
Dodatkową komplikacją w Twoim przypadku jest fakt, że używasz statycznych klas Dicrectory i File, a statycznych klas mockować się nie da. (A przynajmniej nie w rozsądny sposób.) Dlatego musisz utworzyć jakieś klasy opakowujące rzeczywiste wywołania tychże klas, za to posiadające interfejs, który następnie będziesz mógł zamockować w testach.

0

To ja się podepnę. @somekind napisał, że na zewnętrzne zależności stosuje się mocki. OK. Jak mój program zapisuje plik, to proces zapisywania jest mockowany. No dobra, ale co w przypadku, gdy mam własny binarny format danych i chcę go przetestować? Czy powinienem tworzyć test, który najpierw fizycznie zapisze plik, a potem go odczyta, żeby sprawdzić, czy wszystko jest ok? Bo z tego, co widzę, to tak nie powinienem robić. Więc jak w tym przypadku?

0
fasadin napisał(a):

Mockowac mozesz tylko interfejsy. Nie mozesz mockowac prawdziwych obiektow

Nie mozesz dobrac sie do tabeli, bo ona nie istnieje (zapewne). Mock tylko "klonuje" zachowanie obiektu, ale mozesz zdefiniowac, ze funkcja (nie wiem jak z property, musisz popatrzec)
Object.File
zwroci Ci jakis obiekt ktory zbudujesz w tescie

Czyli jednak źle zrozumiałem działanie Moq :(

Nie mogę mockować obiektów, tylko interfejsy - więc jak zmockować obiekt bazy danych żeby później wstawić jakieś dane do property Object.File? Nigdy nie spotkałem się, żeby ktoś pisał interfejs do dbcontextu w asp.net.

tworzysz nowa instancje przez co zwracasz http + null + null ... i test Ci przechodzi bo tekst nie jest pusty. Jest http (to jest przypuszczenie, moze byc bledne w zaleznosci co masz tam zaimplementowane)

Masz rację, sprawdziłem pod debugerem i zwraca null - a, że je porównuję do pustego string to zwraca true - bo faktycznie null != "" :/

0

@Juhas: nieładnie się podpinać. Ale tak, takie coś powinieneś przetestować integracyjnie.

boogi napisał(a):

Nigdy nie spotkałem się, żeby ktoś pisał interfejs do dbcontextu w asp.net.

Ja również. I raczej nie jest to do niczego potrzebne: http://www.jankowskimichal.pl/2016/02/mockowanie-typow-dbcontext-oraz-dbset-z-wykorzystaniem-moq/

1

nigdy nie potrzebowalem mockowac dbcontext. Zapewne nie trzeba pisac do niego interfejsu i raczej nie testuje sie to za pomoca testow jednostkowych

Testy jednostkowe sa po to by "uderzyc" w dany fragment kodu (jezeli masz ifa) ORAZ sprawdzic czy funkcja dziala dobrze. Ta funkcja musi byc jednostkowa (dzialac niezaleznie od innych). Jezeli tak nie jest, to niektore rzeczy mozna zmockowac, ale nie wszystko. (i nie wszystko ma sens mockowac)

Twoja funkcja bardziej wyglada na test integracyjny niz na test jednoskowy

0
fasadin napisał(a):

Twoja funkcja bardziej wyglada na test integracyjny niz na test jednoskowy

Po przeczytaniu Waszych odpowiedzi doszedłem do wniosku, że cały projekt nie nadaje się do testowania jednostkowego.

Otóż aplikacja webowa, którą próbuję przetestować ogranicza się do pobierania danych - ewentualnego obrabiania - i przesyłania do widoku. Lub odbierania danych z widoku - ewentualnego obrabiania - i aktualizacji/dodawania do bazy. Jeśli Was dobrze rozumiem to testy jednostkowe powinny ograniczyć się do rzeczonego obrabiania - ale i to jest koniec końców nie możliwe, bo przecież te dane pochodzą z bazy danych a tego nie mam jak przetestować unit testem (no bo jak się do nich dobrać i zasymulować działanie na nich?).
Ostatecznie unit testy znalazły by zastosowanie do testowania kontrolerów - tam coś otrzymuję z serwisu (a to mogę ustalić, żeby np. dla takich i takich danych zwróciło mi zawsze "coś") i potem na podstawie tego "cosia" przekierowuję do widoku itd.

Coś czuję, że nadinterpretowałem to wszystko. Moglibyście sprostować jeśli gdzieś się mylę?

1

cały projekt nie nadaje się do testowania jednostkowego

Bardzo wątpliwy wniosek. Jeśli kod coś robi to da się go testować. Moze ewentualnie jakby cały twój kod np. robił tylko return externalService.method() to mogloby to nie mieć sensu

nie możliwe, bo przecież te dane pochodzą z bazy danych a tego nie mam jak przetestować unit testem

Od tego właśnie są mocki...

testowania kontrolerów

Jeśli w kontrolerze da się coś testować to jest to najpewniej zły kontroler i napchałeś do niego logikę...

1
Juhas napisał(a):

No dobra, ale co w przypadku, gdy mam własny binarny format danych i chcę go przetestować? Czy powinienem tworzyć test, który najpierw fizycznie zapisze plik, a potem go odczyta, żeby sprawdzić, czy wszystko jest ok? Bo z tego, co widzę, to tak nie powinienem robić. Więc jak w tym przypadku?

gdy masz wlasny binarny format danych to mozesz zapisac cos do np memory streama i porownac z wzorcowa paczka bajtow (zakladajac ze twoj formatter nie ma zahardkodowanego zapisywania do pliku.
jaki niby jest cel testowaniu czy zapis pliku na dysk sie udal? udowodnienie ze api systemowe dziala jak nalezy? ;)

0
Shalom napisał(a):

cały projekt nie nadaje się do testowania jednostkowego

Bardzo wątpliwy wniosek. Jeśli kod coś robi to da się go testować. Moze ewentualnie jakby cały twój kod np. robił tylko return externalService.method() to mogloby to nie mieć sensu

No tak, ale czy w takim przypadku nie lepiej stosować testy integracyjne a nie jednostkowe? Właśnie o to mi chodziło - o bezsens testowania jednostkowego.

nie możliwe, bo przecież te dane pochodzą z bazy danych a tego nie mam jak przetestować unit testem

Od tego właśnie są mocki...

Ale nie stworzę mocka dla dbContextu żeby poudawał moją bazę, bo to klasa a nie interfejs. Poczytałem trochę i niektórzy robią fejkowe dbsety na IDbContext (ktoś to jednak zrobił o.O) ale pojawiają się głosy, że to złe podejście, bo wtedy tak naprawdę nie testujesz kodu na rzeczywistych obiektach/danych tylko ... no właśnie fejkowych, a to stawia pod znakiem zapytania sensowność testowania w ogóle.

testowania kontrolerów

Jeśli w kontrolerze da się coś testować to jest to najpewniej zły kontroler i napchałeś do niego logikę...

Nie, po prostu w zależności od np. parametru fileType pobiera (za pomocą repo) inne dane i zwraca inny partialView.

1

Nie ma czegoś takiego jak lepiej. To są różne rodzaje testów i wszystkie są przydatne. O ile twój kod nie wygląda jak tak pokazałem w przykładzie to jest miejsce na testy jednostkowe.

Ale nie stworzę mocka dla dbContextu żeby poudawał moją bazę, bo to klasa a nie interfejs. Poczytałem trochę i niektórzy robią fejkowe dbsety na IDbContext (ktoś to jednak zrobił o.O) ale pojawiają się głosy, że to złe podejście, bo wtedy tak naprawdę nie testujesz kodu na rzeczywistych obiektach/danych tylko ... no właśnie fejkowych, a to stawia pod znakiem zapytania sensowność testowania w ogóle.

Jak sie pojawiają głosy to trzeba do psychiatry a nie ich słuchać! ;] Należy zrobić testy integracyjne na jakiejś bazie InMemory, ale mockowanie bazy danych i testowanie z użyciem mocka też ma sens. W szczegolnosci jeśli chcesz testować też sytuacje wyjątkowe w stylu timeout albo zerwanie połączenia. Jak chciałbyś to zasymulować na prawdziwej bazie?

Nie, po prostu w zależności od np. parametru fileType pobiera (za pomocą repo) inne dane i zwraca inny partialView.

Więc tą logikę przenieś z kontrolera do jakiegoś serwisu. Kontroler powinien tylko:

  • odbierać parametry z requestu
  • pakować parametry do response
  • informować który widok należy wyrenderować
0
Shalom napisał(a):

Nie ma czegoś takiego jak lepiej. To są różne rodzaje testów i wszystkie są przydatne. O ile twój kod nie wygląda jak tak pokazałem w przykładzie to jest miejsce na testy jednostkowe.

Jak sie pojawiają głosy to trzeba do psychiatry a nie ich słuchać! ;] Należy zrobić testy integracyjne na jakiejś bazie InMemory, ale mockowanie bazy danych i testowanie z użyciem mocka też ma sens. W szczegolnosci jeśli chcesz testować też sytuacje wyjątkowe w stylu timeout albo zerwanie połączenia. Jak chciałbyś to zasymulować na prawdziwej bazie?

Ok, przerobiłem serwisy i teraz wstrzykuję context bazy danych przez konstruktor więc teoretycznie mogę sobie go mockować.
Pojawiło się jednak jedno ale: spójrzmy na piąty listing ze strony MSDN:

    [TestClass] 
    public class QueryTests 
    { 
        [TestMethod] 
        public void GetAllBlogs_orders_by_name() 
        { 
            var data = new List<Blog> 
            { 
                new Blog { Name = "BBB" }, 
                new Blog { Name = "ZZZ" }, 
                new Blog { Name = "AAA" }, 
            }.AsQueryable(); 
 
            var mockSet = new Mock<DbSet<Blog>>(); 
            mockSet.As<IQueryable<Blog>>().Setup(m => m.Provider).Returns(data.Provider); 
            mockSet.As<IQueryable<Blog>>().Setup(m => m.Expression).Returns(data.Expression); 
            mockSet.As<IQueryable<Blog>>().Setup(m => m.ElementType).Returns(data.ElementType); 
            mockSet.As<IQueryable<Blog>>().Setup(m => m.GetEnumerator()).Returns(0 => data.GetEnumerator()); 
 
            var mockContext = new Mock<BloggingContext>(); 
            mockContext.Setup(c => c.Blogs).Returns(mockSet.Object); 
 
            var service = new BlogService(mockContext.Object); 
            var blogs = service.GetAllBlogs(); 
 
            Assert.AreEqual(3, blogs.Count); 
            Assert.AreEqual("AAA", blogs[0].Name); 
            Assert.AreEqual("BBB", blogs[1].Name); 
            Assert.AreEqual("ZZZ", blogs[2].Name); 
        } 
    } 

Chłopaki mają łatwiej bowiem działają na jednej tabeli. Ja w jednej z metod mam joina na czterech, na dodatek posiadają one znacznie więcej kolumn niż te z przykładu powyżej.
I tu pojawia się pytanie: czy to jest właśnie ten moment, w którym powinienem odpuścić test jednostkowy i wybrać integracyjny? Czy może powinienem naklepać te kilkanaście linijek zmyślonych danych, bo to normalna praktyka i m.in dlatego ludzie nienawidzą pisać testów? ;)

PS. Wiem, że się da napisać test jednostkowy dla takiego przypadku. Pytam czy powinno się?

1

Hola hola, ale CO ty chcesz teraz przetestować? Bo jak ustawisz ze mockowa baza ma zwrócić obiekt X a potem zrobisz asercje czy zwróciła X to co ty właściwie przetestowałeś? NIC! Przecież to bez sensu! Test ma testować jakąś logikę w kodzie. Jeśli jakaś metoda jedyne co robi to database.query("bla bla bla"); to widać że nie ma tam żadnej logiki do przetestowania i trzeba to puścić na jakiejś bazie InMemory.
Testem jednostkowym to byś tu sobie najwyżej mógł sprawdzić czy faktycznie wywołano tam metodę query, niemniej wartość takiego testu jest niska bo mała szansa ze ktoś to przypadkiem wykomentuje ;]

0
Shalom napisał(a):

Hola hola, ale CO ty chcesz teraz przetestować? Bo jak ustawisz ze mockowa baza ma zwrócić obiekt X a potem zrobisz asercje czy zwróciła X to co ty właściwie przetestowałeś? NIC! Przecież to bez sensu! Test ma testować jakąś logikę w kodzie. Jeśli jakaś metoda jedyne co robi to database.query("bla bla bla"); to widać że nie ma tam żadnej logiki do przetestowania i trzeba to puścić na jakiejś bazie InMemory.

Innymi słowy: nie ma sensu takiej metody testować testami jednostkowymi, tak?

Druga sprawa: załóżmy, że ta metoda będzie miała ifa, który np. jeśli dostanie liczby większe od 0 to zaciągnie dane z bazy(sposobem takim jak wcześniej zapostowałem), a jeśli mniejsze od 0 to np. zwróci jakieś sztywne dane. Jeśli dobrze zrozumiałem, to w takim przypadku test jednostkowy miałby sens - przetestowałbym czy mój if zachowa się poprawnie dla różnych liczb. Zgadza się?

1

Tak, wtedy to ma sens. Ma też sens jeśli te dane jeszcze są jakośtam przetwarzane w kodzie przed ich zwróceniem.

1
fasadin napisał(a):

sciagnij sobie NBuildera do budowania danych. Bardzo fajny tool

Warto rzucić okiem na Bogus.
https://github.com/bchavez/Bogus

Połączenie NBuildera i Faker.Net.

No i ma polskie locales, choć trochę niedbale zrobione w pewnych miejscach. Tak czy siak działa.

0

Podepnę się do tematu:
Jak testować aplikacji wielowątkowe i do tego z ogromną ilością pół mutable i których działanie jest uzależnione od innych klas (również wątków)?

0

wielowątkowe

ogromną ilością pół mutable

Obawiam sie że testy nie pomogą i sugerowałbym plan B:

fire.jpg

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