从实际对象的实例返回默认模拟结果
Returning default mock results from actual objects' instances
考虑需要为 REST API
控制器编写 integration tests
的场景。我为此使用 MSTest
。逻辑如下:
- 创建新的
database
、运行 init SQL
迁移
- 创建测试
HTTP Client
并针对 endpoints
进行调用
- 处理测试
DB
。
Use-case大部分端点做验证,是否SQL
命令(INSERT
,UPDATE
,DELETE
) return 是否成功(布尔值 true
)。要复制此场景,我需要知道确切的 SQL
故障条件(困难),或者更简单的选择是 能够模拟存储库 。当然,这不再是一个完整的集成测试,但至少我能够以这种方式测试 REST
业务逻辑和 HTTP response status codes
。额外的好处是测试数据库事务回滚功能。
问题 因此,我想知道 - 是否可以创建一个模拟,默认情况下 return 实际对象实例的所有值,除非 setup()
是否提供覆盖?
Example/working 代码 在测试初始化时,我们创建一个 HTTP 测试客户端实例和一个模拟存储库:
_usersRepo = new UsersRepository(); // actual instance of repository
var application = new WebApplicationFactory<Startup>()
.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
_usersRepoMock = new Mock<IUsersRepository>();
// setting mock just for 1 of the methods manually
_usersRepoMock
.Setup(p => p.GetByIdAsync(It.IsAny<long>()))
.Returns(async (long r) => await _usersRepo.GetByIdAsync(r));
services.AddSingleton<IUsersRepository>(_usersRepoMock.Object);
});
});
_client = application.CreateClient(new WebApplicationFactoryClientOptions());
看到我已经为单个方法手动配置了一个模拟,以return来自实际存储库的结果。
然后,在测试 class 中,每当我需要失败时,我都会执行以下操作:
[TestMethod]
public async Task Db_throws_exception()
{
_usersRepoMock.Setup(p => p.GetByIdAsync(It.IsAny<long>())).Throws(new System.Exception("test"));
//.. rest of the code
}
此解决方案按预期工作,但是,问题是为了使此方法工作,我需要手动将所有模拟对象请求代理到实际实例。我在每个回购协议中有大约 4-6 个方法和大约 8 个不同的回购协议(将来会更多)我想避免这样做。可能吗?
这是我想避免做的事情:
_usersRepoMock = new Mock<IUsersRepository>();
_usersRepoMock.Setup(p => p.GetByIdAsync(It.IsAny<long>())).Returns(async (long r) => await _usersRepo.GetByIdAsync(r));
_usersRepoMock.Setup(p => p.Create(It.IsAny<long>())).Returns(async (long r) => await _usersRepo.Create(r));
_usersRepoMock.Setup(p => p.Update(It.IsAny<long>())).Returns(async (long r) => await _usersRepo.Update(r));
_repo2Mock = new Mock<IRepo2>();
_repo2Mock.Setup(p => p.Method1(It.IsAny<long>())).Returns(async (long r) => await _repo2.Method1(r));
_repo2Mock.Setup(p => p.Method2(It.IsAny<long>())).Returns(async (long r) => await _repo2.Method2(r));
_repo2Mock.Setup(p => p.Method3(It.IsAny<long>())).Returns(async (long r) => await _repo2.Method3(r));
_repo2Mock.Setup(p => p.Method4(It.IsAny<long>())).Returns(async (long r) => await _repo2.Method4(r));
_repo2Mock.Setup(p => p.Method5(It.IsAny<long>())).Returns(async (long r) => await _repo2.Method5(r));
_repo3Mock = new Mock<IRepo3>();
_repo3Mock.Setup(p => p.Method1(It.IsAny<long>())).Returns(async (long r) => await _repo3.Method1(r));
_repo3Mock.Setup(p => p.Method2(It.IsAny<long>())).Returns(async (long r) => await _repo3.Method2(r));
_repo3Mock.Setup(p => p.Method3(It.IsAny<long>())).Returns(async (long r) => await _repo3.Method3(r));
_repo3Mock.Setup(p => p.Method4(It.IsAny<long>())).Returns(async (long r) => await _repo3.Method4(r));
_repo3Mock.Setup(p => p.Method5(It.IsAny<long>())).Returns(async (long r) => await _repo3.Method5(r));
Whosebug 上有一些类似的问题,但我找不到与我相同的用例,也找不到可接受的问题答案。这是一个例子:Default behavior for mock inherited from object instance
p.s。我可能花在写这个问题上的时间比写模拟代码的时间还多:)
我认为您可以使用 Mock
构造函数的重载之一实现您想要的。
首先,在您的测试项目中创建 IUsersRepository
的实现,并确保其所有成员都是虚拟的。使用默认行为实现所有成员。
class TestUsersRepository : IUsersRepository
{
public virtual async Task<User> GetByIdAsync(long id)
{
// (...)
}
// (...)
}
然后在您的测试中,使用您的测试存储库创建一个 Mock
。
您现在应该能够使用 Setup
方法覆盖您想要的成员。
[TestMethod]
public async Task Db_throws_exception()
{
var mockRepo = new Mock<TestUsersRepository>(() => new TestUsersRepository());
mockRepo .Setup(p => p.GetByIdAsync(It.IsAny<long>())).Throws(new System.Exception("test"));
//.. rest of the code
}
考虑需要为 REST API
控制器编写 integration tests
的场景。我为此使用 MSTest
。逻辑如下:
- 创建新的
database
、运行 initSQL
迁移 - 创建测试
HTTP Client
并针对endpoints
进行调用
- 处理测试
DB
。
Use-case大部分端点做验证,是否SQL
命令(INSERT
,UPDATE
,DELETE
) return 是否成功(布尔值 true
)。要复制此场景,我需要知道确切的 SQL
故障条件(困难),或者更简单的选择是 能够模拟存储库 。当然,这不再是一个完整的集成测试,但至少我能够以这种方式测试 REST
业务逻辑和 HTTP response status codes
。额外的好处是测试数据库事务回滚功能。
问题 因此,我想知道 - 是否可以创建一个模拟,默认情况下 return 实际对象实例的所有值,除非 setup()
是否提供覆盖?
Example/working 代码 在测试初始化时,我们创建一个 HTTP 测试客户端实例和一个模拟存储库:
_usersRepo = new UsersRepository(); // actual instance of repository
var application = new WebApplicationFactory<Startup>()
.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
_usersRepoMock = new Mock<IUsersRepository>();
// setting mock just for 1 of the methods manually
_usersRepoMock
.Setup(p => p.GetByIdAsync(It.IsAny<long>()))
.Returns(async (long r) => await _usersRepo.GetByIdAsync(r));
services.AddSingleton<IUsersRepository>(_usersRepoMock.Object);
});
});
_client = application.CreateClient(new WebApplicationFactoryClientOptions());
看到我已经为单个方法手动配置了一个模拟,以return来自实际存储库的结果。
然后,在测试 class 中,每当我需要失败时,我都会执行以下操作:
[TestMethod]
public async Task Db_throws_exception()
{
_usersRepoMock.Setup(p => p.GetByIdAsync(It.IsAny<long>())).Throws(new System.Exception("test"));
//.. rest of the code
}
此解决方案按预期工作,但是,问题是为了使此方法工作,我需要手动将所有模拟对象请求代理到实际实例。我在每个回购协议中有大约 4-6 个方法和大约 8 个不同的回购协议(将来会更多)我想避免这样做。可能吗?
这是我想避免做的事情:
_usersRepoMock = new Mock<IUsersRepository>();
_usersRepoMock.Setup(p => p.GetByIdAsync(It.IsAny<long>())).Returns(async (long r) => await _usersRepo.GetByIdAsync(r));
_usersRepoMock.Setup(p => p.Create(It.IsAny<long>())).Returns(async (long r) => await _usersRepo.Create(r));
_usersRepoMock.Setup(p => p.Update(It.IsAny<long>())).Returns(async (long r) => await _usersRepo.Update(r));
_repo2Mock = new Mock<IRepo2>();
_repo2Mock.Setup(p => p.Method1(It.IsAny<long>())).Returns(async (long r) => await _repo2.Method1(r));
_repo2Mock.Setup(p => p.Method2(It.IsAny<long>())).Returns(async (long r) => await _repo2.Method2(r));
_repo2Mock.Setup(p => p.Method3(It.IsAny<long>())).Returns(async (long r) => await _repo2.Method3(r));
_repo2Mock.Setup(p => p.Method4(It.IsAny<long>())).Returns(async (long r) => await _repo2.Method4(r));
_repo2Mock.Setup(p => p.Method5(It.IsAny<long>())).Returns(async (long r) => await _repo2.Method5(r));
_repo3Mock = new Mock<IRepo3>();
_repo3Mock.Setup(p => p.Method1(It.IsAny<long>())).Returns(async (long r) => await _repo3.Method1(r));
_repo3Mock.Setup(p => p.Method2(It.IsAny<long>())).Returns(async (long r) => await _repo3.Method2(r));
_repo3Mock.Setup(p => p.Method3(It.IsAny<long>())).Returns(async (long r) => await _repo3.Method3(r));
_repo3Mock.Setup(p => p.Method4(It.IsAny<long>())).Returns(async (long r) => await _repo3.Method4(r));
_repo3Mock.Setup(p => p.Method5(It.IsAny<long>())).Returns(async (long r) => await _repo3.Method5(r));
Whosebug 上有一些类似的问题,但我找不到与我相同的用例,也找不到可接受的问题答案。这是一个例子:Default behavior for mock inherited from object instance
p.s。我可能花在写这个问题上的时间比写模拟代码的时间还多:)
我认为您可以使用 Mock
构造函数的重载之一实现您想要的。
首先,在您的测试项目中创建 IUsersRepository
的实现,并确保其所有成员都是虚拟的。使用默认行为实现所有成员。
class TestUsersRepository : IUsersRepository
{
public virtual async Task<User> GetByIdAsync(long id)
{
// (...)
}
// (...)
}
然后在您的测试中,使用您的测试存储库创建一个 Mock
。
您现在应该能够使用 Setup
方法覆盖您想要的成员。
[TestMethod]
public async Task Db_throws_exception()
{
var mockRepo = new Mock<TestUsersRepository>(() => new TestUsersRepository());
mockRepo .Setup(p => p.GetByIdAsync(It.IsAny<long>())).Throws(new System.Exception("test"));
//.. rest of the code
}