1. Implementing Generic Repository
You need to create IGenericRepository and GenericRepository as shown bellow
//IGenericRepository.cs
public interface IGenericRepository where T : class
{
List GetAll();
List FindBy(Expression> predicate);
bool Add(T entity);
bool Delete(T entity);
bool Edit(T entity);
T FindById(int id);
}
//GenericRepository.cs
//GenericRepository.cs
public class GenericRepository : IGenericRepository where T : class
{
public GenericRepository(MyStoreContext context)
{
_entities = context;
}
private MyStoreContext _entities;
private MyStoreContext _entities;
public MyStoreContext db
{
get { return _entities; }
set { _entities = value; }
}
public virtual List GetAll()
{
IQueryable query = _entities.Set();
return query.ToList();
}
public List FindBy(System.Linq.Expressions.Expression> predicate)
{
IQueryable query = _entities.Set().Where(predicate);
return query.ToList();
}
public virtual void Attach(T entity)
{
_entities.Set().Attach(entity);
}
public virtual bool Add(T entity)
{
_entities.Set().Add(entity);
return true;
}
public virtual bool Delete(T entity)
{
_entities.Set().Remove(entity);
return true;
}
public virtual bool Edit(T entity)
{
_entities.Entry(entity).State = EntityState.Modified;
return true;
}
public virtual T FindById(int id)
{
return _entities.Set().Find(id);
}
}
2. Implementing Unit of Work
DbContext contains unit of work by default , if you make series of changes to entities and call SaveChanges method , either all the changes make will be persisted to the database or if any of the change is failed to be persisted then all changes will be rolled back to the state prior to calling savechanges.
We created the unit of work to make our transactional change to be testable at the service layer and to get some flexibility.
//IUnitOfWork.cs
public interface IUnitOfWork:IDisposable
{
IGenericRepository ProductRepository { get; }
IGenericRepository OrderRepository { get; }
void Save();
}
//UnitOfWork.cs
public class UnitOfWork : IUnitOfWork
{
private readonly MyStoreContext _context;
private IGenericRepository productRepository;
private IGenericRepository orderRepository;
public UnitOfWork()
{
this._context = new MyStoreContext();
}
public IGenericRepository ProductRepository
{
get { return this.productRepository ?? (this.productRepository = new GenericRepository(_context)); }
}
public IGenericRepository OrderRepository
{
get { return this.orderRepository ?? (this.orderRepository = new GenericRepository(_context)); }
}
public void Save()
{
_context.SaveChanges();
}
private bool disposed = false;
protected virtual void Dispose(bool disposing)
{
if (!this.disposed)
{
if (disposing)
{
_context.Dispose();
}
}
this.disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
Here we have prepared our unit of work, if you want to make our unit of work to be testable you can factor our DbContext and inject to our unit of work.
3. Introducing Service Layer
The service layers the one the controller is going to interact, this service layer can be used for various purposes, and the first one will be to implement CRUD operation. Since service layer is consuming unit of work we can also make changes to the group of objects and call unit of work save method. The will be transactional because unit of work will indirectly call to dbcontext savechanges melthod which of course is transactional.
NOTE: The service layer will be used for transactional execution only if your transaction target only single dbcontext object , if you are targeting multiple dbcontext objects you need to address this issue independently and not covered in this article.
//IProductService.cs
public interface IProductService:IDisposable
{
void AddProduct(Product product);
void UpdateProduct(Product product);
void DeleteProduct(int id);
List GetAllProduct();
Product GetProduct(int productId);
}
//ProductService.cs
public class ProductService:IProductService
{
private readonly IUnitOfWork _unitOfWork;
public ProductService(IUnitOfWork unitOfWork)
{
this._unitOfWork = unitOfWork;
}
public void AddProduct(Product product)
{
_unitOfWork.ProductRepository.Add(product);
_unitOfWork.Save();
}
public void UpdateProduct(Product product)
{
_unitOfWork.ProductRepository.Edit(product);
_unitOfWork.Save();
}
public void DeleteProduct(int id)
{
var org = _unitOfWork.ProductRepository.FindById(id);
_unitOfWork.ProductRepository.Delete(org);
_unitOfWork.Save();
}
public List GetAllProduct()
{
return _unitOfWork.ProductRepository.GetAll();
}
public Product GetProduct(int productId)
{
return _unitOfWork.ProductRepository.FindById(productId);
}
public void Dispose()
{
_unitOfWork.Dispose();
}
}
4. Setting Up Ninject
Since this design favours isoloation of each layer , we need dependency resolver to instantiate and supply dependent objects from one layer to another, for this purpose I have used Ninject dependency resolver.
1. You have to install Ninject in your MVC project from nugget package
http://expertester.wordpress.com/2011/08/18/how-to-add-ninject-to-your-project-using-visual-studio-2010-sp1/
2. Create Dependency Resolver Class in your MVC project
//NinjectDependencyResolver.cs
public class NinjectDependencyResolver:IDependencyResolver
{
private IKernel kernel;
public NinjectDependencyResolver()
{
kernel = new StandardKernel();
AddBindings();
}
public object GetService(Type serviceType)
{
return kernel.TryGet(serviceType);
}
public IEnumerable
{
return kernel.GetAll(serviceType);
}
private void AddBindings()
{
kernel.Bind().To().WhenInjectedInto();
kernel.Bind().To();
}
}
3. Register NinjectDependencyResolver in the global.asax
// Global.asax.cs –Application_Start()
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
DependencyResolver.SetResolver(new NinjectDependencyResolver());
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
5. Consuming Service Layer at the controller
The ProductController receives service layer as a constructor injection
//ProductController.cs
public class ProductController : Controller
{
private readonly IProductService productService;
public int PageSize = 4;
//
// GET: /Product/
public ProductController(IProductService productServiceParam)
{
this.productService = productServiceParam;
}
public ViewResult Index(int page=1)
{
ProductListViewModel model = new ProductListViewModel
{
Products = productService.GetAllProduct()
.OrderBy(p => p.ProductId)
.Skip((page - 1)*PageSize)
.Take(PageSize),
PagingInfo = new PagingInfo
{
CurrentPage = page,
ItemsPerPage = PageSize,
TotalItems =
productService.GetAllProduct().Count()
}
};
return View(model);
}
//
// GET: /Product/Details/5
public ActionResult Details(int id = 0)
{
Product product = productService.GetProduct(id);
if (product == null)
{
return HttpNotFound();
}
return View(product);
}
//
// GET: /Product/Create
public ActionResult Create()
{
return View();
}
//
// POST: /Product/Create
[HttpPost]
public ActionResult Create(Product product)
{
if (ModelState.IsValid)
{
productService.AddProduct(product);
return RedirectToAction("Index");
}
return View(product);
}
//
// GET: /Product/Edit/5
public ActionResult Edit(int id = 0)
{
Product product = productService.GetProduct(id);
if (product == null)
{
return HttpNotFound();
}
return View(product);
}
//
// POST: /Product/Edit/5
[HttpPost]
public ActionResult Edit(Product product)
{
if (ModelState.IsValid)
{
productService.UpdateProduct(product);
return RedirectToAction("Index");
}
return View(product);
}
//
// GET: /Product/Delete/5
public ActionResult Delete(int id = 0)
{
Product product = productService.GetProduct(id);
if (product == null)
{
return HttpNotFound();
}
return View(product);
}
//
// POST: /Product/Delete/5
[HttpPost, ActionName("Delete")]
public ActionResult DeleteConfirmed(int id)
{
productService.DeleteProduct(id);
return RedirectToAction("Index");
}
protected override void Dispose(bool disposing)
{
productService.Dispose();
base.Dispose(disposing);
}
}
}
6. Implementing Test at the business
In order to start writing unit test there are a couple of things you have to know ,
First as we have discussed earlier all the layers are separated nicely so you need something which simulates the DI task , on which we can configure all dependency required to run our test. For this purpose I will use Moq(Read more: https://code.google.com/p/moq/)
1. Install Moq in your test project
https://code.google.com/p/moq/downloads/detail?name=Moq.4.0.10827.Final.zip&can=2&q=
Extract and reference in your test project
2.Write your test class as shown below
//ProductServiceTests.cs
[TestClass]
public class ProductServiceTest
{
[TestMethod]
public void Can_Add_Product_Service()
{
//Arrange
List products = new List()
{
new Product(){Description="P1",ProductId=1,ProductName="p1Name"},
new Product(){Description="P2",ProductId=2,ProductName="p2Name"},
new Product(){Description="P3",ProductId=3,ProductName="p3Name"},
new Product(){Description="P4",ProductId=4,ProductName="p4Name"},
new Product(){Description="P5",ProductId=5,ProductName="p5Name"},
new Product(){Description="P6",ProductId=6,ProductName="p6Name"},
};
Mock> mock1 = new Mock>();
//Here we are going to mock repository GetAll method
mock1.Setup(m => m.GetAll()).Returns(products);
//Here we are going to mock repository Add method
mock1.Setup(m => m.Add(It.IsAny())).Returns((Product target) =>
{
var original = products.FirstOrDefault(
q => q.ProductId == target.ProductId);
if (original != null)
{
return false;
}
products.Add(target);
return true;
});
//Now we have our repository ready for property injection
//Here we are going to mock our IUnitOfWork
Mock mock = new Mock();
//Here we are going to inject our repository to the property
//mock.SetupProperty(m => m.ProductRepository).SetReturnsDefault(mock1.Object);
mock.Setup(m => m.ProductRepository).Returns(mock1.Object);
//Now our UnitOfWork is ready to be injected to the service
//Here we inject UnitOfWork to constractor of our service
ProductService productService = new ProductService(mock.Object);
//Act
productService.AddProduct(new Product
{
ProductId = 7,
ProductName = "P7Name",
Description = "P7"
});
var result = productService.GetAllProduct();
var newProduct = result.FirstOrDefault(t => t.ProductId == 7);
//Assert
Assert.AreEqual(products.Count, result.Count);
Assert.AreEqual("P7Name", newProduct.ProductName);
Assert.AreEqual("P7", newProduct.Description);
}
[TestMethod]
public void Can_Get_All_Product_Service()
{
//Arrange
List products = new List()
{
new Product(){Description="P1",ProductId=1,ProductName="p1Name"},
new Product(){Description="P2",ProductId=2,ProductName="p2Name"},
new Product(){Description="P3",ProductId=3,ProductName="p3Name"},
new Product(){Description="P4",ProductId=4,ProductName="p4Name"},
new Product(){Description="P5",ProductId=5,ProductName="p5Name"},
new Product(){Description="P6",ProductId=6,ProductName="p6Name"},
};
Mock> mock1 = new Mock>();
//Here we are going to mock repository GetAll method
mock1.Setup(m => m.GetAll()).Returns(products);
//Now we have our repository ready for property injection
//Here we are going to mock our IUnitOfWork
Mock mock = new Mock();
//Here we are going to inject our repository to the property
mock.Setup(m => m.ProductRepository).Returns(mock1.Object);
//Now our UnitOfWork is ready to be injected to the service
//Here we inject UnitOfWork to constractor of our service
ProductService productService = new ProductService(mock.Object);
//Act
var result = productService.GetAllProduct();
//Assert
Assert.AreEqual(products,result);
}
[TestMethod]
public void Can_Update_Product_Service()
{
//Arrange
List products = new List()
{
new Product(){Description="P1",ProductId=1,ProductName="p1Name"},
new Product(){Description="P2",ProductId=2,ProductName="p2Name"},
new Product(){Description="P3",ProductId=3,ProductName="p3Name"},
new Product(){Description="P4",ProductId=4,ProductName="p4Name"},
new Product(){Description="P5",ProductId=5,ProductName="p5Name"},
new Product(){Description="P6",ProductId=6,ProductName="p6Name"},
};
Mock> mock1 = new Mock>();
//Here we are going to mock repository Edit method
mock1.Setup(m => m.Edit(It.IsAny())).Returns((Product target) =>
{
var original = products.FirstOrDefault(
q => q.ProductId == target.ProductId);
if (original == null)
{
return false;
}
original.ProductName = target.ProductName;
original.Description = target.Description;
return true;
});
//Now we have our repository ready for property injection
//Here we are going to mock our IUnitOfWork
Mock mock = new Mock();
//Here we are going to inject our repository to the property
mock.Setup(m => m.ProductRepository).Returns(mock1.Object);
//Now our UnitOfWork is ready to be injected to the service
//Here we inject UnitOfWork to constractor of our service
ProductService productService = new ProductService(mock.Object);
//Act
var updatedProduct = new Product() {ProductId = 1, ProductName = "p1NameModified", Description = "p1Modified"};
productService.UpdateProduct(updatedProduct);
var actualProduct = products.FirstOrDefault(t => t.ProductId == 1);
//Assert
Assert.AreEqual(actualProduct.ProductName ,updatedProduct.ProductName);
Assert.AreEqual(actualProduct.Description, updatedProduct.Description);
}
[TestMethod]
public void Can_Delete_Product_Service()
{
//Arrange
List products = new List()
{
new Product(){Description="P1",ProductId=1,ProductName="p1Name"},
new Product(){Description="P2",ProductId=2,ProductName="p2Name"},
new Product(){Description="P3",ProductId=3,ProductName="p3Name"},
new Product(){Description="P4",ProductId=4,ProductName="p4Name"},
new Product(){Description="P5",ProductId=5,ProductName="p5Name"},
new Product(){Description="P6",ProductId=6,ProductName="p6Name"},
};
Mock> mock1 = new Mock>();
//Here we are going to mock repository FindById Method
mock1.Setup(m => m.FindById(It.IsAny())).Returns((int i) => products.Single(x => x.ProductId == i));
//Here we are going to mock repository Delete method
mock1.Setup(m => m.Delete(It.IsAny())).Returns((Product target) =>
{
var original = products.FirstOrDefault(
q => q.ProductId == target.ProductId);
if (original == null)
{
return false;
}
products.Remove(target);
return true;
});
//Now we have our repository ready for property injection
//Here we are going to mock our IUnitOfWork
Mock mock = new Mock();
//Here we are going to inject our repository to the property
mock.Setup(m => m.ProductRepository).Returns(mock1.Object);
//Now our UnitOfWork is ready to be injected to the service
//Here we inject UnitOfWork to constractor of our service
ProductService productService = new ProductService(mock.Object);
//Act
productService.DeleteProduct(6);
var result = products.FirstOrDefault(t => t.ProductId == 6);
//Assert
Assert.IsNull(result);
Assert.IsTrue(products.Count ==5);
}
[TestMethod]
public void Can_Find_By_Id()
{
//Arrange
List products = new List()
{
new Product(){Description="P1",ProductId=1,ProductName="p1Name"},
new Product(){Description="P2",ProductId=2,ProductName="p2Name"},
new Product(){Description="P3",ProductId=3,ProductName="p3Name"},
new Product(){Description="P4",ProductId=4,ProductName="p4Name"},
new Product(){Description="P5",ProductId=5,ProductName="p5Name"},
new Product(){Description="P6",ProductId=6,ProductName="p6Name"},
};
Mock> mock1 = new Mock>();
//Here we are going to mock repository FindById Method
mock1.Setup(m => m.FindById(It.IsAny())).Returns((int i) => products.Single(x => x.ProductId == i));
//Now we have our repository ready for property injection
//Here we are going to mock our IUnitOfWork
Mock mock = new Mock();
//Here we are going to inject our repository to the property
mock.Setup(m => m.ProductRepository).Returns(mock1.Object);
//Now our UnitOfWork is ready to be injected to the service
//Here we inject UnitOfWork to constractor of our service
ProductService productService = new ProductService(mock.Object);
//Act
var result = productService.GetProduct(6);
var expected = products.Single(t => t.ProductId == 6);
//Assert
Assert.AreEqual(expected,result);
}
}
7. Implementing Controller unit test
In implementing test on your controller you can achieve UI Test simply, for this purpose I have created sample tests as shown below.
// ProductControllerTest.cs
[TestClass]
public class ProductControllerTest
{
[TestMethod]
public void Can_Paginate()
{
//Arrange
Mock mock= new Mock();
var products = new List();
mock.Setup(m => m.GetAllProduct()).Returns((new Product[]
{
new Product(){ProductId=1,ProductName = "P1"} ,
new Product(){ProductId=2,ProductName = "P2"} ,
new Product(){ProductId=3,ProductName = "P3"} ,
new Product(){ProductId=4,ProductName = "P4"} ,
new Product(){ProductId=5,ProductName = "P5"} ,
}).ToList());
ProductController controller=new ProductController(mock.Object);
controller.PageSize = 3;
//Act
ProductListViewModel result = (ProductListViewModel)controller.Index(2).Model;
//Assert
Product[] prodArray = result.Products.ToArray();
Assert.IsTrue(prodArray.Length==2);
Assert.AreEqual(prodArray[0].ProductName,"P4");
Assert.AreEqual(prodArray[1].ProductName,"P5");
}
[TestMethod]
public void Can_Generate_Page_Links()
{
//Arrange -define an Html helper -we need to do this
//in order to apply the extension method
HtmlHelper myHelper = null;
//Arrange -create PagingInfo data
PagingInfo pagingInfo = new PagingInfo()
{
CurrentPage = 2,
TotalItems = 28,
ItemsPerPage = 10
};
//Arrange -set up the delegate using a lamda expression
Func pageUrlDelegate = i => "Page" + i;
//Act
MvcHtmlString result = myHelper.PageLinks(pagingInfo, pageUrlDelegate);
//Assert
Assert.AreEqual(result.ToString(),@"1"
+@"2"+
@"3");
}
[TestMethod]
public void Can_Send_Pagination_View_Model()
{
Mock mock = new Mock();
mock.Setup(m => m.GetAllProduct()).Returns((new Product[]
{
new Product(){ProductId=1,ProductName = "P1"} ,
new Product(){ProductId=2,ProductName = "P2"} ,
new Product(){ProductId=3,ProductName = "P3"} ,
new Product(){ProductId=4,ProductName = "P4"} ,
new Product(){ProductId=5,ProductName = "P5"} ,
}).ToList());
ProductController controller = new ProductController(mock.Object);
controller.PageSize = 3;
//Act
ProductListViewModel result = (ProductListViewModel) controller.Index(2).Model;
//Assert
PagingInfo pagingInfo = result.PagingInfo;
Assert.AreEqual(pagingInfo.CurrentPage,2);
Assert.AreEqual(pagingInfo.ItemsPerPage,3);
Assert.AreEqual(pagingInfo.TotalItems,5);
Assert.AreEqual(pagingInfo.TotalPages,2);
}
}
Không có nhận xét nào:
Đăng nhận xét