【问题标题】:How to use Moq in unit test that calls another method in same class如何在单元测试中使用 Moq 调用同一类中的另一个方法
【发布时间】:2013-05-14 08:16:32
【问题描述】:

您好,我是 Moq 框架的新手,对如何使用它有一些疑问。我会举个例子,希望得到答案。

我有两个类,一个接口和一个实现:

public class Vehicle{
   public string RegistrationNumber {get; set;}
   public long VehicleIdentifier { get; set; }
   public Tyre TyreSpecification { get; set; }
}

public class Tyre {
    public long NumberOfTyres {get; set;}
    public long TyreSize { get; set;}
}

public interface ISelecter {
   Vehicle GetVehicleByRegistrationNumber(string registrationNumber);
   Tyre GetTyreSpecification(long vehicleIdentifier);
}

public class Selecter : ISelecter
{
    public Vehicle GetVehicleByRegistrationNumber(string registrationNumber)
    {
        var vehicle = 'Database will give us the vehicle specification';

        //Then we do things with the vehicle object

        //Get the tyre specification
        vehicle.TyreSpecification = GetTyreSpecification(vehicle.VehicleIdentifier);

        return vehicle;

    }

    public Tyre GetTyreSpecification(long vehicleIdentifier)
    {
         var tyre = 'external manufacture system gets the tyre specification';

         //Then do thing with the tyre before returning the object


         return tyre;
    }
}

我想为这些方法编写两个测试。问题是当我为GetVehicleByRegistrationNumber 编写测试时,我不知道如何模拟对GetTyreSpecification 的方法调用。

测试方法如下:

[TestClass]
public class SelecterTest
{
    [TestMethod]
    public void GetTyreSpecification_test()
    {
        //Arrange
        var tyre = new Tyre { NumberOfTyres = 4, TyreSize = 18 };

        var mockSelecter = new Mock<ISelecter>();
        mockSelecter.SetUp(s=>s.GetTyreSpecification(It.IsAny<long>())).Returns(tyre);

        //Act
        var tyreSpec = mockSelecter.Object.GetTyreSpecification(123456);

        //Assert
        Assert.IsTrue(tyreSpec.NumberOfTyres == 4 && tyreSpec.TyreSize == 18);
    }

    [TestMethod]
    public void GetVehicleByRegistrationNumber_test()
    {
        //Arrange
        var vehicle= new Vehicle { VehicleIdentifier = 123456, RegistrationNumber = ABC123, TyreSpecification = new Tyre { Tyresize = 18, NumberOfTyres = 4 }};

        var mockSelecter = new Mock<ISelecter>();
        mockSelecter.SetUp(s=>s.GetVehicleByRegistrationNumber(It.IsAny<string>     ())).Returns(vehicle);

        //Act
        var vehicle = mockSelecter.Object.GetVehicleByregistrationNumber(123456);

        //Assert
        Assert.IsTrue(vehicle.Registrationnumber == "ABC123";
    }
}

在测试方法GetVehicleByRegistrationNumber_test 中如何模拟对getTyreSpecification 的调用?

【问题讨论】:

  • @Nkosi 你的测试很好!不要试图测试比你所说的测试更多的东西——你不应该测试比通过注册号获得车辆更多的东西。任何车辆特定的测试都应该保存在他们自己的测试方法中,就像你上面已经写的一样。一次不要测试超过一件事。

标签: c# unit-testing moq


【解决方案1】:

专注于模拟被测类使您对实际问题视而不见。

来自被测类中的 cmets...

  • '数据库将为我们提供车辆规格'
  • '外部制造系统获取轮胎规格'

您实际上公开了两个应该注入到类中的依赖项。

为了解释这个答案,假设这些依赖项看起来像这样。

public interface IDatabase {
    Vehicle GetVehicleByRegistrationNumber(string registrationNumber);
}

public interface IExternalManufactureSystem {
    Tyre GetTyreSpecification(long vehicleIdentifier);
}

这意味着Selecter 需要重构以期望这些依赖项。

public class Selecter : ISelecter {
    private IDatabase database;
    private IExternalManufactureSystem externalManufactureSystem;

    public Selecter(IDatabase database, IExternalManufactureSystem externalManufactureSystem) {
        this.database = database;
        this.externalManufactureSystem = externalManufactureSystem;
    }

    public Vehicle GetVehicleByRegistrationNumber(string registrationNumber) {
        //'Database will give us the vehicle specification'
        var vehicle = database.GetVehicleByRegistrationNumber(registrationNumber);

        //Then we do things with the vehicle object

        //Get the tyre specification
        vehicle.TyreSpecification = GetTyreSpecification(vehicle.VehicleIdentifier);

        return vehicle;
    }

    public Tyre GetTyreSpecification(long vehicleIdentifier) {
        //'external manufacture system gets the tyre specification'
        var tyre = externalManufactureSystem.GetTyreSpecification(vehicleIdentifier);

        //Then do thing with the tyre before returning the object

        return tyre;
    }
}

从那里开始,只需要模拟显式需要的依赖项来测试被测方法的行为。

selecter.GetTyreSpecification 不需要访问数据库,因此没有理由模拟和注入它进行测试。

[TestMethod]
public void GetTyreSpecification_test() {
    //Arrange
    var vehicleIdentifier = 123456;
    var expected = new Tyre { NumberOfTyres = 4, TyreSize = 18 };

    var mockSystem = new Mock<IExternalManufactureSystem>();
    mockSystem.Setup(s => s.GetTyreSpecification(vehicleIdentifier)).Returns(expected);

    var selecter = new Selecter(null, mockSystem.Object);

    //Act
    var actual = selecter.GetTyreSpecification(vehicleIdentifier);

    //Assert
    Assert.AreEqual(expected, actual);
}

selecter.GetVehicleByRegistrationNumber 但是需要能够从其他方法获取轮胎规范,因此该测试需要模拟两个依赖项才能完成。

[TestMethod]
public void GetVehicleByRegistrationNumber_test() {
    //Arrange
    var vehicleIdentifier = 123456;
    var registrationNumber = "ABC123";
    var tyre = new Tyre { TyreSize = 18, NumberOfTyres = 4 };
    var expected = new Vehicle {
        VehicleIdentifier = vehicleIdentifier,
        RegistrationNumber = registrationNumber,
        TyreSpecification = tyre
    };

    var mockSystem = new Mock<IExternalManufactureSystem>();
    mockSystem.Setup(s => s.GetTyreSpecification(vehicleIdentifier)).Returns(tyre);

    var mockDatabase = new Mock<IDatabase>();
    mockDatabase.Setup(s => s.GetVehicleByRegistrationNumber(registrationNumber)).Returns(expected);

    var selecter = new Selecter(mockDatabase.Object, mockSystem.Object);

    //Act
    var actual = selecter.GetVehicleByRegistrationNumber(registrationNumber);

    //Assert
    Assert.IsTrue(actual.RegistrationNumber == registrationNumber);
}    

现在解决这个问题,例如,如果 Selecter 类将 GetVehicleByRegistrationNumber 作为 virtual 方法,

public virtual Tyre GetTyreSpecification(long vehicleIdentifier) {
    //...code removed for brevity.
}

有一种方法可以使用 moq 来存根被测对象并模拟该方法进行测试。这并不总是最好的设计,被认为是一种代码味道。但是,在某些情况下,您最终会遇到这种特殊情况。

[TestMethod]
public void GetVehicleByRegistrationNumber_test2() {
    //Arrange
    var vehicleIdentifier = 123456;
    var registrationNumber = "ABC123";
    var tyre = new Tyre { TyreSize = 18, NumberOfTyres = 4 };
    var expected = new Vehicle {
        VehicleIdentifier = vehicleIdentifier,
        RegistrationNumber = registrationNumber,
        TyreSpecification = tyre
    };        

    var mockDatabase = new Mock<IDatabase>();
    mockDatabase.Setup(s => s.GetVehicleByRegistrationNumber(registrationNumber)).Returns(expected);

    var selecter = new Mock<Selecter>(mockDatabase.Object, null) {
        CallBase = true //So that base methods that are not setup can be called.
    }

    selecter.Setup(s => s.GetTyreSpecification(vehicleIdentifier)).Returns(tyre);

    //Act
    var actual = selecter.Object.GetVehicleByRegistrationNumber(registrationNumber);

    //Assert
    Assert.IsTrue(actual.RegistrationNumber == registrationNumber);
} 

在上面的例子中,当selecter.Object.GetVehicleByRegistrationNumber(registrationNumber)被调用时,被mock包裹的基础Selecter会被调用,然后它又会调用被mock主题上的设置覆盖的mocked GetTyreSpecification测试。

在测试具有依赖于抽象成员的已实现成员的抽象类时,您往往会看到这一点。

【讨论】:

    【解决方案2】:

    您不应该尝试在您要测试的类上模拟方法。模拟框架用于用虚假调用替换对类接受的依赖项的实际调用,以便您可以专注于测试类的行为,而不会被它所具有的外部依赖项分心。

    Selecter 类没有引入外部依赖项,因此您不需要模拟任何内容。如果您不需要并测试实际代码本身,我将始终主张不要嘲笑。显然,为了让您的测试保持原子性,您需要模拟对外部依赖项的调用(如果有)。

    【讨论】:

    • 我明白了。但是 GetTyresSpecification 有一些我不想执行的东西,当我测试 GetVehicleByRegistrationNumber 这是实际应用程序中问题的简化版本 7 对外部系统的调用是在 getTyreSpecification 中进行的。难道不能模拟这个方法,只检索我想要的 TyreSpecification 数据吗?
    • 您有 2 个选择。您可以模拟在getTyreSpecification 方法中发生的外部调用,或者您可以将该方法拉出到它自己的类中,包装在一个接口中,然后将该接口注入到您的Selecter 类中。这样你就可以模拟它了。
    • 好的,谢谢您的回答。我嘲笑外部电话。这意味着我为测试 GetTyresSpecification 方法复制了代码。 GetTyresSpecification 方法的测试方法中的一个代码 sn-p 和 GetVehicleByregistrationNumber 方法中的一个代码 sn-p 执行完全相同的操作。对吗?
    【解决方案3】:

    一般来说,我们将模拟用于在我们将编写单元测试的类中使用的外部依赖项/其他对象/接口调用。因此,当您为其中一个函数编写测试时,该函数在内部调用同一类中的另一个函数,您不必模拟该函数调用。但是在内部函数中,如果您正在调用外部接口,那么您将不得不模拟外部接口实例并编写具有预期结果的单元测试

    【讨论】:

      【解决方案4】:
       var mockSelecter = new Mock<ISelecter>{ CallBase = true };
       mockSelecter.SetUp(s=>s.GetTyreSpecification(It.IsAny<long>())).Returns(tyre);
      

      【讨论】:

      • 请扩展您的答案以包括对您的代码的解释。
      • 使用 CallBase 时,您需要使用实际的类而不是接口。调用接口的基类没有意义。如果使用的是真实类并且该方法是虚拟的,则上述方法将起作用。
      猜你喜欢
      • 1970-01-01
      • 2020-05-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-12-30
      • 1970-01-01
      • 2020-08-24
      相关资源
      最近更新 更多