从实际对象的实例返回默认模拟结果

Returning default mock results from actual objects' instances

考虑需要为 REST API 控制器编写 integration tests 的场景。我为此使用 MSTest。逻辑如下:

  1. 创建新的 database、运行 init SQL 迁移
  2. 创建测试 HTTP Client 并针对 endpoints
  3. 进行调用
  4. 处理测试 DB

Use-case大部分端点做验证,是否SQL命令(INSERTUPDATEDELETE ) 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
}