使用 DI DbContext 和使用 LINQ 的模型对控制器进行单元测试——EF Core 2
Unit testing a controller with DI DbContext and Model that uses LINQ -- EF Core 2
开始之前:
- 我们使用带有 Postgis 扩展的 PostgreSQL(不能使用 inMemoryDB 选项,因为我们使用的是 Postgis 的几何结构)
- 我们不使用存储库模式
- 我们使用 DbContext 访问 DbModel 并在其上使用 Linq 表达式
- 我们使用数据库优先原则
这种问题以前也有不同程度的问过,但感觉没有回答的很简洁,所以再问一次。
欢迎参考或一般指南,提前致谢。
这是我目前所拥有的。
控制器:
public class StartTripController : Controller
{
private readonly DbContext _dbContext;
public StartTripController(DbContext DbContext) => _DbContext = DbContext;
[Route("connect")]
[HttpGet]
public async Task<IActionResult> StartTrip(MessageDto messageDto, StartTripDto startTripDto)
{
if (ModelState.ErrorCount > 0)
return StatusCode(400);
var userToCheck = await _DbContext.User
.Select(i => new UserDto { UserId = i.Id, PhoneId = i.PhoneId, AppInfoDto = new AppInfoDto { IsAppInDebug = false } })
.SingleOrDefaultAsync(u => u.PhoneId == startTripDto.UserDto.PhoneId); //checks if User is in DB, returns Null if not
if (userToCheck == null) //user does not exist
{
//Make new User entity and save the changes to DB async
UserDto newUserToReturn = new UserDto { UserId = user.Id, AppInfoDto = new AppInfoDto { IsAppInDebug = user.DebugMode } };
return GenerateResponseWithStatus200(messageDto, newUserToReturn);
}
//user exists
return GenerateResponseWithStatus200(messageDto, userToCheck);
}
我的测试是这样的:
public class StartTripControllerTest : ControllerTest<StartTripController>
{
private DbContext _mockDbContext;
protected override StartTripController GetController()
{
var mockDbContext = new Mock<DbContext>();
var userData = new List<User>
{
new User{PhoneId = "Phone1", Id = 1, ReportProviderId = 1, UserPhone = null, DebugMode = true, IpAddress = "empty", DeviceUser = null, Credential = null},
new User{PhoneId = "Phone2", Id = 2, ReportProviderId = 2, UserPhone = null, DebugMode = true, IpAddress = "empty", DeviceUser = null, Credential = null}
};
var mockData = userData.AsQueryable().BuildMock(); //BuildMock is from https://github.com/romantitov/MockQueryable
mockDbContext.Setup(x => ???what do I write here??).Returns(mockData.Object);
return new StartTripController(mockDbContext.Object);
}
[Fact]
public async System.Threading.Tasks.Task StartTrip_ReturnUser_JsonAsync()
{
// Arrange
StartTripController startTripController = GetController();
MessageDto messageDto = new MessageDto ();
StartTripDto startTripDto = new StartTripDto();
//code omitted for readiblity
var result = await startTripController.StartTrip(messageDto, startTripDto);
}
}
我发现的事情:
- 您必须模拟接口才能使异步方法工作As per Microsoft's post
我坚持的事情:
- 在这种情况下,如何模拟 DbContext 或猜测模型,以便我可以像使用常规代码一样使用 LINQ 表达式?
所以我想分享我对这个问题的回答。这主要是@Fabio 建议与我在原始问题中发布的第一个参考主题混合使用的内容。
如果您愿意,我将我的数据库交互移动到了 "UserManager" 或服务。现在我的控制器看起来像这样:
public class StartTripController : Controller
{
private readonly IUserManager _userManager;
public StartTripController( IUserManager userManager)
{
_userManager = userManager;
}
[Route("connect")]
[HttpGet]
public async Task<IActionResult> StartTrip(MessageDto messageDto, StartTripDto startTripDto)
{
messageDto.Message = Any.Pack(startTripDto);
if (ModelState.ErrorCount > 0)
return StatusCode(400);
var userToCheck = await _userManager.FindUser(startTripDto.UserDto);
if (userToCheck == null) //user does not exist
{
var newUser = await _userManager.AddUser(startTripDto.UserDto);
return GenerateResponseWithStatus200(messageDto, newUser);
}
//user exists
await _userManager.StartTripExistingUser(userToCheck);
return GenerateResponseWithStatus200(messageDto, userToCheck);
}
}
这自动更改了我的测试,因为我没有模拟数据库,这大大简化了我的问题。
我的测试是这样的:
public class StartTripControllerTest : 控制器测试
{
protected override StartTripController GetController()
{
var mockUserManager = new Mock<IUserManager>();
AppInfoDto appInfoDto = new AppInfoDto {IsAppInDebug = true};
UserDto userDto = new UserDto {UserId = 1818, PhoneId = "Phone1", AppInfoDto = appInfoDto};
mockUserManager.Setup(p => p.FindUser(It.IsAny<UserDto>())).Returns(Task.FromResult(userDto));
return new StartTripController(mockUserManager.Object);
}
[Fact]
[Trait("Unit", "Controller")]
public void StartTrip_ReturnUser_BadRequestAsync()
{
// Arrange
StartTripController startTripController = GetController();
MessageDto messageDto = new MessageDto { ApiVersion = "1.3" };
AppInfoDto appInfoDto = new AppInfoDto { IsAppInDebug = true };
UserDto userDto = new UserDto { PhoneId = "Phone1", AppInfoDto = appInfoDto };
StartTripDto startTripDto = new StartTripDto { UserDto = userDto };
startTripController.ModelState.AddModelError(ModelBinderError.MissingUserId.errorKey, ModelBinderError.MissingUserId.errorValue);
var result = startTripController.StartTrip(messageDto, startTripDto).Result as StatusCodeResult;
Assert.True(result.StatusCode == 400);
}
}
}
以上示例展示了使用 "GetController" 方法初始化控制器,还展示了如何使用 ModelState。
然而,这并不能解决根本问题,只是将其移至系统的不同部分。当您必须测试 UserManager 时,模拟数据库仍然存在问题。
为了测试这部分系统,您确实需要进行交互测试。
对于 SQLServer,您可以使用 InMemoryDatabase,但是因为我使用的是 Postgresql,所以我需要使用 TestDatabase。
总而言之,我的 UserManager 测试看起来像这样:
public class UserManagerIntegrationTests
{
private readonly TestServer _server;
private readonly HttpClient _client;
public UserManagerIntegrationTests()
{
// Arrange
_server = new TestServer(new WebHostBuilder()
.UseStartup<StartupWithTestDatabase>());//Startup file contains the TestDatabase connection string
_client = _server.CreateClient();
}
// ...
}
P.S。我现在正在看的是Fluent Assertions,它基本上用改变的方法代替了正常的Assert.True()。
P.P.S 仅供将来参考,我在 ASP.NET Core 2 的单元测试中找到的最新教程是 this one,它还展示了一个您进行集成的示例使用控制器和与数据库交互的服务进行测试。
开始之前:
- 我们使用带有 Postgis 扩展的 PostgreSQL(不能使用 inMemoryDB 选项,因为我们使用的是 Postgis 的几何结构)
- 我们不使用存储库模式
- 我们使用 DbContext 访问 DbModel 并在其上使用 Linq 表达式
- 我们使用数据库优先原则
这种问题以前也有不同程度的问过
这是我目前所拥有的。 控制器:
public class StartTripController : Controller
{
private readonly DbContext _dbContext;
public StartTripController(DbContext DbContext) => _DbContext = DbContext;
[Route("connect")]
[HttpGet]
public async Task<IActionResult> StartTrip(MessageDto messageDto, StartTripDto startTripDto)
{
if (ModelState.ErrorCount > 0)
return StatusCode(400);
var userToCheck = await _DbContext.User
.Select(i => new UserDto { UserId = i.Id, PhoneId = i.PhoneId, AppInfoDto = new AppInfoDto { IsAppInDebug = false } })
.SingleOrDefaultAsync(u => u.PhoneId == startTripDto.UserDto.PhoneId); //checks if User is in DB, returns Null if not
if (userToCheck == null) //user does not exist
{
//Make new User entity and save the changes to DB async
UserDto newUserToReturn = new UserDto { UserId = user.Id, AppInfoDto = new AppInfoDto { IsAppInDebug = user.DebugMode } };
return GenerateResponseWithStatus200(messageDto, newUserToReturn);
}
//user exists
return GenerateResponseWithStatus200(messageDto, userToCheck);
}
我的测试是这样的:
public class StartTripControllerTest : ControllerTest<StartTripController>
{
private DbContext _mockDbContext;
protected override StartTripController GetController()
{
var mockDbContext = new Mock<DbContext>();
var userData = new List<User>
{
new User{PhoneId = "Phone1", Id = 1, ReportProviderId = 1, UserPhone = null, DebugMode = true, IpAddress = "empty", DeviceUser = null, Credential = null},
new User{PhoneId = "Phone2", Id = 2, ReportProviderId = 2, UserPhone = null, DebugMode = true, IpAddress = "empty", DeviceUser = null, Credential = null}
};
var mockData = userData.AsQueryable().BuildMock(); //BuildMock is from https://github.com/romantitov/MockQueryable
mockDbContext.Setup(x => ???what do I write here??).Returns(mockData.Object);
return new StartTripController(mockDbContext.Object);
}
[Fact]
public async System.Threading.Tasks.Task StartTrip_ReturnUser_JsonAsync()
{
// Arrange
StartTripController startTripController = GetController();
MessageDto messageDto = new MessageDto ();
StartTripDto startTripDto = new StartTripDto();
//code omitted for readiblity
var result = await startTripController.StartTrip(messageDto, startTripDto);
}
}
我发现的事情:
- 您必须模拟接口才能使异步方法工作As per Microsoft's post
我坚持的事情:
- 在这种情况下,如何模拟 DbContext 或猜测模型,以便我可以像使用常规代码一样使用 LINQ 表达式?
所以我想分享我对这个问题的回答。这主要是@Fabio 建议与我在原始问题中发布的第一个参考主题混合使用的内容。
如果您愿意,我将我的数据库交互移动到了 "UserManager" 或服务。现在我的控制器看起来像这样:
public class StartTripController : Controller
{
private readonly IUserManager _userManager;
public StartTripController( IUserManager userManager)
{
_userManager = userManager;
}
[Route("connect")]
[HttpGet]
public async Task<IActionResult> StartTrip(MessageDto messageDto, StartTripDto startTripDto)
{
messageDto.Message = Any.Pack(startTripDto);
if (ModelState.ErrorCount > 0)
return StatusCode(400);
var userToCheck = await _userManager.FindUser(startTripDto.UserDto);
if (userToCheck == null) //user does not exist
{
var newUser = await _userManager.AddUser(startTripDto.UserDto);
return GenerateResponseWithStatus200(messageDto, newUser);
}
//user exists
await _userManager.StartTripExistingUser(userToCheck);
return GenerateResponseWithStatus200(messageDto, userToCheck);
}
}
这自动更改了我的测试,因为我没有模拟数据库,这大大简化了我的问题。
我的测试是这样的: public class StartTripControllerTest : 控制器测试 {
protected override StartTripController GetController()
{
var mockUserManager = new Mock<IUserManager>();
AppInfoDto appInfoDto = new AppInfoDto {IsAppInDebug = true};
UserDto userDto = new UserDto {UserId = 1818, PhoneId = "Phone1", AppInfoDto = appInfoDto};
mockUserManager.Setup(p => p.FindUser(It.IsAny<UserDto>())).Returns(Task.FromResult(userDto));
return new StartTripController(mockUserManager.Object);
}
[Fact]
[Trait("Unit", "Controller")]
public void StartTrip_ReturnUser_BadRequestAsync()
{
// Arrange
StartTripController startTripController = GetController();
MessageDto messageDto = new MessageDto { ApiVersion = "1.3" };
AppInfoDto appInfoDto = new AppInfoDto { IsAppInDebug = true };
UserDto userDto = new UserDto { PhoneId = "Phone1", AppInfoDto = appInfoDto };
StartTripDto startTripDto = new StartTripDto { UserDto = userDto };
startTripController.ModelState.AddModelError(ModelBinderError.MissingUserId.errorKey, ModelBinderError.MissingUserId.errorValue);
var result = startTripController.StartTrip(messageDto, startTripDto).Result as StatusCodeResult;
Assert.True(result.StatusCode == 400);
}
}
}
以上示例展示了使用 "GetController" 方法初始化控制器,还展示了如何使用 ModelState。
然而,这并不能解决根本问题,只是将其移至系统的不同部分。当您必须测试 UserManager 时,模拟数据库仍然存在问题。
为了测试这部分系统,您确实需要进行交互测试。 对于 SQLServer,您可以使用 InMemoryDatabase,但是因为我使用的是 Postgresql,所以我需要使用 TestDatabase。
总而言之,我的 UserManager 测试看起来像这样:
public class UserManagerIntegrationTests
{
private readonly TestServer _server;
private readonly HttpClient _client;
public UserManagerIntegrationTests()
{
// Arrange
_server = new TestServer(new WebHostBuilder()
.UseStartup<StartupWithTestDatabase>());//Startup file contains the TestDatabase connection string
_client = _server.CreateClient();
}
// ...
}
P.S。我现在正在看的是Fluent Assertions,它基本上用改变的方法代替了正常的Assert.True()。
P.P.S 仅供将来参考,我在 ASP.NET Core 2 的单元测试中找到的最新教程是 this one,它还展示了一个您进行集成的示例使用控制器和与数据库交互的服务进行测试。