Mocking Database Context In Repository Unit Test Using Effort

Seringkali kita ingin membuat unit test di level repository untuk menguji query yang kita buat pada sebuah repository. Karena pada unit testing, prinsipnya adalah melakukan isolasi dari external class ataupun external resource dan eksekusi unit testing harus bisa secepat mungkin, maka kita hindari menggunakan database secara langsung.

Jika menggunakan database secara langsung, sangat berpengaruh pada kecepatan eksekusi unit testing dan masalah data yang harus diinsert dan dihapus ketika selesai digunakan oleh unit testing. Salah satu solusinya adalah menggunakan in-memory-database. In-memory database ini akan dihancurkan oleh Garbage collector ketika sudah tidak digunakan.

Untuk mempermudah kita bisa menggunakan Effort. Instalasi cukup mudah, cukup di install lewat nuget package manager.

Pada initialisasi unit test kita mockup context kita.

Pertama, buat connection untuk Effort.

var connection = DbConnectionFactory.CreateTransient();

Selanjutnya pada class context, kita tambahkan satu constuctor untuk melewatkan connection dari Effort tersebut.

public BookStoreEntities(DbConnection connection) : base(connection, true)
{
}

pada unit test initialization method lewatkan connection Effort tadi ke context dan tambahkan code untuk membuat database jika tidak ada. Ini untuk menghindari munculnya error migrations data nantinya.

_context = new BookStoreEntities(connection);
_context.Database.CreateIfNotExists();

selanjutnya, insert data untuk table table yang akan kita gunakan nantinya.

_context.Categories.Add(new Category { Name = "Programming" });
_context.SaveChanges();
var category = _context.Categories.First();
_context.Books.Add(new Book { Title="Effort In Action", Category = category });
_context.Books.Add(new Book { Title = "C# In Action", Category = category });
_context.SaveChanges();

Kode program selengkapnya seperti ini.

[TestClass]
public class BookRepositoryTest
{
private BookStoreEntities _context;
private IUnitOfWork _unitOfWork;
private IGenericRepository _bookRepository;
[TestInitialize]
public void Setup()
{

var connection = DbConnectionFactory.CreateTransient();
_context = new BookStoreEntities(connection);
_context.Database.CreateIfNotExists();

_context.Categories.Add(new Category { Name = "Programming" });
_context.SaveChanges();
var category = _context.Categories.First();
_context.Books.Add(new Book { Title="Effort In Action", Category = category });
_context.SaveChanges();
_unitOfWork = new EFUnitOfWork(_context);
_bookRepository = _unitOfWork.GetGenericRepository();
}
[TestCleanup]
public void Cleanup()
{
_context = null;
_unitOfWork = null;
_bookRepository = null;
}
[TestMethod]
public void GetAll()
{
var books = _bookRepository.Get().ToList();
Assert.AreNotEqual(0, books.Count);
}
}

[TestMethod]
public void FindByName()
{
var books = _bookRepository.Get(x=>x.Title.Contains("Effor")).ToList();
Assert.AreNotEqual(0, books.Count);
}
[TestMethod]
public void GetById()
{
var book = _bookRepository.GetById(1);
Assert.IsNotNull(book);
}

Jika unit Test kita jalankan hasilnya seperti ini.

unit-test-effort

 

Selamat mencoba!

Iklan

SOLID Principles-O-Open Closed Principle

“software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification

Untuk mengimplement OCP, sebelumnya kode kita harus SRP – lihat pada tulisan SOLID Prinsiples – S-Single Responsibility, sehingga kita dapat mengindentifikasi dan memisahkan behavior yang berubah kedalam berbagai class. Output dari OCP adalah sekumpulan class yang memiliki behavior yang sama tapi dengan implementasi yang berbeda-beda. Perhatikan potongan kode program di bawah ini.

public class DiscountService
{
private const int GOLD = 1;
private const int SILVER = 2;
private double _totalSales;
public DiscountService(double TotalSales)
{
_totalSales = TotalSales;
}

private int _custType;
public int CustType
{
get { return _custType; }
set { _custType = value; }
}

public double GetDiscount(int CustTpe)
{
if (_custType == GOLD)
{
return _totalSales - 100;
}
else if (_custType == SILVER)
{
return _totalSales - 50;
}
return _totalSales;
}
}

Masalah timbul ketika kita akan menambahkan beberapa tipe customer lain, maka akan sangat banyak kondisi “IF” di dalam kode program. Menambahkan kondisi “IF” baru artinya kita mengubah class CustomerService dan itu melanggar prinsip Close for modification.

Lantas bagaimana caranya mengimplementasikan prinsip Open for extension? Yaitu dengan menggunakan inheritance (lewat parent classnya) atau abstraksi (bisa menggunakan abstract class ataupun interface).

public interface ICustomer
{
string Name { get; }
double GetDiscount(double TotalSales);
}

Kemudian kita buat beberapa class konkrit untuk implementasi interface ICustomer di atas.

public class GoldCustomer : ICustomer
{
public string Name
{
get
{
return "Gold";
}
}

public double GetDiscount(double TotalSales)
{
return TotalSales - 100;
}
}
public class SilverCustomer : ICustomer
{
public string Name
{
get
{
return "Silver";
}
}

public double GetDiscount(double TotalSales)
{
return TotalSales - 50;
}
}

Kemudian mari kita refactor kode program class DiscountService di atas sehingga jika ingin menambah tipe customer lagi, tidak perlu mengubah implementasi dari class DiscountService.

public class DiscountService
{
private readonly ICustomer _discount;
private double _totalSales;
public DiscountService(double TotalSales, ICustomer discount)
{
_totalSales = TotalSales;
_discount = discount;
}
public virtual double GetDiscount()
{
return _discount.GetDiscount(_totalSales);
}
}

class Program
{
static void Main(string[] args)
{
double totalSales = 1000;
DiscountService discountService;
var listCustomer = new List();
listCustomer.Add(new GoldCustomer());
listCustomer.Add(new SilverCustomer());
foreach (var customer in listCustomer)
{
discountService = new DiscountService(totalSales, customer);
Console.WriteLine("Customer Type: " + customer.Name);
Console.WriteLine("Total after discount : " + discountService.GetDiscount());
Console.WriteLine("----------------------");
}

Console.ReadKey();

}
}

ocp

Sekarang kita bisa menambahkan 1 jenis customer lagi, tanpa harus mengubah code pada class Discount Service.

listCustomer.Add(new PlatinumCustomer());

ocp-2.png

SOLID Prinsiples – S-Single Responsibility

S.O.L.I.D prinsip adalah kumpulan dari best-practices untuk Object Oriented design. Semua design pattern pada Gang Of Four juga dibangun berdasarkan prinsip ini.

SOLID merupakan  prinsip dasar dalam membangun arsitektur software yang baik. SOLID merupakan singkatan dari 5 prinsip:

  1. S dari SRP (Single Responsibility Principle)
  2. O dari OCP (Open Closed Principle)
  3. L dari LSV (Liskov Substitution Principle)
  4. I dari ISP (Interface segregation principle)
  5. D dari DIP ( Dependency Inversion Principle)

S-Single Responsibility Principle

“A class should have only one reason to change.” – Robert C Martin

Sebuah class seharusnya diubah itu satu alasan aja. Terlalu banyak tanggung jawab yang dibebankan pada sebuah class sehingga membuat satu perubahan kode program bisa jadi menimbulkan banyak efek kepada yang lain. Fokus pada satu tanggung jawab memudahkan maintenance code.

public class Customer
{
public void Add()
{
try
{
// Database code goes here
}
catch (Exception ex)
{
System.IO.File.WriteAllText(@"c:\Error.txt", ex.ToString());
}
}
public void Delete()
{
try
{
// Database code goes here
}
catch (Exception ex)
{
System.IO.File.WriteAllText(@"c:\Error.txt", ex.ToString());
}
}
}

Pada class Customer method Add dan Deleta harusnya hanya menghandle satu high level abstraksi. Di dalam class tersebut juga terdapat duplikasi kode program penulisan log.
Penulisan log harusnya ditangani oleh class lain dalam hal ini class FileLogger. Sehingga jika ada perubahan pada proses penulisan log seperti path file yang diubah atau bahkan penulisan log dipindahkan ke database, kita bisa fokus hanya pada class FileLogger saja. Ingat! SRP bukan berarti satu class hanya boleh memiliki satu method saja, tapi hanya butuh satu alasan untuk memodifikasi class tersebut.

public class Customer
    {

        FileLogger _logger = new FileLogger();
        public void Add()
        {
            try
            {
                // Database code goes here
            }
            catch (Exception ex)
            {
                _logger.WriteLog(ex.Message);
            }
        }
        public void Delete()
        {
            try
            {
                // Database code goes here
            }
            catch (Exception ex)
            {
                _logger.WriteLog(ex.Message);
            }
        }
    }

    public class FileLogger
    {
        public void WriteLog(string message)
        {
            System.IO.File.WriteAllText(@"c:\Error.txt", message);

        }

Apa manfaat dari SRP?
1. Reuse, lebih mudah untuk mendaya-guna kode program
2. Clarity, kode yang kita menjadi sangat jelas karena ditulis hanya focus pada satu hal
3. Naming, tentu dengan hanya berfokus pada satu hal, penamaan class jadi lebih gampang.
4. Readability, karena berkaitan dengan Clarity dan Naming, maka menjadi sangat membantu dalam pembacaan kode program
5. Reduced Coupling, menurunkan tingkat kebergantungan antar class. Semakin tinggi kebergantungan, semakin susah untuk dimodifikasi

Custom Configuration Handler

File configurasi baik itu app.config maupun web.config adalah file xml yang berisi configurasi external dari aplikasi yang kita buat. Seringkali kita melakukan pemanggilan nilai pada file configurasi seperti ini.

var baseUrl = ConfigurationManager.AppSettings["BaseUrl"].ToString();

Semakin kompleks aplikasi, terkadang membuat bertambahnya nilai yang perlu di store di configurasi. Supaya configurasi kita mudah di maintenance jika ada perubahan key pada configurasi dan menghindari kesalahan pengetikan pada key, kita bisa membuat class untuk menghandle issue tersebut.

public class BLLConfiguration : ConfigurationSection
{
public string BaseUrl { get; set; }
public static BLLConfiguration GetConfig()
{
var section = ConfigurationManager.GetSection("Astral/BusinessLogic");
if (section == null)
throw new ConfigurationErrorsException(
ErrorMessages.ConfigurationErrorsException);

return (BLLConfiguration)ConfigurationManager.GetSection("Astral/BusinessLogic");
}

public void LoadConfigValues(XmlNode node)
{
var attributeCollection = node.Attributes;
if (attributeCollection["BaseUrl"] != null)
BaseUrl = attributeCollection["BaseUrl"].Value;
}
}

public class BLLConfigurationHandler : IConfigurationSectionHandler
{
public Object Create(Object parent, Object configContext, XmlNode node)
{
var config = new BLLConfiguration();
config.LoadConfigValues(node);
return config;
}
}

Pada web config/appconfig daftarkan section tersebut. Pada type, formatnya adalah {Namespace.ClassName},{AssemblyName}

custom_config.png

Kemudian, assign value pada confurasi dibuat dalam satu section berdasarkan pada nama sectionGroup dan section-nya.

custom_config_1.png

Pemanggilan value dari configurasi jadinya seperti ini.

var baseUrl = BLL.BLLConfiguration.GetConfig().BaseUrl;

Reflection – Printing Property Value Of Object

Anggaplah kita mempunya sebuah class Person seperti ini.

public class Person
{
public Person()
{
Childs = new List();
}
public string Name { get; set; }
public int Age { get; set; }
public Address PostalAddress { get; set; }
public Person Parent { get; set; }
public List Childs { get; set; }

}

public class Address
{
public string Street { get; set; }

}

Kita ingin mencetak seluruh property yang tidak null dari object Person di atas termasuk object dan collection yang ada di dalamnya menggunakan reflection.

private static void PrintProperties(object obj, int indent)
{
if (obj == null) return;
string indentString = new string(' ', indent);
Type objType = obj.GetType();
PropertyInfo[] properties = objType.GetProperties();
foreach (PropertyInfo property in properties)
{
object propValue = property.GetValue(obj, null);
var elems = propValue as IList;
if (elems != null)
{
if (elems.Count > 0)
{
Console.WriteLine("{0}{1}:", indentString, property.Name);
foreach (var item in elems)
{
PrintProperties(item, indent + 3);
}
}
}else
{
if (property.PropertyType.Assembly == objType.Assembly)
{
if (propValue != null)
{
Console.WriteLine("{0}{1}:", indentString, property.Name);
PrintProperties(propValue, indent + 2);
}
}else
{
Console.WriteLine("{0}{1}: {2}", indentString, property.Name, propValue);
}
}
}
}

var child1 = new Person() { Name = "Abe", Age = 1 };
var child2 = new Person() { Name = "Lisa", Age = 5 };
var parent = new Person()
{
Name = "Me",
Age = 40,
PostalAddress = new Address
{
Street = "Bandung Soekarno Hatta"
}
};
parent.Childs.Add(child1);
parent.Childs.Add(child2);
PrintProperties(parent, 1);

Ketika dijalankan, hasilnya seperti ini:

reflection

Automapper and Entity Framework Query Performance

Ketika kita membuat sebuah aplikasi dengan arsitektur N-layer, dimana object entities dari Data Access Layer dimapping menggunakan AutoMapper terhadap object Dto pada Business Layer. Mapping tersebut perlu kita perhatikan, terutama jika kita menggunakan lazy loading pada Entity Framework sebagai ORM dan navigasi properties yang kita mapping adalah collection.

public class Price
{
public Price()
{
this.Benefits = new HashSet();
this.PriceCalculates = new HashSet();
}

public int Id { get; set; }
public string Name { get; set; }
public virtual PriceTemplate PriceTemplate { get; set; }
public virtual ICollection Benefits { get; set; }
public virtual ICollection PriceCalculates { get; set; }
}
public class PriceDto
{
public PriceDto()
{
this.BenefitIds = new List();
this.PriceCalculates = new List();
}

public int Id { get; set; }
public string Name { get; set; }
public PriceTemplate PriceTemplate { get; set; }
public List BenefitIds { get; set; }
public List PriceCalculates { get; set; }
}

Contoh, kita meregistrasi mapping antara class Price entities dengan class PriceDto sebagai object Dto.

CreateMap<Price, PriceDTO>()
.ForMember(dest => dest.PriceCalculates, opt => opt.Ignore())
.ForMember(dest => dest.BenefitsId,
opt => opt.MapFrom(src => src.Benefits.Select(benefit => benefit.Id).ToList()))
.IgnoreAllNonExisting();

Ternyata sesuai requirement pada tampilan list aplikasi kita, kita tidak perlu menampilkan data PriceCalculates. Hanya perlu ditampilkan pada form detail saja.

Jika generic repository kita menggunakan lazy loading, maka akan ada hit ke database sebanyak jumlah data yang ditampilkan. Untuk menghindari issue tersebut,Ignore mapping semua navigation properties yang tidak digunakan pada Dto. Kenapa perlu diignore secara explisit padahal sudah menggunakan extension .IgnoreAllNonExisting() ? karena secara default automapper akan melakukan mapping jika property class sumber dan destinasi namanya sama (tidak case sensitif).

CreateMap<Price, PriceDTO>()
.ForMember(dest => dest.PriceCategories, opt => opt.Ignore())
.ForMember(dest => dest.PriceCalculates, opt => opt.Ignore())
.ForMember(dest => dest.BenefitsId,
opt => opt.MapFrom(src => src.Benefits.Select(benefit => benefit.Id).ToList()))
.IgnoreAllNonExisting();

Perhatikan, kita tetap butuh map dari navigaiton property Benefits pada Price terhadap property BenefitsId. Agar data Benefits diambil sekaligus pada saat melakukan query Price, tambahkan sebagai include properties

var includedProperties = "Benefits";
var prices = _priceRepo.Get(filter, orderBy, includedProperties);

sebagai tambahan, implementasi dari method Get pada Price Repository di atas (_priceRepo) seperti berikut ini, lihat Generic Repository Entity Framework Part I

public virtual IEnumerable Get(
Expression<Func<Class, bool>> filter = null,
Func<IQueryable, IOrderedQueryable> orderBy = null,
string includeProperties = "")
{
IQueryable query = dbSet;

if (filter != null)
{
query = query.Where(filter);
}

foreach (var includeProperty in includeProperties.Split
(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty);
}

if (orderBy != null)
{
return orderBy(query).ToList();
}
else
{
return query.ToList();
}
}

Assembly Loader Error

System.IO.FileLoadException: Could not load file or assembly ‘NLog, Version=2.1.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c’ or one of its dependencies. The located assembly’s manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040)

Jika suatu saat anda mendapat error seperti di atas, artinya assembly loader tidak menemukan assembly yang di reference. Ternyata untuk kasus saya, di app.config ternyata mereference assembly version yang berbeda.

<dependentAssembly>
    <assemblyIdentity name="NLog" publicKeyToken="5120e14c03d0593c" culture="neutral" />
    <bindingRedirect oldVersion="0.0.0.0-4.0.0.0" newVersion="4.0.0.0" />
 </dependentAssembly>

Ada 2 cara untuk menyelesaikan issue ini.

  1. hapus configurasi tersebut di app.config
  2. Kita paksa untuk semua version tertentu dalam kasus ini versi .4.0.0.0 di redirect ke version 2.1.0.0.
<dependentAssembly>
     <assemblyIdentity name="NLog" publicKeyToken="5120e14c03d0593c" culture="neutral" />
     <bindingRedirect oldVersion="0.0.0.0-4.0.0.0" newVersion="2.1.0.0" />
</dependentAssembly>