Mock class 继承自 abstract class 并实现接口

Mock class which inherit from abstract class and implements interface

假设以下场景:我有一个 PhoneController class,它使用 Phone class。 Phone 是一个 class 继承自抽象 class Device 并且它实现了 IPhone 接口。为了测试 PhoneController 我想模拟 Phone class,但我不知道如何使用 NSubstitute 来完成,因为 Phone class 继承了抽象 class 并且它还实现了接口。

示例代码:

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);
    }
}

第二种情况:

控制器使用 Device 抽象 class 中的 GetStatus 方法,因此 _phone 不能更改为 IPhone 类型

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);
    }
}

有几种方法可以做到这一点,您选择哪一种取决于您要测试的内容。

  1. 您可以模拟 IPhone 接口(就像您在注释掉的代码中所做的那样。

  2. 您可以子class Phone class(手动,或使用 NSubstitute 的 .ForPartsOf<>). See here 用于博客 post这个.

我发现如果我使用 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);
}

编辑 - 基于更新的评论

您不能对抽象 class 进行单元测试,因为 class 不包含(根据定义)任何代码。在您的场景中,看起来您要做的是测试具体的 Phone class。如果是这样,那么只需创建 Phone class 的实例并对其进行测试;你不需要让控制器参与进来:

// Arrange
var phone = new Phone();

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

// Assert
Assert.Equal("New", result);

如果 PhoneController 使用 Phone,那么您将不得不使用 PhonePhone 的子 class。使用 Substitute.For<Device, IPhone>() 将生成一个有点像这样的类型:

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

这与 Phone 不同。

所以有几个选项。理想情况下,我会考虑 PhoneController 取而代之的是 IPhone。通过让它依赖于一个接口而不是具体的东西,我们获得了一些灵活性:PhoneController 现在可以使用任何遵循 IPhone 接口的东西,包括一个模拟的 IPhone 实例。这也意味着我们可以通过其他实现来更改生产代码的行为(人为的示例,可能是 EncryptedPhone : IPhone 加密调用,而不需要更改 PhoneController)。

如果您确实需要将特定的 Phone class 耦合到 PhoneController,那么模拟会立即变得更加困难。毕竟,您是在声明 "this only works with this specific class",然后尝试让它与另一个 class(替代的 class)一起工作。对于这种方法,如果您能够创建 Phone class virtual 的所有相关成员,那么您可以使用 Substitute.For<Phone>() 创建替代。如果你还需要运行一些原来的Phone代码,但替换其他部分,那么你可以使用Substitute.ForPartsOf<Phone>()作为。请记住 NSubstitute(以及许多其他 .NET 模拟库)不会模拟非 virtual 成员,因此在模拟 classes.

时请记住这一点

最后,值得考虑的是完全不模拟 Phone 而是使用真实的 class。如果 PhoneController 取决于特定 Phone 实现的细节,那么用假版本测试它不会告诉你它是否有效。如果相反它只需要一个兼容的接口,那么它是使用替代品的一个很好的候选者。模拟库会自动创建替代类型以与 class 一起使用,但它们不会自动拥有适应该类型使用的设计。 :)

为第二个场景编辑

我认为我之前的回答仍然适用于附加场景。回顾一下:我的第一种方法是尝试将 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);

或者我们可以创建 IPhoneIDevice 然后有:

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

我以前看到过像这样的接口继承不鼓励,所以你可能想在选择这个选项之前先研究一下。

您还可以结合这些方法,将上面示例中的 IDevice 替换为您当前的 Device 基础 class 并模拟它(免责声明:非 virtual s).

我认为您需要回答的主要问题是您希望如何将 PhoneController 与其依赖项耦合?考虑它需要哪些具体依赖关系,以及可以用逻辑接口表达什么。答案将决定您的测试选项。