Phần 2 sẽ được dịch từ:

Phần 1 xem tại đây.

Giới thiệu

Tôi giả sử rằng bạn đã thấy được những lợi ích cũng như những hạn chế của unit test. Tôi đã đọc hàng tá blog, xem nhiều video, và đọc vài quyển sách về unit test và những cách tiếp cận cũng như những framework khác nhau. Tôi đã làm việc với một vài trong số những framework này cho cả các chương trình nhỏ và cả các phần mềm cho doanh nghiệp. Từ đó nhận thấy rằng (chỉ theo quan điểm cá nhân thôi) NUnitMoq kết hợp tốt với nhau giúp cho việc viết các test đơn giản, nhanh hơn và theo chuẩn thống nhất. Mục đích của blog này là giúp bạn nhanh chóng hiểu và viết được unit test bằng NUnitMoq. Nếu bạn muốn biết rõ hơn và học sâu về unit test sử dụng các mock framework như Moq, FakeItEasy và Typemock Isolator, tôi khuyên bạn nên xem thử The Art of Unit Testing: with examples in C#

Moq là một thư viện chỉ dành cho .NET, tận dụng lợi ích từ các tính năng mới của .NET 3.5 (LinQ expression) và C# 3.0 (Lambda Expression) làm nó thành thư viện hiệu quả, type-safe, và dễ dàng refactor nhất cho mock. Với sự hỗ trợ tốt cho cả interface và class, API của Moq cực kỳ đơn giản, không đòi hỏi bạn cần biết trước và có kinh nghiệm làm việc với mock.

Cài đặt Moq

Việc cài đặt Moq khá là dễ dàng, hoặc bằng cách tải từ trang source của Moq, sau đó thêm các reference thích hợp vào project của bạn, hoặc là có thể cài đặt thông qua Nuget. Ở đây tôi sẽ cài thông qua NuGet Command-Line:

Install-Package Moq

Đặt tên cho unit test

Cách đặt tên cho unit test sẽ khác nhau theo quan điểm của mỗi người. Tôi đã thấy có rất nhiều topic xoay quanh vấn đề này, và sự tranh cãi diễn ra vô cùng kịch liệt. Lời khuyên của tôi là bạn nên đặt tên sao cho test của bạn có thể mô tả được những gì bạn đang làm. Khi nhìn vào tên của test thì bạn có thể thấy được mục đích của nó. Đừng lo lắng khi test có tên quá dài, điều đó không quan trọng, cái quan trọng là những gì nó mô tả. Dưới đây là một vài quy tắc đặt tên, bạn có thể tham khảo.

Pascal case:
public class WhenACustomerIs
{
    public void AddedAndCustomerIsNullAnExceptionIsThrown()
    {
    }
}
Mỗi từ được ngăn bằng dấu gạch dưới:
public class When_A_Customer_Is
{
    public void Added_And_Customer_Is_Null_Exception_Is_Thrown()
    {

    }
}
Hoặc là TênPhươngThức_TrạngTháiĐượcTest_HànhViMongMuốn:
public class Customer
{
    public void Add_CustomerIsNull_ThrowsInvalidOperationException()
    {

    }
}

Cá nhân tôi thích hai cách sau hơn. Và dùng cách nào lại dựa vào project mà tôi đang làm việc. Bạn có thể chọn cách mà bạn thấy thoải mái khi dùng nó, và nếu bạn muốn tạo ra quy tắc đặt tên của riêng mình, hãy dùng nó và truyền bá cho thế giới nhé.

Mocking and Verification (Giả lập và kiểm tra)

Một định nghĩa về Mocking khá là hay từ Wikipedia:

Trong lập trình hướng đối tượng, mock object là object giả lập hành vi của object thật theo cách mà chúng ta muốn. Lập trình viên sử dụng mock object để test hành vi của các object khác. Nó cũng tương tự như việc nhà sản xuất ôtô giả lập một vụ va chạm để xem xét những ảnh hưởng đến những người ngồi trong đó.

Moq hỗ trợ việc tạo mock object một cách dễ dàng:

Mock<IContainer> mockContainer = new Mock<IContainer>();
Mock<ICustomerView> mockView = new Mock<ICustomerView>();

Để lấy một instance của mock object, chúng ta sử dụng thuộc tính Object, là một strongly typed:

Mock<ICustomerView> mockView = new Mock<ICustomerView>();
ICustomerView view = mockView.Object;

Moq cung cấp các phương thức để xác nhận rằng một phương thức cụ thể đã được thực hiện trên mock object của bạn. Lấy ví dụ, bạn có thể muốn xác nhận rằng một phương thức, hoặc setter hay getter của một thuộc tính được gọi. Hay bạn muốn biết số lần gọi một phương thức. Trong trường hợp này, chúng ta sử dụng phương thức Verify của mock object:

mockCustomerRepository.Verify(t => t.Add(It.IsAny<Customer>()));

Đoạn code trên kiểm tra phương thức Add với tham số bất kỳ (có kiểu Customer) được gọi hay không. Bạn cũng có thể kiểm tra với một Customer xác định được truyền vào sử dụng phương thức It.Is:

mockCustomerRepository.Verify(t =>; t.Add(It.Is<Customer>(t => t.Name == "Jon")));

Hay trong trường hợp khác muốn xác nhận số lần một phương thức được gọi:

// Verify that DoesSomething was called only once
myInterfaceMock.Verify((m => m.DoesSomething()), Times.Once());

// Verify that DoesSomething was never called
myInterfaceMock.Verify((m => m.DoesSomething()), Times.Never());

Cú pháp AAA (Arrange, Act, Assert)

Một cấu trúc unit test thường được sử dụng là dạng cú pháp AAA. Lưu ý rằng cú pháp AAA chỉ là một cách để tổ chức unit test của bạn, được hỗ trợ bởi bất kỳ framework nào. Cái chính ở đây là bạn tạo tất cả các phụ thuộc mà phương thức được test cần (Arrange), test phương thức đó (Act) và xác nhận rằng nó đạt được kết quả mong muốn (Assert). Một ví dụ đơn giản:

[Test]
public void Save_CustomerIsNotNull_GetsAddedToRepository()
{
    //Arrange
    Mock<IContainer> mockContainer = new Mock<IContainer>();
    Mock<ICustomerView> mockView = new Mock<ICustomerView>();

    CustomerViewModel viewModel = new CustomerViewModel(mockView.Object, mockContainer.Object);
    viewModel.CustomersRepository = new CustomersRepository();
    viewModel.Customer = new Mock<Customer>().Object;

    //Act
    viewModel.Save();

    //Assert
    Assert.IsTrue(viewModel.CustomersRepository.Count == 1);
}

Exceptions

Một tính năng hay của NUnit framework là khả năng test cả exception. Nó có nghĩa là bạn mong muốn đoạn code cần test tạo ra exception. Để làm điều này, bạn vẫn thiết lập các phần Arrange và Act, sau đó thêm attribute ExpectedException vào phương thức test:

[Test]
[ExpectedException(ExpectedException = typeof(InvalidOperationException))]
public void When_Adding_Null_Customer_Should_Throw_Invalid_Operation_Exception()
{
    ICustomerViewModel viewModel = new CustomerViewModel(_mockView.Object, _mockContainer.Object);
    viewModel.CustomersRepository = _mockCustomerRepository.Object;

    //Act
    viewModel.Save();

    //Assert
}

Trong đoạn code trên, nếu exception InvalidOperationException được tạo ra bất kỳ nơi nào trong thân test thì test sẽ pass. Ngược lại test sẽ fail.

Recursive mocking (mock đệ quy)

Bạn sẽ thường đối mặt với tình huống mà bạn muốn mock complex type bên ngoài đối với các complex type lồng nhau. Moq đủ thông minh để nhận ra điều này và tự động mock các complex type ở bên trong giúp bạn. Như tình huống sau, bạn có một Customer object, chứa một complex type Address, trong Address lại có một complex type khác là GeoCoordinate, rồi lại có một vài thuộc tính khác như là:  AltitudeLatitudeLongitude,… Bạn muốn chỉ rõ giá trị trả về cho Latitude. Một vài mock framework sẽ yêu cầu bạn mock các object khác (Customer, Address, GeoCoordinate), nhưng đối với Moq điều này sẽ tự động được thực hiện. Cách sử dụng rất đơn giản, thông qua phương thức Setup như bình thường:

mockCustomerRepository.Setup(t => t.Customer.Address.Geocoordinate.Longitude).Returns(13.92);

Trả về các object khác nhau mỗi khi phương thức được gọi

Sẽ có nhiều lúc bạn muốn trả về các giá trị hay object khác nhau mỗi khi phương thức làm giả được gọi. Ví dụ, bạn đang mock data access layer (DAL) và mỗi khi gọi phương thức GetID thì nó trả về một ID mới:

var i = 1;
_mockCustomerRepository.Setup(t => t.GetId()).Returns(() => i).Callback(() => i++);

Sau khi gọi GetID thì phương thức Returns sẽ được gọi, tiếp đó phần logic của Callback sẽ được thực thi.

Mock Repositories

Khi mà bạn có nhiều mock object trong phần test, đi kèm với đó là cả đống phương thức Setup, tình cảnh tương tự với Verify:

mockCustomerRepository.Verify();
mockView.Verify();
mockContainer.Verify();
mockCustomerRepository.Verify();

Code này không sai, nhưng mà trông nó khá là lộn xộn, và… dài dòng. Bạn có thể dùng MockRepository để dọn dẹp mớ lộn xộn này:

[Test]
public void Customer_Should_Be_Added_To_The_Repository()
{
    var factory = new MockRepository(MockBehavior.Loose);
    var mockView = factory.Create<ICustomerView>();
    var mockContainer = factory.Create<IContainer>();
    var mockCustomerRepository = factory.Create<IRepository<Customer>>();

    //Arrange
    ICustomerViewModel viewModel = new CustomerViewModel(mockView.Object, mockContainer.Object);
    viewModel.CustomersRepository = mockCustomerRepository.Object;

    //Act
    viewModel.Save();

    //Assert
    factory.Verify();
}

Bạn có thể khai báo hành vi của mock object được tạo bởi MockRepository trong contructor của repository, hoặc cũng có thể khai báo bằng việc truyền vào một MockBehavior vào phương thức MockRepository.Create.

Tổng kết

Moq thực sự là một mock framework mạnh mẽ và linh hoạt. Khi kết hợp nó với NUnit, bạn sẽ có trong tay sức mạnh để viết test nhanh hơn, hữu ích hơn, từ đó tăng độ tin cậy cho những đoạn code mà bạn tạo ra.

Các bạn có thể xem phần 3 tại đây: lưu ý đối với một số method trong Moq.