如何在单元测试中使用Moq调用同一类中的另一个方法

51

你好,我是 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的调用?

1
@Nkosi 你的测试很好!不要试图测试超出你所声明测试范围的内容——你不应该测试除了通过注册号获取车辆以外的任何其他内容。任何特定车辆的测试都应该保留在它们自己的测试方法中,就像你已经编写的那样。不要一次测试多个事情。 - Travis Sharp
3个回答

37

过分嘲笑被测试类的做法已经使你忽略了真正的问题。

从被测试类的注释中可以看出...

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

实际上,您暴露了应该注入到该类中的两个依赖项。

为了解释这个答案,让我们假设这些依赖关系看起来像这样。

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)时,被模拟包装的基本Selecter将被调用,然后该基类将调用在测试主题下被设置覆盖的模拟GetTyreSpecification
您往往会在测试具有依赖于抽象成员的实现成员的抽象类时看到这一点。

29

在测试类时,不应该试图模拟被测试类的方法。模拟框架用于替换类所需的依赖项的实际调用,以便您可以专注于测试类的行为而不会受到其外部依赖项的干扰。

由于您的Selecter类没有任何外部依赖项,因此您不需要模拟任何内容。如果可以请尽量避免模拟并测试实际代码本身。当然,如果有任何外部依赖项,则需要模拟对其的调用以保持测试原子性。


我明白。但是GetTyresSpecification方法中有一些我不想执行的内容,当我测试GetVehicleByRegistrationNumber时,这是实际应用程序中问题的简化版本,在getTyreSpecification中进行了7次对外部系统的调用。难道不能模拟这个方法并只检索我想要的轮胎规格数据吗? - user2227138
2
你有两个选择。你可以模拟在 getTyreSpecification 方法内部发生的外部调用,或者你可以将该方法提取到其自己的类中,包装在接口中,并将接口注入到 Selecter 类中。这样做可以让你进行模拟。 - levelnis
1
好的,谢谢你的回答。我模拟了外部调用。这意味着我在测试GetTyresSpecification方法时有重复的代码。一个是在GetTyresSpecification方法的测试方法中,另一个是在GetVehicleByregistrationNumber方法中执行完全相同的操作。正确吗? - user2227138

6

通常,我们在编写单元测试时会使用模拟器对外部依赖项/其他对象/接口调用进行处理。因此,当您为其中一个函数编写测试时,该函数内部调用同一类中的另一个函数时,无需模拟该函数调用。但是,在内部函数中,如果您要调用外部接口,则必须模拟外部接口实例,并编写带有预期结果的单元测试。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接