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 Moq và xUnit, đồ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.
Ở 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.
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ế.
Đây là kiến trúc cơ bản của ứng dụng:
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.
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()
và 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 đã 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.
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:
List<Player>
trống._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._playerRepo.GetForTeam()
được gọi ít nhất một lần nhưng giá trị List<Player>
trống được trả về.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.
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.
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());
}
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());
}
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
.
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.
SearchDateDirection
là NewerThan
VÀ không đội nào được tìm thấy.SearchDateDirection
là NewerThan
VÀ đội được tìm thấy.SearchDateDirection
là OlderThan
VÀ không đội nào được tìm thấy.SearchDateDirection
là OlderThan
VÀ đội được tìm thấy.Để 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)
}
};
}
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.
[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());
}
[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());
}
[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());
}
[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());
}
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.
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.
Bạn có thể vui lòng tắt trình chặn quảng cáo ❤️ để hỗ trợ chúng tôi duy trì hoạt động của trang web.
Trong bài viết này, chúng ta sẽ tìm hiểu cách sử dụng thư viện Playwright kết hợp với XUnit để kiểm tra các ứng dụng web ASP.NET Core như người dùng có thể.
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.
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
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.