Entity Framework vs TDD – starcie pierwsze!

Prace postępują. Zanim jednak dodam do aplikacji mechanizm pobierania informacji o filmach z wybranych serwisów, stworzę część administracyjną, która pozwoli na zarządzanie danymi gromadzonymi w bazie. Przechowywać tam będziemy między innymi powiązania filmów z różnymi serwisami (dostawcy VOD, serwisy z ocenami), które system będzie starał się wychwycić automatycznie. Rodzi to naturalnie ryzyko pomyłek – musimy mieć możliwość ich poprawienia. Generalnie potrzebujemy zatem mechanizmu i interfejsu użytkownika do edycji obiektów w bazie danych.

W poprzednim odcinku

Poprzednio wykorzystaliśmy klasę z testami do controllera wygenerowanego automatycznie przez Visual Studio – bardzo prostego. Posłużyła ona do zaprezentowania ogólnej postaci testu jednostkowego. Będę starał się jednak unikać komplikowania controllerów tak, by trzeba je było testować. Z reguły będę w nich odwoływał się do modelu, by przekazać go do widoku i odwrotnie. Cała logika aplikacji będzie znajdować się na niższym poziomie – w warstwie usług.

Repository – a kysz!

Zanim zacząłem dodawać fukcjonalności związane z edycją danych musiałem jednak zastanowić się jak zorganizować w aplikacji odwołania do bazy danych, by można je wygodnie przetestować. To znów temat, którego realizacje widziałem różne. Popularny wszędzie jest wzorzec projektowy Repository i UnitOfWork (używany np. we wspomnianych przeze mnie poprzenio projektach EFMVC i SocialGoal). Napotkałem jednak głosy (wiele głosów!) przeciw takiemu rozwiązaniu, jako niepotrzebnie komplikującemu system – np. tutaj i tutaj. W aplikacji będę korzystał z Entity Framework, które samo w sobie implementuje już wzorzec Repository i UnitOfWork. Dodatkowa warstwa wydaje się zatem zbędna, spróbuję korzystać bezpośrednio z Entity Framework i jego DbContext i DbSet.

Czy to się uda?

Jak się do tego zabrałem pokażę na przykładzie funkcji pobierania listy filmów z bazy danych. Entity Framework stanowi warstwę DAL (obiekty z nią związane w mojej aplikacji będą siedzieć w projekcie VodSearcher.Data), nad nią zaś siedzi warstwa logiki biznesowej – u mnie w projekcie VodSearcher.Services. Na początku utworzyłem zatem klasę Movie oraz MovieContext jak poniżej – jeszcze nie wiem, czy będą potrzebne do nich testy, na razie wydają się być proste.

namespace VodSearcher.Data
{
    public class Movie
    {
        public int Id { get; set; }
        public string Title { get; set; }
    }
}

namespace VodSearcher.Data
{
    public class MovieContext: DbContext
    {
        public virtual DbSet<Movie> Movies { get; set; }
    }
}

Widzimy, że nasz MovieContext dziedziczy z DbContext i udostępnia kolekcję obiektów typu Movie.

Testów wymagać będzie na pewno klasa udostępniająca dane o filmach controllerowi – MovieService. W myśl TDD spróbuję zatem najpierw napisać dla niej test, a potem implementację.

Przebieranki!

Pojawia się tutaj problem – jak przetestować funkcje związane z bazą danych (pośrednio Entity Framework), skoro testy jednostkowe muszą być niezależne od zewnętrznych systemów? Trzeba odwołania do bazy danych zasymulować. Z wielu blibliotek stworzonych do takich celów wybrałem popularną Moq. Klasa testów MovieService z wykorzystaniem obiektów mockowanych wyglada tak:

using System;
using System.Linq;
using System.Collections.Generic;
using System.Data.Entity;
using Xunit;
using Moq;
using VodSearcher.Services;
using VodSearcher.Data;

namespace VodSearcher.Services.Tests
{
    public class MovieServiceTest
    {
        private Mock<MovieContext> mockMovieContext;
        private Mock<DbSet<Movie>> mockMovies;
        private MovieService movieService;

        public MovieServiceTest()
        {
            mockMovieContext = new Mock<MovieContext>();
            mockMovies = new Mock<DbSet<Movie>>();
            mockMovieContext.Setup(m => m.Movies).Returns(mockMovies.Object);
            movieService = new MovieService(mockMovieContext.Object);
        }

        [Fact]
        public void GetAllMovies_returns_all_movies()
        {
            var data = new List<Movie>
            {
                new Movie { Id=1, Title = "Terminator" },
                new Movie { Id=2, Title = "Dead Poet Society" },
                new Movie { Id=3, Title = "Death in Venice" },
                new Movie { Id=4, Title = "Evil Dead 2" }
            }.AsQueryable();

            mockMovies.As<IQueryable<Movie>>().Setup(m => m.Provider).Returns(data.Provider);
            mockMovies.As<IQueryable<Movie>>().Setup(m => m.Expression).Returns(data.Expression);
            mockMovies.As<IQueryable<Movie>>().Setup(m => m.ElementType).Returns(data.ElementType);
            mockMovies.As<IQueryable<Movie>>().Setup(m => m.GetEnumerator()).Returns((IEnumerator<Movie>) data.GetEnumerator());

            var resultSet = movieService.GetAllMovies();
            Assert.Equal(4, resultSet.Count());
        }
    }
}

Widzimy, że by użyć obiektów, które mają udawać nasz MovieContext należy je utworzyć z wykorzystaniem Moq właśnie, a następnie wypełnić danymi. Dlaczego listę filmów tworzę bezpośrednio w teście, a nie w konstruktorze? Dla innych testów klasy MovieService bardziej odpowiednie mogą być inne zestawy danych – będą tworzone wg potrzeb.

Tak przygotowany test po uruchomieniu oczywiście kończy się porażką – nie ma wszak nawet deklaracji klasy MovieService. Teraz nadchodzi moment, by ją dodać. Np. tak:

namespace VodSearcher.Services
{
    public class MovieService: IMovieService
    {
        private MovieContext movieContext;
        public MovieService(MovieContext _movieContext)
        {
            movieContext = _movieContext;
        }

        public IQueryable<Movie> GetAllMovies()
        {
            return movieContext.Movies;
        }
    }
}

Tutaj właśnie widać pominięcie warstwy, w której siedziałoby Repository – ono siedzi tu nadal jako movieContext (które pełni rolę UnitOfWork) i jego property Movies (która implementuje wzorzec Repository). Całość jest prostsza i czytelniejsza – przynajmniej dla mnie.

W podobny sposób rozszerzam klasę testów o kolejne testy i funkcje i po kolei je implementuję.

W następnym odcinku zajmiemy się utworzeniem rzeczywistej bazy danych – w podejściu Code First.

Marcin

4 Comments

  • Dawid K Kwiecień 4, 2016 at 3:25 pm

    No dobrze, tylko czemu chcesz w serwisie zwracać wszystkie filmy i to na dodatek jako IQueryable? To przecież jest wyciek szczegółów implementacji warstwy DAL w warstwie logiki biznesowej.

    Reply
    • Marcin Kwiecień 7, 2016 at 5:48 pm

      Dziękuję za uwagi :) Przykład był pierwszą próbą wykorzystania Entity Framework jako źródła danych bez sztucznej warstwy Repository/UnitOfWork. MovieService będzie z EF korzystał, ale udostępniał już tylko rzeczy oparte o ViewModels potrzebne kontrolerom/widokom, a nie modele danych. No i zamiast IQueryable będzie zwracał np. IList, a parametry filtrowania dostawał od kontrolera.

      Reply
  • brogowski Kwiecień 5, 2016 at 9:52 pm

    Słówko przestrogi z mojej strony.

    Jeżeli planujesz posiadać granicę pomiędzy aplikacją a warstwą bazodanową to odradzam stosowania modeli EF w interfejsach tworzących ów granicę.

    Zauważ iż aplikacja korzystając z IMovieService musi wiedzieć o zwracanym typie IQueryable.
    Jeżeli klasa Movie odzwierciedla tabelkę w SQLu (korzysta z niej EF) to cała aplikacja zostaje uzależniona od schematu tejże tableki. Struktura bazy danych wlewa się w aplikację.

    Nie mówię, że to źle – bo jeżeli korzystać będziesz tylko z bazy danych SQL i tego jednego schematu to jest to jak najbardziej w porządku. Tylko przestrzegam 😉

    PS: Polecam testować EF na bazie danych trzymanej w pamięci.
    https://github.com/tamasflamich/effort

    Reply
    • Marcin Kwiecień 7, 2016 at 5:48 pm

      Dzięki za przestrogę :) Na pewno będę starał się odseparować warstwę prezentacji od logiki biznesowej – i użyć osobnych modeli przekazywanych kontrolerom/widokom (jak napisałem wyżej w odpowiedzi na komentarz Dawida). Użycie Effort również mam w planach – opiszę wrażenia w osobnej notce :)

      Reply

Dodaj komentarz