Ложный класс, который наследуется от абстрактного класса и реализует интерфейс - PullRequest
0 голосов
/ 10 мая 2018

Предположим следующий сценарий: у меня есть класс PhoneController, который использует класс Phone. Phone - это класс, который наследуется от абстрактного класса Device, и реализует интерфейс IPhone. Для тестирования PhoneController я хочу смоделировать Phone класс, но я не знаю, как это можно сделать с помощью NSubstitute, потому что Phone класс наследует абстрактный класс и дополнительно реализует интерфейс.

Пример кода:

public abstract class Device
{
    protected string Address { get; set; }
}

public interface IPhone
{
    void MakeCall();
}

public class Phone : Device, IPhone
{
    public void MakeCall()
    {
        throw new NotImplementedException();
    }
}

public class PhoneController
{
    private Phone _phone;

    public PhoneController(Phone phone)
    {
        _phone = phone;
    }
}

[TestClass]
public class PhoneControllerTests
{
    [TestMethod]
    public void TestMethod1()
    {
        // How mock Phone class? 
        //var mock = Substitute.For<Device, IPhone>();

        //usage of mock
        //var controller = new PhoneController(mock);
    }
}

Второй сценарий:

Контроллер использует GetStatus метод из Device абстрактного класса, поэтому _phone нельзя изменить на IPhone type

public abstract class Device
{
    protected string Address { get; set; }

    public abstract string GetStatus();
}

public interface IPhone
{
    void MakeCall();
}

public class Phone : Device, IPhone
{
    public void MakeCall()
    {
        throw new NotImplementedException();
    }

    public override string GetStatus()
    {
        throw new NotImplementedException();
    }
}

public class PhoneController
{
    private Phone _phone;

    public PhoneController(Phone phone)
    {
        _phone = phone;
    }

    public string GetDeviceStatus()
    {
        return _phone.GetStatus();
    }

    public void MakeCall()
    {
        _phone.MakeCall();
    }
}

[TestClass]
public class PhoneControllerTests
{
    [TestMethod]
    public void TestMethod1()
    {
        // How mock Phone class? 
        //var mock = Substitute.For<Device, IPhone>();

        //usage of mock
        //var controller = new PhoneController(mock);
    }
}

Ответы [ 2 ]

0 голосов
/ 11 мая 2018

Если PhoneController принимает Phone, то вам придется использовать Phone или подкласс Phone.Использование Substitute.For<Device, IPhone>() сгенерирует тип, подобный следующему:

public class Temp : Device, IPhone { ... }

Это не то же самое, что Phone.

Так что есть несколько вариантов.В идеале я рассмотрел бы предложение @ pm_2 о том, чтобы вместо PhoneController взять IPhone.Заставляя его зависеть от интерфейса, а не от чего-то конкретного, мы получаем некоторую гибкость: PhoneController теперь может работать со всем, что соответствует интерфейсу IPhone, включая поддельный экземпляр IPhone.Это также означает, что мы можем изменить поведение производственного кода с помощью других реализаций (надуманный пример, может быть EncryptedPhone : IPhone, который шифрует вызов, не требуя изменения на PhoneController).

Если вам нужно связать определенный класс Phone с PhoneController, то насмешка сразу становится более сложной.В конце концов, вы заявляете «это работает только с этим конкретным классом», а затем пытаетесь заставить его работать с другим классом (замещенным классом).Для этого подхода, если вы можете сделать все соответствующие члены Phone класса virtual, вы можете создать замену, используя Substitute.For<Phone>().Если вам также нужно выполнить какой-то оригинальный код Phone, но заменить другие части, то вы можете использовать Substitute.ForPartsOf<Phone>(), как @ pm_2 предложил .Помните, что NSubstitute (и многие другие библиотеки .NET для насмешек) не будут издеваться над не virtual членами, так что имейте это в виду, когда насмехаетесь над классами.

Наконец, стоит подумать о том, чтобы вообще не насмехаться Phoneи используя реальный класс вместо этого.Если PhoneController зависит от деталей конкретной реализации Phone, то тестирование ее с поддельной версией не скажет вам, работает ли она.Если вместо этого ему нужен только совместимый интерфейс, то он является хорошим кандидатом для использования замены.Библиотеки-насмешки автоматизируют создание альтернативного типа для использования с классом, но они не будут автоматизировать создание дизайна, который бы соответствовал использованию этого типа.:)

Изменить для второго сценария

Я думаю, что мой предыдущий ответ все еще применим для дополнительного сценария.Напомним: мой первый подход - попытаться отделить PhoneController от конкретной реализации;тогда я бы подумал о замене Phone напрямую (с оговоркой о не virtual методах).И я бы всегда помнил, что вообще не надо насмехаться (это, вероятно, должно быть первым вариантом).

Есть много способов достичь первого варианта.Мы можем обновить PhoneController, чтобы получить IDevice (извлекать интерфейс из Device) и IPhone, с конструктором PhoneController(IPhone p, IDevice d).Тогда реальный код может быть:

var phone = new Phone();
var controller = new PhoneController(phone, phone);

В то время как тестовый код может быть:

var phone = Substitute.For<IPhone>();
var device = Substitute.For<IDevice>();
var testController = new PhoneController(phone, device);
// or
var phone = Substitute.For<IPhone, IDevice>();
var testController = new PhoneController(phone, (IDevice) phone);

В качестве альтернативы мы можем создать IPhone, IDevice и затем иметь:

interface IPhoneDevice : IPhone, IDevice { }
public class Phone : IPhoneDevice { ... }

Я видел, что наследование интерфейса, как это раньше, не поощрялось, поэтому вы можете сначала изучить его, прежде чем использовать эту опцию.

Вы также можете объединить эти подходы, заменив IDevice вПриведенные выше примеры с вашим текущим Device базовым классом и насмешкой над этим (заявление об отказе: не- virtual с).

Я думаю, что основной вопрос, на который вам нужно ответить, - как вы хотите соединить PhoneControllerк его зависимостям?Подумайте об этом с точки зрения того, какие конкретные зависимости ему нужны, и что можно выразить в терминах логических интерфейсов.Ответ на этот вопрос определит ваши варианты тестирования.

0 голосов
/ 10 мая 2018

Есть несколько способов сделать это, и выбор зависит от того, что вы пытаетесь проверить.

  1. Вы можете смоделировать интерфейс IPhone (как вы сделали в закомментированном коде.

  2. Вы можете создать подкласс класса Phone (либо вручную, либо использовать NSubstitute's .ForPartsOf<>). См. здесь для блога по этому вопросу.

Я обнаружил, что если я структурирую свой тест с использованием метода Arrange / Act / Assert, то будет проще понять, что я пытаюсь проверить (в идеале в разделе Act должен быть один вызов; например:

[TestMethod]
public void TestMethod1()
{
    // Arrange
    var mock = Substitute.For<IPhone>();
    var controller = new PhoneController(mock);

    // Act
    int result = controller.Method();

    // Assert
    Assert.Equal(result, 3);
}

РЕДАКТИРОВАТЬ - На основе обновленных комментариев

Вы не можете модульно протестировать абстрактный класс, потому что этот класс не содержит (по определению) никакого кода. В вашем сценарии похоже, что вы пытаетесь протестировать конкретный класс Phone. Если это так, просто создайте экземпляр класса Phone и протестируйте его; вам не нужно привлекать контроллер:

// Arrange
var phone = new Phone();

// Act
string result = phone.GetStatus();

// Assert
Assert.Equal("New", result);
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...