Generic Repository ve Unit of Work Pattern, Entity Framework, Unit Testing, Autofac IoC Container ve ASP.NET MVC [Part 3]

Son 2 makalemdeki yaptığımız şeylere göz gezdirelim:

Entity Framework ile Generic Repository ve Unit Of Work İmplementasyonu

Autofac ve CRUD İşlemleri ile MVC Dependency Injection

Unit Testing:

SampleArch.Test projesini oluşturmuştuk. Şimdi mocking işlemleri için Moq kütüphanesini kullanacağız. Moq kullanmak için Package Manager Console ekranına Install-Package Moq satırını ekleyin ve çalıştırın. Bunu diğer tüm projelere referans olarak ekleyin.

Controller Test:

Controller test için Mock servis objesi ve controller methodları oluşturacağız. Basit bir örnek olması için CountryController test edeceğiz.

[TestClass]
  public class CountryControllerTest
  {
      private Mock<ICountryService> _countryServiceMock;
      CountryController objController;
      List<Country> listCountry;
      [TestInitialize]
      public void Initialize()
      {
          _countryServiceMock = new Mock<ICountryService>();
          objController = new CountryController(_countryServiceMock.Object);
          listCountry = new List<Country>() {
           new Country() { Id = 1, Name = "US" },
           new Country() { Id = 2, Name = "India" },
           new Country() { Id = 3, Name = "Russia" }
          };
      }
      [TestMethod]
      public void Country_Get_All()
      {
          //Arrange
          _countryServiceMock.Setup(x => x.GetAll()).Returns(listCountry);
          //Act
          var result = ((objController.Index() as ViewResult).Model) as List<Country>;
          //Assert
          Assert.AreEqual(result.Count, 3);
          Assert.AreEqual("US", result[0].Name);
          Assert.AreEqual("India", result[1].Name);
          Assert.AreEqual("Russia", result[2].Name);
      }
      [TestMethod]
      public void Valid_Country_Create()
      {
          //Arrange
          Country c = new Country() { Name = "test1"};
          //Act
          var result = (RedirectToRouteResult)objController.Create(c);
          //Assert
          _countryServiceMock.Verify(m => m.Create(c), Times.Once);
          Assert.AreEqual("Index", result.RouteValues["action"]);
         
      }
      [TestMethod]
      public void Invalid_Country_Create()
      {
          // Arrange
          Country c = new Country() { Name = ""};
          objController.ModelState.AddModelError("Error", "Something went wrong");
          //Act
          var result = (ViewResult)objController.Create(c);
          //Assert
          _countryServiceMock.Verify(m => m.Create(c), Times.Never);
          Assert.AreEqual("", result.ViewName);
      }
  }

Initialize: Mock servis objesi, controller ve diğer objeleri initialize eder.
Country_Get_All: Test controllerın index methodu.
Valid_Country_Create: Create actionunun Modelstate’i valid olduğu andaki test methodu.
Invalid_Country_Create: Create actionunun Modelstate’i invalid olduğu andaki yani hata fırlattığı andaki test methodu.

Service Testing:

Bunun için Mock repository ve diğer servis methodlarını kullanan objeleri oluşturacağız.

[TestClass]
  public class CountryServiceTest
  {
      private Mock<ICountryRepository> _mockRepository;
      private ICountryService _service;
      Mock<IUnitOfWork> _mockUnitWork;
      List<Country> listCountry;
      [TestInitialize]
      public void Initialize()
      {
          _mockRepository = new Mock<ICountryRepository>();
          _mockUnitWork = new Mock<IUnitOfWork>();
          _service = new CountryService(_mockUnitWork.Object, _mockRepository.Object);
          listCountry = new List<Country>() {
           new Country() { Id = 1, Name = "US" },
           new Country() { Id = 2, Name = "India" },
           new Country() { Id = 3, Name = "Russia" }
          };
      }
      [TestMethod]
      public void Country_Get_All()
      {
          //Arrange
          _mockRepository.Setup(x => x.GetAll()).Returns(listCountry);
          //Act
          List<Country> results = _service.GetAll() as List<Country>;
          //Assert
          Assert.IsNotNull(results);
          Assert.AreEqual(3, results.Count);
      }
      [TestMethod]
      public void Can_Add_Country()
      {
          //Arrange
          int Id = 1;
          Country emp = new Country() { Name = "UK" };
          _mockRepository.Setup(m => m.Add(emp)).Returns((Country e) =>
          {
              e.Id = Id;
              return e;
          });
         
          //Act
          _service.Create(emp);
          //Assert
          Assert.AreEqual(Id, emp.Id);
          _mockUnitWork.Verify(m => m.Commit(), Times.Once);
      }
  }

Initialize: Mock repository, mock unit of work ve servis objelerini initalize eder.
Country_Get_All:  GetAll methodunu test eder.
Can_Add_Country:  Create methodunu test eder.

Repository Testing:

It is less required to test repository because EF is already well tested.

Teste başlamadan önce testcontext oluşturalım ve şu classları ve kod satırlarını ekleyelim: TestContext.cs

public class TestContext : DbContext
  {
      public TestContext()
          : base("Name=TestContext")
      {
      }
      public TestContext(bool enableLazyLoading, bool enableProxyCreation)
          : base("Name=TestContext")
      {
          Configuration.ProxyCreationEnabled = enableProxyCreation;
          Configuration.LazyLoadingEnabled = enableLazyLoading;
      }
      public TestContext(DbConnection connection)
          : base(connection, true)
      {
          Configuration.LazyLoadingEnabled = false;
      }
      public DbSet<Person> Persons { get; set; }
      public DbSet<Country> Countries { get; set; }
      protected override void OnModelCreating(DbModelBuilder modelBuilder)
      {
          // Suppress code first model migration check         
          Database.SetInitializer<TestContext>(new AlwaysCreateInitializer());
          modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
          modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>();
          modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
          base.OnModelCreating(modelBuilder);
      }
      public void Seed(TestContext Context)
      {
          var listCountry = new List<Country>() {
           new Country() { Id = 1, Name = "US" },
           new Country() { Id = 2, Name = "India" },
           new Country() { Id = 3, Name = "Russia" }
          };
          Context.Countries.AddRange(listCountry);
          Context.SaveChanges();
      }
      public class DropCreateIfChangeInitializer : DropCreateDatabaseIfModelChanges<TestContext>
      {
          protected override void Seed(TestContext context)
          {
              context.Seed(context);
              base.Seed(context);
          }
      }
      public class CreateInitializer : CreateDatabaseIfNotExists<TestContext>
      {
          protected override void Seed(TestContext context)
          {
              context.Seed(context);
              base.Seed(context);
          }
      }
      public class AlwaysCreateInitializer : DropCreateDatabaseAlways<TestContext>
      {
          protected override void Seed(TestContext context)
          {
              context.Seed(context);
              base.Seed(context);
          }
      }
  }

1. Burada iki tip constructor bulunmakta. Bunlardan bir tanesi connectionstring kullanırken bir tanesi de DBConnection  nesnesini kullanıyor. Biz bu iki tipi de kullanacağız.
2. Datasource tekrardan oluşturulduğunda, recreate edildiğinde her defasında tekrardan initialize etmesi için AlwaysCreateInitializer Initializer nesneni kullanılıyorum.

1
Database.SetInitializer<TestContext>(new AlwaysCreateInitializer());

İhtiyacınıza göre DropCreateIfChangeInitializer,CreateInitializer initializer nesnelerini de kullanabilirsiniz.

Test için 2 yol daha var bunlar :
1. Memory database içinde kullanma
2. Diğer bir database’i kullanarak test etmek.

Biz iki yolu da kullanacağız.

1. Memory database İçinde Kullanma

Bunun için Effort kütüphanesini kullanacağız. EF6 için Effort kütüphanesini install etmek için Package Manager Console ekranına :

Install-Package Effort.EF6

kod satırını ekleyip çalıştırmamız yeterlidir.

[TestClass]
  public class CountryRepositoryTest
  {
      DbConnection connection;
      TestContext databaseContext;
      CountryRepository objRepo;
      [TestInitialize]
      public void Initialize()
      {
          connection = Effort.DbConnectionFactory.CreateTransient();
          databaseContext = new TestContext(connection);
          objRepo = new CountryRepository(databaseContext);
         
      }
      [TestMethod]
      public void Country_Repository_Get_ALL()
      {
          //Act
          var result = objRepo.GetAll().ToList();
          //Assert
          Assert.IsNotNull(result);
          Assert.AreEqual(3, result.Count);
          Assert.AreEqual("US", result[0].Name);
          Assert.AreEqual("India", result[1].Name);
          Assert.AreEqual("Russia", result[2].Name);
      }
      [TestMethod]
      public void Country_Repository_Create()
      {
          //Arrange
          Country c = new Country() {Name  = "UK" };
          //Act
          var result = objRepo.Add(c);
          databaseContext.SaveChanges();
          var lst = objRepo.GetAll().ToList();
          //Assert
          Assert.AreEqual(4, lst.Count);
          Assert.AreEqual("UK", lst.Last().Name);
      }
  }

Initialize: Effort, context ve repository nesnesini initialize etmek için.
Country_Repository_Get_ALL: Repositorynin GetAll methodunu check etmek için kullanılır.
Country_Repository_Create: Repositorynin Add methodunu check etmek için kullanılır.

2. Use another database for testing

Appconfig içerisine connectionstring ekleyelim.

1
2
3
<connectionStrings>
    <add name="TestContext" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=SampleArchTest;Integrated Security=SSPI;AttachDBFilename=E:\Project SA\Arch\SampleArch\SampleArch\SampleArch.Test\DB\SampleArchTest.mdf" providerName="System.Data.SqlClient"/>
  </connectionStrings>
[TestClass]
   public class CountryRepositoryTestWithDB
   {
    
       TestContext databaseContext;
       CountryRepository objRepo;
       [TestInitialize]
       public void Initialize()
       {
         
           databaseContext = new TestContext();
           objRepo = new CountryRepository(databaseContext);
          
       }
       [TestMethod]
       public void Country_Repository_Get_ALL()
       {
           //Act
           var result = objRepo.GetAll().ToList();
           //Assert
           Assert.IsNotNull(result);
           Assert.AreEqual(3, result.Count);
           Assert.AreEqual("US", result[0].Name);
           Assert.AreEqual("India", result[1].Name);
           Assert.AreEqual("Russia", result[2].Name);
       }
       [TestMethod]
       public void Country_Repository_Create()
       {
           //Arrange
           Country c = new Country() {Name  = "UK" };
           //Act
           var result = objRepo.Add(c);
           databaseContext.SaveChanges();
           var lst = objRepo.GetAll().ToList();
           //Assert
           Assert.AreEqual(4, lst.Count);
           Assert.AreEqual("UK", lst.Last().Name);
       }
   }

Burada kendi regular provider’ımız kullanıyoruz ve her defasında databasei tekrardan oluşturuyoruz.

unit-test

 

ÖZET:

Bu makalede uygulamamızın kodlarına dokunmadan farklı katmanlarının nasıl test edildiğini görmüş olduk.

İyi Çalışmalar.

Reklamlar

Generic Repository ve Unit of Work Pattern, Entity Framework, Unit Testing, Autofac IoC Container ve ASP.NET MVC [Part 2]

Bir önceki makalemde model, servis ve repository katmanları oluşturdum. Bu makalemde ise oluşturduğum katmanlarla CRUD işlemlerini yapacağız ve Autofac IoC Container kuracağım.

Setup AutoFac:

Mvc projesi içinde Package Manager Console ekranındaki command satırına şu komutu yazın:

Install-Package Autofac.Mvc5

Ayrıca bu komut Autofac’ı da install edecektir.

Autofac kurulumu ile classlarımızı da register etmeye başlayabiliriz. Modules adında bir folder oluşturalım ve içine şu dosyaları alsın:

RepositoryModule.cs

public class RepositoryModule : Autofac.Module
   {
       protected override void Load(ContainerBuilder builder)
       {
           builder.RegisterAssemblyTypes(Assembly.Load("SampleArch.Repository"))
                  .Where(t => t.Name.EndsWith("Repository"))
                  .AsImplementedInterfaces()
                 .InstancePerLifetimeScope();
       }
   }

Bu işlem Autofac içindeki bütün classların sonuna “Repository” getirerek classları register edecektir.ServiceModule.cs

public class ServiceModule : Autofac.Module
  {
      protected override void Load(ContainerBuilder builder)
      {
          builder.RegisterAssemblyTypes(Assembly.Load("SampleArch.Service"))
                    .Where(t => t.Name.EndsWith("Service"))
                    .AsImplementedInterfaces()
                    .InstancePerLifetimeScope();
      }
  }

Bu işlem Autofac içindeki bütün classların sonuna “Service” getirerek classları register edecektir.

EFModule.cs

public class EFModule : Autofac.Module
    {
        protected override void Load(ContainerBuilder builder)
        {
            builder.RegisterModule(new RepositoryModule());
            builder.RegisterType(typeof(SampleArchContext)).As(typeof(DbContext)).InstancePerLifetimeScope();
            builder.RegisterType(typeof(UnitOfWork)).As(typeof(IUnitOfWork)).InstancePerRequest();        
        }
    }

Global.asax dosyası içindeki Application_Start methodu içerisine bazı satırlar ekleyelim:

//Autofac Configuration
          var builder = new Autofac.ContainerBuilder();
          builder.RegisterControllers(typeof(MvcApplication).Assembly).PropertiesAutowired();
          builder.RegisterModule(new RepositoryModule());
          builder.RegisterModule(new ServiceModule());
          builder.RegisterModule(new EFModule());
          var container = builder.Build();
          DependencyResolver.SetResolver(new AutofacDependencyResolver(container));

Ayrıca şu namespaceleri de eklememiz gerekmektedir.

using Autofac.Integration.Mvc;
using Autofac;

Artık service objesini controller classları içerisine enjekte edebiliriz.

Country CRUD Implementasyonu:

Mvc projesinin içerisine Model ve Servis projelerinin referanslarını ekleyelim. Yeni bir controller ekleyip optionunu “MVC 5 Controller with Views using Entity Framework” seçelim. Name : CountryController,  Select Model : “Country“, DataContext:SampleArchContext, Set “Generate Views” true, Click Add deyip controller ekleme işini tamamlayalım. Böylece EF kullanarak action ve viewları implement etmiş olduk. Şimdi controllerımızı service katmanına göre modifiye edelim:

public class CountryController : Controller
   {
       //initialize service object
       ICountryService _CountryService;
       public CountryController(ICountryService CountryService)
       {
           _CountryService = CountryService;
       }
       //
       // GET: /Country/
       public ActionResult Index()
       {
           return View(_CountryService.GetAll());
       }      
       //
       // GET: /Country/Create
       public ActionResult Create()
       {
           return View();
       }
       //
       // POST: /Country/Create
       [HttpPost]
       [ValidateAntiForgeryToken]
       public ActionResult Create(Country country)
       {
           // TODO: Add insert logic here
           if (ModelState.IsValid)
           {
               _CountryService.Create(country);
               return RedirectToAction("Index");
           }
           return View(country);
       }
       //
       // GET: /Country/Edit/5
       public ActionResult Edit(int id)
       {           
           Country country = _CountryService.GetById(id);
           if (country == null)
           {
               return HttpNotFound();
           }
           return View(country);
       }
       //
       // POST: /Country/Edit
       [HttpPost]
       public ActionResult Edit(Country country)
       {
           if (ModelState.IsValid)
           {
               _CountryService.Update(country);
               return RedirectToAction("Index");
           }
           return View(country);
       }
       //
       // GET: /Country/Delete/5
       public ActionResult Delete(int id)
       {
           Country country = _CountryService.GetById(id);
           if (country == null)
           {
               return HttpNotFound();
           }
           return View(country);
       }
       //
       // POST: /Country/Delete/5
       [HttpPost, ActionName("Delete")]
       [ValidateAntiForgeryToken]
       public ActionResult Delete(int id, FormCollection data)
       {
           Country country = _CountryService.GetById(id);
           _CountryService.Delete(country);
           return RedirectToAction("Index");
       }
   }

Yukarıda gördüğünüz gibi Controller classımızın constructoruna servis katmanından gelen ICountryService objesini enjekte ettik. Constructor Dependency injection controller classımız teste uygun hale getirir.

Şimdi relationship içeren diğer CRUD işlemlerinin gözden geçirelim :

Person CRUD Implementasyonu :

CountryControllara benzer bir şekilde PersonController ekliyoruz. Aşağıda Entity Framework yapısı kullanarak Country dropdownList oluşturacağız. CountryController’da olduğu gibi servis katmanı ile PersonController classını da modifiye edelim.

public class PersonController : Controller
  {
      IPersonService _PersonService;
      ICountryService _CountryService;
      public PersonController(IPersonService PersonService, ICountryService CountryService)
      {
          _PersonService = PersonService;
          _CountryService = CountryService;
      }
      // GET: /Person/
      public ActionResult Index()
      {
          return View(_PersonService.GetAll());
      }
      // GET: /Person/Details/5
      public ActionResult Details(long? id)
      {
          if (id == null)
          {
              return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
          }
          Person person = _PersonService.GetById(id.Value);
          if (person == null)
          {
              return HttpNotFound();
          }
          return View(person);
      }
      // GET: /Person/Create
      public ActionResult Create()
      {
          ViewBag.CountryId = new SelectList(_CountryService.GetAll(), "Id", "Name");
          return View();
      }
      // POST: /Person/Create
      // To protect from overposting attacks, please enable the specific properties you want to bind to, for
      // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
      [HttpPost]
      [ValidateAntiForgeryToken]
      public ActionResult Create([Bind(Include = "Id,Name,Phone,Address,State,CountryId")] Person person)
      {
          if (ModelState.IsValid)
          {
              _PersonService.Create(person);
              return RedirectToAction("Index");
          }
          ViewBag.CountryId = new SelectList(_CountryService.GetAll(), "Id", "Name", person.CountryId);
          return View(person);
      }
      // GET: /Person/Edit/5
      public ActionResult Edit(long? id)
      {
          if (id == null)
          {
              return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
          }
          Person person = _PersonService.GetById(id.Value);
          if (person == null)
          {
              return HttpNotFound();
          }
          ViewBag.CountryId = new SelectList(_CountryService.GetAll(), "Id", "Name", person.CountryId);
          return View(person);
      }
      // POST: /Person/Edit/5
      // To protect from overposting attacks, please enable the specific properties you want to bind to, for
      // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
      [HttpPost]
      [ValidateAntiForgeryToken]
      public ActionResult Edit([Bind(Include = "Id,Name,Phone,Address,State,CountryId")] Person person)
      {
          if (ModelState.IsValid)
          {
              _PersonService.Update(person);
              return RedirectToAction("Index");
          }
          ViewBag.CountryId = new SelectList(_CountryService.GetAll(), "Id", "Name", person.CountryId);
          return View(person);
      }
      // GET: /Person/Delete/5
      public ActionResult Delete(long? id)
      {
          if (id == null)
          {
              return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
          }
          Person person = _PersonService.GetById(id.Value);
          if (person == null)
          {
              return HttpNotFound();
          }
          return View(person);
      }
      // POST: /Person/Delete/5
      [HttpPost, ActionName("Delete")]
      [ValidateAntiForgeryToken]
      public ActionResult DeleteConfirmed(long id)
      {
          Person person = _PersonService.GetById(id);
          _PersonService.Delete(person);
          return RedirectToAction("Index");
      }
  }

Yukarda da gördüğümüz bu controllerımızın constructoruna üzere hem CountryService , hem de PersonService nesnesini enjekte ettik. Çünkü controllerımızda her iki servisimize de erişmemiz gerekti. CountryService, ülkelerin dropdown seçimi için, PersonService ise CRUD işlemleri için gerekliydi.

Sonuç olarak view ekranımız şu şekilde olacaktır:

crud-autofac

ÖZET:

Bu makalemde Autofac kurulumunu ve Entity Framework ve Service Katmanları ile Generic Repository ve Unit of Work Pattern üzerinden base alarak CRUD işlemlerinin nasıl yapıldığını öğrendik. Diğer makalemde farklı katmanların Unit Test implementasyonunu anlatacağım.

İyi Çalışmalar.

Unit Test Json Türündeki Verilen Testi

Action methoddan Json türünde veri döndürmemiz gereken durumlar olabilir. Bu durumların testleri yapılırken actiondan alınan veriler orjinal türüne dönüştürülüp olması gereken değerle, özellik bazında değerler karşılaştırılarak sonucun doğruluğu kontrol edilebilir.

Controllerda Json tipindeki Action Methodu:

public JsonResult JsonUrun()
        {
            var urun = new Urun
            {
                Id = 1,
                Ad = "Bilgisayar",
                Fiyat = 2000
            };
            return Json(urun);
        }

Unit Testteki test methodumuz:

[TestMethod]
        public void UrunJson_Test()
        {
            HomeController controller = new HomeController();

            var urun = controller.JsonUrun();
            var sonuc = urun.Data as Urun;
            var olmasiGereken = new Urun
            {
                Id = 1,
                Ad = "Bilgisayar",
                Fiyat = 2000
            };
            Assert.AreEqual(sonuc.Id, olmasiGereken.Id);
            Assert.AreEqual(sonuc.Ad, olmasiGereken.Ad);
            Assert.AreEqual(sonuc.Fiyat, olmasiGereken.Fiyat);
        }

Bu şekilde test methodu içerisinde tanımladığınız bir nesneyle, controllerdan elde ettiğiniz json tipindeki nesneyi birbiriyle eşitleyerek karşılaştırmasını yapabilirsiniz.

Bir Solutiondaki Unit Test Projesinde Test Methodu Çalışmadan Önce Kod Çalıştırma

Birden fazla test methodunda ortak kullanılan değişkenlerin oluşturulması ya da test methodu çalışmadan önce çalıştırılması gereken farklı methodlar olabilir. Bu durumlarda TestInitialize attribute’u kullanılır.

Örneğin HomeController sınıfında yapılan Unit Testlerin bulunduğu test sınıfındaki tüm test methodlarında HomeController türünden bir değişken oluşturulup bu değişken üzerinden testler yapılabilir. Her test methodu içinde bu değişkeni tekrar tekrar oluşturmak yerine test methodu çalışmadan hemen önce bir kere bu değişkeni tanımlayıp tüm methodlarda kullanmak daha mantıklı olacaktır.

[TestClass]
public class HomeControllerTest
{
HomeController controller;

[TestInitialize]
 public void Initialize()
 {
 controller = new HomeController();
 }

[TestMethod]
public void Index()
{
ViewResult result = controller.Index() as ViewResult;

string isim = result.ViewBag.Isim;

Assert.IsNotNull(result);
}

[TestMethod]
public void About()
{
ViewResult result = controller.About() as ViewResult;
Assert.AreEqual("Your application description page.", result.ViewBag.Message);
}

}

Kodlamayı tamamladıktan sonra sonra Test Explorer üzerinden 3 methodu da çalıştırdığınızda Initialize methoduna breakpoint eklerseniz her methoddan önce bir kez bu methodun çalıştığını göreceksiniz.