Как использовать Moq в unit test, который вызывает другой метод в том же классе

Привет Я новичок в платформе 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

Вы не должны пытаться высмеять метод в классе, который вы пытаетесь проверить. Mocking frameworks используются для замены фактических вызовов, сделанных зависимостями, которые ваш класс использует с поддельными вызовами, чтобы вы могли сосредоточиться на тестировании поведения вашего класса, не отвлекаясь на внешние зависимости, которые у него есть.

В вашем классе Selecter нет внешних зависимостей, поэтому вам не нужно ничего издеваться. Я всегда защищал бы насмешку, если вам не нужно и проверять сам код. Очевидно, чтобы сохранить свой тестовый атомный, вам нужно было бы высмеивать вызовы внешних зависимостей, если они были.

Ответ 2

Акцент на насмешку над тестируемым классом ослепил вас к реальной проблеме.

Из комментариев в классе под тестом...

  • 'База данных предоставит нам спецификацию транспортного средства
  • 'внешняя система производства получает спецификацию шины

вы фактически выставляете две зависимости, которые должны быть введены в класс.

В целях объяснения этого ответа можно сказать, что эти зависимости выглядели следующим образом.

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, который был переопределен настройкой на издеваемую тему тест.

Вы склонны видеть это при тестировании абстрактных классов с реализованными членами, которые имеют зависимости от абстрактных членов.

Ответ 3

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

Ответ 4

В общем случае мы будем использовать mocks для внешних зависимостей/других вызовов объектов/интерфейсов, используемых в нашем классе, для которых мы будем писать модульные тесты. Поэтому, когда вы пишете тест для одной из ваших функций, которые внутренне делают вызов другой функции в одном классе, вам не нужно издеваться над этим вызовом функции. Однако во внутренней функции, если вы вызываете внешний интерфейс, вам придется издеваться над внешним интерфейсом и написать unit test с ожидаемым результатом