Unit test cho controller trong ASP.NET Core với xUnit và Moq
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.
Viết unit test cho lớp LeagueController
Đầ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:
- KHÔNG CÓ giải đấu nào được trả về.
- CÓ các giải đấu được trả về.
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;
}
}
Unit test #1: KHÔNG CÓ giải đấu nào được trả về
[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());
}
Unit test #2: CÓ các giải đấu được trả về
[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
.
Viết 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:
- Trong phiên bản HTTP GET của phương thức hành động
Search()
, hãy kiểm tra xemViewResult
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. - Trong phiên bản HTTP POST của phương thức hành động
Search()
, nếuModelState.IsValid
làfalse
, hãy kiểm tra xemRedirectToActionResult
có được trả về không. - Trong phiên bản HTTP POST của phương thức hành động
Search()
, nếuModelState.IsValid
làtrue
VÀ không có đội nào được trả về, hãy kiểm tra xemRedirectToActionResult
có được trả về không. - Trong phiên bản HTTP POST của phương thức hành động
Search()
, nếuModelState.IsValid
làtrue
và có đội được trả về, hãy kiểm tra xemViewResult
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;
}
}
Unit test #1: Search GET
[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ức
Assert.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.
Unit test #2: Search POST, ModelState không hợp lệ
[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.
Unit test #3: Search POST, ModelState hợp lệ, không có kết quả tìm kiếm
[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());
}
Unit test #4: Search POST, ModelState hợp lệ, có kết quả tìm kiếm
[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());
}
Viết unit test cho lớp PlayerController
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:
- Trong phương thức hành động
Index()
, Id cầu thủ không hợp lệ trả vềRedirectToActionResult
. - Trong phương thức hành động
Index()
, Id cầu thủ hợp lệ trả vềViewResult
. - Trong phương thức hành động
League()
, Id giải đấu không hợp lệ trả vềRedirectToActionResult
. - Trong phương thức hành động
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;
}
}
Unit test #1: Phương thức hành động Index, Id cầu thủ không hợp lệ
[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());
}
Unit test #2: Phương thức hành động Index, Id cầu thủ hợp lệ
[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());
}
Unit test #3: Phương thức hành động League, Id giải đấu không hợp lệ
[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.
Unit test #4: Phương thức hành động League, Id giải đấu hợp lệ
[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());
}
Nhận xét
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à:
- Bạn phải thiết lập các thuộc tính của lớp
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ínhModelState.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ượngController
, 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. - Tốt hơn là sử dụng
Assert.IsAssignableFrom<>()
để kiểm tra xem kiểu dữ liệuIActionResult
trả về có đúng như những gì bạn mong đợi hay không.
Tóm lược
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ã.