Unit test cho tầng nghiệp vụ trong ASP.NET Core với xUnit và Moq

Trong phần đầu tiên của loạt bài này, chúng ta đã xem tổng quan về cách sử dụng MoqxUnit, đồng thời tham khảo một số lời khuyên cơ bản về thời điểm và cách viết các bài kiểm tra unit test.

Tổng quan về unit test với ASP.NET Core, xUnit và Moq
Trong loạt bài này, bạn sẽ tìm hiểu tổng quan về unit test và cách viết unit test cho ứng dụng ASP.NET Core sử dụng xUnit và Moq.

Ở phần thứ hai, chúng ta đã tạo các lớp fluent mock cho phép chúng ta dễ dàng viết các bài kiểm tra unit test bằng cách sử dụng Moq, xUnit và ASP.NET Core.

Sử dụng Moq để viết unit test trong ASP.NET Core
Trong bài viết này, chúng ta sẽ tạo các lớp fluent mock cho các bài kiểm tra unit test bằng cách sử dụng Moq, xUnit và ASP.NET Core.

Trong phần này của loạt bài, chúng ta sẽ sử dụng các ý tưởng từ phần đầu tiên và các lớp fluent mock từ phần hai để viết một số bài kiểm tra đơn vị thực tế.

Kiến trúc ứng dụng và mô hình dữ liệu

Đây là kiến ​​trúc cơ bản của ứng dụng:

Kiến trúc ứng dụng và mô hình dữ liệu

Tầng Repository làm việc với cơ sở dữ liệu. Tầng Service xử lý các nghiệp vụ của ứng dụng,  gọi Repository để lấy dữ liệu từ cơ sở dữ liệu hoặc lưu trữ dữ liệu vào cơ sở dữ liệu. Tầng Controller xử lý các yêu cầu của client, nó gọi Service để xử lý nghiệp vụ và trả về phản hồi cho client.

Theo mục đích của bài đăng này, "tầng nghiệp vụ" của ứng dụng là Service. Các lớp của tầng Service là những gì chúng ta sẽ thử nghiệm.

Viết unit test cho lớp PlayerService

Hãy bắt đầu bằng cách đọc mã của lớp PlayerService:

public class PlayerService : IPlayerService
{
    private readonly IPlayerRepository _playerRepo;
    private readonly ITeamRepository _teamRepo;
    private readonly ILeagueRepository _leagueRepo;

    public PlayerService(IPlayerRepository playerRepo,
                            ITeamRepository teamRepo,
                            ILeagueRepository leagueRepo)
    {
        _playerRepo = playerRepo;
        _teamRepo = teamRepo;
        _leagueRepo = leagueRepo;
    }

    public Player GetByID(int id)
    {
        return _playerRepo.GetByID(id);
    }

    public List<Player> GetForLeague(int leagueID)
    {
        var isValidLeague = _leagueRepo.IsValid(leagueID);
        if (!isValidLeague)
        {
            return new List<Player>();
        }

        List<Player> players = new List<Player>();

        var teams = _teamRepo.GetForLeague(leagueID);

        foreach(var team in teams)
        {
            players.AddRange(_playerRepo.GetForTeam(team.ID));
        }

        return players;
    }
}

Chúng ta có hai phương thức trong lớp PlayerService là: GetByID()GetForLeague(). Câu hỏi bây giờ là, chúng ta thậm chí có cần phải viết unit test chúng không?

Theo tôi, chúng ta cần viết unit test cho phương thức GetForLeague(). Phương thức này thực hiện một số logic nội bộ (cụ thể là kiểm tra xem Id giải đấu được gửi có hợp lệ hay không) cần được kiểm tra. Câu hỏi thực sự là: chúng ta có cần viết unit test cho phương thức GetByID() không?

Nếu bạn muốn bao phủ toàn bộ mã (code coverage), bạn cần phải viết unit test cho tất cả các phương thức. Tuy nhiên, có rất nhiều ý kiến ​​cho rằng việc bao phủ toàn bộ mã là không cần thiết, thậm chí là lãng phí thời gian.

Cá nhân tôi đồng ý với ý kiến ​​đó, vì vậy chúng ta sẽ không viết viết unit test cho phương thức GetByID(), bởi vì tất cả những gì nó làm được chuyển đến kho lưu trữ cấp thấp hơn. Trong một ứng dụng thực tế, chúng tôi cũng sẽ viết các bài kiểm tra unit test cho repository.

Chúng ta sẽ kiểm tra những gì?

Chúng ta đã biết những phương thức nào cần phải viết unit test, nhưng chúng ta nên viết những loại kiểm thử nào?

Có nhiều cách để xác định điều này. Một cách mà chúng ta sẽ sử dụng là xem xét các "loại" đầu vào và đầu ra của phương thức, xác định số lượng các kết hợp có thể có cho mỗi cách trong số chúng và viết một bài kiểm tra cho mỗi kết hợp.

Đầu tiên, hãy kiểm tra các đầu vào của phương thức. Trong phương thức này, chúng ta thấy một tham số đầu vào int leagueID. Chỉ có hai "loại" giá trị có thể có cho tham số này: không hợp lệ và hợp lệ. Vì vậy, chúng ta cần ít nhất hai bài kiểm tra unit test.

Trong trường hợp tham số "không hợp lệ", phương thức trả về ngay lập tức mà không cần thực hiện thêm bất kỳ thao tác nào. Vì vậy, đối với trường hợp "không hợp lệ", chúng ta chỉ cần một bài kiểm tra unit test.

Nhưng trong trường hợp "hợp lệ", có nhiều khả năng hơn. Ví dụ: nếu Id giải đấu hợp lệ, nhưng không tìm thấy đội nào trong giải đấu đó? Điều gì sẽ xảy ra nếu đội được tìm thấy, nhưng không tìm thấy người chơi cho bất kỳ đội nào?

Cuối cùng, chúng ta cần xem xét trường hợp hợp lệ: Id giải đấu hợp lệ, VÀ đội được tìm thấy, VÀ người chơi được tìm thấy cho các đội đó. Đây là ba trường hợp kiểm thử unit test, cộng với một trường hợp cho tình huống "không hợp lệ".

Tóm lại, chúng ta cần viết bốn bài kiểm tra unit test. Bây giờ, chúng ta hãy thảo luận về các kịch bản kiểm thử unit test mà chúng ta cần tính đến.

Các tình huống thử nghiệm unit test

Dưới đây là một định nghĩa chính thức hơn một chút về các tình huống thử nghiệm unit test của chúng ta:

  1. Id giải đấu đã gửi không hợp lệ. Trong trường hợp này, phương thức trả về danh sách List<Player> trống.
  2. Không tìm thấy đội nào khi gọi phương thức _teamRepo.GetForLeague(). Trong trường hợp này, phương thức được gọi chính xác một lần và chúng ta trả về một danh sách trống.
  3. Không tìm thấy người chơi nào cho các đội được tìm thấy. Trong trường hợp này, phương thức _playerRepo.GetForTeam() được gọi ít nhất một lần nhưng giá trị List<Player> trống được trả về.
  4. Người chơi được tìm thấy cho các đội đã cho, và do đó chúng ta trả về một danh sách List<Player> không trống.

Chúng ta sẽ viết các bài kiểm tra unit test cho từng tình huống này. Đừng quên rằng chúng ta sẽ sử dụng các lớp Repository giả mà chúng ta đã tạo trong bài trước.

Unit test #1: Id giải đấu không hợp lệ

Kịch bản đầu tiên được cho là đơn giản nhất. Đây là bài kiểm tra unit test:

[Fact]
public void GetForLeague_InvalidLeague()
{
    //Arrange - Setup the mock IsValid method
    var mockLeagueRepo = new MockLeagueRepository().MockIsValid(false);

    //Create the Service instance
    var playerService = new PlayerService(new MockPlayerRepository().Object,
                                            new MockTeamRepository().Object,
                                            mockLeagueRepo.Object);

    //Act - Call the method being tested
    var allPlayers = playerService.GetForLeague(1);

    //Assert
    //First, assert that the player list returned is empty.
    Assert.Empty(allPlayers);
    //Also assert that IsValid was called exactly once.
    mockLeagueRepo.VerifyIsValid(Times.Once());
}

Vì công việc mà chúng ta đã làm trong bài viết trước của loạt bài này, nên đoạn mã này khá rõ ràng và dễ đọc.

Unit test #2: Không tìm thấy đội nào

Kịch bản này phức tạp hơn một chút, không tìm thấy đội nào cho Id giải đấu.

Lần này, chúng ta cần giả lập cho tất cả các Repository và một vài phương thức của chúng. Chúng ta cũng kiểm tra phương thức GetForTeam() của lớp PlayerRepository không được gọi, điều này là cần thiết trong trường hợp này vì nếu không tìm thấy đội nào, chúng ta không nên tìm kiếm người chơi.

Tất cả điều trên sẽ được thực hiện trong bài kiểm tra unit test dưới đây:

[Fact]
public void PlayerService_GetForLeague_ValidLeagueNoTeams()
{
    //Arrange
    var mockLeagueRepo = new MockLeagueRepository().MockIsValid(true);
    var mockPlayerRepo = new MockPlayerRepository();
    var mockTeamRepo = new MockTeamRepository()
                             .MockGetForLeague(new List<Team>());

    var playerService = new PlayerService(mockPlayerRepo.Object, 
                                          mockTeamRepo.Object,
                                          mockLeagueRepo.Object);

    //Act
    var allPlayers = playerService.GetForLeague(1);

    //Assert
    Assert.Empty(allPlayers);
    mockPlayerRepo.VerifyGetForTeam(Times.Never());
    mockTeamRepo.VerifyGetForLeague(Times.Once());
    mockLeagueRepo.VerifyIsValid(Times.Once());
}

Unit test #3: Không có người chơi

Kịch bản này khá gần với kịch bản trước. Bây giờ chúng ta cần một danh sách giả lập các đội được phương thức PlayerRepository.GetForTeam() trả về và chúng ta cần đảm bảo rằng danh sách đó được gọi ít nhất một lần.

[Fact]
public void PlayerService_GetForLeague_ValidLeagueNoPlayers()
{
    //Arrange
    var mockLeagueRepo = new MockLeagueRepository().MockIsValid(true);
    var mockPlayerRepo = new MockPlayerRepository()
                               .MockGetForTeam(new List<Player>());

    var mockTeams = new List<Team>()
    {
        new Team()
        {
            ID = 1
        }
    };
    var mockTeamRepo = new MockTeamRepository().MockGetForLeague(mockTeams);

    var playerService = new PlayerService(mockPlayerRepo.Object, 
                                          mockTeamRepo.Object,
                                          mockLeagueRepo.Object);

    //Act
    var allPlayers = playerService.GetForLeague(1);

    //Assert
    Assert.Empty(allPlayers);
    mockPlayerRepo.VerifyGetForTeam(Times.AtLeastOnce());
    mockTeamRepo.VerifyGetForLeague(Times.Once());
    mockLeagueRepo.VerifyIsValid(Times.Once());
}

Unit test #4: Tìm thấy người chơi

Bài kiểm tra unit test này tương tự như các bài kiểm tra unit test ở trên:

[Fact]
public void PlayerService_GetForLeague_ValidCompleteLeague()
{
    //Arrange
    var mockLeagueRepo = new MockLeagueRepository().MockIsValid(true);

    var mockPlayers = new List<Player>()
    {
        new Player()
        {
            ID = 1
        }
    };
    var mockPlayerRepo = new MockPlayerRepository().MockGetForTeam(mockPlayers);

    var mockTeams = new List<Team>()
    {
        new Team()
        {
            ID = 1
        }
    };
    var mockTeamRepo = new MockTeamRepository().MockGetForLeague(mockTeams);

    var playerService = new PlayerService(mockPlayerRepo.Object, 
                                          mockTeamRepo.Object, 
                                          mockLeagueRepo.Object);

    //Act
    var allPlayers = playerService.GetForLeague(1);

    //Assert
    Assert.NotEmpty(allPlayers);
    mockPlayerRepo.VerifyGetForTeam(Times.AtLeastOnce());
    mockTeamRepo.VerifyGetForLeague(Times.Once());
    mockLeagueRepo.VerifyIsValid(Times.Once());
}

Với bốn kịch bản trên, chúng ta đã hoàn thành thử nghiệm unit test của mình cho lớp PlayerService. Bây giờ chúng ta sẽ thực hành thêm với lớp TeamService.

Viết unit test cho lớp TeamService

Hãy xem mã của lớp TeamService để quyết định loại bài kiểm tra nào chúng ta cần viết.

public class TeamService : ITeamService
{
    private readonly ITeamRepository _teamRepo;
    private readonly ILeagueRepository _leagueRepo;

    public TeamService(ITeamRepository teamRepo,
                        ILeagueRepository leagueRepo)
    {
        _teamRepo = teamRepo;
        _leagueRepo = leagueRepo;
    }

    public List<Team> Search(TeamSearch search)
    {
        //If we are searching for an invalid or unknown League...
        var isValidLeague = _leagueRepo.IsValid(search.LeagueID);
        if (!isValidLeague)
        {
            return new List<Team>(); //Return an empty list.
        }

        //Otherwise get all teams in the specified league...
        var allTeams = _teamRepo.GetForLeague(search.LeagueID);

        //... and filter them by the specified Founding Date and Direction.
        if(search.Direction == Enums.SearchDateDirection.OlderThan)
        {
            return allTeams.Where(x => x.FoundingDate <= search.FoundingDate)
                           .ToList();
        }
        
        return allTeams.Where(x => x.FoundingDate >= search.FoundingDate)
                       .ToList();
    }
}

public class TeamSearch
{
    [Required]
    [Range(1, 1000)]
    public int LeagueID { get; set; }
    public DateTime FoundingDate { get; set; }
    public SearchDateDirection Direction { get; set; }

    public List<Team> Results { get; set; }
}

public enum SearchDateDirection
{
    OlderThan,
    NewerThan
}

Phương thức Search() phức tạp hơn so với các phương thức mà chúng ta đã viết unit test. Phương thức này có năm tình huống khác nhau có thể xảy ra mà chúng ta cần thử nghiệm.

  1. Khi SearchDateDirectionNewerThan VÀ không đội nào được tìm thấy.
  2. Khi SearchDateDirectionNewerThan VÀ đội được tìm thấy.
  3. Khi SearchDateDirectionOlderThan VÀ không đội nào được tìm thấy.
  4. Khi SearchDateDirectionOlderThan VÀ đội được tìm thấy.
  5. Khi Id giải đấu không hợp lệ.

Thiết lập phương thức GetMockTeams()

Để làm cho việc viết các bài kiểm tra unit test cho lớp TeamService trở nên gọn gàng hơn một chút, tôi đã tạo một phương thức trả về danh sách các đội (Team) với các ngày thành lập (Founding Date) khác nhau.

private List<Team> GetMockTeams()
{
    return new List<Team>()
    {
        new Team()
        {
            ID = 1,
            FoundingDate = new DateTime(1970, 1, 1)
        },
        new Team()
        {
            ID = 2,
            FoundingDate = new DateTime(1994, 12, 1)
        },
        new Team()
        {
            ID = 3,
            FoundingDate = new DateTime(2012, 5, 12)
        }
    };
}

Unit test #1: Khi SearchDateDirection là NewerThan VÀ không đội nào được tìm thấy

Sử dụng phương thức GetMockTeams() này, chúng ta có thể viết một bài kiểm tra unit test cho kịch bản đầu tiên của chúng ta.

[Fact]
public void TeamService_Search_NewerThan_Invalid()
{
    //Arrange
    var mockTeams = GetMockTeams();

    var mockTeamRepo = new MockTeamRepository().MockGetForLeague(mockTeams);
    var mockLeagueRepo = new MockLeagueRepository().MockIsValid(true);

    var teamService = new TeamService(mockTeamRepo.Object, 
    								  mockLeagueRepo.Object);

    var searchParams = new TeamSearch()
    {
        LeagueID = 1,
        FoundingDate = new DateTime(2017, 1, 1),
        Direction = SearchDateDirection.NewerThan
    };

    //Act
    var results = teamService.Search(searchParams);

    //Assert
    Assert.Empty(results);
    mockLeagueRepo.VerifyIsValid(Times.Once());
    mockTeamRepo.VerifyGetForLeague(Times.Once());
}

Vì lý do đơn giản, tại thời điểm này, tôi sẽ giả định rằng bạn, độc giả thân yêu của tôi, nhìn thấy được mẫu của những gì chúng ta đang làm. Do đó, tôi chỉ để lại mã cho bốn kịch bản thử nghiệm unit test tiếp theo, không có lời giải thích.

Unit test #2: Khi SearchDateDirection là NewerThan VÀ đội được tìm thấy

[Fact]
public void TeamService_Search_NewerThan_Valid()
{
    //Arrange
    var mockTeams = GetMockTeams();

    var mockTeamRepo = new MockTeamRepository().MockGetForLeague(mockTeams);
    var mockLeagueRepo = new MockLeagueRepository().MockIsValid(true);

    var teamService = new TeamService(mockTeamRepo.Object, 
                                      mockLeagueRepo.Object);

    var searchParams = new TeamSearch()
    {
        LeagueID = 1,
        FoundingDate = new DateTime(1969, 1, 1),
        Direction = SearchDateDirection.NewerThan
    };

    //Act
    var results = teamService.Search(searchParams);

    //Assert
    Assert.NotEmpty(results);
    mockLeagueRepo.VerifyIsValid(Times.Once());
    mockTeamRepo.VerifyGetForLeague(Times.Once());
}

Unit test #3: Khi SearchDateDirection là OlderThan VÀ không đội nào được tìm thấy

[Fact]
public void TeamService_Search_OlderThan_Invalid()
{
    //Arrange
    var mockTeams = GetMockTeams();

    var mockTeamRepo = new MockTeamRepository().MockGetForLeague(mockTeams);
    var mockLeagueRepo = new MockLeagueRepository().MockIsValid(true);

    var teamService = new TeamService(mockTeamRepo.Object, 
                                      mockLeagueRepo.Object);

    var searchParams = new TeamSearch()
    {
        LeagueID = 1,
        FoundingDate = new DateTime(1966, 1, 1),
        Direction = SearchDateDirection.OlderThan
    };

    //Act
    var results = teamService.Search(searchParams);

    //Assert
    Assert.Empty(results);
    mockLeagueRepo.VerifyIsValid(Times.Once());
    mockTeamRepo.VerifyGetForLeague(Times.Once());
}

Unit test #4: Khi SearchDateDirection là OlderThan VÀ đội được tìm thấy

[Fact]
public void TeamService_Search_OlderThan_Valid()
{
    //Arrange
    var mockTeams = GetMockTeams();

    var mockTeamRepo = new MockTeamRepository().MockGetForLeague(mockTeams);
    var mockLeagueRepo = new MockLeagueRepository().MockIsValid(true);

    var teamService = new TeamService(mockTeamRepo.Object, 
                                      mockLeagueRepo.Object);

    var searchParams = new TeamSearch()
    {
        LeagueID = 1,
        FoundingDate = new DateTime(2013, 1, 1),
        Direction = SearchDateDirection.OlderThan
    };

    //Act
    var results = teamService.Search(searchParams);

    //Assert
    Assert.NotEmpty(results);
    mockLeagueRepo.VerifyIsValid(Times.Once());
    mockTeamRepo.VerifyGetForLeague(Times.Once());
}

Unit test #5: Khi Id giải đấu không hợp lệ

[Fact]
public void TeamService_Search_InvalidLeague()
{
    //Arrange
    var mockTeams = GetMockTeams();

    var mockTeamRepo = new MockTeamRepository().MockGetForLeague(mockTeams);
    var mockLeagueRepo = new MockLeagueRepository().MockIsValid(false);

    var teamService = new TeamService(mockTeamRepo.Object,              
                                      mockLeagueRepo.Object);

    var searchParams = new TeamSearch()
    {
        LeagueID = 1,
        FoundingDate = new DateTime(1997, 1, 1),
        Direction = SearchDateDirection.NewerThan
    };

    //Act
    var results = teamService.Search(searchParams);

    //Assert
    Assert.Empty(results);
    mockLeagueRepo.VerifyIsValid(Times.Once());
    mockTeamRepo.VerifyGetForLeague(Times.Never());
}

Nhận xét

Có nhiều cách để làm cho các bài kiểm tra trên ngắn gọn hơn nữa, ví dụ: bằng cách viết thêm phương thức khởi tạo cho lớp TeamSearch, nhưng tôi đã loại bỏ điều đó ra khỏi bài viết này để bạn dễ hình dung hơn.

Cũng lưu ý rằng bạn có thể sẽ viết nhiều mã kiểm tra unit test hơn là mã ứng dụng. Điều này là bình thường và có thể chấp nhận được, nhưng các bài kiểm tra unit test có thể được viết nhanh hơn.

Tóm lược

Khi viết các bài kiểm tra unit test, những điều chính bạn cần xem xét là đầu vào, đầu ra và hành vi của phương thức thay đổi như thế nào dựa trên từng tổ hợp đầu vào, đầu ra và hành vi.

Bạn có thể cần một bài kiểm tra unit test cho từng tổ hợp đầu vào, đầu ra và hành vi, nhưng điều đó là tùy thuộc vào quyết định của bạn và kinh nghiệm của bạn cho bạn biết.

Sử dụng Moq và xUnit, chúng ta có thể làm cho các bài kiểm tra unit test này ngắn gọn hơn và dễ hiểu hơn so với cách khác.

Trong phần tiếp theo của loạt bài này, chúng ta sẽ viết thêm một số bài kiểm tra unit test cho Controller của ứng dụng ASP.NET Core MVC. Điều này yêu cầu một chút thiết lập khác, mặc dù phương pháp được sử dụng để tạo các bài kiểm tra unit test vẫn rất giống nhau.

Unit test cho controller trong ASP.NET Core với xUnit và Moq
Trong bài viết này, chúng ta sẽ viết một số bài kiểm tra unit test cho Controller của ứng dụng ASP.NET sử dụng xUnit và Moq.
Unit TestXUnitMoqMockASP.NET CoreASP.NET Core MVC
Bài Viết Liên Quan:
Tạo DataAttribute tùy chỉnh cho Theory của xUnit để tải dữ liệu từ file JSON
Trung Nguyen 22/04/2021
Tạo DataAttribute tùy chỉnh cho Theory của xUnit để tải dữ liệu từ file JSON

Trong bài viết này hướng dẫn bạn cách tạo lớp DataAttribute tùy chỉnh để tải dữ liệu cho bài kiểm tra unit test viết bằng xUnit của bạn.

Tạo các bài kiểm tra unit test được tham số hóa trong xUnit với Theory, InlineData, ClassData và MemberData
Trung Nguyen 18/04/2021
Tạo các bài kiểm tra unit test được tham số hóa trong xUnit với Theory, InlineData, ClassData và MemberData

Trong bài viết này, tôi sẽ hướng dẫn bạn tạo các bài kiểm tra unit test được tham số hóa trong xUnit với Theory, InlineData, ClassData và MemberData

Sử dụng Theory và InlineData của xUnit để viết unit test
Trung Nguyen 18/04/2021
Sử dụng Theory và InlineData của xUnit để viết unit test

Trong bài này, chúng ta sẽ sử dụng thuộc tính [Theory] và [InlineData] của xUnit để nhanh chóng viết một loạt các bài kiểm tra unit test.

Unit test cho controller trong ASP.NET Core với xUnit và Moq
Trung Nguyen 18/04/2021
Unit test cho controller trong ASP.NET Core với xUnit và Moq

Trong bài viết này, chúng ta sẽ viết một số bài kiểm tra unit test cho Controller của ứng dụng ASP.NET sử dụng xUnit và Moq.