ASP.NET MVC3 i Entity Framework Code first architecture

Moje poprzednie pytanie skłoniło mnie do ponownego zastanowienia się nad warstwami, repozytorium, iniekcją zależności i takimi rzeczami architektonicznymi.

Moja Architektura wygląda teraz tak:
Najpierw używam kodu EF, więc zrobiłem klasy POCO i kontekst. Tworzy db i model.
Poziom wyższy To klasy warstwy biznesowej (dostawcy). Używam innego dostawcy dla każdej domeny... jak MemberProvider, RoleProvider, TaskProvider itp. i robię nową instancję mojego DbContext w każdym z tych dostawców.
Następnie konfiguruję tych dostawców w moich kontrolerach, pobieram Dane i wysyłam je do widoków.

Moja początkowa Architektura zawierała repozytorium, którego pozbyłem się, ponieważ powiedziano mi, że tylko dodaje złożoności, więc dlaczego nie używam tylko EF. Chciałem to zrobić.. praca z EF bezpośrednio ze sterowników, ale muszę pisać testy i było to trochę skomplikowane z rzeczywistą bazą danych. Musiałem jakoś sfałszować dane. Więc zrobiłem interfejs dla każdego dostawcy i zrobili fałszywych dostawców z zakodowanymi danymi na listach. I z tym wróciłem do czegoś, gdzie nie jestem pewien, jak postępować poprawnie.

Te rzeczy zaczynają się zbyt szybko komplikować... wiele podejść i "patternów"... tworzy zbyt dużo szumu i bezużyteczny kod.

Czy istnieje jakaś prosta i testowalna Architektura do tworzenia i ASP.NET aplikacja MVC3 z Entity Framework?

Author: Community, 2011-04-10

4 answers

Jeśli chcesz używać TDD (lub jakiegokolwiek innego podejścia testowego o dużym zasięgu testowym) i EF razem musisz napisać testy integracyjne lub end-to-end. Problem polega na tym, że każde podejście z kpiącym kontekstem lub repozytorium tworzy test, który może przetestować logikę górnej warstwy (która używa tych kpiących), ale nie aplikację.

Prosty przykład:

Zdefiniujmy ogólne repozytorium:

public interface IGenericRepository<TEntity> 
{
    IQueryable<TEntity> GetQuery();
    ...
}

I napiszmy jakąś metodę biznesową:

public IEnumerable<MyEntity> DoSomethingImportant()
{
    var data = MyEntityRepo.GetQuery().Select((e, i) => e);
    ...
}

Teraz, jeśli wyśmiewasz repozytorium użyjesz Linq-to-Objects i będziesz miał zielony test, ale jeśli uruchomisz aplikację z Linq-To-Entities otrzymasz wyjątek, ponieważ select overload with indexes nie jest obsługiwany w L2E.

To był prosty przykład, ale to samo może się zdarzyć z użyciem metod w zapytaniach i innych typowych błędów. Co więcej, ma to również wpływ na metody takie jak Add, Update, Delete Zwykle ujawnione w repozytorium. Jeśli nie napiszesz makiety, która dokładnie symuluje zachowanie kontekstu EF i uczciwości referencyjnej nie będziesz testował swojej implementacji.

Kolejną częścią historii są problemy z leniwym ładowaniem, które również trudno wykryć w testach jednostkowych przeciwko mockom.

Z tego powodu należy również wprowadzić integrację lub testy end-to-end, które będą działać na bazie danych przy użyciu rzeczywistego kontekstu EF ane L2E. Btw. do prawidłowego użycia TDD wymagane jest stosowanie testów end-to-end. Do pisania testów end-to-end w ASP.NET MVC można WatiN i ewentualnie również SpecFlow dla BDD, ale to naprawdę doda wiele pracy, ale będziesz miał swoją aplikację naprawdę przetestowaną. Jeśli chcesz przeczytać więcej o TDD polecam tę książkę (jedyną wadą jest to, że przykłady są w Javie).

Testy integracyjne mają sens, jeśli nie używasz generycznego repozytorium i ukrywasz swoje zapytania w jakiejś klasie, która nie ujawni IQueryable, ale zwróci bezpośrednio dane.

Przykład:

public interface IMyEntityRepository
{
    MyEntity GetById(int id);
    MyEntity GetByName(string name); 
}

Teraz możesz po prostu napisać test integracyjny, aby przetestować implementacja tego repozytorium, ponieważ zapytania są ukryte w tej klasie i nie są narażone na wyższe warstwy. Ale ten typ repozytorium jest w jakiś sposób uważany za starą implementację używaną z procedurami składowanymi. Z tą implementacją stracisz wiele funkcji ORM lub będziesz musiał wykonać wiele dodatkowej pracy - na przykład dodaj wzorzec specyfikacji , aby móc zdefiniować zapytanie w górnej warstwie.

In ASP.NET MVC można częściowo zastąpić testy end-to-end z testy integracyjne na poziomie kontrolera.

Edit based on comment:

Nie mówię, że potrzebujesz testów jednostkowych, integracyjnych i kompleksowych. Mówię, że tworzenie sprawdzonych aplikacji wymaga znacznie więcej wysiłku. Ilość i rodzaje potrzebnych testów zależy od złożoności aplikacji, oczekiwanej przyszłości aplikacji, umiejętności i umiejętności innych członków zespołu.

Małe proste projekty mogą być tworzone bez testów w ogóle (ok, jest nie jest to dobry pomysł, ale wszyscy to zrobiliśmy i na końcu zadziałało) ale gdy projekt przejdzie jakiś treshold, można zauważyć, że wprowadzenie nowych funkcji lub utrzymanie projektu jest bardzo trudne, ponieważ nigdy nie jesteś pewien, czy to łamie coś, co już działało - to się nazywa regresja. Najlepszą obroną przed regresją jest dobry zestaw testów automatycznych.

  • testy jednostkowe pomagają w badaniu metody. Takie testy powinny idealnie obejmować wszystkie ścieżki wykonania w metodzie. Badania te powinny być bardzo krótki i łatwy do napisania-do skomplikowanej części może być ustawienie zależności (mocks, faktes, stubs).
  • testy integracyjne pomagają w testowaniu funkcjonalności na wielu warstwach i zazwyczaj na wielu procesach (aplikacji, bazie danych). Nie musisz ich mieć do wszystkiego, bardziej chodzi o doświadczenie, aby wybrać, gdzie są pomocne.
  • testy End-to-end są czymś w rodzaju walidacji przypadków użycia / historii użytkownika / funkcji. Powinny one obejmować cały przepływ wymagania.

Nie jest konieczne wielokrotne testowanie feture - jeśli wiesz, że funkcja jest testowana w end-to-end test, nie musisz pisać testu integracji dla tego samego kodu. Również jeśli wiesz, że metoda ma tylko jedną ścieżkę wykonawczą, która jest objęta testem integracji, nie musisz pisać dla niej testu jednostkowego. Działa to znacznie lepiej z podejściem TDD, gdzie zaczynasz od dużego testu (end-to-end lub integracja) i idziesz głębiej do testów jednostkowych.

W zależności od Twojego podejście programistyczne nie musisz zaczynać od wielu rodzajów testów, ale możesz je wprowadzić później, ponieważ Twoja aplikacja stanie się bardziej złożona. Wyjątkiem jest TDD / BDD, gdzie należy zacząć używać co najmniej end-to-end i testów jednostkowych przed napisaniem pojedynczej linii innego kodu.

Więc zadajesz złe pytanie. Pytanie nie brzmi, co jest prostsze? Pytanie brzmi, co pomoże ci na końcu i jaka złożoność pasuje do Twojej aplikacji? Jeśli chcesz mając łatwo przetestowaną logikę aplikacji i biznesu powinieneś zawinąć kod EF do innych klas, które można wyśmiać. Ale w tym samym czasie należy wprowadzić inne rodzaje testów, aby upewnić się, że kod EF działa.

Nie mogę powiedzieć, jakie podejście będzie pasować do Twojego środowiska / projektu / zespołu / itp. Ale mogę wyjaśnić przykład z mojego poprzedniego projektu:

Pracowałem nad projektem przez około 5-6 miesięcy z dwoma kolegami. Projekt został oparty na ASP.NET MVC 2 + jQuery + EFv4 i było opracowany w sposób Przyrostowy i iteracyjny. Miał wiele skomplikowanej logiki biznesowej i wiele skomplikowanych zapytań do bazy danych. Zaczęliśmy od generycznych repozytoriów i wysokiego pokrycia kodu z testami jednostkowymi + testami Integracyjnymi w celu walidacji mapowania (proste testy do wstawiania, usuwania, aktualizacji i wyboru encji). Po kilku miesiącach okazało się, że nasze podejście nie działa. Mieliśmy więcej niż 1.200 testy jednostkowe, pokrycie kodu około 60% (to nie jest bardzo dobre) i wiele problemów regresji. Zmiana czegokolwiek w modelu EF może spowodować nieoczekiwane problemy w częściach, które nie były dotykane przez kilka tygodni. Okazało się, że brakuje nam testów integracyjnych lub testów end-to-end dla naszej logiki aplikacji. Ten sam wniosek został wyciągnięty w przypadku równoległego zespołu pracującego nad innym projektem i wykorzystanie testów integracyjnych zostało uznane za rekomendację dla nowych projektów.

 93
Author: Ladislav Mrnka,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-04-10 15:00:39

Czy używanie wzorca repozytorium zwiększa złożoność? W Twoim scenariuszu nie sądzę. To sprawia, że TDD jest łatwiejsze, a Twój kod łatwiejszy do zarządzania. Spróbuj użyć generycznego wzorca repozytorium dla większej separacji i czystszego kodu.

Jeśli chcesz dowiedzieć się więcej o TDD i wzorcach projektowych w Entity Framework, spójrz na: http://msdn.microsoft.com/en-us/ff714955.aspx

Jednak wygląda na to, że szukasz podejścia do symulowania struktury jednostki testowej. Jedno rozwiązanie byłoby użyj wirtualnej metody seed do generowania danych podczas inicjalizacji bazy danych. Zajrzyj do działu Seed pod adresem: http://blogs.msdn.com/b/adonet/archive/2010/09/02/ef-feature-ctp4-dbcontext-and-databases.aspx{[4]

Można również użyć niektórych szyderczych frameworków. Najbardziej znane to:

Aby zobaczyć pełniejszą listę. NET Framework, sprawdź: https://stackoverflow.com/questions/37359/what-c-mocking-framework-to-use

Innym podejściem byłoby użycie dostawcy bazy danych w pamięci, takiego jak SQLite. Dowiedz się więcej na czy istnieje dostawca in-memory for Entity Framework?

Wreszcie, oto kilka dobrych linków na temat Unit testing Entity Framework (Niektóre linki odnoszą się do Entity Framework 4.0. Ale wpadniesz na pomysł.):

Http://social.msdn.microsoft.com/Forums/en/adodotnetentityframework/thread/678b5871-bec5-4640-a024-71bd4d5c77ff{[4]

Http://mosesofegypt.net/post/Introducing-Entity-Framework-Unit-Testing-with-TypeMock-Isolator.aspx{[4]

Jaki jest sposób na sfałszowanie warstwy bazy danych w teście jednostkowym?

 13
Author: Kamyar,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:24:09

To co robię to używam prostego obiektu ISession i EFSession, które są łatwe do wyśmiania w moim kontrolerze, łatwe do uzyskania przez Linq i mocno wpisane. Wstrzyknąć DI używając Ninject.

public interface ISession : IDisposable
    {
        void CommitChanges();
        void Delete<T>(Expression<Func<T, bool>> expression) where T : class, new();
        void Delete<T>(T item) where T : class, new();
        void DeleteAll<T>() where T : class, new();
        T Single<T>(Expression<Func<T, bool>> expression) where T : class, new();
        IQueryable<T> All<T>() where T : class, new();
        void Add<T>(T item) where T : class, new();
        void Add<T>(IEnumerable<T> items) where T : class, new();
        void Update<T>(T item) where T : class, new();
    }

public class EFSession : ISession
    {
        DbContext _context;

        public EFSession(DbContext context)
        {
            _context = context;
        }


        public void CommitChanges()
        {
            _context.SaveChanges();
        }

        public void Delete<T>(System.Linq.Expressions.Expression<Func<T, bool>> expression) where T : class, new()
        {

            var query = All<T>().Where(expression);
            foreach (var item in query)
            {
                Delete(item);
            }
        }

        public void Delete<T>(T item) where T : class, new()
        {
            _context.Set<T>().Remove(item);
        }

        public void DeleteAll<T>() where T : class, new()
        {
            var query = All<T>();
            foreach (var item in query)
            {
                Delete(item);
            }
        }

        public void Dispose()
        {
            _context.Dispose();
        }

        public T Single<T>(System.Linq.Expressions.Expression<Func<T, bool>> expression) where T : class, new()
        {
            return All<T>().FirstOrDefault(expression);
        }

        public IQueryable<T> All<T>() where T : class, new()
        {
            return _context.Set<T>().AsQueryable<T>();
        }

        public void Add<T>(T item) where T : class, new()
        {
            _context.Set<T>().Add(item);
        }

        public void Add<T>(IEnumerable<T> items) where T : class, new()
        {
            foreach (var item in items)
            {
                Add(item);
            }
        }

        /// <summary>
        /// Do not use this since we use EF4, just call CommitChanges() it does not do anything
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="item"></param>
        public void Update<T>(T item) where T : class, new()
        {
            //nothing needed here
        }

Jeśli chcę przełączyć się z EF4 na powiedzmy MongoDB, muszę tylko zrobić Mongosesję, która implementuje ISession...

 2
Author: VinnyG,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-04-10 15:41:28

Miałem ten sam problem decydując się na ogólny projekt mojej aplikacji MVC. Ten projekt CodePlex autorstwa Shiju Varghese był bardzo pomocny. Odbywa się w ASP.net MVC3, EF CodeFirst, a także wykorzystuje warstwę usług i warstwę repozytorium, jak również. Iniekcja zależności odbywa się za pomocą Unity. Jest to proste i bardzo łatwe do naśladowania. Jest również wspierany przez kilka bardzo ładnych postów na blogu 4. Warto to sprawdzić. I nie rezygnuj z repozytorium..jeszcze.

 1
Author: Ben,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-04-10 08:44:08