【问题标题】:Asserting a call to a public method on the same mock instance断言对同一模拟实例上的公共方法的调用
【发布时间】:2026-01-26 13:35:01
【问题描述】:

我有以下测试

[Test]
public void Attack_TargetWith3Damage_CausesAttackerToDeal3DamageToTarget()
{
    var realAttacker = CreateCreature(damage: 3);
    var wrappedAttacker = A.Fake<ICreature>(x => x.Wrapping(realAttacker));
    var target = A.Fake<ICreature>();
    wrappedAttacker.Attack(target);
    A.CallTo(() => wrappedAttacker.DealDamage(target, 3)).MustHaveHappened();
}

问题是从Attack 方法调用DealDamage 没有被注册,因为在方法内部,thisrealAttacker 而不是wrappedAttacker 攻击者因此方法调用不是被拦截了。

如何测试这个断言?这可以用 FakeItEasy 完成吗?不同的模拟框架是否允许我对此进行测试?

【问题讨论】:

  • 这也是我在使用 NSubstitute 时遇到的问题。

标签: c# unit-testing mocking nunit fakeiteasy


【解决方案1】:

使用Moq 作为你的模拟框架后,你可以非常接近你现在的样子。

以此为例:

public interface ICreature { ... }

public class Creature : ICreature
{
    ... 

    public void Attack(ICreature creature)
    {
        DealDamage(creature, 3); // Hard-coded 3 to simplify example only
    }

    public virtual void DealDamage(ICreature target, int damage) { ... }
}

.... Test ....
var wrappedAttacker = new Mock<Creature>();
var mockTarget = new Mock<ICreature>();

wrappedAttacker.Object.Attack(mockTarget.Object);

wrappedAttacker.Verify(x => x.DealDamage(mockTarget.Object, 3), Times.Once());

在这种情况下,我将Creature instance“包装”在一个模拟攻击者角色中,并为目标角色创建一个ICreature 模拟。然后我从攻击者那里调用Attack 方法;验证是否调用了同一攻击者的DealDamage(目标正确,伤害为 3),恰好是一次。

使这种验证在 Moq 中成为可能的原因是 DealDamage 函数被标记为 virtual。对于您的情况,这可能会破坏交易,但它确实解决了“不同的模拟框架允许我对此进行测试吗?”问题。

【讨论】:

  • 所以...OP 需要查看 FakeItEasy 中是否有类似 CallBase 的内容。我将不得不与 NSub 核对,看看是否有类似的方法。我预计大多数模拟框架都会有些相似。
  • @IAbstract 实际上我只是发现在这种情况下甚至不需要CallBase,我已经清理了我的答案。
【解决方案2】:

感谢@ckittel 为我指出了这个答案。为此,Creature 类需要有一个无参数构造函数,并且方法需要是虚拟的。

FakeItEasy 的另一件事似乎是你必须告诉它调用基本方法,否则在概念上它是相同的,只是语法不同。

[Test]
public void Attack_TargetWith3Damage_CausesAttackerToDeal3DamageToTarget()
{
    var attacker = A.Fake<Creature>();
    A.CallTo(attacker).CallsBaseMethod(); //Otherwise it seems all calls are no-ops.
    attacker.Stats.Damage = 3;

    var target = A.Fake<ICreature>();
    attacker.Attack(target);
    A.CallTo(() => attacker.DealDamage(target, 3)).MustHaveHappened();
}

【讨论】:

  • FWIW,就构造函数参数而言,起订量没有这个限制。如有必要,我本可以提供构造函数参数,只是我没有在回答中保持简单。