Viết ASP.NET bằng MVP và NHibernate phần 4 - Castle Windsor
Posted On Friday, October 31, 2008 at at 9:20 PM by UnknownBài viết này được dịch, tóm tắt và bổ sung dựa vào bài viết NHibernate Best Practices with ASP.NET, 1.2nd Ed trên code Project. Các code sample trong bài dựa vào database Northwind của Microsoft và tham khảo 99,99% từ code mẫu của tác giả Billy McCafferty.
I. Giới thiệu Inversion of Control và Castle Windsor
I.1 Giới thiệu
- Có rất nhiều website và blog nói về Inversion of Control và Dependency Injection. Có nguồn cho rằng đây là hai khái niệm khác nhau (MSDN) nhưng sự phụ Martin Fowler thì cho rằng Dependency Injection là tên gọi khác của Inversion of Control để khỏi confused. Tui thì thấy giải thích của bác Martin Fowler khá ổn, dù gì cũng là người lớn tuổi nên chắc không lừa con nít tụi mình . Trên trang MSDN cúng giải thích khái niệm này rất bình dân nên dân đen như tụi mình rất dễ hiểu:
Inversion of Control (IoC) có nghĩa rằng các object không phải tự nó tạo ra các objects mà nó phụ thuộc. Thay vào đó, nó sẽ có những objects nó cần nhờ vào một bên thứ ba.
- Còn Inversion of Control Container là một library dựa trên nguyên tắc của IoC để hỗ trợ chúng ta trong việc tạo và hủy các đối tượng trong chương trình. Khi nhắc đến Dependency Injection, ngoài Spring.NET có lẽ người ta sẽ nghĩ đến Castle Windsor. Castle Windsor là một Container mà phần chính của nó là Castle MicroKernel (cũng là 1 container). Theo như Castle Prolect thì Windsor Container là một library dựa trên library MicroKernel nhưng có hỗ trợ thêm khả năng config uyển chuyển và một số tính năng khác. Nói cách khác, Windsor Container tiện dụng hơn MicroKernel và đối với những ai đã sử dụng Spring.NET thì sẽ thấy rằng Windsor cách dùng đơn giản như đang giỡn
Hình : Lâu đài Windsor ở Anh Quốc
I.2 Cách dùng Windsor
- Trước tiên hãy nghĩ xem Castle Windsor giúp chúng ta giải quyết problem gì. Người ta thường phân chia chương trình thành các module, các layer hay các tầng, tầng này khai báo Interface và tầng kia sẽ có những lớp implement các interface này. Như vậy với cách làm thông thường chắc chắn bằng cách nào đó sẽ có sự phụ thuộc, sẽ có các dòng code đại loại như : IOrderDao orderDao = new NHibernateOrderDao(). Và như vậy rõ ràng nơi nào sử dụng dòng code này đã có sự phụ thuộc vào module/layer chứa lớp NHibernateOrderDao. Người ta muốn tránh sự phụ thuộc này bằng cách thay dòng code có chữ new bằng một cách khác và cách người ta sử dụng chính là Dependency Injection. Có 3 khái niệm Dependency Injection: Constructor injection, Setter Injection và Interface Injection nhưng cái chúng ta thường gặp nhất là Interface Injection. Trong phần này ta sẽ tìm hiểu cách sử dụng Windsor Conainer và Interface Injection.
- Để khỏi phải new 1 object NHibernateOrderDao như trên, ta sẽ sử dụng Castle Windsor. Ta tạo 1 file config tên là Windsor.config rồi add vào project, nội dung của file này như sau:
<configuration>
service="MyProjectName.Core.DataInterfaces.IOrderDao, MyProjectName.Core">
<parameters>
<nameOfParameter2>100</nameOfParameter2>
</parameters>
</component>
</components>
</configuration>
Code 1: Nội dung file config cho Castle Windsor
- Bước thứ hai, mình mở mở file config và thêm vào như sau:
<configuration>
</configSections>
<castle>
</castle>
</configuration>
Code 2: Thêm config cho Windsor vào file app/web.config
- Theo như config trên thì file app/web config và file Windsor.config phải ở cùng thư mục với nhau. Trong Code1, ta khai báo một component với id là orderDao (có thể đặt tên gì cũng được), trong đó type và service lần lượt là Interface và class implement tương ứng. Trong phần parameters, ta khai báo các value cần thiết cho constructor của lớp NHibernateOrderDao. Và cuối cùng trong chương trình, ta sẽ dùng code sau đây để lấy được instance của class NHibernateOrderDao như mong muốn:
IOrderDao = (IOrderDao)windsorContainer.Resolve("orderDao");
// Or : IOrderDao = (IOrderDao)windsorContainer.Resolve(typeof(IOrderDao));
Code 3: Lấy instance của class NHibernateOrderDao như mong muốn
II. Tạo Web project và cấu hình Windsor, NHibernate
II.1 Load config cho Windsor trong global.asax
- Sau khi tạo một project Web, add file Windsor.config và sửa file web.config như đã nói ở trên xong thì điều tiếp theo ta sẽ nghĩ xem nên load cái config ấy như thế nào. Rõ ràng không nên cứ load config này mỗi lần muốn Resolve 1 object. Tương tự như khi sử dụng log4net cho web application, chúng ta phải load config log4net 1 lần duy nhất và code cho hành động này được đặt ở file global.asax. Nhưng khác với log4net, chúng ta cần một cách nào đó để giữ lại instance của WindsorContainer nên không thể thực hiện trong Global.asax được. Cách của bác Billy rất hay, đó là viết 1 lớp CustomHttpApplication, đặt tất cả các cấu hình cần thiết vào Application_Start, kế đến khai báo một public property để giữ instance của Windsor Container trong suốt thời gian sống của Application và vì thế instance của Windsor Container có thể được sử dụng bất cứ nơi nào trong tầng Web, cụ thể là các Page và các UserControl:
public static IWindsorContainer WindsorContainer
{
get { return windsorContainer; }
}
public void Application_Start(object sender, EventArgs e) {
// Initialize log4net
XmlConfigurator.Configure();
// Create the Windsor Container for IoC.
// Supplying "XmlInterpreter" as the parameter tells Windsor
// to look at web.config for any necessary configuration.
windsorContainer = new WindsorContainer(new XmlInterpreter());
}
public void Application_End(object sender, EventArgs e) {
windsorContainer.Dispose();
}
Code 4: Giữ instance của WindsorContainer trong HttpApplication
II.2 Open Session In View Principle
- Nếu google cụm từ trên thì các bạn sẽ thấy rất nhiều topic viết về chủ đề này trong đó tui thấy nên đọc nhất là trang của Hibernate.org. Vấn đề chúng ta mong muốn giải quyết ở đây là hạn chế số lần NHibernate mở connection đến database. Trong một view bất kì (page/usercontrol) có thể ta sẽ cần load nhiều dữ liệu, gọi nhiều query để load nhiều thông tin khác nhau để hiển thị trên form. Nếu với cách làm bình thường mỗi lần cần 1 Dao object nào đó select dữ liệu ta lại để NHibernate mở một connection thì performance của chương trình sẽ rất thấp, bởi vậy người ta nghĩ ra ý tưởng chỉ mở 1 connection ứng với mỗi View. Kĩ thuật của tác giả là khi có một request đến một view, một connection hay còn gọi là NHibernate session sẽ được tạo ra nếu chưa có, sau khi được sử dụng session NHibernate này sẽ được lưu trong 1 Hashtable, và nếu trong view đó tiếp tục còn Request khác thì session NHibernate này sẽ được lấy ra để tiếp tục sử dụng. Cuối cùng khi không còn Request nào nữa, ứng với sự kiện EndRequest của 1 view thì NHibernate session này sẽ được lấy ra lần cuối để close đi, sau đó empty Hashtable. Vậy nếu trên 1 trang aspx của ta có 3 usercontrol tương ứng 3 view sẽ có 3 lần mở connection đến database mà thôi thay vì mỗi usercontrol bản thân nó lại mở cả đống connection. Vậy làm sao để NHibernate Session chỉ mở một lần ứng với 1 View? Người ta có thể dùng HttpModule để thực hiện mục đích này.
- Như chúng ta biết HttpModule được khai báo trong file web.config và cho phép chúng ta implement những function bổ xung cho web application. Một HttpModule sẽ được gọi ngay vào đầu và sau khi có request đến 1 View bất kì. Thông thường người ta sử dụng HttpModule để phục vụ việc Logging hay check Security, trong trường hợp này ta đã implement lớp NHibernateSessionModule(Xem bài 3) để quản lý việc đóng và commit transaction. Lớp này được đặt trong project ProjectBase.Data và Pattern này có thể được sử dụng trong nhiều prolect khác nhau của bạn:
/// Commits and closes the NHibernate session provided by the supplied <see cref="NHibernateSessionManager"/>.
/// Assumes a transaction was begun at the beginning of the request; but a transaction or session does
/// not *have* to be opened for this to operate successfully.
/// </summary>
private void CommitAndCloseSession(object sender, EventArgs e) {
OpenSessionInViewSection openSessionInViewSection = GetOpenSessionInViewSection();
try {
// Commit every session factory that's holding a transactional session
foreach (SessionFactoryElement sessionFactorySettings in openSessionInViewSection.SessionFactories) {
if (sessionFactorySettings.IsTransactional) {
NHibernateSessionManager.Instance.CommitTransactionOn(sessionFactorySettings.FactoryConfigPath);
}
}
}
finally {
// No matter what happens, make sure all the sessions get closed
foreach (SessionFactoryElement sessionFactorySettings in openSessionInViewSection.SessionFactories) {
NHibernateSessionManager.Instance.CloseSessionOn(sessionFactorySettings.FactoryConfigPath);
}
}
}
Code 5: Implement 1 lớp HttpModule để apply Open In View Principle
- Và đây là một phần của file web.config để khai báo HttpModule này:
<httpModules>
</httpModules>
</system.web>
Code 6: Khai báo HttpModule trong web.config
III. Cải tiến DaoFactory bằng Generic và Castle Windsor
- Hiện tại có một vấn đề với DaoFactory của chúng ta. Khi cần tạo mới một class XXXDao nào đó cho một entity mới, chúng ta phải mở code, sửa lại interface IDaoFactory và thêm vào một dòng IXXXDao GetXXXDao(). Tất nhiên chúng ta cũng phải sửa lại lớp NHibernateDaoFactory, implement thêm method GetXXXDao(). Như vậy khá là bất tiện. Bác huynguyen_fisherman (đồng nghiệp, PM của project NHAU) đã đề nghị tui cách làm khác như sau:
1/ Refactoring lại interface IDao, thay tên của nó thành IGenericDao:
Hình : Refactor lại interface IDao
2/ Thêm vào một interface rỗng IDao và để IGeneric Dao inherit từ interface IDao này
3/ Sửa lại interface IDaoFactory như sau:
Code 7: Sửa lại interface IDaoFactory
4/ Implement lại class NHibernateDaoFactory:
{
public NHibernateDaoFactory()
{
}
// id is "component id" that we "declare" in the CastleComponents.config
public T GetDao
{
Check.Require(string.IsNullOrEmpty(id) == false, "component id cannot be null or empty");
return (T)_windsorContainer.Resolve(typeof(T));
}
private IWindsorContainer _windsorContainer = new WindsorContainer(new XmlInterpreter());
}
Code 8: Sửa lại class NHibernateDaoFactory
5/ Và đây là cách sử dụng để lấy 1 Dao object như ý muốn
ICustomerDao customerDao = daoFactory.GetDao<ICustomerDao>("CustomerDao");
Code 9: Cách lấy một Dao bằng NHibernateDaoFactory
IV. Tóm tắt & Kết luận
- Castle Windsor là một IOC Container rất được ưa thích, nếu các bạn xem ở blog này http://www.hanselman.com/blog/ListOfNETDependencyInjectionContainersIOC.aspx sẽ thấy rằng Windsor xếp đầu bảng và ngoài nó ra còn rất nhiều library Dependcy Injection nữa. Trong số đó thì có Unity và NInjection là hai IoC Container tui muốn tìm hiểu khi có thời gian :D.
- Trong bài này ta đã tìm hiểu cách áp dụng Generic và IoC Container để làm cho code gọn gàng, dễ sửa đổi. Chúng ta chỉ mới tạo project web và chuẩn bị trước một số thứ như config NHibernate và Windsor cũng như làm quen với Open In View Principle. Trong phần tiếp theo chúng ta sẽ tìm hiểu cách áp dụng pattern MVP để làm các trang aspx. Cách làm mới có gì khác so với cách làm truyền thống? Hãy chờ hồi sau sẽ rõ
Code phần 4: http://nthoaiblog.googlepages.com/EnterpriseSample-part4.zip
Các đoạn code minh họa trong bài viết được tui rút gọn cho dễ hiểu, code được implement cuối cùng trong demo sẽ có nhiều điểm khác biệt...
(Còn tiếp)
Tham khảo:
http://martinfowler.com/articles/injection.html
http://msdn.microsoft.com/en-us/library/aa973811.aspx
http://www.castleproject.org/container/documentation/v1rc3/concepts/ioc.html
http://en.wikipedia.org/wiki/Inversion_of_control
http://sourceforge.net/forum/message.php?msg_id=2847509
http://www.hibernate.org/43.html
http://www.builderau.com.au/program/dotnet/print.htm?TYPE=story&AT=339284537-339028399t-320002019c
http://dotnetslackers.com/articles/designpatterns/InversionOfControlAndDependencyInjectionWithCastleWindsorContainerPart1.aspx
http://dotnetslackers.com/articles/designpatterns/InversionOfControlAndDependencyInjectionWithCastleWindsorContainerPart2.aspx
Viết ASP.NET bằng MVP và NHibernate phần 3 - Unit Testing
Posted On Friday, October 24, 2008 at at 10:33 AM by UnknownBài viết này được dịch, tóm tắt và bổ sung dựa vào bài viết NHibernate Best Practices with ASP.NET, 1.2nd Ed trên code Project. Các code sample trong bài dựa vào database Northwind của Microsoft và tham khảo 99,99% từ code mẫu của tác giả Billy McCafferty.
I. Giới thiệu Unit Testing
- Xin mở đầu bằng một tình huống thế này: Anh Nguyễn Văn Chuối được assign một task là viết một hàm kiểm tra tính hợp lệ cho dữ liệu nhập vào một text box. Dữ liệu này là một chuỗi các chữ số và dấu chấm của một số kiểu double có giá trị lớn hơn hoặc bằng 0. Giá trị chuỗi chỉ được chứa tối đa 1 chữ số sau dấu chấm. Chuỗi số này có thể là giá trị của số nguyên tức là không có dấu chấm nào cả, và cuối cùng giá trị của số nhập vào phải nằm trong khoảng 0 đến 100. Với nhiều điều kiện ràng buộc như vậy anh Chuối quyết định áp dụng Test Driven Development kết hợp với Unit Test để thực hiện và anh Chuối đã viết một hàm test thể hiện mọi yêu cầu như sau:
Code 1: Ví dụ về một hàm test
- Tất nhiên sau khi viết hàm Test, anh Chuối sẽ bắt tay vào implement class NumberChecker để cái test này pass. Anh Chuối cho rằng nên kết hợp Regular Expression và các hàm Parse của kiểu double là nhanh nhất , do đó anh Chuối đã làm như sau:
Code 2: Implement lớp checknumber để Unit Test pass
- Lúc đó, trong team của anh Chuối có chị Bưởi là một QC khét tiếng khó chịu, chị Bưởi này có tật là test và soi mói chương trình rất kĩ và đã phát hiện ra rất nhiều bug hiểm hóc mà một developer chân chính như anh Chuối không ngờ tới. Sau khi test, chị Bưởi phát hiện ra rằng nếu người ta nhập vào 000 thì chương trinh vẫn cho nhập, ngược lại khi nhập +100 thì chương trình báo lỗi không hợp lệ. Phát hiện được bug này chị Bưởi rất vui và assign liền 1 bug cho anh Chuối với status là critical.
- Rõ ràng là anh Chuối đã tính toán thiếu một số trường hợp. Hàm test sẽ được thêm vào một số dòng code để check các trường hợp chị Bưởi liệt kê, đồng thời regular expression của anh Chuối phải được sửa lại cho đúng.
- Trong ngành phần mềm, thuật ngữ Unit Testing là một phương pháp dùng để kiểm tra tính đúng đắn của một đơn vị source code. Một Unit (đơn vị) source code là phần nhỏ nhất có thể test được của chương trình. Trong lập trình thủ tục, một unit có thể là cả chương trình, một function hay một procedure. Còn trong lập trình hướng đối tượng, đơn vị nhỏ nhất có lẽ là một method của một class nào đó.
- Điều kiện lý tưởng nhất là mỗt test case phải độc lập với những test case khác. Người ta có thể dùng nhiều kĩ thuật như stubs, mock hoặc fake objects, … để phục vụ việc test các module trong chương trình. Viết Unit test là trách nhiệm, nghĩa vụ và quyền lợi của các lập trình viên, lập trình viên chúng ta nên sử dụng Unit Test để bảo đảm những gì mình viết chạy đúng như yêu cầu phần mềm, và nhất là đúng với cách mình hiểu.
I.1 Lợi ích của Unit Test
- Nhiều . Mục đích của Unit Test là cô lập từng phần của chương trình và đảm bảo những phần đó chạy đúng như yêu cầu. Unit test giúp bảo đảm tính chính xác của chương trình, nó giúp thiết lập những ràng buộc và những phần code của chúng ta phải thực hiện chính xác những ràng buộc đó. Kết quả là Unit Test đem lại rất nhiều lợi ích, nhưng rõ ràng nhất là nó giúp phát hiện lỗi và những vấn đề liên quan ngay từ những phase đầu tiên của quá trình phát triển phần mềm.
I.2 Unit Test giúp cho việc sửa đổi dễ dàng hơn
- Trên lý thuyết, Unit Test cho phép lập trình viên refactor code và bảo đảm những gì anh ta viết vẫn chạy đúng sau khi code bị thay đổi. Để làm được điều này, người ta buộc phải viết các test case cho tất cả các function và methods và do đó bất cứ một thay đổi nào làm chương trình chạy sai sẽ bị phát hiện kịp thời và buộc người gây lỗi phải fix ngay. Còn trong thực tế để những test case của bạn cover hết toàn bộ những trường hợp trong chương trình lại là một vấn đề khác.
- Nếu như team của bạn có sử dụng những hệ thống build tự động như CruiseControl.NET có sử dụng Nunit test thì mỗi lần commit code gây lỗi sẽ dễ dàng phát hiện thủ phạm như hình dưới đây:
Hình 1: Giao diện Report của CruiseControl.NET
I.3 Unit test giúp tính hợp code dễ hơn
- Bạn làm việc trong một team, mỗi người làm một phần của chương trình và mỗi phần bạn viết đều đã apply unit test kĩ càng. Đến khi kết hợp những thành phần của team với nhau, quá trình đó nói chung sẽ rất xuông sẽ và ít lỗi hơn nhiều so với việc mạnh ai nấy code rồi cuối cùng merge lại với nhau.
I.4 Document và Design
- Mỗi một test case bạn viết có thể được xem như API document cho chính method được test. Một team member vào sau bạn có thể dự vào test case đó để hiểu hàm này công dụng là gì, input thế nào và output ra sao.
- Trong quá trình phát triển phần mềm, document của chương trình bao gồm các design, requirement có thể bị bỏ quên và trở nên “out of date” nhưng những Unit Test Cases sẽ luôn chính xác những gì chương trình thực hiện và vì vậy ở một khía cạnh nào đó, Unit Test có thể được xem như một dạng document của chương trình.
I.5 Những hạn chế của Unit Test
- Người ta khó có thể viết Unit Test để bắt tất cả các lỗi của 1 chương trình. Thêm vào đó, những test case ta viết chỉ kiểm lỗi những unit nhỏ nhất của chương trình do đó không thể nào lường trước những vấn đề có thể xảy ra khi kết hợp các module với nhau. Unit testing sẽ thể hiện được hiệu quả rõ nhất khi kết hợp nó với những kĩ thuật test khác và tất nhiên sẽ cần tới sức người. Unit Testing không thể nào thay thế được QC – Tester và cũng như nhiều kiểu test khác, nó chỉ có thể kiểm tra được những lỗi đã biết chứ không thể sử dụng nó để tìm ra các lỗi tiềm ẩn của chương trình.
- Software testing là một tổ hợp của nhiều trường hợp. Ví dụ như để kiểm tra một hàm trả về kiểu boolean, tức là có hai trường hợp trả về chúng ta thường phải viết ít nhất hai dòng code để test lần gọi hàm đó. Anh Nguyễn Văn Chuối rất thường viết những hàm dài cả trăm dòng code với nhiều if / else, làm sao bảo đảm rằng anh Chuối có thể viết một test case có thể cover hết những trường hợp có thể xảy ra. Trong trường hợp đó anh Chuối có thể refactor code để chia nhỏ thân hàm thành nhiều hàm nhỏ hơn rồi từ đó test các hàm nhỏ đó. Nếu team của bạn có sử dụng một Continuous Enviroment với NCoverExplorer thì sẽ dễ dàng phát hiện test case của bạn cover bao nhiêu % chương trình:
Hình 2: Giao diện Report của NCover trong dashboard của CruiseControl.NET
- Có nhiều trường hợp khác chúng ta không thể nào sử dụng Unit Test, chẳng hạn như không thế test private class, private method, … nên nói chung Unit Test là một công cụ hỗ trợ chứ không thể thay thế các kĩ thuật test đang được nhiều người sử dụng.
II. Một số tool và framework hỗ trợ Unit Testing
II.1 NUnit:
Là một unit-testing framework cho ngôn ngữ lập trình .NET được port từ Junit. NUnit có hai dạng là console và GUI. Thực sự thì NUnit thường được sử dụng kết hợp với CruiseControl.NET và dùng để test tự động trên build server, và lập trình viên bình thường cũng không cần download về máy làm gì.II.2 TestDriven.NET:
Là một trong những tool không thể thiếu đối với dân .NET. Khi install vào máy, nó sẽ tích hợp một menu vào Visual Studio.NET và cho phép chúng test, debug các class/method rất tiện lợi, ngoài ra ta có thể sử dụng assembly nunit.framework trong thư mục cài đặt của TestDriven.NET để sử dụng cho project test.Hình 3: Menu run test khi cài TestDriven.NET
II.3 NCover, NCoverExplorer:
Như giới thiệu ở trên, các tool này giúp chúng ta kiểm soát mức độ cover của các test case đối với source code, và cũng giống như NUnit, chúng thường được kết hợp với CruiseControl.NET để report sau khi source code được build tự động.II.4 NMock, NMock2, Rhino Mock và TypeMock
- Các tool trên giúp chúng ta giả lập một object để test một component của chương trình khi mà component này có reference đến một component khác. Chúng ta sẽ sử dụng các kĩ thuật mock này để test project EnterpriseSample.Core bằng cách giả lập các object kiểu IxxxDao mà không cần đến EnterpriseSample.Data. Cách sử dụng các tool trên tương đối giống nhau và sẽ được ví dụ bằng Rhino Mock trong phần dưới đây, thông dụng nhất có lẽ là Rhino Mock và Type Mock
III. Tạo project test sử dụng NUnit và Rhino Mock
- Người ta thường tạo một project dạng class library dành cho các test class. Project này theo đúng tên gọi của nó chỉ có ý nghĩa để test và không có vai trò gì trong sản phầm phần mềm cuối cùng. Thực ra NUnit có thể test bất kí test class nào bên trong một assembly bất kì nên project test có thể là Console application, window application, v.v nhưng thông thường người ta sẽ chọn project loại class library. Có một lưu ý là test class của bạn phải được khai báo public, test method cũng thế. Khi sử dụng NUnit.Framework, các bạn sẽ phải làm quen với những Attribute như [TestFixture], [Test], [Setup], [TearDown], … xin được giải thích ngắn gọn những Attribute thường được sử dụng nhất như sau:
[TestFixture]: Dùng để đánh đấu 1 class là test class, những class khác không có Attribute này sẽ mặc định bị ignore khi NUnit test assembly của bạn.
[Test]: Dùng để đánh dấu 1 method là test method, ý nghĩa của nó tương tự như TestFixture nhưng scope ở cấp method.
[Setup]: Dùng để đánh dấu 1 method sẽ được gọi trước khi 1 test case được gọi. Nếu trong 1 test class có 10 method test, thì mỗi lần một method test được chạy thì NUnit sẽ chạy method được đánh dấu với Setup trước tiên.
[TearDown]: Ngược với Setup, chạy sau mỗi test method.
[TestFixtureSetup]: Tương tự như Setup nhưng ở cấp của class, khi 1 test class được test thì method nào được đánh dấu với attribute này sẽ được chạy trước tiên.
[TestFixtureTearDown]: Ngược với TestFixtureSetup.
- Vậy để apply NUnit Test thì công việc vô cùng đơn giản: tạo một project class library, thêm reference đến dll nunit.framework, thêm 1 class mới, khai báo nó thành public, thêm using nunit.framework, thêm attribute [TestFixture] vào đầu của class, viết một method test và khai báo với attribute [Test]. Cơ bản như vậy là đủ để test, bạn có thể kết hợp nhiều attribute khác cũng như nguyên tắc Inheritance của lập trình hướng đối tượng để có một project test uyển chuyển. Người ta thường sử dụng [Setup] để mở một transaction scope, sau đó dùng [TearDown] để roll back transaction khi test các Dao, như vậy sẽ không có dữ liệu bị thêm xóa vào database và bảo đảm dữ liệu test sẽ như nhau trước khi test các method.
III.1 Tạo dữ liệu test với NUnit
- Trên nguyên tắc, trước khi test bất kì một method test nào thì dữ liệu test phải như nhau. Ví dụ như bạn muốn test xem một Customer có thể thêm và xóa Order hay không thì trước khi test hàm AddOrder và DeleteOrder thông tin về Customer cũng như số lượng Order mà Customer đó đang giữ phải như nhau. Vì vậy người ta thường tạo những lớp Factory chỉ dành riêng để tạo ra dữ liệu Test nhất quán.
- Dữ liệu test của chúng ta trong trường hợp này là các object Customer, Order và HistoricalOrderSummary. Thế nên ta sẽ tạo ra các lớp Factory để tạo các List những object này, các lớp Factory này được đặt trong folder TestFactories bên trong project Test. Ví dụ nội dung lớp TestCustomerFactory như sau:
Code 3: Lớp Factory để tạo các object làm dữ liệu test
III.2 Tạo các Mock Factory và Stub objects
- Nếu các bạn còn nhớ thì trong project EnterpriseSample.Core, ta đã khai báo các Interface DAO, các lớp Domain như Customer, Order sẽ reference đến những Interface này. Còn implementation thực sự của các interface Dao để truy xuất database được đặt ở project EnterpriseSample.Data. Như vậy khi test project EnterpriseSample.Core, người ta thường sử dụng các kĩ thuật Mock hoặc tạo một class implement các Interface này để test. Các Mock hay Stub này sẽ là cascadeur cho các lớp Dao khi ta test EnterpriseSample.Core. Đoạn code dưới đây sử dụng RhinoMock để tạo ra một mock object kiểu ICustomerDao, đóng thể cho CustomerDao:
Code 4: Sử dụng Rhino Mock để tạo một Mocked Dao object
- Anh Nguyễn Văn Chuối thuyết minh đoạn code trên như thế này: tui dùng MockRepository tạo ra một mock object thuộc kiểu ICustomerDao, đặt tên nó là mockedCustomerDao rồi nói với nó là: "lỡ ai có biểu mày lại hỏi mày có biết GetAll hay không thì mày trả lời là biết và đưa cho người ta danh sách Customer của thằng TestCustomersFactory. Còn ai hỏi mày biết GetById không thì cũng trả lời như vậy nghe chưa!". Cuối cùng tui dùng MockRepository để ghi nhớ thằng mock Object vừa được dặn dò kĩ lưỡng, bất cứ ai hỏi đển thằng mocked object này tui sẽ biểu nó ra nói chuyện.
- Thực ra trong bài viết này tác giả Billy McCafferty có thể sử dụng kĩ thuật Mock là đủ, nhưng theo tui nghĩ bác Billy McCafferty muốn cho chúng ta thấy có những cách khác mà không cần dùng Mock, vì thế nên có sự xuất hiện của lớp OrderDaoStub:
Code 5: Ví dụ một lớp Dao Stub dùng để test
- Khi implement 1 interface, buộc lòng chúng ta phải implement tất cả những gì được khai báo trong interface đó nên các bạn thấy rằng lớp Stub này phải khai báo rất nhiều hàm trong khi chúng ta chỉ muốn fake hàm GetByExample. Vì vậy dân đen như tụi mình cư dùng các kĩ thuật Mock cho lành.
III.3 Test Các Domain classes
- Trên nguyên tắc, tất cả các dòng code của bạn viết phải được test qua có nghĩa là từng constructor, từng putblic setter, getter đều nên được test. Nhưng đối với những người có máu lười như tôi thì có thể bỏ qua một số thứ. Các lớp để test các domain class được đặt trong folder Domain bên trong project Test. Nếu bạn có 10 lớp Domain trong chương trình hãy viết 10 lớp test tương ứng ví dụ như sau:
Code 6: Viết Unit Test cho các Domain Classes
III. 4 Test Nhibernate Dao
- Trong phần 3 này chúng ta hãy cứ tiếp tục chấp nhận điều sau: Khi một Dao cần truy xuất database, nó sẽ cần một Nhibernate Session để làm chuyện đó. Nó sẽ lấy Session này ở đâu? Nó sẽ lấy Session nhờ vào lớp NhibernateSessionManager và kết hợp với một giá trị string chứa đường dẫn của một file config chứa các setting cần thiết như Connection String đến database thực. Và đường dẫn này được hard code như là một static property của lớp TestGlobals.cs. Để tiếp tục, yêu cầu các bạn đang sử dụng db server SQL Express 2005 và đã có database Northwind. Nếu chưa có các bạn có thể download ở đây rồi attach Northwnd.MDF vào db server.
- Các lớp Nhibernate Dao là những lớp trực tiếp truy xuất database và chúng ta chuẩn bị test nó. Để test các lớp Dao này chúng ta cần một database thực sư và chúng ta đã chuẩn bị như đã nói ở trên. Xin nhắc lại một lần nữa là trên nguyên tắc, các hàm test nên không ảnh hưởng đến kết quả test của những hàm test khác, điều này có nghĩa là dữ liệu trước và sau khi thực hiện một hàm test là nhất quán. Để đạt được mục đích này, chúng ta tạo một lớp NhibernateTestCase, các lớp test case khác sẽ inherit từ lớp này. Trước khi tìm hiểu tại sao làm vậy, hãy xem implementation của nó:
Code 7: Lớp Test base
- Vậy bất kì lớp test nào inherit từ lớp này sẽ kế thừa được TestFixtureSetup và TestFixtureTearDown của nó. Có nghĩa là trước khi một lớp test được thực thi, NHIbernate Session Manager sẽ mở một transaction và rollback ngay sau khi test xong, nhờ thế dữ liệu test sẽ không bao giờ bị thay đổi. Còn bây giờ là nội dung một lớp Dao Test:
Code 8: Lớp Test NHibernate Dao
- Trong phần 2, chúng ta đã có một lớp Generic Dao giúp tiết kiệm code cho rất nhiều Dao Object khác nhau. Điều này dẫn đến việc là lớp Dao nào nên được test và lớp nào không? Để trả lời câu hỏi này, tác giả đã đưa ra các kinh nghiệm của mình khi viết Test Class:
+ Phải thực hiện test mọi method của Generic Dao. Nếu như bạn có 10 lớp Daos inherit generic Dao này thì chỉ một lớp bất kì trong số các lớp Daos này được test là đủ.
+ Phải test tất cả các method phụ của mỗi Dao nếu bạn có implement thêm.
+ Nếu có một lớp Dao nào không inherit từ Generic Dao như lớp HistoricalOrderSummaryDao thì lớp đó phải được test.
+ Phải chắc chắn dữ liệu test nhất quán trước và sau khi một Dao unit test được gọi và các unit test phải độc lập với nhau.
IV. Tóm tắt & Kết luận
- Trong phần 3 này ta đã làm quen với Unit Testing, các tool và framework phụ trợ, ta cũng đã tìm hiểu qua công dụng và ý nghĩa của từng lớp, từng folder bên trong một project Test. Cách tổ chức lớp cũng như cách tác giả viết Unit Test rất tốt để tham khảo. Bản thân tôi cũng có viết Unit Test nhưng sau khi xem bài viết của Billy McCafferty thì đã quyết định từ nay về sau nếu có viết test sẽ theo cách làm của bác Billy.
- Viết Unit Test tuy không bắt buộc nhưng nó đóng vai trò quan trọng trong qúa trình làm phần mềm. Đối với một số khách hàng lớn họ có thể yêu cầu chúng ta viết Unit Test và phải thoả mãn cover 80% code chẳng hạn. Unit Test không hẳn chỉ để test chương trình, ta có thể sử dụng nó như là một công cụ hỗ trợ debug nhanh khi implement một chức năng nào đó khá phức tạp. Kết hợp với một số kĩ thuật Mock, ta có thể test ngay một module của chương trình khi chưa có hoặc chưa hoàn thành xong các module khác…
- Chắc hẳn chúng ta vẫn còn nhiều thắc mắc đối với cách hoạt động của lớp NhibernateSessionManager. Lớp này thực sự có công dụng gì và được tổ chức thế nào? Hãy chờ hồi sau sẽ rõ
Code phần 3: http://nthoaiblog.googlepages.com/EnterpriseSample-part3.zip
Các đoạn code minh họa trong bài viết được rút gọn cho dễ hiểu, code được implement cuối cùng trong project sẽ có nhiều điểm khác biệt...
(Còn tiếp)
Viết ASP.NET bằng MVP và NHibernate phần 2 - Implement Data Layer
Posted On Monday, October 20, 2008 at at 6:10 PM by UnknownBài viết này được dịch, tóm tắt và bổ sung dựa vào bài viết NHibernate Best Practices with ASP.NET, 1.2nd Ed trên code Project. Các code sample trong bài dựa vào database Northwind của Microsoft và tham khảo 99,99% từ code mẫu của tác giả Billy McCafferty.
I. Nhắc lại kĩ thuật Separated Interface
- Separated Interface là một kĩ thuật trong lập trình nhằm đạt được mục tiêu phân chia rạch ròi sự phụ thuộc giữa các tầng (tiers) trong chương trình. Kĩ thuật này được giới thiệu lần đầu bởi sư phụ Martin Fowler, nó cũng là một trong những nguyên tắc lập trình nổi tiếng được nhắc đến trong cuốn sách Agile Software Development của Robert Martin. Trong thực tế, kĩ thuật này thường được áp dụng khi người ta implement Domain Layer và Data Access Layer. Có một ví dụ thế này, trong domain layer ta tạo một lớp Customer. Lớp Customer này sẽ có nhiều method, một trong số đó là method GetAllCustomer(). Vậy lớp Customer cần phải sử dụng một data access object để xử lý yêu cầu đó. Kết quả là đối tượng Customer ít nhất đã có một sự phụ thuộc nhất định đến CustomerDao (nằm trong Data Access Layer). Mặt khác đối với lớp CustomerDao, nó sẽ trực tiếp truy xuất vào database và trả về kết quả là một mảng/list các đối tượng kiểu Customer, vì thế nó phải phụ thuộc ngược lại Domain Layer vì như chúng ta đã thống nhất với nhau mình sẽ khai báo các class Entity trong Domain Layer (Project EnterpriseSample.Core). Nếu chưa/không biết kĩ thuật này, có thể chúng ta có thể dùng cheat code bằng cách tách tất cả những lớp định nghĩa Entity ra một project khác ví dụ như EnterpriseSampe.Entities. Còn nếu đã biết Dependency Inversion Priciple rồi thì ta sẽ làm một cách sạch sẽ và trong sáng hơn nhiều .
- Có một giải pháp còn cheat ghê hơn nữa là đưa tất cả các lớp DAO, nói chung là mọi thứ trong Data Access Layer vào chung một assembly với Domain Layer, tức là vào chung project EnterpriseSample.Core. Và để các lớp này khỏi chung chạ với nhau, ta phải tạo ra một sự độc lập ảo bằng cách cho chúng vào những folder khác nhau trong project chẳng hạn như folder Domain và folder Data. Cách làm này sẽ gây ra những vấn đề sau đây:
+ Domain layer và Data layer sẽ phụ thuộc hai chiều vào nhau.
+ Ở chung một nhà lại phụ thuộc nhiều thế này thì lâu ngày … sẽ có con chung . Code của 2 layer này sẽ trùng lặp với nhau không chừng.
+ Nếu trong code của Entity object sử dụng trực tiếp một instance của class DAO thì sẽ rất khó Unit Test nó mà không cần database thực. Vì khó test nên có thể người ta sẽ lười test, mà lười rồi thì khỏi viết Unit Test .
- Thực ra cách mà tác giả đề nghị là domain và data layers nên được đặt trong các assembly độc lập. Chẳng hạn như XXXX.Core và XXXX.Data. Domain layer (assembly XXXX.Core) sẽ chứa những lớp domain và DAO Interface. Còn Data Layer (assembly XXXX.Data) chỉ chứa những lớp DAO, những lớp DAO này là những lớp sẽ implement các Interface nói trên trong domain layer. Các bạn hãy xem lại hình vẽ cấu trúc này lần nữa:
Hình 1: Quan hệ giữa project Core và project Data
- Cách làm trong sáng này có một số lợi ích sau:
+Domain Layer sẽ hoàn toàn độc lập với các assembly liên quan đến database như NHibernate hay System.Data.SqlClient
+Domain Layer không cần quan tâm xem data layer hoạt động thế nào, truy xuất loại database gì… Vì thế việc chuyển đổi các loại database khác nhau cũng như thay đổi chi tiết implementation của tầng data layer rất dễ dàng và không hề ảnh hưởng đến code của domain layer.
+ Vì hai layer này quan hệ một chiều với nhau như vậy nên những fan của Unit testing có thể sử dụng các kĩ thuật “Mocked” data-access layer trong domain layer để test các lớp Entity mà không cần có database thực.
II. Khai báo các Interface cần thiết
- Bây giờ chúng ta sẽ từng bước khai báo các interface này. Trước hết là interface IDao, đây là một interface chung nhất khai báo các function cơ bản nhất mà bất cứ một data access object nào cũng có, nội dung interface này như sau:
Code 1: Khai báo các function chung trong interface IDao
- Đây là 1 generic interface với hai kiểu generic là T và IdT. Nếu ta nhìn phần khai báo các function thì cũng đoán ra nếu một lớp DAO nào implement interface này hoặc một interface nào inherit từ interface này sẽ phải khai báo kiểu của Entity và kiểu Id của Entity đó. Ví dụ như hai interface ICustomerDao và ISupplierDao như sau:
Code 2: Các interface kế thừa từ Generic interface IDao
- Vậy đến khi implement thực sự các Data Access Object, ta sẽ implement các interface như ICustomerDao, ISupplierDao. Ở phần 1, chúng ta đã tạo 1 lớp là HistoricalOrderSummaryDao để giữ giá trị khi gọi Stored Procedure. Vì đây là một “value class“ đặc biệt, không phải là một Entity chính thức nên nó không cần Id; do đó interface DAO tương ứng cũng không cần phải kế thừa từ interface IDao:
Code 3: Interface IHistoricalOrderSummaryDao không cần kế thừa từ IDao
- Cuối cùng chúng ta sẽ tạo một interface IDaoFactory, nếu các bạn đã đọc về Factory Pattern thì trong trường hợp này chúng ta đang sử dụng Abstract Factory. Sẽ có 1 lớp Factory implement interface này và tùy chúng ta sử dụng ORM nào thì các lớp DAO tương ứng sẽ được tạo ra. Cho nên trong phần sau chúng ta sẽ viết 1 lớp NHibernateDaoFactory với mục đích tạo ra các NHibernate Dao Object:
Code 4: Nội dung của interface IDaoFactory
III. Làm việc với NHibernate Session
- Khi viết chương trình có sử dụng database, người ta thường tìm cách giới hạn số lần mở connection đến database server sao cho càng ít kết nối càng tốt. Nếu các bạn quan tâm đến vẫn đề này hẵn các bạn sẽ biết đến khái niệm Transaction. Trong NHibernate cũng có một khái niệm như vậy và chúng ta hãy tạm chấp nhận một NHibernate Session là một connection đến database server bằng NHibernate. Khi viết một ứng dụng web, việc giảm thiểu các connection đến database là điều rất đáng để quan tâm. Giả sử chúng ta viết 1 trang aspx trên trang đó có sử dụng 10 usercontrol, mỗi usercontrol lại cần kết nối với database để load dữ liệu riêng của nó. Như vậy một lần mở trang web ta đã mở 10+ connection đến database server. Chúng ta có thể giải quyết vấn đề nhức đầu này bằng cách đưa tất cả các request đó vào cùng một transtaction. Cụ thể thế nào thì chúng ta sẽ bàn vào bài sau, còn trong phạm vi bài này chúng ta hãy chấp nhận rằng mỗi một Dao object khi cần kết nối đến database để đọc/ghi dữ liệu nó sẽ cần đến một NHibernate.ISession. Và tác giả đã implement một lớp NHibernateSessionManager cho chúng ta sử dụng, đối số cần truyền cho method GetSesssionFrom() là một giá trị string chứa đường dẫn đến file config của NHibernate. Cách làm này của tác giả có rất nhiều mục đích như: hỗ trợ truy xuất nhiều database một lúc và sử dụng một kĩ thuật gọi là OpenSessionInView để quản lý connection đến database server,.. Tác giả đã implement các lớp Configuration để hỗ trợ đọc các file config NHibernate vì các config này được đặt vào một file xml riêng và không còn nằm chung trong file web/app.config. Các lớp này được viết một lần nhưng ta có thể sử dụng chúng cho nhiều project khác nên chúng được đặt trong một assembly riêng tên là ProjectBase.Data. Chúng được đặt trong thư mục NhibernateSessionMgmt; nếu các bạn quan tâm có thể tìm hiểu khái niệm Configuration của .NET Framework 2.0
Sau đây là một ví dụ có sử dụng NHibernate session:
Code 9: Sử dụng NHibernate Session
IV. Implement DaoFactory và các lớp Dao tương ứng
- Vậy là cơ bản chúng ta đã hoàn thành xong Domain Layer. Xin nhắc lại project EnterpriseSample.Core chỉ chứa các lớp Entity và các interface Dao, không chứa bất kì một lớp Dao nào. Ngay sau đây chúng ta sẽ tạo các lớp Dao này trong project EnterpriseSample.Data:
IV.1 Generic DAO
- Các lớp Dao là những lớp trực tiếp sử dụng NHibernate để thực hiện các lệnh thêm xóa, sửa, update vào database. Ví dụ ta có CustomerDao sẽ implement các function như UpdateCustomer, GetAllCustomers. Nhìn chung thì mỗi một Entity trong chương trình đều có các method tương tự nhau như: GetById, Save, Delete, Update. Thử tưởng tượng bạn có 20 tables, ứng với 20 class Entity trong chương trình và mỗi một Entity như vậy phải implement ít nhất 4 function GetById, Save, Delete, Update thì đúng là một cực hình. Người ta sẽ giải quyết vấn đề trùng lặp code này bằng cách sử dụng Generic của .NET framework 2.0, tất cả những hàm tương tự nhau trong các lớp Entity sẽ được implement trong một class Generic:
Code 5: Lớp Generic Dao AbstractNHibernateDao
- Các lớp Dao trong chương trình ngoài implement interface IxxxDao sẽ inherit từ lớp abstract này:
Code 6: Cách sử dụng lớp Generic AbstractNHibernateDao
IV.2 DAO Factory
- Dao Factory là gì và tại sao ta lại cần một lớp Factory? Nếu bạn có quan tâm đến Design Pattern chắc là bạn đã nghe đến các pattern như Abstract Factory, Factory Method. Một Dao Factory có khả năng tạo ra các Dao Object theo một tiêu chuẩn nào đó. Trong project này ta sử dụng NHibernate để thao tác với cơ sở dữ liệu, vậy thì Dao Factory của ta sẽ tạo ra các NHibernate Dao Object để tương tác với NHibernate. Ta sẽ tạo ra một lớp là NHibernateDaoFactory với tiêu chí đó, code như sau:
Code 7: Lớp NHibernateDaoFactory
IV.3 Nhận kết quả từ Stored Procedured như thế nào?
- Trong bài trước chúng ta có implement lớp HistoricalOrderSumary để giữ giá trị trả về khi gọi một stored procedure. Ta cũng đã làm một file mapping xml tương ứng cho nó. Nhưng để thực hiện các bước cụ thể nhằm map các kết quả từ database, ta phải làm quen một khái niệm trong NHibernate là IQuery và Transform. Chúng ta sẽ tạo một instance kiểu IQuery bằng static method GetNamedQuery với parameter là tên query được khai báo trong file mapping XML, từ đó hướng dẫn NHibernate map kết quả trả về bằng constructor của lớp HistoricalOrderSumary:
Code 8: Cách map kết quả từ stored procedure
V. Kết luận
- Qua phần hai này, chúng ta đã làm quen với khái niệm Session của NHibernate một cách rất khái quát , trong bài kế tiếp chúng ta sẽ tìm hiều kĩ hơn tại sao tác giả lại tổ chức lớp lang như vậy. Các lớp phục vụ Session management này được viết một lần và sử dụng lại ở nhiều project khác nhau nên tác giả đề nghị ta đưa các lớp này qua một assembly khác để tái sử dụng. Interface IDao và lớp generic Dao AbstractNHibernateDao cũng được đặt trong assembly này vì chúng rất generic và có thể tái sử dụng ở những project khác mà không cần sửa code. Trong assembly EnterpriseSample.Data, chúng ta implement các lớp Dao cũng như NHibernateDaoFactory để tạo ra các NHibernate Dao. Nhờ sử dụng Generic nên rất nhiều code được sử dụng lại nên các bạn thấy rằng rất ít lớp trong EnterpriseSamle.Data và code của những lớp này cũng khá là gọn. Các lớp Dao cũng như lớp Generic Dao sử dụng một instance NHibernate.ISession để thao tác với database, ta chỉ cần truyền đường dẫn file config và Session Manager sẽ trả về một NHibernate session. Mục đích của cách làm này là giúp giảm thiểu số lần mở connection đến database server để tăng performance. Chúng ta sẽ còn bàn nhiều về các kĩ thuật liên quan trong bài thứ 4.
Code phần 2: http://nthoaiblog.googlepages.com/EnterpriseSample-part2.zip
Các đoạn code minh họa trong bài viết được rút gọn cho dễ hiểu, code được implement cuối cùng trong project sẽ có nhiều điểm khác biệt với hình...
(Còn tiếp)