Not
Bu sayfaya erişim yetkilendirme gerektiriyor. Oturum açmayı veya dizinleri değiştirmeyi deneyebilirsiniz.
Bu sayfaya erişim yetkilendirme gerektiriyor. Dizinleri değiştirmeyi deneyebilirsiniz.
Scott Allen
Yayımlanma Tarihi: Mayıs 2010
Introduction
Bu teknik incelemede, ADO.NET Entity Framework 4.0 ve Visual Studio 2010 ile test edilebilir kodun nasıl yazıldığı açıklanır ve gösterilmektedir. Bu makale, test temelli tasarım (TDD) veya davranış temelli tasarım (BDD) gibi belirli bir test metodolojisine odaklanmayı denemez. Bunun yerine bu makale, ADO.NET Entity Framework kullanan ancak otomatik bir şekilde yalıtmak ve test etmek için kolay kalan kodun nasıl yazılacağına odaklanacaktır. Veri erişim senaryolarında test yapmayı kolaylaştıran yaygın tasarım desenlerini gözden geçirecek ve çerçeveyi kullanırken bu desenlerin nasıl uygulanacağını göreceğiz. Bu özelliklerin test edilebilir kodda nasıl çalışabileceğini görmek için çerçevenin belirli özelliklerine de göz atacağız.
Test Edilebilir Kod nedir?
Otomatik birim testlerini kullanarak bir yazılım parçasını doğrulama özelliği birçok istenen avantaj sunar. Herkes iyi testlerin bir uygulamadaki yazılım hatalarının sayısını azaltacağını ve uygulamanın kalitesini artıracağını bilir- ancak birim testlerinin yerinde olması yalnızca hataları bulmanın çok ötesine geçer.
İyi bir birim testi paketi, geliştirme ekibinin zaman kazanmasını ve oluşturdukları yazılımın denetimini sürdürmesini sağlar. Bir ekip mevcut kodda değişiklik yapabilir, yeni gereksinimleri karşılamak için yazılımı yeniden düzenleyebilir, yeniden tasarlayabilir ve yeniden yapılandırabilir ve test paketinin uygulamanın davranışını doğrulayabildiğini bilerek uygulamaya yeni bileşenler ekleyebilir. Birim testleri, değişikliği kolaylaştırmak ve karmaşıklık arttıkça yazılımın sürdürülebilirliğini korumak için hızlı geri bildirim döngüsünün bir parçasıdır.
Ancak birim testinin bir bedeli vardır. Bir ekibin birim testleri oluşturmak ve bakımını yapmak için zaman harcaması gerekir. Bu testleri oluşturmak için gereken çaba miktarı, temel alınan yazılımın test edilebilirliğiyle doğrudan ilgilidir. Yazılım ne kadar kolay test edilir? Test edilebilirliği göz önünde bulundurarak yazılım tasarlayan bir ekip, test edilemeyen yazılımlarla çalışan ekipten daha hızlı etkili testler oluşturur.
Microsoft, test edilebilirliği göz önünde bulundurarak ADO.NET Entity Framework 4.0 'ı (EF4) tasarladı. Bu, geliştiricilerin çerçeve kodunun kendisine karşı birim testleri yazacağı anlamına gelmez. Bunun yerine, EF4 için test edilebilirlik hedefleri, çerçevenin üzerinde derlenebilen test edilebilir kod oluşturmayı kolaylaştırır. Belirli örneklere bakmadan önce test edilebilir kodun niteliklerini anlamak faydalı olabilir.
Test Edilebilir Kodun Nitelikleri
Test etmek kolay olan kod her zaman en az iki özellik sergiler. İlk olarak, test edilebilir kodu gözlemlemek kolaydır. Bazı girişler göz önüne alındığında, kodun çıkışını gözlemlemek kolay olmalıdır. Örneğin, yöntem bir hesaplamanın sonucunu doğrudan döndürdüğünden aşağıdaki yöntemin test edilmesi kolaydır.
public int Add(int x, int y) {
return x + y;
}
Yöntemin hesaplanan değeri bir ağ yuvasına, veritabanı tablosuna veya aşağıdaki kod gibi bir dosyaya yazması durumunda yöntemin test edilmesi zordur. Testin değeri almak için ek çalışma yapması gerekir.
public void AddAndSaveToFile(int x, int y) {
var results = string.Format("The answer is {0}", x + y);
File.WriteAllText("results.txt", results);
}
İkinci olarak, test edilebilir kodu yalıtmak kolaydır. Test edilebilir kodun kötü bir örneği olarak aşağıdaki sahte kodu kullanalım.
public int ComputePolicyValue(InsurancePolicy policy) {
using (var connection = new SqlConnection("dbConnection"))
using (var command = new SqlCommand(query, connection)) {
// business calculations omitted ...
if (totalValue > notificationThreshold) {
var message = new MailMessage();
message.Subject = "Warning!";
var client = new SmtpClient();
client.Send(message);
}
}
return totalValue;
}
Yöntemi kolayca gözlemleyebiliriz. Bir sigorta poliçesi geçirebilir ve dönüş değerinin beklenen sonuçla eşleşip eşleşmedığını doğrulayabiliriz. Ancak, yöntemini test etmek için doğru şemayla yüklenmiş bir veritabanımız olması ve yöntemin e-posta göndermeye çalışması durumunda SMTP sunucusunu yapılandırmamız gerekir.
Birim testi yalnızca yöntemin içindeki hesaplama mantığını doğrulamak istiyor, ancak e-posta sunucusu çevrimdışı olduğundan veya veritabanı sunucusu taşındığından test başarısız olabilir. Bu hataların ikisi de testin doğrulamak istediği davranışla ilişkili değil. Davranışı yalıtmak zordur.
Test edilebilir kod yazmaya çalışan yazılım geliştiricileri genellikle yazdıkları koddaki endişelerin ayrımını sürdürmek için çaba gösterir. Yukarıdaki yöntem iş hesaplamalarına odaklanmalı ve veritabanı ve e-posta uygulama ayrıntılarını diğer bileşenlere devretmelidir. Robert C. Martin buna Tek Sorumluluk İlkesi diyor. Bir nesne, bir ilkenin değerini hesaplama gibi tek ve dar bir sorumluluğu kapsüllemelidir. Diğer tüm veritabanı ve bildirim çalışmaları başka bir nesnenin sorumluluğunda olmalıdır. Tek bir göreve odaklandığından bu şekilde yazılmış kodu yalıtmak daha kolaydır.
.NET'te Tek Sorumluluk İlkesi'ni izlememiz ve yalıtıma ulaşmamız gereken soyutlamalar vardır. Arabirim tanımlarını kullanabilir ve kodu somut bir tür yerine arabirim soyutlamasını kullanmaya zorlayabiliriz. Bu makalenin ilerleyen bölümlerinde, yukarıda gösterilen kötü örnek gibi bir yöntemin veritabanıyla konuşacak gibi görünen arabirimlerle nasıl çalışabileceğini göreceğiz. Ancak test zamanında, veritabanıyla konuşmayan ancak verileri bellekte tutan sahte bir uygulamanın yerine kullanabiliriz. Bu sahte uygulama, kodu veri erişim kodu veya veritabanı yapılandırmasındaki ilgisiz sorunlardan yalıtacaktır.
Yalıtımın ek avantajları vardır. Son yöntemdeki iş hesaplamasının yürütülmesi yalnızca birkaç milisaniye sürmelidir, ancak kod ağ çevresinde atlayıp çeşitli sunucularla konuştuğunda testin kendisi birkaç saniye boyunca çalıştırılabilir. Birim testleri küçük değişiklikleri kolaylaştırmak için hızlı çalıştırılmalıdır. Testle ilgili olmayan bir bileşende sorun olduğundan birim testleri de yinelenebilir ve başarısız olmamalıdır. Kolayca gözlemlenebilen ve yalıtılacak kod yazma, geliştiricilerin kod için test yazmak için daha kolay zamanları olacağı, testlerin yürütülmesini beklemeye daha az zaman ayıracağı ve daha da önemlisi var olmayan hataları izlemeye daha az zaman ayıracağı anlamına gelir.
Test etme avantajlarını takdir edebilir ve test edilebilir kodun sergilediğini niteliklerini anlayabilirsiniz. Gözlemlenebilir ve yalıtılması kolay kalırken verileri veritabanına kaydetmek için EF4 ile çalışan kod yazma işlemine değinmek üzereyiz, ancak önce veri erişimi için test edilebilir tasarımları tartışmak üzere odağımızı daraltacağız.
Veri Kalıcılığı için Tasarım Desenleri
Daha önce sunulan her iki kötü örneğin de çok fazla sorumluluğu vardı. İlk hatalı örnek bir hesaplama yapmak ve bir dosyaya yazmak zorunda kaldı. İkinci kötü örneğin veritabanındaki verileri okuması , iş hesaplaması yapması ve e-posta göndermesi gerekiyordu. Endişeleri ayıran ve sorumluluğu diğer bileşenlere devreden daha küçük yöntemler tasarlayarak test edilebilir kod yazma konusunda büyük adımlar atmış olursunuz. Amaç, küçük ve odaklanmış soyutlamalardan eylemler oluşturarak işlevsellik oluşturmaktır.
Veri kalıcılığı söz konusu olduğunda, aradığımız küçük ve odaklanmış soyutlamalar o kadar yaygın ki tasarım desenleri olarak belgelenmiştir. Martin Fowler'ın Enterprise Application Architecture Patterns (Kurumsal Uygulama Mimarisi Desenleri) kitabı, bu desenleri baskıda açıklayan ilk çalışmaydı. Bu ADO.NET Entity Framework'ün bu desenleri nasıl uyguladığını ve bunlarla nasıl çalıştığını göstermeden önce aşağıdaki bölümlerde bu desenlerin kısa bir açıklamasını sağlayacağız.
Depo Düzeni
Fowler, bir deponun "etki alanı nesnelerine erişmek için koleksiyon benzeri bir arabirim kullanarak etki alanı ve veri eşleme katmanları arasında aracılık eder" diyor. Depo düzeninin amacı, kodu veri erişiminin en düşük düzeyinden yalıtmaktır ve daha önce gördüğümüz gibi yalıtım test edilebilirlik için gerekli bir özelliktir.
Yalıtımın anahtarı, deponun nesneleri koleksiyon benzeri bir arabirim kullanarak açığa çıkarma şeklidir. Depoyu kullanmak için yazdığınız mantık, deponun istediğiniz nesneleri nasıl oluşturacağı hakkında hiçbir fikri yoktur. Depo bir veritabanıyla konuşabilir veya yalnızca bellek içi bir koleksiyondaki nesneleri döndürebilir. Kodunuzun bilmesi gereken tek şey, deponun koleksiyonu korumak için göründüğü ve koleksiyondan nesneleri alabildiğiniz, ekleyebileceğiniz ve silebileceğinizdir.
Mevcut .NET uygulamalarında somut bir depo genellikle aşağıdaki gibi genel bir arabirimden devralınır:
public interface IRepository<T> {
IEnumerable<T> FindAll();
IEnumerable<T> FindBy(Expression<Func\<T, bool>> predicate);
T FindById(int id);
void Add(T newEntity);
void Remove(T entity);
}
EF4 için bir uygulama sağladığımızda arabirim tanımında birkaç değişiklik yapacağız, ancak temel kavram aynı kalır. Kod, bir varlığı birincil anahtar değerine göre almak, bir koşulun değerlendirmesine göre varlık koleksiyonunu almak veya yalnızca kullanılabilir tüm varlıkları almak için bu arabirimi uygulayan somut bir depo kullanabilir. Kod ayrıca depo arabirimi aracılığıyla varlık ekleyebilir ve kaldırabilir.
Çalışan nesnelerinden oluşan bir IRepository verildiğinde, yazılım aşağıdaki işlemleri gerçekleştirebilir.
var employeesNamedScott =
repository
.FindBy(e => e.Name == "Scott")
.OrderBy(e => e.HireDate);
var firstEmployee = repository.FindById(1);
var newEmployee = new Employee() {/*... */};
repository.Add(newEmployee);
Kod bir arabirim (IRepository of Employee) kullandığından, kodu arabirimin farklı uygulamalarıyla sağlayabiliriz. Uygulamalardan biri EF4 tarafından yedeklenen ve nesneleri Microsoft SQL Server veritabanında kalıcı hale getiren bir uygulama olabilir. Farklı bir uygulama (test sırasında kullandığımız uygulama), çalışan nesnelerinin bellek içi listesi tarafından yedeklenebilir. Arabirim, kodda yalıtım elde etmeye yardımcı olur.
IRepository<T> arabiriminin Kaydetme işlemini kullanıma sunmadığını göreceksiniz. Mevcut nesneleri nasıl güncelleştirebiliriz? Kaydet işlemini içeren IRepository tanımlarıyla karşılaşabilirsiniz ve bu depoların uygulamalarının bir nesneyi veritabanında hemen kalıcı hale gelmesi gerekir. Ancak, birçok uygulamada nesneleri tek tek kalıcı hale getirmek istemeyiz. Bunun yerine, farklı depolardaki nesneleri hayata geçirmek, bu nesneleri bir iş etkinliğinin parçası olarak değiştirmek ve ardından tüm nesneleri tek bir atomik işlemin parçası olarak kalıcı hale getirmek istiyoruz. Neyse ki, bu tür davranışlara izin veren bir desen vardır.
İş Birimi Düzeni
Fowler, bir çalışma biriminin "bir iş işleminden etkilenen nesnelerin listesini tutacağını ve değişikliklerin yazılıp yazıldığını ve eşzamanlılık sorunlarının çözümünü koordine edeceği" görüşünde. Bir depodan hayata geçirdiğimiz nesnelerdeki değişiklikleri izlemek ve iş birimine değişiklikleri işlemesini istediğimizde nesnelerde yaptığımız değişiklikleri kalıcı hale getirmek iş biriminin sorumluluğundadır. Tüm depolara eklediğimiz yeni nesneleri alarak veritabanına yerleştirmek ve silme işlemlerini yönetmek de iş biriminin sorumluluğundadır.
ADO.NET DataSets ile herhangi bir iş yaptıysanız, iş düzeni birimi hakkında zaten bilgi sahibi olursunuz. ADO.NET DataSets, DataRow nesnelerinin güncelleştirmelerini, silmelerini ve eklenmesini izleme yeteneğine sahipti ve (TableAdapter'ın yardımıyla) veritabanındaki tüm değişikliklerimizi mutabık kılabilirdi. Ancak DataSet nesneleri, temel alınan veritabanının bağlantısı kesilmiş bir alt kümesini modeller. İş düzeni birimi aynı davranışı sergiler, ancak veri erişim kodundan yalıtılmış ve veritabanının farkında olmayan iş nesneleri ve etki alanı nesneleriyle çalışır.
.NET kodundaki çalışma birimini modellemeye yönelik bir soyutlama aşağıdaki gibi görünebilir:
public interface IUnitOfWork {
IRepository<Employee> Employees { get; }
IRepository<Order> Orders { get; }
IRepository<Customer> Customers { get; }
void Commit();
}
Çalışma biriminde veri havuzu referanslarını erişilebilir hale getirerek, tek bir çalışma birimi nesnesinin bir işlem sırasında oluşturulmuş tüm varlıkları izleyebilmesini sağlayabiliriz. Gerçek bir iş birimi için Commit yönteminin uygulanması, bellek içi değişiklikleri veritabanıyla mutabık hale getirmek için tüm sihrin gerçekleştiği yerdir.
IUnitOfWork başvurusu göz önüne alındığında kod, bir veya daha fazla depodan alınan iş nesnelerde değişiklik yapabilir ve atomik Commit işlemini kullanarak tüm değişiklikleri kaydedebilir.
var firstEmployee = unitofWork.Employees.FindById(1);
var firstCustomer = unitofWork.Customers.FindById(1);
firstEmployee.Name = "Alex";
firstCustomer.Name = "Christopher";
unitofWork.Commit();
Gecikmeli Yük Deseni
Fowler, "ihtiyacınız olan tüm verileri içermeyen ancak nasıl edinildiğini bilen bir nesne" tanımlamak için gecikmeli yük adını kullanır. Saydam gecikmeli yükleme, test edilebilir iş kodu yazarken ve ilişkisel bir veritabanıyla çalışırken sahip olunan önemli bir özelliktir. Örnek olarak aşağıdaki kodu göz önünde bulundurun.
var employee = repository.FindById(id);
// ... and later ...
foreach(var timeCard in employee.TimeCards) {
// .. manipulate the timeCard
}
TimeCards koleksiyonu nasıl doldurulur? İki olası yanıt vardır. Bir yanıt, çalışan deposunun bir çalışanı getirmesi istendiğinde, çalışanın ilişkili zaman kartı bilgileriyle birlikte her ikisini de almak için bir sorgu oluşturmasıdır. İlişkisel veritabanlarında bu genellikle JOIN yan tümcesine sahip bir sorgu gerektirir ve uygulamanın ihtiyaç duyduğundan daha fazla bilgi alınmasına neden olabilir. Uygulamanın TimeCards özelliğine hiçbir zaman dokunması gerekmediyse ne olur?
İkinci bir yanıt, TimeCards özelliğini "isteğe bağlı" yüklemektir. Kod zaman kartı bilgilerini almak için özel API'leri çağırmadığından bu gecikmeli yükleme örtük ve iş mantığına saydamdır. Kod, gerektiğinde zaman kartı bilgilerinin mevcut olduğunu varsayar. Gecikmeli yükleme, genellikle yöntem çağrılarının çalışma zamanındaki engellenmesi gibi bazı gizemli işlemler içerir. Yakalama kodu, veritabanı ile iletişim kurmaktan ve zaman kartı bilgilerini alırken iş mantığını serbest bırakmaktan sorumludur. Bu tembel yükleme tekniği, iş kodunun kendisini veri alma işlemlerinden yalıtmasını ve daha test edilebilir kodla sonuçlanmasını sağlar.
Gecikmeli yüklemenin dezavantajı, bir uygulamanın zaman kartı bilgilerine ihtiyacı olduğunda kodun ek bir sorgu yürütmesidir. Bu birçok uygulama için sorun teşkil etmese de, performans hassasiyeti olan uygulamalar veya bir dizi çalışan nesneleri üzerinde döngü kurup, döngünün her adımında zaman kartlarını almak için sorgu yürüten uygulamalar (sıklıkla N+1 sorgu problemi olarak bilinen), gecikmeli yükleme performansı düşürebilir. Bu senaryolarda bir uygulama, zaman kartı bilgilerini mümkün olan en verimli şekilde hevesle yüklemek isteyebilir.
Neyse ki, sonraki bölüme geçip bu desenleri uygularken EF4'in hem örtük yavaş yükleri hem de verimli istekli yükleri nasıl desteklediğini göreceğiz.
Entity Framework ile Desenleri Uygulama
İyi haber, son bölümde açıkladığımız tüm tasarım desenlerinin EF4 ile kolayca uygulanabilecek olmasıdır. Çalışanları ve ilişkili zaman kartı bilgilerini düzenlemek ve görüntülemek için basit bir ASP.NET MVC uygulaması kullanacağımızı göstermek için. Başlangıç olarak aşağıdaki "düz eski CLR nesneleri" (POCO' lar) kullanacağız.
public class Employee {
public int Id { get; set; }
public string Name { get; set; }
public DateTime HireDate { get; set; }
public ICollection<TimeCard> TimeCards { get; set; }
}
public class TimeCard {
public int Id { get; set; }
public int Hours { get; set; }
public DateTime EffectiveDate { get; set; }
}
EF4'ün farklı yaklaşımlarını ve özelliklerini incelediğimizde bu sınıf tanımları biraz değişecektir, ancak amaç bu sınıfları mümkün olduğunca kalıcılık bilgisiz (PI) tutmaktır. PI nesnesi, tuttuğu durumun bir veritabanında olup olmadığını ve nasıl olduğunu bilmez. PI ve POCO'lar test edilebilir yazılımlarla el ele gider. POCO yaklaşımı kullanan nesneler veritabanı olmadan çalışabildiğinden daha az kısıtlanmış, daha esnek ve test etmek daha kolaydır.
POCO'lar hazır durumda olduğu için Visual Studio'da bir Varlık Veri Modeli (EDM) oluşturabiliriz (bkz. şekil 1). Varlıklarımız için kod oluşturmak için EDM'yi kullanmayacağız. Bunun yerine, el ile sevgiyle oluşturduğumuz varlıkları kullanmak istiyoruz. EDM'yi yalnızca veritabanı şemamızı oluşturmak ve EF4'in nesneleri veritabanına eşlemek için ihtiyaç duyduğu meta verileri sağlamak için kullanacağız.
Şekil 1
Not: Önce EDM modelini geliştirmek istiyorsanız, EDM'den temiz, POCO kodu oluşturmak mümkündür. Bunu, Veri Programlama ekibi tarafından sağlanan bir Visual Studio 2010 uzantısıyla yapabilirsiniz. Uzantıyı indirmek için, Visual Studio'daki Araçlar menüsünden Uzantı Yöneticisi'ni başlatın ve çevrimiçi şablon galerisinde "POCO" araması yapın (Bkz. Şekil 2). EF için kullanılabilen birkaç POCO şablonu vardır. Şablonu kullanma hakkında daha fazla bilgi için bkz. " İzlenecek yol: Entity Framework için POCO Şablonu".
Şekil 2
Bu POCO başlangıç noktasından test edilebilir koda yönelik iki farklı yaklaşımı keşfedeceğiz. İş ve depo birimlerini uygulamak için Entity Framework API'sinden soyutlamalardan yararlandığı için EF yaklaşımını adlandırdığım ilk yaklaşım. İkinci yaklaşımda kendi özel depo soyutlamalarımızı oluşturacak ve ardından her yaklaşımın avantajlarını ve dezavantajlarını göreceğiz. EF yaklaşımını keşfederek başlayacağız.
EF Merkezli Uygulama
bir ASP.NET MVC projesinden aşağıdaki denetleyici eylemini göz önünde bulundurun. Eylem, bir Çalışan nesnesi alır ve çalışanın ayrıntılı görünümünü göstermek için bir sonuç döndürür.
public ViewResult Details(int id) {
var employee = _unitOfWork.Employees
.Single(e => e.Id == id);
return View(employee);
}
Kod test edilebilir mi? Eylemin davranışını doğrulamak için ihtiyacımız olan en az iki test vardır. İlk olarak eylemin doğru görünümü (kolay bir test) döndürdüğünden emin olmak istiyoruz. Ayrıca eylemin doğru çalışanı aldığından emin olmak için bir test yazmak ve veritabanını sorgulamak için kod yürütmeden bunu yapmak istiyoruz. Test altındaki kodu yalıtmak istediğimizi unutmayın. Yalıtım, veri erişim kodu veya veritabanı yapılandırmasındaki bir hata nedeniyle testin başarısız olmamasını sağlar. Test başarısız olursa, alt düzey bir sistem bileşeninde değil denetleyici mantığında bir hata olduğunu anlarız.
Yalıtım elde etmek için depolar ve çalışma birimleri için daha önce sunduğumuz arabirimler gibi bazı soyutlamalara ihtiyacımız olacaktır. Depo düzeninin, etki alanı nesneleriyle veri eşleme katmanı arasında aracılık yapmak için tasarlandığını unutmayın. Bu senaryoda EF4, veri eşleme katmanıdır ve zaten IObjectSet<T> adlı bir depo benzeri soyutlama sağlar (System.Data.Objects ad alanından). Arabirim tanımı aşağıdaki gibi görünür.
public interface IObjectSet<TEntity> :
IQueryable<TEntity>,
IEnumerable<TEntity>,
IQueryable,
IEnumerable
where TEntity : class
{
void AddObject(TEntity entity);
void Attach(TEntity entity);
void DeleteObject(TEntity entity);
void Detach(TEntity entity);
}
IObjectSet<T> , bir nesne koleksiyonuna benzediğinden (IEnumerable<T> aracılığıyla) depo gereksinimlerini karşılar ve sanal koleksiyona nesne ekleme ve kaldırma yöntemleri sağlar. Ekleme ve Ayırma yöntemleri EF4 API'sinin ek özelliklerini kullanıma sunar. IObjectSet<T'yi> depoların arabirimi olarak kullanmak için, depoları birbirine bağlamak için bir iş soyutlaması birimine ihtiyacımız vardır.
public interface IUnitOfWork {
IObjectSet<Employee> Employees { get; }
IObjectSet<TimeCard> TimeCards { get; }
void Commit();
}
Bu arabirimin somut bir uygulaması SQL Server ile konuşacaktır ve EF4'ten ObjectContext sınıfını kullanarak kolayca oluşturulabilir. ObjectContext sınıfı, EF4 API'sindeki gerçek iş birimidir.
public class SqlUnitOfWork : IUnitOfWork {
public SqlUnitOfWork() {
var connectionString =
ConfigurationManager
.ConnectionStrings[ConnectionStringName]
.ConnectionString;
_context = new ObjectContext(connectionString);
}
public IObjectSet<Employee> Employees {
get { return _context.CreateObjectSet<Employee>(); }
}
public IObjectSet<TimeCard> TimeCards {
get { return _context.CreateObjectSet<TimeCard>(); }
}
public void Commit() {
_context.SaveChanges();
}
readonly ObjectContext _context;
const string ConnectionStringName = "EmployeeDataModelContainer";
}
Bir IObjectSet<T'yi> hayata geçirmek, ObjectContext nesnesinin CreateObjectSet yöntemini çağırmak kadar kolaydır. Arka planda çerçeve, somut bir ObjectSet<T> oluşturmak için EDM'de sağladığımız meta verileri kullanır. İstemci kodunda test edilebilirliği korumaya yardımcı olacağı için IObjectSet<T> arabirimini döndürmeye devam edeceğiz.
Bu somut uygulama üretimde yararlıdır, ancak testi kolaylaştırmak için IUnitOfWork soyutlamamızı nasıl kullanacağımıza odaklanmamız gerekir.
Test Çiftleri
Denetleyici eylemini yalıtmak için gerçek iş birimi (ObjectContext tarafından desteklenir) ile bir test çifti veya "sahte" iş birimi (bellek içi işlemler gerçekleştirme) arasında geçiş yapabilmemiz gerekir. Bu tür bir geçiş gerçekleştirmenin yaygın yaklaşımı, MVC denetleyicisinin bir iş birimi oluşturmasına izin vermek değil, bunun yerine çalışma birimini denetleyiciye oluşturucu parametresi olarak geçirmektir.
class EmployeeController : Controller {
publicEmployeeController(IUnitOfWork unitOfWork) {
_unitOfWork = unitOfWork;
}
...
}
Yukarıdaki kod, bağımlılık ekleme örneğidir. Denetleyicinin bağımlılığını (iş birimi) oluşturmasına izin vermiyoruz, ancak bağımlılığı denetleyiciye ekliyoruz. Bir MVC projesinde, bağımlılık eklemeyi otomatikleştirmek için bir ters denetim (IoC) kapsayıcısı ile birlikte özel bir denetleyici fabrikası kullanmak yaygın bir durumdur. Bu konular bu makalenin kapsamı dışındadır, ancak bu makalenin sonundaki başvuruları izleyerek daha fazla bilgi edinebilirsiniz.
Test için kullanabileceğimiz sahte bir iş uygulaması birimi aşağıdaki gibi görünebilir.
public class InMemoryUnitOfWork : IUnitOfWork {
public InMemoryUnitOfWork() {
Committed = false;
}
public IObjectSet<Employee> Employees {
get;
set;
}
public IObjectSet<TimeCard> TimeCards {
get;
set;
}
public bool Committed { get; set; }
public void Commit() {
Committed = true;
}
}
Sahte iş birimi bir Commit edilmiş özelliği ortaya çıkarır. Bazen testi kolaylaştıran sahte bir sınıfa özellik eklemek yararlı olabilir. Bu durumda, İşlendi özelliğini kontrol ederek kodun bir işlem birimi olup olmadığını gözlemlemek kolaydır.
Ayrıca Employee ve TimeCard nesnelerini bellekte tutmak için sahte bir IObjectSet<T> gerekir. Genel değerleri kullanarak tek bir uygulama sağlayabiliriz.
public class InMemoryObjectSet<T> : IObjectSet<T> where T : class
public InMemoryObjectSet()
: this(Enumerable.Empty<T>()) {
}
public InMemoryObjectSet(IEnumerable<T> entities) {
_set = new HashSet<T>();
foreach (var entity in entities) {
_set.Add(entity);
}
_queryableSet = _set.AsQueryable();
}
public void AddObject(T entity) {
_set.Add(entity);
}
public void Attach(T entity) {
_set.Add(entity);
}
public void DeleteObject(T entity) {
_set.Remove(entity);
}
public void Detach(T entity) {
_set.Remove(entity);
}
public Type ElementType {
get { return _queryableSet.ElementType; }
}
public Expression Expression {
get { return _queryableSet.Expression; }
}
public IQueryProvider Provider {
get { return _queryableSet.Provider; }
}
public IEnumerator<T> GetEnumerator() {
return _set.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator() {
return GetEnumerator();
}
readonly HashSet<T> _set;
readonly IQueryable<T> _queryableSet;
}
Bu test, işlerinin çoğunu altta yatan bir HashSet<T> nesnesine aktarır. IObjectSet<T'nin> T'yi bir sınıf (başvuru türü) olarak zorunlu kılan genel bir kısıtlama gerektirdiğini ve ayrıca bizi IQueryable<T'yi> uygulamaya zorladığını unutmayın. Standart LINQ işleci AsQueryable kullanılarak bellek içi bir koleksiyonun IQueryable<T> olarak görünmesini sağlamak kolaydır.
Testler
Geleneksel birim testleri, tek bir MVC denetleyicisindeki tüm eylemlerin tüm testlerini tutmak için tek bir test sınıfı kullanır. Bellek içi taklitleri kullanarak bu testleri veya herhangi bir tür birim testini yazabiliriz. Ancak, bu makale için monolitik test sınıfı yaklaşımından kaçınacak ve bunun yerine testlerimizi belirli bir işlev parçasına odaklanacak şekilde gruplandıracağız. Örneğin, "yeni çalışan oluştur" test etmek istediğimiz işlev olabilir, bu nedenle yeni bir çalışan oluşturmakla sorumlu tek denetleyici eylemini doğrulamak için tek bir test sınıfı kullanacağız.
Tüm bu ayrıntılı test sınıfları için ihtiyacımız olan bazı yaygın kurulum kodları vardır. Örneğin, her zaman bellek içi depolarımızı ve sahte iş birimimizi oluşturmamız gerekir. Ayrıca sahte iş birimi eklenmiş çalışan denetleyicisinin bir örneğine de ihtiyacımız var. Bu ortak kurulum kodunu temel sınıf kullanarak test sınıfları arasında paylaşacağız.
public class EmployeeControllerTestBase {
public EmployeeControllerTestBase() {
_employeeData = EmployeeObjectMother.CreateEmployees()
.ToList();
_repository = new InMemoryObjectSet<Employee>(_employeeData);
_unitOfWork = new InMemoryUnitOfWork();
_unitOfWork.Employees = _repository;
_controller = new EmployeeController(_unitOfWork);
}
protected IList<Employee> _employeeData;
protected EmployeeController _controller;
protected InMemoryObjectSet<Employee> _repository;
protected InMemoryUnitOfWork _unitOfWork;
}
Temel sınıfta kullandığımız "nesne ana" test verileri oluşturmak için yaygın bir desendir. Bir nesne ana, birden çok test armatürlerinde kullanılmak üzere test varlıklarının örneğini oluşturacak fabrika yöntemleri içerir.
public static class EmployeeObjectMother {
public static IEnumerable<Employee> CreateEmployees() {
yield return new Employee() {
Id = 1, Name = "Scott", HireDate=new DateTime(2002, 1, 1)
};
yield return new Employee() {
Id = 2, Name = "Poonam", HireDate=new DateTime(2001, 1, 1)
};
yield return new Employee() {
Id = 3, Name = "Simon", HireDate=new DateTime(2008, 1, 1)
};
}
// ... more fake data for different scenarios
}
Bir dizi test fikstürünün temel sınıfı olarak EmployeeControllerTestBase kullanabiliriz (bkz. şekil 3). Her test fikstür belirli bir denetleyici eylemini test edecektir. Örneğin, bir test fikstür bir HTTP GET isteği sırasında kullanılan Oluşturma eylemini test etmeye odaklanır (çalışan oluşturma görünümünü görüntülemek için) ve farklı bir fikstür bir HTTP POST isteğinde kullanılan Oluştur eylemine odaklanır (kullanıcı tarafından çalışan oluşturmak için gönderilen bilgileri almak için). Türetilen her sınıf yalnızca kendi bağlamında gereken kurulumdan ve belirli test bağlamı için sonuçları doğrulamak için gereken onayları sağlamakla sorumludur.
Şekil 3
Burada sunulan adlandırma kuralı ve test stili test edilebilir kod için gerekli değildir; yalnızca bir yaklaşımdır. Şekil 4'te, Visual Studio 2010 için Jet Brains Resharper test çalıştırıcı eklentisinde çalıştırılan testler gösterilmektedir.
Şekil 4
Paylaşılan kurulum kodunu işlemek için bir temel sınıfla, her denetleyici eylemi için birim testleri küçüktür ve kolayca yazılır. Testler hızlı bir şekilde yürütülür (bellek içi işlemler gerçekleştirdiğimiz için) ve ilişkili olmayan altyapı veya çevresel sorunlar nedeniyle başarısız olmamalıdır (çünkü üniteyi test altında yalıttık).
[TestClass]
public class EmployeeControllerCreateActionPostTests
: EmployeeControllerTestBase {
[TestMethod]
public void ShouldAddNewEmployeeToRepository() {
_controller.Create(_newEmployee);
Assert.IsTrue(_repository.Contains(_newEmployee));
}
[TestMethod]
public void ShouldCommitUnitOfWork() {
_controller.Create(_newEmployee);
Assert.IsTrue(_unitOfWork.Committed);
}
// ... more tests
Employee _newEmployee = new Employee() {
Name = "NEW EMPLOYEE",
HireDate = new System.DateTime(2010, 1, 1)
};
}
Bu testlerde, temel sınıf kurulum çalışmalarının çoğunu yapar. Temel sınıf oluşturucusunun bellek içi depoyu, sahte bir iş birimini ve EmployeeController sınıfının bir örneğini oluşturduğunu unutmayın. Test sınıfı bu temel sınıftan türetilir ve Create yöntemini test etme özelliklerine odaklanır. Bu durumda ayrıntılar, herhangi bir birim testi yordamında göreceğiniz "düzenleme, harekete geçirme ve onaylama" adımlarına kadar uzanır:
- Gelen verilerin benzetimini yapmak için yeni birEmployee nesnesi oluşturun.
- EmployeeController'ın Create eylemini çağırın ve newEmployee'yi geçirin.
- Oluştur eyleminin beklenen sonuçları ürettiğini doğrulayın (çalışan depoda görünür).
Oluşturduğumuz şey EmployeeController eylemlerinden herhangi birini test etmemizi sağlar. Örneğin, Çalışan denetleyicisinin Dizin eylemi için testler yazdığımızda, testlerimiz için aynı temel kurulumu oluşturmak üzere test temel sınıfından devralabiliriz. Yine temel sınıf bellek içi depoyu, sahte iş birimini ve EmployeeController örneğini oluşturur. Dizin eylemi testlerinin yalnızca Dizin eylemini çağırmaya ve eylemin döndürdüğü modelin niteliklerini test etmeye odaklanması gerekir.
[TestClass]
public class EmployeeControllerIndexActionTests
: EmployeeControllerTestBase {
[TestMethod]
public void ShouldBuildModelWithAllEmployees() {
var result = _controller.Index();
var model = result.ViewData.Model
as IEnumerable<Employee>;
Assert.IsTrue(model.Count() == _employeeData.Count);
}
[TestMethod]
public void ShouldOrderModelByHiredateAscending() {
var result = _controller.Index();
var model = result.ViewData.Model
as IEnumerable<Employee>;
Assert.IsTrue(model.SequenceEqual(
_employeeData.OrderBy(e => e.HireDate)));
}
// ...
}
Bellek içi sahtelerle oluşturduğumuz testler, yazılımın durumunu test etmeye yöneliktir. Örneğin, Oluştur eylemini test ederken, oluşturma eylemi yürütülürken deponun durumunu incelemek istiyoruz; depo yeni çalışanı barındırıyor mu?
[TestMethod]
public void ShouldAddNewEmployeeToRepository() {
_controller.Create(_newEmployee);
Assert.IsTrue(_repository.Contains(_newEmployee));
}
Daha sonra etkileşim tabanlı testlere göz atacağız. Etkileşim tabanlı test, test altındaki kodun nesnelerimizde uygun yöntemleri çağırıp çağırmadığı ve doğru parametreleri geçirip geçirmediğini sorar. Şimdilik, başka bir tasarım desenine – Lazy Load – geçeceğiz.
İstekli Yükleme ve Gecikmeli Yükleme
ASP.NET MVC web uygulamasının bir noktasında bir çalışanın bilgilerini görüntülemek ve çalışanın ilişkili zaman kartlarını eklemek isteyebiliriz. Örneğin, çalışanın adını ve sistemdeki toplam zaman kartı sayısını gösteren bir zaman kartı özet ekranımız olabilir. Bu özelliği uygulamak için kullanabileceğimiz çeşitli yaklaşımlar vardır.
Yansıtma
Özeti oluşturmak için kolay bir yaklaşım, görünümde görüntülemek istediğimiz bilgilere ayrılmış bir model oluşturmaktır. Bu senaryoda model aşağıdaki gibi görünebilir.
public class EmployeeSummaryViewModel {
public string Name { get; set; }
public int TotalTimeCards { get; set; }
}
EmployeeSummaryViewModel'in bir varlık olmadığını, başka bir deyişle veritabanında kalıcı olmasını istediğimiz bir şey olmadığını unutmayın. Bu sınıfı yalnızca verileri güçlü bir şekilde tiplenmiş olarak görünüme taşımak için kullanacağız. Görünüm modeli bir veri aktarım nesnesine (DTO) benzer çünkü hiçbir davranış (yöntem içermez) – yalnızca özellikler içerir. Özellikler, taşımamız gereken verileri barındıracaktır. LINQ'in standart projeksiyon işleci olan Select işlecini kullanarak bu görünüm modelinin örneğini kolayca oluşturabilirsiniz.
public ViewResult Summary(int id) {
var model = _unitOfWork.Employees
.Where(e => e.Id == id)
.Select(e => new EmployeeSummaryViewModel
{
Name = e.Name,
TotalTimeCards = e.TimeCards.Count()
})
.Single();
return View(model);
}
Yukarıdaki kodun iki önemli özelliği vardır. İlk olarak, kodu test etmek kolaydır çünkü yine de gözlemlemek ve yalıtmak kolaydır. Select işleci, gerçek iş birimi üzerinde olduğu gibi bellek içi sahtelerimizde de aynı şekilde çalışır.
[TestClass]
public class EmployeeControllerSummaryActionTests
: EmployeeControllerTestBase {
[TestMethod]
public void ShouldBuildModelWithCorrectEmployeeSummary() {
var id = 1;
var result = _controller.Summary(id);
var model = result.ViewData.Model as EmployeeSummaryViewModel;
Assert.IsTrue(model.TotalTimeCards == 3);
}
// ...
}
İkinci önemli özellik, kodun EF4'in çalışan ve zaman kartı bilgilerini bir araya getirmek için tek ve verimli bir sorgu oluşturmasına nasıl izin verdiğidir. Özel API'ler kullanmadan çalışan bilgilerini ve zaman kartı bilgilerini aynı nesneye yükledik. Kod yalnızca bellek içi veri kaynaklarına ve uzak veri kaynaklarına karşı çalışan standart LINQ işleçlerini kullanarak gerekli bilgileri ifade etti. EF4, LINQ sorgusu ve C# derleyicisi tarafından oluşturulan ifade ağaçlarını tek ve verimli bir T-SQL sorgusuna çevirebildi.
SELECT
[Limit1].[Id] AS [Id],
[Limit1].[Name] AS [Name],
[Limit1].[C1] AS [C1]
FROM (SELECT TOP (2)
[Project1].[Id] AS [Id],
[Project1].[Name] AS [Name],
[Project1].[C1] AS [C1]
FROM (SELECT
[Extent1].[Id] AS [Id],
[Extent1].[Name] AS [Name],
(SELECT COUNT(1) AS [A1]
FROM [dbo].[TimeCards] AS [Extent2]
WHERE [Extent1].[Id] =
[Extent2].[EmployeeTimeCard_TimeCard_Id]) AS [C1]
FROM [dbo].[Employees] AS [Extent1]
WHERE [Extent1].[Id] = @p__linq__0
) AS [Project1]
) AS [Limit1]
Bir görünüm modeli veya DTO nesnesiyle değil, gerçek varlıklarla çalışmak istemediğimiz başka zamanlar da vardır. Bir çalışana ve çalışanın zaman kartlarına ihtiyacımız olduğunu bildiğimizde, ilgili verileri göze çarpmayan ve verimli bir şekilde hevesle yükleyebiliriz.
Açık İstekli Yükleme
İlgili varlık bilgilerini hevesle yüklemek istediğimizde, depoya olan isteğini ifade etmek için iş mantığına (veya bu senaryoda denetleyici eylem mantığına) yönelik bir mekanizmaya ihtiyacımız vardır. EF4 ObjectQuery<T> sınıfı, sorgu sırasında alınacak ilgili nesneleri belirtmek için bir Include yöntemi tanımlar. EF4 ObjectContext'in ObjectQuery<> T'den devralan somut ObjectSet<T> sınıfı aracılığıyla varlıkları kullanıma çıkardığını unutmayın. Denetleyici eylemimizde ObjectSet<T> başvurularını kullandıysak, her çalışan için zaman kartı bilgilerinin hevesle yüklenmesini belirtmek üzere aşağıdaki kodu yazabiliriz.
_employees.Include("TimeCards")
.Where(e => e.HireDate.Year > 2009);
Ancak, kodumuzu test edilebilir tutmaya çalıştığımız için ObjectSet<T'yi> iş sınıfının gerçek biriminin dışından açığa çıkarmıyoruz. Bunun yerine, sahtesi daha kolay olan IObjectSet<T> arabirimini kullanırız, ancak IObjectSet<T> bir Include yöntemi tanımlamaz. LINQ'in güzelliği, kendi Include işlecimizi oluşturabilmektir.
public static class QueryableExtensions {
public static IQueryable<T> Include<T>
(this IQueryable<T> sequence, string path) {
var objectQuery = sequence as ObjectQuery<T>;
if(objectQuery != null)
{
return objectQuery.Include(path);
}
return sequence;
}
}
Bu Include işlecinin IObjectSet<T> yerine IQueryable<T> için bir uzantı yöntemi olarak tanımlandığına dikkat edin. Bu, yöntemini IQueryable<T, IObjectSet>T, ObjectQuery<T>< ve ObjectSet>T<> gibi daha geniş bir olası tür aralığıyla kullanma olanağı sağlar. Temel dizi gerçek bir EF4 ObjectQuery<T> değilse, bu durum herhangi bir zarara yol açmaz ve Include operatörü işlevsiz olur. Temel alınan sıra bir ObjectQuery T ise (veya ObjectQuery<>T'den<> türetilmişse), EF4 ek veri gereksinimimizi görür ve uygun SQL sorgusunu formüle eder.
Bu yeni operatör devrede olduğunda, depodan zaman kartı bilgilerinin hemen yüklenmesini açıkça talep edebiliriz.
public ViewResult Index() {
var model = _unitOfWork.Employees
.Include("TimeCards")
.OrderBy(e => e.HireDate);
return View(model);
}
Gerçek bir ObjectContext üzerinde çalıştırıldığında kod aşağıdaki tek sorguyu oluşturur. Sorgu, çalışan nesnelerini gerçekleştirmek ve TimeCards özelliğini tam olarak doldurmak için veritabanından tek bir seferde yeterli bilgi toplar.
SELECT
[Project1].[Id] AS [Id],
[Project1].[Name] AS [Name],
[Project1].[HireDate] AS [HireDate],
[Project1].[C1] AS [C1],
[Project1].[Id1] AS [Id1],
[Project1].[Hours] AS [Hours],
[Project1].[EffectiveDate] AS [EffectiveDate],
[Project1].[EmployeeTimeCard_TimeCard_Id] AS [EmployeeTimeCard_TimeCard_Id]
FROM ( SELECT
[Extent1].[Id] AS [Id],
[Extent1].[Name] AS [Name],
[Extent1].[HireDate] AS [HireDate],
[Extent2].[Id] AS [Id1],
[Extent2].[Hours] AS [Hours],
[Extent2].[EffectiveDate] AS [EffectiveDate],
[Extent2].[EmployeeTimeCard_TimeCard_Id] AS
[EmployeeTimeCard_TimeCard_Id],
CASE WHEN ([Extent2].[Id] IS NULL) THEN CAST(NULL AS int)
ELSE 1 END AS [C1]
FROM [dbo].[Employees] AS [Extent1]
LEFT OUTER JOIN [dbo].[TimeCards] AS [Extent2] ON [Extent1].[Id] = [Extent2].[EmployeeTimeCard_TimeCard_Id]
) AS [Project1]
ORDER BY [Project1].[HireDate] ASC,
[Project1].[Id] ASC, [Project1].[C1] ASC
En iyi haber, eylem yönteminin içindeki kodun tamamen test edilebilir kalmasıdır. Include işlecini desteklemek için sahtelerimiz için herhangi bir ek özellik sağlamamıza gerek yoktur. Kötü haber ise kalıcılığı cahil tutmak istediğimiz kodun içinde Include işlecini kullanmak zorunda kaldık. Bu, test edilebilir kod oluştururken değerlendirmeniz gereken denge türlerinin en önemli örneğidir. Performans hedeflerini karşılamak için kalıcılık endişelerinin depo soyutlaması dışında sızmasına izin vermeniz gereken zamanlar vardır.
Tembel yükleme, istekli yüklemenin alternatifidir. Gecikmeli yükleme, ilişkili verilerin gereksinimini açıkça duyurmak için iş kodumuza ihtiyacımız olmadığı anlamına gelir. Bunun yerine, uygulamadaki varlıklarımızı kullanırız ve ek veriler gerekirse Entity Framework verileri isteğe bağlı olarak yükler.
Gecikmeli Yükleme
Bir iş mantığının hangi verilere ihtiyaç duyacağını bilmediğimiz bir senaryo hayal etmek kolaydır. Mantığın bir çalışan nesnesine ihtiyacı olduğunu biliyor olabiliriz, ancak bu yollardan bazılarının çalışandan zaman kartı bilgisi gerektirdiği ve bazılarının gerekmediği farklı yürütme yollarına dalabiliriz. Bunun gibi senaryolar, örtük (tembel) yükleme için mükemmeldir çünkü gerektiğinde veriler adeta sihirli bir şekilde ortaya çıkar.
Ertelenmiş yükleme olarak da bilinen gecikmeli yükleme, varlık nesnelerimize bazı gereksinimler yerleştirir. Gerçek kalıcılıktan habersiz POCO'lar herhangi bir kalıcılık katmanı gereksinimiyle karşılaşmaz, ancak gerçek kalıcılıktan habersizliği başarmak neredeyse imkansızdır. Bunun yerine kalıcılık cehaletini göreli derecelerde ölçeriz. Kalıcılık odaklı bir temel sınıftan devralmamız veya POCO'larda gecikmeli yükleme yapmak için özel bir koleksiyon kullanmamız gerekiyorsa talihsiz bir durum olacaktır. Neyse ki EF4 daha az müdahaleci bir çözüme sahiptir.
Neredeyse Algılanamayan
POCO nesneleri kullanılırken EF4 varlıklar için dinamik olarak çalışma zamanı proxy'leri oluşturabilir. Bu proxy'lar, gerçekleştirilmiş POCO'ları görünmez bir şekilde sarar ve ek işler gerçekleştirmek için her özellik alma ve ayarlama işlemini keserek ek hizmetler sağlar. Bu tür hizmetlerden biri, aradığımız gecikmeli yükleme özelliğidir. Başka bir hizmet, program bir varlığın özellik değerlerini değiştirdiğinde kaydedebilen verimli bir değişiklik izleme mekanizmasıdır. Değişikliklerin listesi, Update komutlarını kullanarak değiştirilmiş varlıkları kalıcı hale getirmek için SaveChanges yöntemi sırasında ObjectContext tarafından kullanılır.
Ancak bu proxy'lerin çalışabilmesi için, bir varlık üzerinde özellik alma ve ayarlama işlemlerine erişmeleri gerekir ve proxy'ler, sanal üyeleri geçersiz kılarak bu hedefe ulaşırlar. Bu nedenle, örtük gecikmeli yükleme ve verimli değişiklik izlemesine sahip olmak istiyorsak POCO sınıf tanımlarımıza geri dönmemiz ve özellikleri sanal olarak işaretlememiz gerekir.
public class Employee {
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual DateTime HireDate { get; set; }
public virtual ICollection<TimeCard> TimeCards { get; set; }
}
Yine de Çalışan varlığının çoğunlukla kalıcılık bilgisi olmadığını söyleyebiliriz. Tek gereksinim sanal üyeleri kullanmaktır ve bu, kodun test edilebilirliğini etkilemez. Herhangi bir özel temel sınıftan türetmemiz, hatta gecikmeli yüklemeye ayrılmış özel bir koleksiyon kullanmamız gerekmez. Kodda gösterildiği gibi, ICollection<T> uygulayan tüm sınıflar ilgili varlıkları tutmak için kullanılabilir.
Çalışma birimimizde yapmamız gereken küçük bir değişiklik de var. Bir ObjectContext nesnesiyle doğrudan çalışırken gecikmeli yükleme varsayılan olarak kapalıdır . Gecikmeli yüklemeyi etkinleştirmek için ContextOptions özelliğinde ayarlayabileceğiniz bir özellik vardır ve her yerde gecikmeli yüklemeyi etkinleştirmek istiyorsak bu özelliği gerçek çalışma birimimizde ayarlayabiliriz.
public class SqlUnitOfWork : IUnitOfWork {
public SqlUnitOfWork() {
// ...
_context = new ObjectContext(connectionString);
_context.ContextOptions.LazyLoadingEnabled = true;
}
// ...
}
Örtük gecikmeli yükleme etkinleştirildiğinde, uygulama kodu bir çalışanı ve çalışanın ilişkili zaman kartlarını kullanabilir ve EF'nin ek verileri yüklemesi için gereken çalışmanın farkında olmadan işlemlerini sürdürebilir.
var employee = _unitOfWork.Employees
.Single(e => e.Id == id);
foreach (var card in employee.TimeCards) {
// ...
}
Tembel yükleme, uygulama kodunu yazmayı kolaylaştırır ve proxy kullanımı ile kodun tamamen test edilebilir olması sağlanır. Çalışma biriminin bellek içi sahteleri, test sırasında gerektiğinde ilişkili verilerle sahte varlıkları önceden yükleyebilir.
Bu noktada dikkatimizi IObjectSet<T> kullanarak depolar oluşturmaktan döndürecek ve kalıcılık çerçevesinin tüm işaretlerini gizlemek için soyutlamalara göz atacağız.
Özel Depolar
Bu makalede iş tasarım deseni birimini ilk kez sunduğumuzda, çalışma biriminin nasıl görünebileceğine ilişkin bazı örnek kodlar sağladık. Üzerinde çalıştığımız çalışan ve çalışan zaman kartı senaryolarını kullanarak bu özgün fikri yeniden sunalım.
public interface IUnitOfWork {
IRepository<Employee> Employees { get; }
IRepository<TimeCard> TimeCards { get; }
void Commit();
}
Bu çalışma birimiyle son bölümde oluşturduğumuz çalışma birimi arasındaki birincil fark, bu çalışma biriminin EF4 çerçevesinin soyutlamalarını kullanmamasıdır (IObjectSet<T> yoktur). IObjectSet<T> , depo arabirimi olarak iyi çalışır, ancak kullanıma sunmuş olduğu API, uygulamamızın gereksinimleriyle tam olarak uyumlu olmayabilir. Bu yaklaşan yaklaşımda özel bir IRepository<T> soyutlaması kullanarak depoları temsil edeceğiz.
Test temelli tasarım, davranış temelli tasarım ve etki alanı odaklı metodoloji tasarımını izleyen birçok geliştirici, çeşitli nedenlerle IRepository<T> yaklaşımını tercih eder. İlk olarak, IRepository<T> arabirimi bir "bozulma önleme" katmanını temsil eder. Eric Evans'ın Etki Alanı Odaklı Tasarım kitabında açıklandığı gibi, bozulma önleme katmanı, etki alanı kodunuzu kalıcılık API'leri gibi altyapı API'lerinden uzak tutar. İkinci olarak, geliştiriciler depoda bir uygulamanın tam gereksinimlerini karşılayan yöntemler oluşturabilir (testler yazılırken keşfedildiği gibi). Örneğin, depo arabirimine bir FindById yöntemi ekleyebilmemiz için genellikle kimlik değeri kullanarak tek bir varlığı bulmamız gerekebilir. IRepository<T> tanımımız aşağıdaki gibi görünür.
public interface IRepository<T>
where T : class, IEntity {
IQueryable<T> FindAll();
IQueryable<T> FindWhere(Expression<Func\<T, bool>> predicate);
T FindById(int id);
void Add(T newEntity);
void Remove(T entity);
}
Varlık koleksiyonlarını kullanıma açmak için IQueryable<T> arabirimini kullanmaya geri döneceğiz. IQueryable<T> , LINQ ifade ağaçlarının EF4 sağlayıcısına akmasını sağlar ve sağlayıcıya sorgunun bütünsel bir görünümünü verir. İkinci bir seçenek de IEnumerable<T> döndürmektir. Bu, EF4 LINQ sağlayıcısının yalnızca depo içinde oluşturulan ifadeleri göreceği anlamına gelir. Depo dışında yapılan gruplandırma, sıralama ve projeksiyon, veritabanına gönderilen SQL komutunda oluşturulmayacak ve bu da performansı etkileyebilir. Öte yandan, yalnızca IEnumerable<T> sonuçlarını döndüren bir depo, yeni bir SQL komutuyla sizi hiçbir zaman şaşırtmaz. Her iki yaklaşım da çalışır ve her iki yaklaşım da test edilebilir olmaya devam eder.
Genel değerleri ve EF4 ObjectContext API'sini kullanarak IRepository<T> arabiriminin tek bir uygulamasını sağlamak kolaydır.
public class SqlRepository<T> : IRepository<T>
where T : class, IEntity {
public SqlRepository(ObjectContext context) {
_objectSet = context.CreateObjectSet<T>();
}
public IQueryable<T> FindAll() {
return _objectSet;
}
public IQueryable<T> FindWhere(
Expression<Func\<T, bool>> predicate) {
return _objectSet.Where(predicate);
}
public T FindById(int id) {
return _objectSet.Single(o => o.Id == id);
}
public void Add(T newEntity) {
_objectSet.AddObject(newEntity);
}
public void Remove(T entity) {
_objectSet.DeleteObject(entity);
}
protected ObjectSet<T> _objectSet;
}
IRepository<T> yaklaşımı, bir istemcinin bir varlığa ulaşmak için bir yöntem çağırması gerektiğinden sorgularımız üzerinde ek denetim sağlar. yönteminin içinde uygulama kısıtlamalarını zorlamak için ek denetimler ve LINQ işleçleri sağlayabiliriz. Arabirimin genel tür parametresinde iki kısıtlaması olduğunu fark edin. İlk kısıtlama ObjectSet<T> tarafından gereken sınıf dezavantajlarıdır ve ikinci kısıtlama, varlıklarımızı uygulama için oluşturulan bir soyutlama olan IEntity'yi uygulamaya zorlar. IEntity arabirimi varlıkları okunabilir bir Kimlik özelliğine sahip olacak şekilde zorlar ve bu özelliği FindById yönteminde kullanabiliriz. IEntity aşağıdaki kodla tanımlanır.
public interface IEntity {
int Id { get; }
}
Varlıklarımızın bu arabirimi uygulaması gerektiğinden, IEntity, küçük bir kalıcılık bağımsızlığı ihlali olarak kabul edilebilir. Kalıcılık bilgisizliğinin dengelerle ilgili olduğunu ve birçok kişi için FindById işlevselliğinin, teknik arabirimin getirdiği kısıtlamadan daha önemli olacağını unutmayın. Arabirimin test edilebilirlik üzerinde hiçbir etkisi yoktur.
Canlı bir IRepository<T> örneği oluşturmak için EF4 ObjectContext gerekir, bu nedenle somut bir iş uygulaması birimi örneklemeyi yönetmelidir.
public class SqlUnitOfWork : IUnitOfWork {
public SqlUnitOfWork() {
var connectionString =
ConfigurationManager
.ConnectionStrings[ConnectionStringName]
.ConnectionString;
_context = new ObjectContext(connectionString);
_context.ContextOptions.LazyLoadingEnabled = true;
}
public IRepository<Employee> Employees {
get {
if (_employees == null) {
_employees = new SqlRepository<Employee>(_context);
}
return _employees;
}
}
public IRepository<TimeCard> TimeCards {
get {
if (_timeCards == null) {
_timeCards = new SqlRepository<TimeCard>(_context);
}
return _timeCards;
}
}
public void Commit() {
_context.SaveChanges();
}
SqlRepository<Employee> _employees = null;
SqlRepository<TimeCard> _timeCards = null;
readonly ObjectContext _context;
const string ConnectionStringName = "EmployeeDataModelContainer";
}
Özel Depoyu Kullanma
Özel depomuzu kullanmak, IObjectSet<T'yi> temel alan depoyu kullanmaktan önemli ölçüde farklı değildir. LINQ işleçlerini doğrudan bir özelliğe uygulamak yerine öncelikle bir IQueryable<T> başvurusu almak için deponun yöntemlerinden birini çağırmamız gerekir.
public ViewResult Index() {
var model = _repository.FindAll()
.Include("TimeCards")
.OrderBy(e => e.HireDate);
return View(model);
}
Daha önce uyguladığımız özel Include işlecinin değişiklik olmadan çalışacağına dikkat edin. FindById yöntemi, depodaki yinelenen mantığı kaldırarak tek bir varlığı almaya çalışan eylemleri basitleştirir.
public ViewResult Details(int id) {
var model = _repository.FindById(id);
return View(model);
}
İncelediğimiz iki yaklaşımın test edilebilirliği açısından önemli bir fark yoktur. Aynı son bölümde yaptığımız gibi HashSet<Çalışanı> tarafından desteklenen somut sınıflar oluşturarak IRepository<T'nin> sahte uygulamalarını sağlayabiliriz. Ancak bazı geliştiriciler, sahte nesneler oluşturmak yerine, mock nesneler ve mock nesne çerçeveleri kullanmayı tercih ediyor. Uygulamamızı test etmek için sahteleri kullanmayı ele alacağız ve sonraki bölümde sahtelerle sahteler arasındaki farkları tartışacağız.
Mocks ile test etme
Martin Fowler'ın "test çifti" olarak adlandırdığı şeyi oluşturmanın farklı yaklaşımları vardır. Test dublörü (filmdeki dublör gibi), testler sırasında gerçek üretim nesnelerinin yerine "geçmek" için oluşturduğunuz bir nesnedir. Oluşturduğumuz bellek içi depolar, SQL Server ile konuşan depolar için test çiftleridir. Kodu yalıtmak ve testlerin hızlı çalışmasını sağlamak için birim testleri sırasında bu test çiftlerinin nasıl kullanılacağını gördük.
Oluşturduğumuz test çiftlerinin gerçek ve çalışan uygulamaları vardır. Arka planda her biri somut bir nesne koleksiyonu saklar ve bir test sırasında deposu üzerinde yaptığımız değişikliklerle bu koleksiyona nesne ekleyip çıkarırlar. Bazı geliştiriciler, gerçek kod ve çalışan uygulamalarla testlerini bu şekilde derlemeyi sever. Bu test çiftlerine sahte olarak adlandırdığımız şeyler denir. Çalışan uygulamaları vardır, ancak üretim kullanımı için yeterince gerçek değildir. Sahte depo aslında veritabanına yazmıyor. Sahte SMTP sunucusu aslında ağ üzerinden bir e-posta iletisi göndermez.
Mock Nesneler ve Sahte Nesneler
Mock olarak bilinen başka bir test dublörü türü de vardır. Sahtelerin çalışan uygulamaları olsa da, sahteler hiçbir uygulama olmadan gelir. Sahte nesne çerçevesinin yardımıyla bu sahte nesneleri çalışma zamanında oluşturur ve bunları test çiftleri olarak kullanırız. Bu bölümde açık kaynak mock (taklit) çerçevesi Moq'u kullanacağız. Burada, çalışan deposu için dinamik olarak test çifti oluşturmak üzere Moq kullanmanın basit bir örneği verilmiştir.
Mock<IRepository<Employee>> mock =
new Mock<IRepository<Employee>>();
IRepository<Employee> repository = mock.Object;
repository.Add(new Employee());
var employee = repository.FindById(1);
Moq'dan bir IRepository<Çalışanı> uygulaması istiyoruz ve dinamik olarak bir uygulama oluşturuyor. Sahte<T> nesnesinin Object özelliğine erişerek IRepository<Çalışan> uygulayan nesneye ulaşabiliriz. Bu, denetleyicilerimize geçirebileceğimiz bu iç nesnedir ve bunun bir test çifti mi yoksa gerçek depo mu olduğunu bilmezler. Aynı gerçek bir uygulama içeren bir nesnede yöntemleri çağırdığımız gibi nesne üzerinde yöntemleri çağırabiliriz.
Add yöntemini çağırdığımızda sahte deponun ne yapacağını merak ediyor olmalısınız. Sahte nesnenin arkasında hiçbir uygulama olmadığından, Add hiçbir şey yapmaz. Arka planda yazdığımız sahte verilerle oluşturduğumuz gibi somut bir veri kümesi olmadığından çalışan atılır. FindById değerinin dönüş değeri ne olacak? Bu durumda sahte nesne, yapabileceği tek şey olan varsayılan bir değeri döndürür. Bir başvuru türü (Çalışan) döndürdiğimizden, dönüş değeri null bir değerdir.
Mocklar değersiz gibi gelebilir; ancak, daha önce bahsetmediğimiz iki özelliği daha vardır. İlk olarak, Moq çerçevesi sahte nesnede yapılan tüm çağrıları kaydeder. Daha sonra kodda, Herhangi birinin Add yöntemini çağırmış olup olmadığını veya herhangi birinin FindById yöntemini çağırmış olup olmadığını Moq'a sorabiliriz. Bu "kara kutu" kayıt özelliğini testlerde nasıl kullanabileceğimizi daha sonra göreceğiz.
İkinci harika özellik, Beklentileri olan sahte bir nesneyi programlamak için Moq'ı nasıl kullanabileceğimizdir. Bir beklenti, sahte nesneye belirli bir etkileşime nasıl yanıt verileceğini bildirir. Örneğin, bir beklentiyi sahte modelimize programlayabilir ve birisi FindById'yi çağırdığında bir çalışan nesnesi döndürmesini söyleyebiliriz. Moq çerçevesi, bu beklentileri programlamak için kurulum API'sini ve lambda ifadelerini kullanır.
[TestMethod]
public void MockSample() {
Mock<IRepository<Employee>> mock =
new Mock<IRepository<Employee>>();
mock.Setup(m => m.FindById(5))
.Returns(new Employee {Id = 5});
IRepository<Employee> repository = mock.Object;
var employee = repository.FindById(5);
Assert.IsTrue(employee.Id == 5);
}
Bu örnekte Moq'dan dinamik olarak bir depo oluşturmasını istiyoruz ve ardından depoyu bir beklentiyle programlıyoruz. Beklenti, biri 5 değerini geçiren FindById yöntemini çağırdığında sahte nesneye 5 Kimlik değerine sahip yeni bir çalışan nesnesi döndürmesini söyler. Bu test başarılı olur ve sahte IRepository<T> için tam bir uygulama derlememiz gerekmez.
Daha önce yazdığımız testleri tekrar gözden geçirelim ve fake yerine mock kullanmak için tekrardan gözden geçirelim. Daha önce olduğu gibi, denetleyicinin tüm testlerine yönelik ihtiyacımız olan ortak altyapı parçalarını ayarlamak için bir temel sınıf kullanacağız.
public class EmployeeControllerTestBase {
public EmployeeControllerTestBase() {
_employeeData = EmployeeObjectMother.CreateEmployees()
.AsQueryable();
_repository = new Mock<IRepository<Employee>>();
_unitOfWork = new Mock<IUnitOfWork>();
_unitOfWork.Setup(u => u.Employees)
.Returns(_repository.Object);
_controller = new EmployeeController(_unitOfWork.Object);
}
protected IQueryable<Employee> _employeeData;
protected Mock<IUnitOfWork> _unitOfWork;
protected EmployeeController _controller;
protected Mock<IRepository<Employee>> _repository;
}
Kurulum kodu çoğunlukla aynı kalır. Sahteleri kullanmak yerine Sahte nesneler oluşturmak için Moq kullanacağız. Temel sınıf, kod Employees özelliğini çağırdığında sahte bir depo döndürmesi için sahte iş birimini ayarlar. Sahte kurulumun geri kalanı, belirli senaryolara ayrılmış test fikstürlerinin içinde gerçekleşecektir. Örneğin, Dizin eylemi için test fikstür, sahte depoyu, eylem sahte deponun FindAll yöntemini çağırdığında çalışanların listesini döndürecek şekilde ayarlar.
[TestClass]
public class EmployeeControllerIndexActionTests
: EmployeeControllerTestBase {
public EmployeeControllerIndexActionTests() {
_repository.Setup(r => r.FindAll())
.Returns(_employeeData);
}
// .. tests
[TestMethod]
public void ShouldBuildModelWithAllEmployees() {
var result = _controller.Index();
var model = result.ViewData.Model
as IEnumerable<Employee>;
Assert.IsTrue(model.Count() == _employeeData.Count());
}
// .. and more tests
}
Beklentilerimiz dışında, testlerimiz daha önce yaptığımız testlere benzer. Ancak, sahte çerçevenin kayıt özelliğiyle testlere farklı bir açıdan yaklaşabiliriz. Sonraki bölümde bu yeni perspektife göz atacağız.
Durum ve Etkileşim Testi
Sahte nesnelerle yazılımı test etmek için kullanabileceğiniz farklı teknikler vardır. Yaklaşımlardan biri, şu ana kadar bu makalede yaptığımız durum tabanlı testi kullanmaktır. Durum tabanlı test, yazılımın durumu hakkında onaylar yapar. Son testte denetleyicide bir eylem yöntemi çağırıp oluşturması gereken model hakkında bir onay yaptık. Test durumunun diğer bazı örnekleri aşağıda verilmiştir:
- Oluşturma gerçekleştirildiğinde deponun yeni çalışan nesnesini içerdiğini doğrulayın.
- Dizin yürütüldükten sonra modelin tüm çalışanların listesini tuttuğunu doğrulayın.
- Delete işlemi yürütüldükten sonra veri havuzunun belirtilen bir çalışanı içermediğini doğrulayın.
Sahte nesnelerde göreceğiniz bir diğer yaklaşım da etkileşimleri doğrulamaktır. Durum tabanlı test, nesnelerin durumu hakkında onaylar yaparken, etkileşim tabanlı test nesnelerin nasıl etkileşime geçtiğini onaylar. Örneğin:
- Create yürütülürken denetleyicinin deponun Add yöntemini çağırdığını doğrulayın.
- Denetleyicinin İndeks yöntemi yürütüldüğünde deponun FindAll yöntemini çağırdığını doğrulayın.
- Denetleyicinin, Düzenle işlemi yürütüldüğünde, değişiklikleri kaydetmek için iş biriminin Commit yöntemini çağırdığını doğrulayın.
Etkileşim testi genellikle daha az test verisi gerektirir, çünkü koleksiyonların içine girmiyoruz ve sayıları doğrulamıyoruz. Örneğin, Ayrıntılar eyleminin deponun FindById yöntemini doğru değerle çağırdığını biliyorsak, eylem büyük olasılıkla doğru şekilde davranıyordur. FindById'den döndürülecek test verilerini ayarlamadan bu davranışı doğrulayabiliriz.
[TestClass]
public class EmployeeControllerDetailsActionTests
: EmployeeControllerTestBase {
// ...
[TestMethod]
public void ShouldInvokeRepositoryToFindEmployee() {
var result = _controller.Details(_detailsId);
_repository.Verify(r => r.FindById(_detailsId));
}
int _detailsId = 1;
}
Yukarıdaki test fikstürlerinde gereken tek kurulum, temel sınıf tarafından sağlanan kurulumdur. Denetleyici eylemini çağırdığımızda Moq, sahte depoyla etkileşimleri kaydeder. Moq'un Verify API'sini kullanarak Moq'a denetleyicinin FindById'yi uygun kimlik değeriyle çağırdığını sorabiliriz. Denetleyici yöntemini çağırmadıysa veya beklenmeyen bir parametre değeriyle yöntemini çağırdıysa, Verify yöntemi bir özel durum oluşturur ve test başarısız olur.
Oluştur eyleminin geçerli iş biriminde Commit'i çağırdığını doğrulamaya yönelik başka bir örnek aşağıda verilmiştir.
[TestMethod]
public void ShouldCommitUnitOfWork() {
_controller.Create(_newEmployee);
_unitOfWork.Verify(u => u.Commit());
}
Etkileşim testinin bir tehlikesi, etkileşimleri aşırı belirtme eğilimidir. Sahte nesnenin, sahte nesneyle her etkileşimi kaydedip doğrulayabilmesi, testin her etkileşimi doğrulamayı denemesi gerektiği anlamına gelmez. Bazı etkileşimler uygulama ayrıntılarıdır ve yalnızca geçerli testi karşılamak için gereken etkileşimleri doğrulamanız gerekir.
Sahteler veya sahteler arasındaki seçim büyük ölçüde test ettiğiniz sisteme ve kişisel (veya ekip) tercihlerinize bağlıdır. Sahte nesneler, test çiftlerini uygulamak için ihtiyaç duyduğunuz kod miktarını önemli ölçüde azaltabilir. Ancak, herkes beklentileri programlamak ve etkileşimleri doğrulamak konusunda rahat hissetmeyebilir.
Sonuçlar
Bu makalede, veri kalıcılığı için ADO.NET Entity Framework kullanırken test edilebilir kod oluşturmaya yönelik çeşitli yaklaşımlar gösterdik. IObjectSet<T> gibi yerleşik soyutlamalardan yararlanabilir veya IRepository<T> gibi kendi soyutlamalarımızı oluşturabiliriz. Her iki durumda da, ADO.NET Entity Framework 4.0'daki POCO desteği, bu soyutlamaların tüketicilerinin kalıcı cahil ve yüksek oranda test edilebilir kalmasını sağlar. Örtük gecikmeli yükleme gibi ek EF4 özellikleri, iş ve uygulama hizmeti kodunun ilişkisel veri deposunun ayrıntıları konusunda endişelenmeden çalışmasını sağlar. Son olarak, oluşturduğumuz soyutlamalar, birim testleri içinde kolaylıkla taklit edilebilir veya sahte şekilde oluşturulabilir ve hızlı çalışan, yüksek oranda yalıtılmış ve güvenilir testler elde etmek için bu test dublörlerini kullanabiliriz.
Ek Kaynaklar
- Martin Fowler, Kurumsal Uygulama Mimarisi Desenlerinden Desen kataloğu
- Griffin Caprio, " Bağımlılık Enjeksiyonu"
- Veri Programlama Blogu, " İzlenecek Yol: Entity Framework 4.0 ile Test Temelli Geliştirme".
- Veri Programlanabilirliği Blogu, " Entity Framework 4.0 ile Depo ve çalışma birimi desenlerini kullanma"
- Aaron Jensen, " Makine Özelliklerine Giriş"
- Eric Lee, " MSTest ile BDD"
- Eric Evans, " Etki Alanı Odaklı Tasarım"
- Martin Fowler, "Sahte Nesneler Kukla Değildir"
- Martin Fowler, " Test Double"
- Moq
Biyografi
Scott Allen, Pluralsight teknik kadrosunun bir üyesidir ve OdeToCode.com'nin kurucusudur. 15 yıllık ticari yazılım geliştirme sürecinde Scott, 8 bit tümleşik cihazlardan yüksek oranda ölçeklenebilir ASP.NET web uygulamalarına kadar her şey için çözümler üzerinde çalıştı. Scott'a OdeToCode'daki blogundan veya Twitter'da https://twitter.com/OdeToCode adresinden ulaşabilirsiniz.