Trong phần trước của loạt bài, chúng ta đã viết một số bài kiểm tra unit test cho tầng Service của ứng dụng ASP.NET sử dụng xUnit và Moq.
Hãy tiếp tục bằng cách viết một bộ kiểm tra unit test cho tầng Controller của ứng dụng ASP.NET Core MVC.
Đầu tiên, chúng ta hãy xem mã của lớp LeagueController
như sau:
public class LeagueController : Controller
{
private readonly ILeagueService _leagueService;
public LeagueController(ILeagueService leagueService)
{
_leagueService = leagueService;
}
public IActionResult Index()
{
var leagues = _leagueService.GetAll();
return View(leagues);
}
}
Chỉ có một phương thức hành động là Index()
. Vì vậy, chúng tôi chỉ cần xem xét các trường hợp thử nghiệm cho phương thức hành động này. Do không có đầu vào, tôi chỉ thấy hai tình huống thử nghiệm:
Do đó, các thử nghiệm của chúng ta phải phù hợp với các tình huống này. Đầu tiên chúng ta cần tạo lớp fluent mock cho lớp LeagueService
như sau:
public class MockLeagueService : Mock<ILeagueService>
{
public MockLeagueService MockIsValid(bool result)
{
Setup(x => x.IsValid(It.IsAny<int>()))
.Returns(result);
return this;
}
public MockLeagueService VerifyIsValid(Times times)
{
Verify(x => x.IsValid(It.IsAny<int>()), times);
return this;
}
public MockLeagueService MockGetAll(List<League> results)
{
Setup(x => x.GetAll())
.Returns(results);
return this;
}
public MockLeagueService VerifyGetAll(Times times)
{
Verify(x => x.GetAll(), times);
return this;
}
}
[Fact]
public void LeagueController_Index_NoLeagues()
{
//Arrange
var mockLeagueService = new MockLeagueService().MockGetAll(new List<League>());
var controller = new LeagueController(mockLeagueService.Object);
//Act
var result = controller.Index();
//Assert
Assert.IsAssignableFrom<ViewResult>(result);
mockLeagueService.VerifyGetAll(Times.Once());
}
[Fact]
public void LeagueController_Index_LeaguesExist()
{
//Arrange
var mockLeagues = new List<League>()
{
new League()
{
ID = 1,
FoundingDate = new DateTime(1933, 5, 3)
}
};
var mockLeagueService = new MockLeagueService().MockGetAll(mockLeagues);
var controller = new LeagueController(mockLeagueService.Object);
//Act
var result = controller.Index();
//Assert
Assert.IsAssignableFrom<ViewResult>(result);
mockLeagueService.VerifyGetAll(Times.Once());
}
Cho đến lúc này, các bài kiểm tra unit test cho tầng controller của chúng ta không có sự khác biệt đáng kể so tầng nghiệp vụ của ứng dụng. Tuy nhiên sẽ có một chút khác biệt khi chúng ta viết các bài kiểm tra unit test cho lớp TeamController
.
Đây là mã của lớp TeamController
:
public class TeamController : Controller
{
private readonly ITeamService _teamService;
public TeamController(ITeamService teamService)
{
_teamService = teamService;
}
[HttpGet]
public IActionResult Search()
{
var teamSearch = new TeamSearch();
return View(teamSearch);
}
[HttpPost]
public IActionResult Search(TeamSearch search)
{
if(!ModelState.IsValid)
{
return RedirectToAction("Search"); //POST-REDIRECT-GET pattern assumed, but not implemented.
}
var results = _teamService.Search(search);
if(!results.Any())
{
return RedirectToAction("Search");
}
search.Results = results;
return View(search);
}
}
Bây giờ chúng ta có hai phương thức hành động và một trong chúng dựa vào thuộc tính ModelState
để đưa ra các quyết định hợp lý.
Từ những phương thức hành động này, tôi thấy bốn trường hợp thử nghiệm sau:
Search()
, hãy kiểm tra xem ViewResult
có được trả về không. Không cần kiểm tra thêm, vì đó là kết quả duy nhất có thể có của phương thức hành động này.Search()
, nếu ModelState.IsValid
là false
, hãy kiểm tra xem RedirectToActionResult
có được trả về không.Search()
, nếu ModelState.IsValid
là true
VÀ không có đội nào được trả về, hãy kiểm tra xem RedirectToActionResult
có được trả về không.Search()
, nếu ModelState.IsValid
là true
và có đội được trả về, hãy kiểm tra xem ViewResult
có được trả về không.Bạn có thể tự hỏi tại sao kịch bản 2 và 3 được liệt kê riêng biệt, vì chúng được mong đợi sẽ trả về cùng một kết quả trong các điều kiện tương tự.
Lý do chính là trong kịch bản 3, phương thức _teamService.Search()
sẽ được gọi, trong khi trong kịch bản 2, nó sẽ không được gọi.
Bởi vì unit test cũng là kiểm thử hồi quy, chúng ta muốn biết khi nào hành động thay đổi các yếu tố phụ thuộc của nó và kiểm tra unit test các tình huống đó một cách riêng biệt là một cách tốt để làm điều đó.
Với những tình huống này, chúng ta hãy tạo lớp fluent mock và viết các bài kiểm tra:
public class MockTeamService : Mock<ITeamService>
{
public MockTeamService MockSearch(List<Team> results)
{
Setup(x => x.Search(It.IsAny<TeamSearch>()))
.Returns(results);
return this;
}
public MockTeamService VerifySearch(Times times)
{
Verify(x => x.Search(It.IsAny<TeamSearch>()), times);
return this;
}
}
[Fact]
public void TeamController_Search_Get_Valid()
{
//Arrange
var teamResults = new List<Team>()
{
new Team() { ID = 1 }
};
var mockTeamService = new MockTeamService().MockSearch(teamResults);
var controller = new TeamController(mockTeamService.Object);
//Act
var result = controller.Search();
//Assert
Assert.IsAssignableFrom<ViewResult>(result);
}
Lưu ý việc sử dụng phương thứcAssert.IsAssignableFrom<>()
. Đây là cách tuyệt vời để kiểm tra kiểu dữ liệuIActionResult
thường được trả về từ các lớp controller trong ứng dụng ASP.NET Core MVC.
[Fact]
public void TeamController_Search_Post_ModelStateInvalid()
{
//Arrange
var teamResults = new List<Team>()
{
new Team() { ID = 1 }
};
var mockTeamService = new MockTeamService().MockSearch(teamResults);
var controller = new TeamController(mockTeamService.Object);
controller.ModelState.AddModelError("Test", "Test");
//Act
var result = controller.Search(new TeamSearch());
//Assert
Assert.IsAssignableFrom<RedirectToActionResult>(result);
mockTeamService.VerifySearch(Times.Never());
var redirectToAction = (RedirectToActionResult)result; //See note below.
Assert.Equal("Search", redirectToAction.ActionName);
}
Tôi muốn bạn đặc biệt chú ý đến hai dòng cuối cùng trong bài kiểm tra unit test này. Nó kiểm tra xem phương thức hành động được chuyển hướng có phải là hành động mà chúng ta mong đợi hay không và đây là cách thực hiện.
Bạn có thể muốn thực hiện điều này khi một hành động có thể chuyển hướng đến nhiều nơi khác nhau, tùy thuộc vào đầu vào và logic của phương thức.
[Fact]
public void TeamController_Search_Post_NoResults()
{
//Arrange
var mockTeamService = new MockTeamService().MockSearch(new List<Team>());
var controller = new TeamController(mockTeamService.Object);
//Act
var result = controller.Search(new TeamSearch());
//Assert
Assert.IsAssignableFrom<RedirectToActionResult>(result);
mockTeamService.VerifySearch(Times.Once());
}
[Fact]
public void TeamController_Search_Post_Valid()
{
//Arrange
var teamResults = new List<Team>()
{
new Team() { ID = 1 }
};
var mockTeamService = new MockTeamService().MockSearch(teamResults);
var controller = new TeamController(mockTeamService.Object);
//Act
var result = controller.Search(new TeamSearch());
//Assert
Assert.IsAssignableFrom<ViewResult>(result);
mockTeamService.VerifySearch(Times.Once());
}
Cuối cùng, hãy xem mã của lớp PlayerController
như sau:
public class PlayerController : Controller
{
private readonly ILeagueService _leagueService;
private readonly IPlayerService _playerService;
public PlayerController(ILeagueService leagueService,
IPlayerService playerService)
{
_leagueService = leagueService;
_playerService = playerService;
}
public IActionResult Index(int id)
{
try
{
var player = _playerService.GetByID(id);
return View(player);
}
catch (Exception)
{
return RedirectToAction("Error", "Home");
}
}
public IActionResult League(int id)
{
if (!_leagueService.IsValid(id))
{
return RedirectToAction("Error", "Home");
}
var players = _playerService.GetForLeague(id);
return View(players);
}
}
Có hai phương thức hành động trong controller này, mỗi phương thức hành động có hai kịch bản cần được kiểm tra. Chúng được liệt kê ở dưới đây:
Index()
, Id cầu thủ không hợp lệ trả về RedirectToActionResult
.Index()
, Id cầu thủ hợp lệ trả về ViewResult
.League()
, Id giải đấu không hợp lệ trả về RedirectToActionResult
.League()
, Id giải đấu hợp lệ trả về ViewResult
.Bây giờ, chúng ta có thể viết lớp fluent mock và các bài kiểm tra unit test cho các kịch bản này.
public class MockPlayerService : Mock<IPlayerService>
{
public MockPlayerService MockGetByID(Player result)
{
Setup(x => x.GetByID(It.IsAny<int>()))
.Returns(result);
return this;
}
public MockPlayerService MockGetByIDInvalid()
{
Setup(x => x.GetByID(It.IsAny<int>()))
.Throws(new Exception("Player with that ID was not found!"));
return this;
}
public MockPlayerService VerifyGetByID(Times times)
{
Verify(x => x.GetByID(It.IsAny<int>()), times);
return this;
}
public MockPlayerService MockGetForLeague(List<Player> results)
{
Setup(x => x.GetForLeague(It.IsAny<int>()))
.Returns(results);
return this;
}
public MockPlayerService VerifyGetForLeague(Times times)
{
Verify(x => x.GetForLeague(It.IsAny<int>()), times);
return this;
}
}
[Fact]
public void PlayerController_Index_Invalid()
{
//Arrange
var mockPlayerService = new MockPlayerService().MockGetByIDInvalid();
var controller = new PlayerController(new MockLeagueService().Object,
mockPlayerService.Object);
//Act
var result = controller.Index(1); //ID doesn't matter
//Assert
Assert.IsAssignableFrom<RedirectToActionResult>(result);
mockPlayerService.VerifyGetByID(Times.Once());
}
[Fact]
public void PlayerController_Index_Valid()
{
//Arrange
var mockPlayer = new Player()
{
ID = 1
};
var mockPlayerService = new MockPlayerService().MockGetByID(mockPlayer);
var controller = new PlayerController(new MockLeagueService().Object,
mockPlayerService.Object);
//Act
var result = controller.Index(1); //ID doesn't matter
//Assert
Assert.IsAssignableFrom<ViewResult>(result);
mockPlayerService.VerifyGetByID(Times.Once());
}
[Fact]
public void PlayerController_League_Invalid()
{
//Arrange
var mockLeagueService = new MockLeagueService().MockIsValid(false);
var mockPlayerService = new MockPlayerService().MockGetForLeague(new List<Player>());
var controller = new PlayerController(mockLeagueService.Object,
mockPlayerService.Object);
//Act
var result = controller.League(1); //ID doesn't matter
//Assert
Assert.IsAssignableFrom<RedirectToActionResult>(result);
mockPlayerService.VerifyGetForLeague(Times.Never());
mockLeagueService.VerifyIsValid(Times.Once());
}
Lưu ý rằng trong trường hợp này, chúng tôi muốn xác nhận rằng _playerService.GetForLeague()
chưa bao giờ được gọi.
[Fact]
public void PlayerController_League_Valid()
{
//Arrange
var mockPlayers = new List<Player>()
{
new Player()
{
ID = 1
}
};
var mockLeagueService = new MockLeagueService().MockIsValid(true);
var mockPlayerService = new MockPlayerService().MockGetForLeague(mockPlayers);
var controller = new PlayerController(mockLeagueService.Object,
mockPlayerService.Object);
//Act
var result = controller.League(1); //ID doesn't matter
//Assert
Assert.IsAssignableFrom<ViewResult>(result);
mockPlayerService.VerifyGetForLeague(Times.Once());
mockLeagueService.VerifyIsValid(Times.Once());
}
Sự khác biệt chính mà bạn thấy khi viết các bài kiểm tra unit test cho tầng Controller của ứng dụng ASP.NET MVC là:
Controller
theo cách thủ công mà bạn mong đợi (ví dụ: đoạn mã controller.ModelState.AddModelError("Test", "Test");
sẽ làm cho thuộc tính ModelState.IsValid
có giá trị là false
). Điều này cũng có nghĩa là nếu bạn cần các giá trị trong các thuộc tính khác của đối tượng Controller
, chẳng hạn như Request
, được thiết lập để chức năng của bạn được kiểm tra, bạn phải thiết lập chúng trước khi chạy thử nghiệm.Assert.IsAssignableFrom<>()
để kiểm tra xem kiểu dữ liệu IActionResult
trả về có đúng như những gì bạn mong đợi hay không.Việc viết các bài kiểm tra unit test cho Controller của ASP.NET Core MVC không quá khác biệt so với các bài kiểm tra unit test cho các lớp khác, chỉ khác nhau ở thiết lập lớp controller và sử dụng Assert.IsAssignableFrom<>()
để kiểm tra kiểu dữ liệu kết quả trả về của các phương thức hành động.
Trong phần tiếp theo và cũng là bài viết cuối cùng của loạt bài này, chúng ta sẽ sử dụng các thuộc tính [Theory]
và [InlineData]
của XUnit để viết unit test. Nó cho phép bạn có thể chạy nhiều thử nghiệm với cùng một kết quả mong đợi chỉ trong một vài dòng mã.
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.