Trong bài Nhập môn Unit Testing trong .NET chúng ta đã tìm hiểu cơ bản về phương pháp kiểm thử Unit Testing, các đơn vị kiểm thử (Unit Tests), lớp Assert và các thuộc tính TestFixture và Test. Chúng ta cũng đã tìm hiểu và cài NUnit, framework chúng ta sẽ dùng cùng với ngôn ngữ C# trong quá trình tìm hiểu Unit Testing.
Bài viết này sẽ khám phá sâu hơn về sự khác nhau giữa mã kiểm thử và mã cần được kiểm thử, cấu trúc của các đơn vị kiểm thử, các thành phần của lớp Assert, v.v.
Để tiện theo dõi, chúng ta sẽ tạo một ứng dụng Console App tên UnitTestingDemo (không liên quan gì ứng dụng trong bài viết Nhập môn Unit Testing trong .NET ) và thêm một lớp tên Cmp. Thay đổi lớp Cmp với phương thức tĩnh Largest như sau:
public class Cmp { public static int Largest(int[] list) { int max = 0; for (int index = 0; index<list.Length-1; index++) { if (list[index] > max) { max = list[index]; } } return max; } }
Phương thức Largest sẽ trả về phần tử lớn nhất trong danh sách các số nguyên list. Một vài dữ liệu mẫu:
[7, 8, 9] -> 9
[8, 7, 9] -> 9
[1] -> 1
[-7, -9, -8] -> -7
Chúng ta cũng thêm đến dự án một lớp tên CmpTest chứa các phương thức dùng để kiểm thử phương thức Largest. Thay đổi lớp CmpTest như sau:
using NUnit.Framework; namespace UnitTestingDemo { class CmpTest { [Test] public void LargestOf3() { } [Test] public void One() { } [Test] public void Negative() { } } }
Cấu trúc của các đơn vị kiểm thử
Các đoạn mã dùng để xây dựng các đơn vị kiểm thử ví dụ các phương thức LargestOf3(), One(), Negative() được gọi là các Test Code; các đoạn mã được kiểm thử ví dụ phương thức Largest() gọi là Production Code.
Nếu Production Code sẽ được cung cấp đến người dùng và khách hàng đầu cuối thì Test Code chỉ dành riêng cho chúng ta – những người phát triển phần mềm. Production Code đến tay khách hàng hay người dùng mà không có Test Code. Mối quan hệ giữa Test Code và Production Code có thể trực quan như sau:
Lớp Assert
Chúng ta đã tìm hiểu một cách sơ lược về lớp Assert trong bài Nhập môn Unit Testing trong .NET và trong bài này chúng ta sẽ tìm hiểu chi tiết hơn về các phương thức hay thuộc tính của lớp này.
Sau đây là các phương thức trong lớp Assert
Phương thức |
Mô tả |
Assert.AreEqual(expected, actual [, string message])
Assert.AreEqual(expected, actual [, string message]) |
Là phương thức thường xuyên được dùng trong quá trình kiểm thử. expected là giá trị kỳ vọng; actual là giá trị thực; message là thông điệp sẽ hiển thị nếu kiểm thử thất bại. Hạn chế sử dụng message trong kiểm thử.
Xác nhận expected và actual khác nhau. message là thông điệp sẽ hiển thị nếu kiểm thử thất bại. Hạn chế sử dụng message trong kiểm thử. |
Assert.Less(x, y)
Assert.Greater(x,y) |
Xác nhận x < y
Xác nhận x > y Với x, y là kiểu số hay kiểu IComparable. |
Assert.GreaterOrEqual(x, y)
Assert.LessOrEqual(x,y) |
Xác nhận x <= y
Xác nhận x >= y Với x, y là kiểu số hay kiểu IComparable. |
Assert.IsNull(object [, string message])
Assert.IsNotNull(object [, string message]) |
Xác nhận một đối tượng là null, ngược lại kiểm thử thất bại.
Xác nhận một đối tượng là khác null, ngược lại kiểm thử thất bại. message là thông điệp tùy chọn khi kiểm thử thất bại. Hạn chế sử dụng message trong kiểm thử. |
Assert.AreSame(expected, actual [, string message])
Assert.AreNotSame(expected, actual [, string message]) |
Xác nhận expected và actual tham chiếu đến cùng một đối tượng, ngược lại kiểm thử thất bại.
Xác nhận expected và actual không tham chiếu đến cùng một đối tượng, ngược lại kiểm thử thất bại. message là thông điệp tùy chọn khi kiểm thử thất bại. Hạn chế sử dụng message trong kiểm thử. |
Assert.IsTrue(bool condition [, string message]) | Xác nhận điều kiện đã cho là đúng, ngược lại kiểm thử thất bại.
message là thông điệp tùy chọn khi kiểm thử thất bại. Hạn chế sử dụng message trong kiểm thử. |
Assert.Fail([string message]) | Kiểm thử thất bại ngay lập tức với thông điệp message. Phương thức này đánh dấu những phần mã sẽ không được dùng. Phương thức này ít dùng trong thực tế |
Lớp Assert được cải tiến với các phương thức mới dùng kiểm thử có điều kiện. Danh sách như sau:
Phương thức |
Mô tả |
Assert.That(actual, Is.EqualTo(expected)) | Tương đương Assert.AreEqual(). Phương thức Is.EqualTo thuộc namespace NUnit.Framework.SyntaxHelpers và trả về đối tượng EqualConstraint. Đoạn mã sau là tương đương
Assert.That(actual, new EqualConstraint(expected)) |
Assert.That(actual, Is.Not.EqualTo(expected)) | Tương đương Assert.AreNotEqual(). Bằng cách dùng Not, nó sẽ bọc một đối tượng EqualConstraint trong một đối tượng NotConstraint:
Assert.That(actual, new NotConstraint(new EqualConstraint(expected))) |
Assert.That(actual, Is.AtMost(expected)) | Tương đương Assert.LessOrEqual(x,y) |
Assert.That(expected, Is.Null) | Xác nhận giá trị expected là null; ngược lại sẽ thất bại. Hai cách dùng đối lập của phương thức này là:
Assert.That(expected, Is.Not.Null); Assert.That(expected, !Is.Null); |
Assert.That(expected, Is.Empty) | Xác nhận giá trị expected là một collection hay một chuỗi rỗng; ngược lại sẽ thất bại. |
Assert.That(actual, Is.AtLeast(expected)) | Tương đương Assert.GreaterOrEqual(x, y) |
Assert.That(actual, Is.InstanceOfType(expected)) | Xác nhận actual là kiểu của expected hay thừa kế từ kiểu expected. |
Assert.That(actual, Has.Length(expected)) | Xác nhận rằng actual có thuộc tính Length trả về giá trị expected. |
Bây giờ chúng ta sẽ vận dụng một vài phương thức đã nêu trên để hoàn thiện các phương thức kiểm thử trong lớp CmpTest
[TestFixture] class CmpTest { [Test] public void LargestOf3() { int[] arr = new int[3]; arr[0] = 8; arr[1] = 7; arr[2] = 9; Assert.That(Cmp.Largest(arr), Is.EqualTo(9)); } [Test] public void One() { Assert.That(Cmp.Largest(new int[] { 1 }), Is.EqualTo(1)); } [Test] public void Negative() { int[] negatives = new int[] { -9, -8, -7 }; Assert.That(Cmp.Largest(negatives), Is.EqualTo(-7)); } }
Thực thi các phương thức kiểm thử bằng cách vào Test > Windows > Test Explorer
Nhấn Run All để thực thi tất cả các phương thức sẽ có kết quả sau đây:
Cả 3 phương thức đều thất bại. Chọn phương thức LargestOf3 và xem phần thông báo bên dưới:
Giá trị lớn nhất trả về từ phương thức Largest là 8 (actual) trong khi chúng ta mong đợi là 9 (expected). Cùng xem lại phương thức Largest:
int max = 0; for (int index = 0; index<list.Length-1; index++) { if (list[index] > max) { max = list[index]; } } return max;
Sai sót đến từ việc gán giá trị đến index, chính xác phải là:
for (int index = 0; index<list.Length; index++)
Thực thi lại tất cả các phương thức kiểm thử, kết quả:
LargestOf3 và One đã thành công nhưng phương thức Negative vẫn thất bại. Chọn phương thức Negative và xem thông báo:
Giá trị kỳ vọng (expected) của chúng ta là -7 nhưng kết quả từ phương thức Largest là 0. Tại sao? Vấn đề chính là chúng ta khởi tạo max:
int max = 0;
Với việc khởi tạo max = 0, giá trị trả về từ Largest sẽ luôn luôn là 0 nếu mảng chứa các giá trị nguyên âm. Giải pháp sẽ là gán cho max giá trị nhỏ hơn tất cả mọi số nguyên âm:
int max = Int32.MinValue;
Thực thi lại tất cả các phương thức (có thể chỉ cần kiểm tra Negative nhưng chúng ta muốn xem thay đổi vừa thực hiện có ảnh hưởng đến các phương thức khác không):
Cả 3 phương thức OK.
Phân loại các đơn vị kiểm thử
NUnit hỗ trợ thuộc tính Category giúp chúng ta phân loại các đơn vị kiểm thử để có thể sử dụng một cách hiệu quả hơn. Ví dụ, quan sát trên Test Explorer chúng ta thấy thời gian thực thi của phương thức LargestOf3 là 14ms trong khi đó Negative và One thời gian nhỏ hơn 1ms. Chúng ta có thể phân loại hai phương thức Negative và One trong một nhóm gọi là Short và đặt phương thức LargestOf3 trong nhóm gọi là Long như sau:
[TestFixture] class CmpTest { [Test] [Category("Long")] public void LargestOf3() { int[] arr = new int[3]; arr[0] = 8; arr[1] = 7; arr[2] = 9; Assert.That(Cmp.Largest(arr), Is.EqualTo(9)); } [Test] [Category("Short")] public void One() { Assert.That(Cmp.Largest(new int[] { 1 }), Is.EqualTo(1)); } [Test] [Category("Short")] public void Negative() { int[] negatives = new int[] { -9, -8, -7 }; Assert.That(Cmp.Largest(negatives), Is.EqualTo(-7)); } }
Trước khi kiểm tra các phương thức được phân loại như thế nào trong Test Explorer, chúng ta vào mục Build > Clean Solution để xóa sạch các kết quả thực thi trước đó. Mở lại Test Explorer:
Nhấn chuột phải vào Not Run Tests chọn Group By > Traits
Kết quả
Có thể mở rộng
Bây giờ nhấn Run All để thực thi tất cả các phương thức kiểm thử, kết quả:
Thuộc tính Setup và Teardown
NUnit cho phép chúng ta thực thi một đơn vị kiểm thử độc lập với các đơn vị kiểm thử khác bằng cách dùng các thuộc tính Setup và Teardown.
Những phương thức được đánh dấu với thuộc tính Setup sẽ thực hiện đầu tiên trước khi các phương thức đánh dấu bởi thuộc tính Test thực thi và các phương thức đánh dấu Teardown sẽ thực hiện cuối cùng sau khi các phương thức đánh dấu Test hoàn thành ngay cả khi các phương thức này phát sinh ngoại lệ. Ví dụ chúng ta cần kiểm tra một vài thao tác đến cơ sở dữ liệu, để tránh tình trạng kết nối và ngắt kết nối lặp lại, chúng ta sẽ dùng các thuộc tính Setup và Teardown như sau:
[TestFixture] public class DBTest { private Connection dbConn; [SetUp] public void PerTestSetup() { dbConn = new Connection("oracle", 1521, user, pw); dbConn.Connect(); } [TearDown] public void PerTestTeardown() { dbConn.Disconnect(); dbConn.Dispose(); } [Test] public void AccountAccess() { // Sử dụng dbConn } [Test] public void EmployeeAccess() { // Sử dụng dbConn } }
Phương thức PerTestSetup() sẽ được gọi trước khi thực thi phương thức AccountAccess() và phương thức PerTestTeardown() sẽ được gọi khi phương thức AccountAccess() hoàn thành. Kế tiếp phương thức PerTestSetup() sẽ được gọi trở lại trước khi phương thức EmployeeAccess() thực thi và PerTestTeardown() được gọi trở lại khi EmployeeAccess() hoàn thành.
NUnit cũng hỗ trợ các thuộc tính TestFixtureSetUp và TestFixtureTearDown cho phép chúng ta bắt đầu và giải phóng một vài thứ sau khi toàn bộ lớp kiểm thử thực thi. Nếu SetUp và TearDown luôn đi theo cặp thì TestFixtureSetUp và TestFixtureTearDown không nhất thiết phải đi theo cặp. Hình ảnh sau minh họa mối quan hệ giữa SetUp, TearDown, TestFixtureSetUp và TestFixtureTearDown:
Tạm dừng các đơn vị kiểm thử
Trong một vài trường hợp chúng ta muốn cập nhật một vài đơn vị kiểm thử và chưa muốn thực thi, NUnit hỗ trợ thuộc tính Ignore để tạm thời bỏ qua các đơn vị kiểm thử. Ví dụ dùng thuộc tính Ignore cho phương thức LargestOf3() như sau:
[Test] [Category("Long")] [Ignore("Out of time. Will Continue Monday. --AH")] public void LargestOf3() { int[] arr = new int[3]; arr[0] = 8; arr[1] = 7; arr[2] = 9; Assert.That(Cmp.Largest(arr), Is.EqualTo(9)); }
Khi thực thi các phương thức trong Test Explorer:
Lời kết
Trong bài viết này chúng ta đã tìm hiểu về mối quan hệ giữa Test Code và Production Code, các phương thức cơ bản trong lớp Assert, phân loại các phương thức kiểm thử dùng thuộc tính Category, tạo tính độc lập cho các phương thức kiểm thử dùng các thuộc tính SetUp và TearDown, và tạm thời trì hoãn thực thi các đơn vị kiểm thử dùng thuộc tính Ignore. Hiểu rõ các phương thức và thuộc tính hỗ trợ bởi NUnit sẽ giúp chúng ta thực hiện việc kiểm thử một cách hiệu quả hơn.
Ý kiến bài viết