Làm quen với Unit Testing, các tool và framework phụ trợ
Bài đăng này đã không được cập nhật trong 9 năm
Bà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 C đượ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 C đã 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 C sẽ bắt tay vào implement class NumberChecker để cái test này pass. Anh C 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 C đã làm như sau: http://kwnhuw.blu.livefilestore.com/y1pM6Y8UGQ4avVsoBRgf2ayr7TlOGb4FjE5aEnWiVT-Ztx5J7cL1C3t0gslbwi78SwIw3GxsWfNLFs/Code 2.PNG
Code 2: Implement lớp checknumber để Unit Test pass
-
Lúc đó, trong team của anh C có chị B là một QC khét tiếng khó chịu, chị B 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 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 rất vui và assign liền 1 bug cho anh C với status là critical.
-
Rõ ràng là anh C đã 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 liệt kê, đồng thời regular expression của anh C phải được sửa lại cho đúng. ASP.NET, NHibernate và MVP, Unit Testing
-
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
-
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: http://kwnhuw.blu.livefilestore.com/y1pD4MYyBH0923YlDqOQxuEzBdHLjmOGRLvcwx2XLpkVrtTQnljqHj4SZXBPOZIDgPfKU5gJXD8I_E/Hinh 1.PNG
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: http://kwnhuw.blu.livefilestore.com/y1p-Zw2iCg0_-2vTc2ZOhAUaubXEm3lqN7iF2TgvM4ZLdl4RF_qvv9DGnVxzBYqR_f3kbmSR_v-9LA/Hinh 2.PNG
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: ASP.NET, NHibernate và MVP
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::bbpxtay:
- 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…
Nguồn: blogspot.com (http://nthoai.blogspot.com)
All rights reserved