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

本文关键字:一类 Moq 另一个 方法 单元测试 调用 | 更新日期: 2023-09-27 18:13:20

嗨,我是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的调用

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

把注意力集中在模拟被测试的类上,使您忽略了实际的问题。

从被测试类的注释中…

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

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

为了解释这个答案,我们假设这些依赖关系是这样的:

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,然后调用被测试的模拟主题上的设置覆盖的模拟GetTyreSpecification

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

您不应该试图在您要测试的类上模拟方法。mock框架被用来代替对类的依赖的实际调用,这样你就可以专注于测试类的行为,而不会被它所拥有的外部依赖分散注意力。

你的Selecter类没有外部依赖,所以你不需要模拟任何东西。如果没有必要,我总是提倡不要嘲弄,而是测试实际的代码本身。显然,为了保持测试的原子性,如果存在外部依赖项,则需要模拟对外部依赖项的调用。

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