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

Giới thiệu

Trong lập trình hướng đối tượng, mock object là các object mô phỏng các hành vi của object thật. Mock thường được sử dụng trong unit test.

Nói một cách chính xác thì mock object được tạo ra để giúp kiểm tra hành vi của object (thật) khác. Thế nên, mock là việc làm giả object thật và được kiểm soát sao cho luôn trả về giá trị mong muốn.

Vậy tại sao lại dùng mock object? Có một vài lý do cho việc này. Tưởng tượng rằng bạn có một object thật có các đặc tính dưới đây (danh sách được lấy từ Wikipedia):

  • Object cung cấp các kết quả không xác định.
  • Có các trạng thái khó tạo ra hoặc tạo lại (ví dụ: lỗi đường truyền).
  • Chạy chậm (ví dụ truy vấn database, và nó cần phải được khởi tạo trước khi test).
  • Các hành vi có thể không tồn tại hoặc thay đổi.
  • Sẽ phải bao gồm thông tin và phương pháp dành riêng cho mục đích thử nghiệm.

Những kiểu object này nên được mock, có nghĩa là được thay thế bởi một object giả và trả về giá trị chúng ta cần hoặc mong muốn. Nói chung, có ba loại object giả: Mock, StubFake. Bạn có thể xem bài viết phân tích sự khác nhau giữa ba loại này tại đây.

Khi chúng ta tạo một mock object, chúng ta có thể cài đặt hành vi cho nó là strict hoặc loose behavior. Strict behavior có nghĩa là exception sẽ được tạo ra nếu những gì chưa được cài đặt trong interface được gọi. Trái lại, loose behaviour sẽ không tạo ra exception trong trường hợp này. Mặc định, mock được cài đặt loose behaviour.

Moq là gì?

Moq là một thư viện được viết cho .NET được phát triển từ đầu để tận dụng tối đa .NET 3.5 (tức là các LinQ expression tree) và những tính năng trong C# 3.0 (tức là Lambda Expression) làm cho nó trở thành thư viện mock dễ sử dụng nhất, type-safe và tái cấu trúc có sẵn. Và nó hỗ trợ mock interface cũng như class. API của nó cực kỳ đơn giản, và không yêu cầu bất kỳ kiến ​​thức hoặc kinh nghiệm nào trước đây với mock. Tuy nhiên trước đó bạn phải quen với việc sử dụng Lambda Expression.

Moq hướng đến sự đơn giản, strongly-typed và tối giản (nhưng vẫn đầy đủ tính năng), sử dụng theo hướng AAA (Arrange – Act – Assert).

Tác giả của Moq gọi nó là thư viện mock đơn giản nhất cho .NET. Tôi không chắc chắn lắm về điều đó, nhưng tôi có thể xác nhận rằng nó thật sự là một thư viện đơn giản nhưng mạnh mẽ, đó cũng là lý do mà tôi đặc biệt thích dùng Moq.

Bạn có thể định nghĩa mock cho:

  • Phương thức của Object (Object Methods)
  • Thuộc tính (Properties)
  • Event (Events)
  • Callback (Callbacks)
  • Xác nhận (Verifications)
  • Một vài tình huống nâng cao như xác nhận và thực thi nhiều interface.

Những framework khác

Moq chỉ là một framework có thể dùng để tạo mock object. Sau đây là một vài framework khác:

  • Nsubstitute: Hoàn hảo cho những người mới tiếp cận việc test, hoặc cho những người muốn test của họ có ít lambda hơn.
  • Rhino Mocks: Mã nguồn mở! Rhino Mocks là một mock framework động cho nền tảng .NET. Mục đích của nó là làm cho việc test dễ dàng hơn bằng cách cho phép lập trình viên tạo mock thực thi custom object và xác nhận sự tương tác sử dụng unit test.
  • TypeMock, EasyMock.NET, NMock, FakeItEasy.

Cài đặt

Có hai cách để cài đặt Moq, hoặc là sử dụng Nuget, hoặc là cài đặt thủ công.

  • Để cài đặt thông qua Nuget, hãy click chuột phải vào Solution, chọn Nuget Package Manager, sau đó tìm kiếm “Moq” và tiến hành cài đặt.
  • Nếu cài đặt thủ công, chỉ cần download nó từ trang chủ, sau đó thêm reference vào project.

System Under Test

Lấy một đoạn code chúng ta cần test như sau:

public class CustomerService
{
    private ICustomerRepository _repository;

    public CustomerService(ICustomerRepository repository)
    {
        _repository = repository;
    }

    public Customer GetCustomer(int id)
    {
        return _repository.GetCustomer(id);
    }
}

public interface ICustomerRepository
{
    Customer GetCustomer(int id);
}

public class Customer
{
    public string LastName { get; set; }
    public string FirstName { get; set; }
}

Như bạn có thể thấy, chúng ta có CustomerService phụ thuộc vào ICustomerRepository, đóng vai trò là giao diện cho một class, ví dụ như nó sẽ trả về dữ liệu khách hàng từ database. Tôi sẽ bỏ qua phần thực thi giao diện này bởi vì chúng ta sẽ mock nó, do chúng ta không muốn truy cập vào database rồi lấy dữ liệu của một Customer. Như đã nói trước đó, test cần phải có kết quả xác định, không phát sinh những thứ không mong muốn và chạy nhanh.

Ví dụ test đơn giản

Trong test đầu tiên, chúng ta sẽ tạo một mock cho interface IcustomerRepository và cài đặt các thông số mong muốn. Các thông số mong muốn gồm có: những tham số mà phương thức nhận vào, và kết quả trả về ra sao.

[TestMethod]
public void GetCustomer_ValidId()
{
    //-- 1. Arrange ----------------------
    const int CUSTOMER_ID = 1;
    const string FNAME = "John";
    const string LNAME = "Doe";

    //-- Creating a fake ICustomerRepository object
    var repository = new Mock<ICustomerRepository>();

    //-- Setting up the repository in order to return
    //-- exactly what we expect.
    repository
    .Setup(m => m.GetCustomer(CUSTOMER_ID))
    .Returns(new Customer { FirstName = FNAME, LastName = LNAME });

    var service = new CustomerService(repository.Object);

    //-- 2. Act ----------------------
    var customer = service.GetCustomer(CUSTOMER_ID);

    //-- Assert ----------------------
    Assert.IsTrue(customer != null);
    Assert.IsTrue(customer.FirstName == FNAME);
    Assert.IsTrue(customer.LastName == LNAME);
}

Giải thích qua về đoạn test trên:

var repository = new Mock<ICustomerRepository>();

tạo ra một object thực thi tất cả phương thức và thuộc tính đã được định nghĩa trong interface ICustomerRepository.

Mặt khác, Moq sử dụng LinQLambda Expression như sau:

repository
.Setup(m => m.GetCustomer(CUSTOMER_ID))
.Returns(new Customer { FirstName = FNAME, LastName = LNAME });

đoạn code trên làm 2 việc:

  1. Bằng cách sử dụng phương thức Setup, truyền vào một lambda expression, định nghĩa phương thức và giá trị tham số truyền vào mà chúng ta muốn test. Test sẽ fail nếu giá trị tham số truyền vào trong lúc chạy khác với những gì chúng ta định nghĩa trong phương thức Setup. Trong trường hợp này, tham số truyền vào phải bằng với CUSTOMER_ID.
    Nếu muốn truyền vào tham số bất kỳ, hoặc không quan tâm đến tham số truyền vào, tất nhiên vẫn là kiểu int, bạn có thể dùng It.IsAny(). Tức là thay .Setup(m => m.GetCustomer(CUSTOMER_ID)) thành .Setup(m => m.GetCustomer(It.IsAny())).
  2. Phương thức Returns định nghĩa giá trị trả về trong quá trình test chạy.

sau khi cài đặt xong, Moq sẽ tạo ra một object giống như thế này:

public class SomeClassName: ICustomerRepository
{
    public Customer GetCustomer(int id)
    {
        return new Customer {
                FirstName = "John",
                LastName = "Doe"
        };
    }
}

Như bạn có thể thấy, class thực thi từ interface ICustomerRepository trả về giá trị có dạng “hardcoded”. Thay vì sử dụng Moq (hay bất kỳ framework nào khác), chúng ta có thể tự làm điều này, nhưng mà dùng framework làm cho mọi thứ đơn giản hơn.

Kết luận

Moq là framework đầu tiên mà tôi dùng do tính đơn giản cũng như số lượng tính năng mà nó mang lại. Việc giải thích các khái niệm mà Moq triển khai cho thành viên trong team thường rất đơn giản và dễ hiểu. Lưu ý rằng, ví dụ trong bài viết này là ví dụ đơn giản nhất, còn nhiều trường hợp khác test rất phức tạp cũng có thể dùng Moq. Trong phần tiếp theo sẽ giới thiệu rõ hơn cách sử dụng Moq. Happy coding!