W jaki sposób testy jednostkowe ludzi z Entity Framework 6, czy powinieneś się przejmować?


170

Dopiero zaczynam od testów jednostkowych i ogólnie TDD. Parałem się wcześniej, ale teraz jestem zdeterminowany, aby dodać to do mojego przepływu pracy i napisać lepsze oprogramowanie.

Zadałem wczoraj pytanie, które zawierało to, ale wydaje się, że jest to pytanie samo w sobie. Usiadłem, aby rozpocząć wdrażanie klasy usług, której użyję do oderwania logiki biznesowej od kontrolerów i odwzorowania na określone modele i interakcje z danymi za pomocą EF6.

Problem polega na tym, że już sam się zablokowałem, ponieważ nie chciałem wyodrębniać EF z repozytorium (będzie nadal dostępny poza usługami dla określonych zapytań itp.) I chciałbym przetestować moje usługi (zostanie użyty kontekst EF) .

Tutaj wydaje mi się, że jest pytanie, czy warto to robić? Jeśli tak, jak ludzie robią to na wolności w świetle nieszczelnych abstrakcji spowodowanych przez IQueryable i wielu świetnych postów Ladislava Mrnki na temat testów jednostkowych, które nie są proste z powodu różnic w dostawcach Linq podczas pracy z pamięcią implementacja dołączona do określonej bazy danych.

Kod, który chcę przetestować, wydaje się dość prosty. (to tylko fikcyjny kod, aby spróbować zrozumieć, co robię, chcę sterować tworzeniem za pomocą TDD)

Kontekst

public interface IContext
{
    IDbSet<Product> Products { get; set; }
    IDbSet<Category> Categories { get; set; }
    int SaveChanges();
}

public class DataContext : DbContext, IContext
{
    public IDbSet<Product> Products { get; set; }
    public IDbSet<Category> Categories { get; set; }

    public DataContext(string connectionString)
                : base(connectionString)
    {

    }
}

Usługa

public class ProductService : IProductService
{
    private IContext _context;

    public ProductService(IContext dbContext)
    {
        _context = dbContext;
    }

    public IEnumerable<Product> GetAll()
    {
        var query = from p in _context.Products
                    select p;

        return query;
    }
}

Obecnie myślę o zrobieniu kilku rzeczy:

  1. Mockowanie kontekstu EF za pomocą czegoś podobnego do tego podejścia - Mockowanie EF podczas testowania jednostkowego lub bezpośrednio przy użyciu symulowanej struktury w interfejsie, takim jak moq - biorąc ból, który testy jednostkowe mogą przejść, ale niekoniecznie działają od końca do końca i wykonać kopię zapasową za pomocą testów integracji?
  2. Może używając czegoś takiego jak Effort, aby kpić z EF - nigdy go nie używałem i nie jestem pewien, czy ktoś inny używa go na wolności?
  3. Nie przejmujesz się testowaniem czegokolwiek, co po prostu wywołuje z powrotem do EF - więc zasadniczo metody usługi, które bezpośrednio wywołują EF (getAll itp.) Nie są testowane jednostkowo, ale tylko testowane pod kątem integracji?

Czy ktoś faktycznie robi to bez repozytorium i odnosi sukcesy?


Hej Modika, ostatnio o tym myślałem (z powodu tego pytania: stackoverflow.com/questions/25977388/ ... ) W nim staram się nieco bardziej formalnie opisać, jak obecnie pracuję, ale chciałbym usłyszeć jak robisz to.
samy

Cześć @samy, sposobem, w jaki zdecydowaliśmy się to zrobić, nie był test jednostkowy niczego, co bezpośrednio dotykało EF. Zapytania były testowane, ale jako test integracji, a nie testy jednostkowe. Mockowanie EF jest trochę brudne, ale ten projekt był niewielki, więc wpływ na wydajność dużej liczby testów trafiających do bazy danych nie był tak naprawdę problemem, więc moglibyśmy być bardziej pragmatyczni. Nadal nie jestem w 100% pewien, jakie jest najlepsze podejście, aby być z tobą całkowicie prawdomównym, w pewnym momencie uderzysz w EF (i twój DB), a testy jednostkowe nie wydają mi się tutaj odpowiednie.
Modika

Odpowiedzi:


186

To temat, który mnie bardzo interesuje. Jest wielu purystów, którzy twierdzą, że nie powinno się testować technologii takich jak EF czy NHibernate. Mają rację, są już bardzo rygorystycznie przetestowani i jak stwierdzono w poprzedniej odpowiedzi, często bezcelowe jest spędzanie ogromnej ilości czasu na testowaniu tego, czego nie masz.

Jednak jesteś właścicielem bazy danych pod spodem! W tym miejscu moim zdaniem załamuje się to podejście, nie trzeba sprawdzać, czy EF / NH poprawnie wykonują swoją pracę. Musisz sprawdzić, czy twoje mapowania / implementacje działają z twoją bazą danych. Moim zdaniem jest to jedna z najważniejszych części systemu, który możesz przetestować.

Ściśle mówiąc jednak wychodzimy z domeny testów jednostkowych na rzecz testów integracyjnych, ale zasady pozostają takie same.

Pierwszą rzeczą, którą musisz zrobić, jest możliwość mockowania swojego DAL, aby twój BLL mógł być testowany niezależnie od EF i SQL. To są twoje testy jednostkowe. Następnie musisz zaprojektować testy integracji, aby udowodnić swoje DAL, moim zdaniem są one równie ważne.

Należy wziąć pod uwagę kilka kwestii:

  1. Twoja baza danych musi znajdować się w znanym stanie podczas każdego testu. Większość systemów używa do tego kopii zapasowych lub tworzy skrypty.
  2. Każdy test musi być powtarzalny
  3. Każdy test musi być atomowy

Istnieją dwa główne podejścia do konfigurowania bazy danych, pierwsze to uruchomienie skryptu UnitTest do tworzenia bazy danych. Zapewnia to, że baza danych testów jednostkowych będzie zawsze w tym samym stanie na początku każdego testu (możesz to zresetować lub uruchomić każdy test w transakcji, aby to zapewnić).

Inną opcją jest to, co robię, uruchamiam określone konfiguracje dla każdego indywidualnego testu. Uważam, że jest to najlepsze podejście z dwóch głównych powodów:

  • Twoja baza danych jest prostsza, nie potrzebujesz całego schematu do każdego testu
  • Każdy test jest bezpieczniejszy, jeśli zmienisz jedną wartość w swoim skrypcie tworzenia, nie unieważni to dziesiątek innych testów.

Niestety twoim kompromisem jest tutaj szybkość. Uruchomienie wszystkich tych testów wymaga czasu, aby uruchomić wszystkie te skrypty konfiguracyjne / niszczące.

I ostatnia uwaga: napisanie tak dużej ilości kodu SQL w celu przetestowania ORM może być bardzo ciężką pracą. W tym miejscu przyjmuję bardzo paskudne podejście (tutaj puryści się ze mną nie zgodzą). Używam mojego ORM do tworzenia testu! Zamiast mieć osobny skrypt dla każdego testu DAL w moim systemie, mam fazę konfiguracji testu, która tworzy obiekty, dołącza je do kontekstu i zapisuje. Następnie przeprowadzam test.

To dalekie od idealnego rozwiązania, jednak w praktyce wydaje mi się, że jest DUŻO łatwiejsze w zarządzaniu (zwłaszcza gdy masz kilka tysięcy testów), w przeciwnym razie tworzysz ogromną liczbę skryptów. Praktyczność ponad czystość.

Bez wątpienia spojrzę wstecz na tę odpowiedź za kilka lat (miesiące / dni) i nie zgodzę się ze sobą, ponieważ zmieniło się moje podejście - jednak takie jest moje obecne podejście.

Podsumowując wszystko, co powiedziałem powyżej, oto mój typowy test integracji DB:

[Test]
public void LoadUser()
{
  this.RunTest(session => // the NH/EF session to attach the objects to
  {
    var user = new UserAccount("Mr", "Joe", "Bloggs");
    session.Save(user);
    return user.UserID;
  }, id => // the ID of the entity we need to load
  {
     var user = LoadMyUser(id); // load the entity
     Assert.AreEqual("Mr", user.Title); // test your properties
     Assert.AreEqual("Joe", user.Firstname);
     Assert.AreEqual("Bloggs", user.Lastname);
  }
}

Kluczową rzeczą, na którą należy zwrócić uwagę, jest to, że sesje dwóch pętli są całkowicie niezależne. W swojej implementacji RunTest musisz upewnić się, że kontekst jest zatwierdzony i zniszczony, a twoje dane mogą pochodzić z bazy danych tylko w drugiej części.

Edycja 13/10/2014

Powiedziałem, że prawdopodobnie poprawię ten model w ciągu najbliższych miesięcy. Chociaż w dużej mierze opowiadam się za podejściem, które zaleciłem powyżej, nieznacznie zaktualizowałem mój mechanizm testowania. Teraz mam tendencję do tworzenia jednostek w TestSetup i TestTearDown.

[SetUp]
public void Setup()
{
  this.SetupTest(session => // the NH/EF session to attach the objects to
  {
    var user = new UserAccount("Mr", "Joe", "Bloggs");
    session.Save(user);
    this.UserID =  user.UserID;
  });
}

[TearDown]
public void TearDown()
{
   this.TearDownDatabase();
}

Następnie przetestuj każdą właściwość indywidualnie

[Test]
public void TestTitle()
{
     var user = LoadMyUser(this.UserID); // load the entity
     Assert.AreEqual("Mr", user.Title);
}

[Test]
public void TestFirstname()
{
     var user = LoadMyUser(this.UserID);
     Assert.AreEqual("Joe", user.Firstname);
}

[Test]
public void TestLastname()
{
     var user = LoadMyUser(this.UserID);
     Assert.AreEqual("Bloggs", user.Lastname);
}

Istnieje kilka powodów takiego podejścia:

  • Nie ma dodatkowych wywołań bazy danych (jedna konfiguracja, jedna dezaktywacja)
  • Testy są znacznie bardziej szczegółowe, każdy test weryfikuje jedną właściwość
  • Logika Setup / TearDown została usunięta z samych metod testowych

Wydaje mi się, że dzięki temu klasa testów jest prostsza, a testy bardziej szczegółowe ( pojedyncze potwierdzenia są dobre )

Edycja 03.05.2015

Kolejna rewizja tego podejścia. Chociaż konfiguracje na poziomie klasy są bardzo przydatne w testach, takich jak ładowanie właściwości, są mniej przydatne, gdy wymagane są różne konfiguracje. W tym przypadku utworzenie nowej klasy dla każdego przypadku jest przesadą.

Aby w tym pomóc, mam teraz zwykle dwie klasy bazowe SetupPerTesti SingleSetup. Te dwie klasy uwidaczniają platformę zgodnie z wymaganiami.

W SingleSetupmamy bardzo podobny mechanizm, jak opisano w mojej pierwszej edycji. Przykładem może być

public TestProperties : SingleSetup
{
  public int UserID {get;set;}

  public override DoSetup(ISession session)
  {
    var user = new User("Joe", "Bloggs");
    session.Save(user);
    this.UserID = user.UserID;
  }

  [Test]
  public void TestLastname()
  {
     var user = LoadMyUser(this.UserID); // load the entity
     Assert.AreEqual("Bloggs", user.Lastname);
  }

  [Test]
  public void TestFirstname()
  {
       var user = LoadMyUser(this.UserID);
       Assert.AreEqual("Joe", user.Firstname);
  }
}

Jednak odniesienia, które zapewniają, że ładowane są tylko prawidłowe elementy, mogą korzystać z podejścia SetupPerTest

public TestProperties : SetupPerTest
{
   [Test]
   public void EnsureCorrectReferenceIsLoaded()
   {
      int friendID = 0;
      this.RunTest(session =>
      {
         var user = CreateUserWithFriend();
         session.Save(user);
         friendID = user.Friends.Single().FriendID;
      } () =>
      {
         var user = GetUser();
         Assert.AreEqual(friendID, user.Friends.Single().FriendID);
      });
   }
   [Test]
   public void EnsureOnlyCorrectFriendsAreLoaded()
   {
      int userID = 0;
      this.RunTest(session =>
      {
         var user = CreateUserWithFriends(2);
         var user2 = CreateUserWithFriends(5);
         session.Save(user);
         session.Save(user2);
         userID = user.UserID;
      } () =>
      {
         var user = GetUser(userID);
         Assert.AreEqual(2, user.Friends.Count());
      });
   }
}

Podsumowując, oba podejścia działają w zależności od tego, co próbujesz przetestować.


2
Oto inne podejście do testowania integracji. TL; DR - Użyj samej aplikacji, aby skonfigurować dane testowe, wycofać transakcję na test.
Gert Arnold

3
@Liath, świetna odpowiedź. Potwierdziłeś moje podejrzenia co do testowania EF. Moje pytanie jest następujące; twój przykład dotyczy bardzo konkretnego przypadku, co jest w porządku. Jednak, jak zauważyłeś, może być konieczne przetestowanie setek jednostek. Zgodnie z zasadą DRY (nie powtarzaj się), w jaki sposób skalujesz swoje rozwiązanie, nie powtarzając za każdym razem tego samego podstawowego wzorca kodu?
Jeffrey A. Gochin

4
Muszę się z tym nie zgodzić, ponieważ całkowicie omija problem. Testowanie jednostkowe polega na testowaniu logiki funkcji. W przykładzie OP logika jest zależna od magazynu danych. Masz rację, mówiąc, że nie chcesz testować EF, ale to nie jest problem. Problem polega na testowaniu kodu w izolacji od magazynu danych. Testowanie mapowania to zupełnie inny temat. Aby sprawdzić, czy logika prawidłowo oddziałuje z danymi, musisz mieć możliwość kontrolowania sklepu.
Sinaesthetic

7
Nikt nie jest na przeszkodzie, czy sam powinieneś testować jednostki Entity Framework. Dzieje się tak, że musisz przetestować jakąś metodę, która wykonuje pewne rzeczy, a także wykonuje wywołanie EF do bazy danych. Celem jest mockowanie EF, aby można było przetestować tę metodę bez wymagania bazy danych na serwerze kompilacji.
The Muffin Man

4
Bardzo lubię tę podróż. Dziękujemy za dodawanie zmian w czasie - to jak czytanie kontroli źródła i zrozumienie, jak ewoluowało Twoje myślenie. Naprawdę doceniam również rozróżnienie funkcjonalne (z EF) i jednostkowe (wyśmiewany EF).
Tom Leys

21

Informacje zwrotne na temat wysiłków tutaj

Po wielu lekcjach zacząłem używać Effort w moich testach: podczas testów Context jest budowany przez fabrykę, która zwraca wersję w pamięci, co pozwala mi za każdym razem testować na pustej planszy. Poza testami fabryka jest rozwiązywana do takiej, która zwraca cały kontekst.

Mam jednak wrażenie, że testowanie w pełni funkcjonalnej makiety bazy danych zwykle przeciąga testy w dół; zdajesz sobie sprawę, że musisz zadbać o skonfigurowanie całej masy zależności, aby przetestować jedną część systemu. Masz również tendencję do wspólnego organizowania testów, które mogą nie być powiązane, tylko dlatego, że istnieje tylko jeden ogromny obiekt, który obsługuje wszystko. Jeśli nie zwracasz uwagi, może się okazać, że zamiast testów jednostkowych wykonujesz testy integracyjne

Wolałbym przetestować coś bardziej abstrakcyjnego niż ogromny DBContext, ale nie mogłem znaleźć najlepszego miejsca między znaczącymi testami a testami gołymi kościami. Kredytuj to mojemu brakowi doświadczenia.

Dlatego uważam, że Wysiłek jest interesujący; jeśli chcesz zacząć działać, jest to dobre narzędzie do szybkiego rozpoczęcia pracy i uzyskania wyników. Uważam jednak, że następnym krokiem powinno być coś nieco bardziej eleganckiego i abstrakcyjnego i to właśnie zamierzam zbadać dalej. Dodawanie tego posta do ulubionych, aby zobaczyć, dokąd pójdzie dalej :)

Edytuj, aby dodać : rozgrzewka wymaga trochę czasu, więc patrzysz na ok. 5 sekund przy uruchomieniu testowym. Może to stanowić problem, jeśli chcesz, aby Twój zestaw testów był bardzo wydajny.


Edytowano dla wyjaśnienia:

Użyłem programu Effort do przetestowania aplikacji usług sieciowych. Każda wiadomość M, która wchodzi, jest kierowana do a IHandlerOf<M>przez Windsor. Castle.Windsor rozwiązuje problem, IHandlerOf<M>który rozwiązuje zależności składnika. Jedną z tych zależności jest DataContextFactory, która pozwala programowi obsługi zapytać o fabrykę

W moich testach bezpośrednio tworzę instancję komponentu IHandlerOf, mockuję wszystkie komponenty podrzędne SUT i obsługuję DataContextFactoryprocedurę obsługi.

Oznacza to, że nie wykonuję testów jednostkowych w ścisłym tego słowa znaczeniu, ponieważ moje testy trafiają w DB. Jednak, jak powiedziałem powyżej, pozwoliło mi to zacząć działać i szybko przetestować niektóre punkty w aplikacji


Dzięki za wkład, co mogę zrobić, aby uruchomić ten projekt, ponieważ jest to dobrze płatna praca, zacznij od kilku repozytoriów i zobacz, jak sobie radzę, ale wysiłek jest bardzo interesujący. Nie jesteś zainteresowany, na jakiej warstwie wykorzystujesz wysiłek w swoich aplikacjach?
Modika

2
tylko jeśli Effort prawidłowo obsługiwał transakcje
Sedat Kapanoglu,

a wysiłek ma błąd dla łańcuchów z programem ładującym csv, kiedy używamy „” zamiast null w łańcuchach.
Sam

13

Jeśli chcesz przeprowadzić testy jednostkowe , musisz odizolować kod, który chcesz przetestować (w tym przypadku usługę), od zasobów zewnętrznych (np. Baz danych). Prawdopodobnie można to zrobić za pomocą jakiegoś dostawcy EF w pamięci , jednak znacznie bardziej powszechnym sposobem jest wyodrębnienie implementacji EF, np. Za pomocą pewnego rodzaju wzorca repozytorium. Bez tej izolacji wszelkie napisane przez Ciebie testy będą testami integracyjnymi, a nie jednostkowymi.

Jeśli chodzi o testowanie kodu EF - piszę zautomatyzowane testy integracji dla moich repozytoriów, które zapisują różne wiersze do bazy danych podczas ich inicjalizacji, a następnie wywołuję moje implementacje repozytoriów, aby upewnić się, że zachowują się zgodnie z oczekiwaniami (np. Upewniając się, że wyniki są poprawnie filtrowane lub że są posortowane we właściwej kolejności).

Są to testy integracji, a nie testy jednostkowe, ponieważ testy te polegają na posiadaniu połączenia z bazą danych oraz na tym, że docelowa baza danych ma już zainstalowany najnowszy, aktualny schemat.


Dzięki @justin, wiem o wzorcu repozytorium, ale czytanie rzeczy takich jak ayende.com/blog/4784/ ... i lostechies.com/jimmybogard/2009/09/11/wither-the-repository sprawiło, że pomyślałem, że nie Nie chcę tej warstwy abstrakcji, ale z drugiej strony mówią one więcej o podejściu do zapytań, które jest bardzo zagmatwane.
Modika

7
@Modika Ayende wybrała kiepską implementację wzorca repozytorium do krytyki, w wyniku czego jest w 100% poprawna - jest przesadzona i nie oferuje żadnych korzyści. Dobra implementacja izoluje testowane jednostkowo fragmenty kodu od implementacji DAL. Używanie NHibernate i EF bezpośrednio utrudnia (jeśli nie uniemożliwia) testowanie kodu i prowadzi do sztywnej monolitycznej podstawy kodu. Nadal jestem nieco sceptyczny co do wzorca repozytorium, jednak jestem w 100% przekonany, że musisz jakoś odizolować swoją implementację DAL, a repozytorium jest najlepszą rzeczą, jaką do tej pory znalazłem.
Justin

2
@Modika Przeczytaj ponownie drugi artykuł. „Nie chcę tej warstwy abstrakcji” nie jest tym, co mówi. Ponadto przeczytaj o oryginalnym wzorcu repozytorium z Fowler ( martinfowler.com/eaaCatalog/repository.html ) lub DDD ( dddcommunity.org/resources/ddd_terms ). Nie wierz osobom mimowolnym bez pełnego zrozumienia oryginalnej koncepcji. To, co naprawdę krytykują, to niedawne niewłaściwe użycie wzoru, a nie sam wzór (chociaż prawdopodobnie o tym nie wiedzą).
guillaume

1
@ guillaume31 Nie sprzeciwiam się wzorcowi repozytorium (rozumiem to) po prostu próbuję dowiedzieć się, czy potrzebuję go do abstrakcji na tym poziomie i czy mogę go pominąć i przetestować bezpośrednio na EF, szydząc i używam go w moich testach na wyższej warstwie w mojej aplikacji. Ponadto, jeśli nie korzystam z repozytorium, otrzymam korzyści z rozszerzonego zestawu funkcji EF, w przypadku repozytorium mogę tego nie uzyskać.
Modika

Kiedy już wyodrębniłem DAL z repozytorium, potrzebuję „Mock” ​​bazy danych (EF). Jak dotąd mockowanie kontekstu i różnych rozszerzeń asynchronicznych (ToListAsync (), FirstOrDefaultAsync () itp.) Powodowało we mnie frustrację.
Kevin Burton

9

Oto rzecz, Entity Framework jest implementacją, więc pomimo faktu, że abstrakcja złożoność interakcji z bazą danych, bezpośrednia interakcja jest nadal ścisłym sprzężeniem i dlatego testowanie jest mylące.

Testowanie jednostkowe polega na testowaniu logiki funkcji i każdego z jej potencjalnych wyników w oderwaniu od wszelkich zależności zewnętrznych, którymi w tym przypadku jest magazyn danych. Aby to zrobić, musisz mieć możliwość kontrolowania zachowania magazynu danych. Na przykład, jeśli chcesz zapewnić, że twoja funkcja zwraca fałsz, jeśli pobrany użytkownik nie spełnia jakiegoś zestawu kryteriów, wtedy twoja [udawana] składnica danych powinna być skonfigurowana tak, aby zawsze zwracała użytkownika, który nie spełnia kryteriów, i vice versa dla przeciwnego twierdzenia.

Powiedziawszy to i akceptując fakt, że EF jest implementacją, prawdopodobnie poparłbym pomysł abstrakcji repozytorium. Wydaje się trochę zbędne? Tak nie jest, ponieważ rozwiązujesz problem polegający na odizolowaniu kodu od implementacji danych.

W DDD repozytoria zawsze zwracają tylko zagregowane korzenie, a nie DAO. W ten sposób konsument repozytorium nigdy nie musi wiedzieć o implementacji danych (a nie powinien) i możemy to wykorzystać jako przykład rozwiązania tego problemu. W takim przypadku obiekt generowany przez EF jest DAO i jako taki powinien być ukryty w aplikacji. To kolejna zaleta repozytorium, które zdefiniujesz. Obiekt biznesowy można zdefiniować jako typ zwracany zamiast obiektu EF. Teraz repozytorium ukrywa wywołania EF i mapuje odpowiedź EF na ten obiekt biznesowy zdefiniowany w sygnaturze repozytorium. Teraz możesz użyć tego repozytorium zamiast zależności DbContext, którą wstrzykujesz do swoich klas, a co za tym idzie, możesz teraz mockować ten interfejs, aby uzyskać kontrolę potrzebną do przetestowania kodu w izolacji.

To trochę więcej pracy i wielu kciuka na nosie, ale to rozwiązuje prawdziwy problem. Istnieje dostawca pamięci, o którym wspomniano w innej odpowiedzi, która może być opcją (nie próbowałem tego), a samo jego istnienie jest dowodem na potrzebę takiej praktyki.

Zupełnie nie zgadzam się z najlepszą odpowiedzią, ponieważ omija ona prawdziwy problem, jakim jest izolacja kodu, a następnie przechodzi do testowania mapowania. Oczywiście przetestuj swoje mapowanie, jeśli chcesz, ale zajmij się tutaj rzeczywistym problemem i uzyskaj rzeczywiste pokrycie kodu.


8

Nie chciałbym testować kodu jednostkowego, którego nie posiadam. Co tu testujesz, że kompilator MSFT działa?

To powiedziawszy, aby ten kod był testowalny, prawie MUSISZ oddzielić warstwę dostępu do danych od kodu logiki biznesowej. To, co robię, to bierze wszystkie moje rzeczy EF i umieszczam je w (lub wielu) klasach DAO lub DAL, które również mają odpowiedni interfejs. Następnie piszę usługę, w której obiekt DAO lub DAL zostanie wstrzyknięty jako zależność (najlepiej iniekcja konstruktora), do której odwołuje się interfejs. Teraz część, która musi zostać przetestowana (kod), można łatwo przetestować, wyszydzając interfejs DAO i wstrzykując go do instancji usługi w ramach testu jednostkowego.

//this is testable just inject a mock of IProductDAO during unit testing
public class ProductService : IProductService
{
    private IProductDAO _productDAO;

    public ProductService(IProductDAO productDAO)
    {
        _productDAO = productDAO;
    }

    public List<Product> GetAllProducts()
    {
        return _productDAO.GetAll();
    }

    ...
}

Uznałbym warstwy dostępu do danych na żywo za część testów integracyjnych, a nie testów jednostkowych. Widziałem facetów weryfikujących liczbę podróży do bazy danych w stanie hibernacji, ale pracowali nad projektem, który obejmował miliardy rekordów w ich magazynie danych, a te dodatkowe podróże naprawdę się liczyły.


1
Dzięki za odpowiedź, ale jaka byłaby różnica, gdybyśmy powiedzieli Repozytorium, w którym na tym poziomie ukrywacie za nim wewnętrzne elementy EF? Naprawdę nie chcę abstrakcyjnego EF, chociaż nadal mogę to robić z interfejsem IContext? Jestem w tym nowy, bądź delikatny :)
Modika

3
@Modika A Repo też jest w porządku. Jakikolwiek wzór chcesz. „Naprawdę nie chcę abstrakcyjnego EF”. Chcesz przetestować kod, czy nie?
Jonathan Henson

1
@Modika Chodzi mi o to, że nie będziesz mieć ŻADNEGO testowalnego kodu, jeśli nie rozdzielisz swoich obaw. Dostęp do danych i logika biznesowa MUSZĄ znajdować się w osobnych warstwach, aby przeprowadzić dobre, możliwe do utrzymania testy.
Jonathan Henson

2
po prostu nie czułem, że konieczne jest zawijanie EF w abstrakcję repozytorium, ponieważ zasadniczo IDbSets to repozytorium, a kontekst to UOW, zaktualizuję trochę moje pytanie, ponieważ może to być mylące. Problem dotyczy jakiejkolwiek abstrakcji, a głównym celem jest to, co dokładnie testuję, ponieważ moje kolejki nie będą działać w tych samych granicach (linq-to-entity vs linq-to-objects), więc jeśli tylko testuję, czy moja usługa tworzy telefon, który wydaje się trochę marnotrawny, czy mam się tu dobrze?
Modika

1
, Chociaż zgadzam się z Twoimi ogólnymi punktami, DbContext jest jednostką pracy, a IDbSets są zdecydowanie częścią implementacji repozytorium i nie tylko ja tak myślę. Mogę mockować EF, a na jakiejś warstwie będę musiał przeprowadzić testy integracji, czy to naprawdę ma znaczenie, czy zrobię to w repozytorium, czy dalej w usłudze? Bycie ściśle połączonym z DB nie jest tak naprawdę problemem, jestem pewien, że tak się dzieje, ale nie zamierzam planować czegoś, co może się nie zdarzyć.
Modika

8

Czasami grzebałem, aby dojść do tych rozważań:

1- Jeśli moja aplikacja ma dostęp do bazy danych, dlaczego test nie powinien? A jeśli coś jest nie tak z dostępem do danych? Testy muszą to wiedzieć wcześniej i ostrzec się o problemie.

2- Wzorzec repozytorium jest nieco trudny i czasochłonny.

Wymyśliłem więc takie podejście, które nie uważam za najlepsze, ale spełniło moje oczekiwania:

Use TransactionScope in the tests methods to avoid changes in the database.

Aby to zrobić, konieczne jest:

1- Zainstaluj EntityFramework w projekcie testowym. 2- Umieść parametry połączenia w pliku app.config projektu testowego. 3- Odwołaj się do dll System.Transactions w projekcie testowym.

Unikalnym efektem ubocznym jest to, że ziarno tożsamości będzie rosło podczas próby wstawienia, nawet jeśli transakcja zostanie przerwana. Ale ponieważ testy są przeprowadzane na programistycznej bazie danych, nie powinno to stanowić problemu.

Przykładowy kod:

[TestClass]
public class NameValueTest
{
    [TestMethod]
    public void Edit()
    {
        NameValueController controller = new NameValueController();

        using(var ts = new TransactionScope()) {
            Assert.IsNotNull(controller.Edit(new Models.NameValue()
            {
                NameValueId = 1,
                name1 = "1",
                name2 = "2",
                name3 = "3",
                name4 = "4"
            }));

            //no complete, automatically abort
            //ts.Complete();
        }
    }

    [TestMethod]
    public void Create()
    {
        NameValueController controller = new NameValueController();

        using (var ts = new TransactionScope())
        {
            Assert.IsNotNull(controller.Create(new Models.NameValue()
            {
                name1 = "1",
                name2 = "2",
                name3 = "3",
                name4 = "4"
            }));

            //no complete, automatically abort
            //ts.Complete();
        }
    }
}

1
Właściwie to rozwiązanie bardzo mi się podoba. Super proste we wdrożeniu i bardziej realistyczne scenariusze testowania. Dzięki!
slopapa

1
z EF 6 użyłbyś DbContext.Database.BeginTransaction, prawda?
SwissCoder,

5

Krótko mówiąc, nie, sok nie jest wart wyciskania, aby przetestować metodę usługi za pomocą jednej linii, która pobiera dane modelu. Z mojego doświadczenia wynika, że ​​ludzie, którzy dopiero zaczynają przygodę z TDD, chcą przetestować absolutnie wszystko. Stary kasztan abstrakcji fasady do frameworka strony trzeciej tylko po to, abyś mógł stworzyć makietę tego interfejsu API frameworka, za pomocą którego bastardujesz / rozszerzasz, aby można było wstrzykiwać fikcyjne dane, ma dla mnie niewielką wartość. Każdy ma inny pogląd na to, ile testów jednostkowych jest najlepsze. Obecnie jestem bardziej pragmatyczny i zadaję sobie pytanie, czy mój test naprawdę dodaje wartość do produktu końcowego i jakim kosztem.


1
Tak dla pragmatyzmu. Nadal twierdzę, że jakość twoich testów jednostkowych jest gorsza niż jakość twojego oryginalnego kodu. Oczywiście użycie TDD jest przydatne w celu poprawy praktyki kodowania, a także w celu zwiększenia łatwości konserwacji, ale TDD może mieć zmniejszoną wartość. Wszystkie nasze testy przeprowadzamy na bazie danych, ponieważ daje nam to pewność, że nasze użycie EF i samych tabel jest prawidłowe. Testy trwają dłużej, ale są bardziej niezawodne.
Savage

3

Chcę podzielić się podejściem, które zostało skomentowane i krótko omówione, ale pokażę rzeczywisty przykład, którego obecnie używam, aby pomóc w testach jednostkowych usług opartych na EF.

Po pierwsze, chciałbym użyć dostawcy w pamięci z EF Core, ale chodzi o EF 6. Ponadto w przypadku innych systemów magazynowania, takich jak RavenDB, byłbym również zwolennikiem testowania za pośrednictwem dostawcy bazy danych w pamięci. Ponownie - ma to na celu pomóc w testowaniu kodu opartego na EF bez zbytniej ceremonii .

Oto cele, które wyznaczyłem sobie, wymyślając wzór:

  • To musi być łatwe do zrozumienia dla innych programistów w zespole
  • Musi izolować kod EF na najniższym możliwym poziomie
  • Nie może obejmować tworzenia dziwnych interfejsów z wieloma odpowiedzialnością (takich jak „ogólny” lub „typowy” wzorzec repozytorium)
  • Musi być łatwy do skonfigurowania i ustawienia w teście jednostkowym

Zgadzam się z poprzednimi stwierdzeniami, że EF nadal jest szczegółem implementacyjnym i można czuć, że trzeba go wyabstrahować, aby wykonać „czysty” test jednostkowy. Zgadzam się też, że idealnie chciałbym upewnić się, że sam kod EF działa - ale dotyczy to bazy danych piaskownicy, dostawcy w pamięci itp. Moje podejście rozwiązuje oba problemy - można bezpiecznie testować jednostkowo kod zależny od EF i tworzyć testy integracji w celu przetestowania kodu EF w szczególności.

Sposób, w jaki to osiągnąłem, polegał na prostym hermetyzowaniu kodu EF w dedykowanych klasach Query i Command. Pomysł jest prosty: po prostu zawiń dowolny kod EF w klasę i polegaj na interfejsie w klasach, które pierwotnie go używały. Głównym problemem, który musiałem rozwiązać, było uniknięcie dodawania wielu zależności do klas i konfigurowania dużej ilości kodu w moich testach.

Tutaj pojawia się użyteczna, prosta biblioteka: Mediatr . Pozwala na proste przesyłanie komunikatów w trakcie procesu i robi to poprzez oddzielenie „żądań” od programów obsługi, które implementują kod. Ma to dodatkową zaletę w postaci oddzielenia „co” od „jak”. Na przykład, hermetyzując kod EF na małe fragmenty, umożliwia zastąpienie implementacji innym dostawcą lub zupełnie innym mechanizmem, ponieważ wszystko, co robisz, to wysyłanie żądania wykonania akcji.

Wykorzystując iniekcję zależności (z frameworkiem lub bez niego - preferencje użytkownika), możemy łatwo mockować mediatora i kontrolować mechanizmy żądania / odpowiedzi, aby umożliwić testowanie jednostkowe kodu EF.

Po pierwsze, powiedzmy, że mamy usługę, która ma logikę biznesową, którą musimy przetestować:

public class FeatureService {

  private readonly IMediator _mediator;

  public FeatureService(IMediator mediator) {
    _mediator = mediator;
  }

  public async Task ComplexBusinessLogic() {
    // retrieve relevant objects

    var results = await _mediator.Send(new GetRelevantDbObjectsQuery());
    // normally, this would have looked like...
    // var results = _myDbContext.DbObjects.Where(x => foo).ToList();

    // perform business logic
    // ...    
  }
}

Czy zaczynasz dostrzegać korzyści płynące z tego podejścia? Nie tylko jawnie hermetyzujesz cały kod związany z EF w klasach opisowych, ale także pozwalasz na rozszerzalność, usuwając problem implementacji dotyczący sposobu obsługi tego żądania - ta klasa nie obchodzi, czy odpowiednie obiekty pochodzą z EF, MongoDB, lub plik tekstowy.

Teraz dla wnioskodawcy i osoby obsługującej, za pośrednictwem MediatR:

public class GetRelevantDbObjectsQuery : IRequest<DbObject[]> {
  // no input needed for this particular request,
  // but you would simply add plain properties here if needed
}

public class GetRelevantDbObjectsEFQueryHandler : IRequestHandler<GetRelevantDbObjectsQuery, DbObject[]> {
  private readonly IDbContext _db;

  public GetRelevantDbObjectsEFQueryHandler(IDbContext db) {
    _db = db;
  }

  public DbObject[] Handle(GetRelevantDbObjectsQuery message) {
    return _db.DbObjects.Where(foo => bar).ToList();
  }
}

Jak widać, abstrakcja jest prosta i zamknięta. Jest to także absolutnie sprawdzalne , ponieważ w teście integracyjnym, to mógłby przetestować tę klasę indywidualnie - nie ma obawy roboczych mieszane tutaj.

Jak więc wygląda test jednostkowy naszej usługi funkcji? To bardzo proste. W tym przypadku używam Moq do robienia kpiny (używaj tego, co cię uszczęśliwia):

[TestClass]
public class FeatureServiceTests {

  // mock of Mediator to handle request/responses
  private Mock<IMediator> _mediator;

  // subject under test
  private FeatureService _sut;

  [TestInitialize]
  public void Setup() {

    // set up Mediator mock
    _mediator = new Mock<IMediator>(MockBehavior.Strict);

    // inject mock as dependency
    _sut = new FeatureService(_mediator.Object);
  }

  [TestCleanup]
  public void Teardown() {

    // ensure we have called or expected all calls to Mediator
    _mediator.VerifyAll();
  }

  [TestMethod]
  public void ComplexBusinessLogic_Does_What_I_Expect() {
    var dbObjects = new List<DbObject>() {
      // set up any test objects
      new DbObject() { }
    };

    // arrange

    // setup Mediator to return our fake objects when it receives a message to perform our query
    // in practice, I find it better to create an extension method that encapsulates this setup here
    _mediator.Setup(x => x.Send(It.IsAny<GetRelevantDbObjectsQuery>(), default(CancellationToken)).ReturnsAsync(dbObjects.ToArray()).Callback(
    (GetRelevantDbObjectsQuery message, CancellationToken token) => {
       // using Moq Callback functionality, you can make assertions
       // on expected request being passed in
       Assert.IsNotNull(message);
    });

    // act
    _sut.ComplexBusinessLogic();

    // assertions
  }

}

Widzisz, że potrzebujemy tylko jednej konfiguracji i nie musimy nawet niczego konfigurować - to bardzo prosty test jednostkowy. Wyjaśnijmy: jest to całkowicie możliwe bez czegoś takiego jak Mediatr (po prostu zaimplementowałbyś interfejs i wyszywałbyś go np. Do testów IGetRelevantDbObjectsQuery), ale w praktyce dla dużej bazy kodu z wieloma funkcjami i zapytaniami / poleceniami, uwielbiam hermetyzację i wrodzone wsparcie DI oferuje Mediatr.

Jeśli zastanawiasz się, jak organizuję te zajęcia, jest to całkiem proste:

- MyProject
  - Features
    - MyFeature
      - Queries
      - Commands
      - Services
      - DependencyConfig.cs (Ninject feature modules)

Organizowanie według wycinków funkcji nie ma znaczenia, ale pozwala to zachować cały odpowiedni / zależny kod razem i łatwo go znaleźć. Co najważniejsze, oddzielam zapytania od poleceń - kierując się zasadą separacji poleceń / zapytań .

To spełnia wszystkie moje kryteria: jest to niska ceremonia, łatwa do zrozumienia i są dodatkowe ukryte korzyści. Na przykład, jak radzisz sobie z zapisywaniem zmian? Teraz możesz uprościć kontekst Db, używając interfejsu roli (IUnitOfWork.SaveChangesAsync()) i pozorowane wywołania interfejsu pojedynczej roli lub możesz hermetyzować zatwierdzanie / wycofywanie w ramach swoich RequestHandlers - jednak wolisz to robić, o ile jest to możliwe do utrzymania. Na przykład kusiło mnie, aby utworzyć jedno ogólne żądanie / procedurę obsługi, w którym po prostu przekazałbyś obiekt EF i zapisałby / zaktualizował / usunąłby go - ale musisz zapytać, jaki jest twój zamiar i pamiętaj, że jeśli chcesz zamienić moduł obsługi z innym dostawcą / implementacją pamięci masowej, prawdopodobnie powinieneś utworzyć jawne polecenia / zapytania, które reprezentują to, co zamierzasz zrobić. Najczęściej pojedyncza usługa lub funkcja będzie wymagać czegoś konkretnego - nie twórz ogólnych rzeczy, zanim będziesz ich potrzebować.

Istnieją oczywiście zastrzeżenia do tego wzorca - możesz posunąć się za daleko, korzystając z prostego mechanizmu pub / sub. Ograniczyłem swoją implementację tylko do abstrakcyjnego kodu związanego z EF, ale żądni przygód programiści mogli zacząć używać MediatR, aby przesadzić i wysyłać wiadomości - coś, co dobre praktyki przeglądu kodu i recenzje powinny złapać. Jest to problem związany z procesem, a nie problemem z MediatR, więc po prostu bądź świadomy tego, jak używasz tego wzorca.

Potrzebowałeś konkretnego przykładu tego, jak ludzie testują jednostkowe / kpią z EF i jest to podejście, które z powodzeniem sprawdza się w naszym projekcie - a zespół jest bardzo zadowolony z tego, jak łatwo je zastosować. Mam nadzieję, że to pomoże! Podobnie jak w przypadku wszystkich rzeczy w programowaniu, istnieje wiele podejść i wszystko zależy od tego, co chcesz osiągnąć. Cenię prostotę, łatwość obsługi, łatwość konserwacji i wykrywalność - a to rozwiązanie spełnia wszystkie te wymagania.


Dziękuję za odpowiedź, to świetny opis wzorca QueryObject przy użyciu Mediatora i coś, co zaczynam także w moich projektach. Być może będę musiał zaktualizować pytanie, ale nie jestem już testowaniem jednostkowym EF, abstrakcje są zbyt nieszczelne (chociaż SqlLite może być w porządku), więc po prostu testuję integrację moich rzeczy, które sprawdzają bazę danych i reguły biznesowe testów jednostkowych i inną logikę.
Modika,

3

Istnieje Effort, który jest dostawcą bazy danych struktury jednostki pamięci. Właściwie tego nie próbowałem ... Haa właśnie zauważyłem, że wspomniano o tym w pytaniu!

Alternatywnie możesz przełączyć się na EntityFrameworkCore, który ma wbudowanego dostawcę bazy danych w pamięci.

https://blog.goyello.com/2016/07/14/save-time-mocking-use-your-real-entity-framework-dbcontext-in-unit-tests/

https://github.com/tamasflamich/effort

Użyłem fabryki, aby uzyskać kontekst, więc mogę stworzyć kontekst zbliżony do jego użycia. Wydaje się, że działa to lokalnie w Visual Studio, ale nie na moim serwerze kompilacji TeamCity, jeszcze nie wiem, dlaczego.

return new MyContext(@"Server=(localdb)\mssqllocaldb;Database=EFProviders.InMemory;Trusted_Connection=True;");

Cześć andrew, problem nigdy nie polegał na uzyskaniu kontekstu, możesz fabrykować kontekst, co właśnie robiliśmy, wyabstrahować kontekst i zlecić jego zbudowanie przez fabrykę. Największym problemem była spójność tego, co było w pamięci z tym, co robi Linq4Entities, nie są one tym samym, co może prowadzić do wprowadzających w błąd testów. Obecnie zajmujemy się tylko testowaniem integracji z bazą danych, może nie być najlepszym procesem dla wszystkich.
Modika

Ten pomocnik Moq działa ( codeproject.com/Tips/1045590/… ), jeśli masz kontekst do wyszydzenia . Jeśli popierasz sfałszowany kontekst za pomocą listy, nie będzie on zachowywał się jak kontekst wspierany przez bazę danych sql.
pasztet

2

Lubię oddzielać filtry od innych części kodu i testować je zgodnie z opisem na moim blogu tutaj http://coding.grax.com/2013/08/testing-custom-linq-filter-operators.html

Biorąc to pod uwagę, testowana logika filtru nie jest identyczna z logiką filtru wykonywaną podczas uruchamiania programu ze względu na tłumaczenie między wyrażeniem LINQ a bazowym językiem zapytań, takim jak T-SQL. Mimo to pozwala mi to zweryfikować logikę filtra. Nie martwię się zbytnio o występujące tłumaczenia i takie rzeczy, jak rozróżnianie wielkości liter i obsługa wartości zerowych, dopóki nie przetestuję integracji między warstwami.


0

Ważne jest, aby przetestować to, czego oczekujesz od struktury encji (tj. Zweryfikować swoje oczekiwania). Jednym ze sposobów, aby to zrobić, z powodzeniem, jest użycie moq, jak pokazano w tym przykładzie (zbyt długo, aby skopiować do tej odpowiedzi):

https://docs.microsoft.com/en-us/ef/ef6/fundamentals/testing/mocking

Jednak bądź ostrożny ... Nie gwarantuje się, że kontekst SQL zwróci rzeczy w określonej kolejności, chyba że masz w zapytaniu linq odpowiednie "OrderBy", więc możliwe jest zapisanie rzeczy, które przechodzą, gdy testujesz używając listy w pamięci ( linq-to-entity), ale kończy się niepowodzeniem w środowisku uat / live, gdy zostanie użyte (linq-to-sql).

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.