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);
}
}
有几种方法可以做到这一点,您选择哪一种取决于您要测试的内容。
您可以模拟 IPhone 接口(就像您在注释掉的代码中所做的那样。
您可以子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
,那么您将不得不使用 Phone
或 Phone
的子 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);
或者我们可以创建 IPhone
、IDevice
然后有:
interface IPhoneDevice : IPhone, IDevice { }
public class Phone : IPhoneDevice { ... }
我以前看到过像这样的接口继承不鼓励,所以你可能想在选择这个选项之前先研究一下。
您还可以结合这些方法,将上面示例中的 IDevice
替换为您当前的 Device
基础 class 并模拟它(免责声明:非 virtual
s).
我认为您需要回答的主要问题是您希望如何将 PhoneController
与其依赖项耦合?考虑它需要哪些具体依赖关系,以及可以用逻辑接口表达什么。答案将决定您的测试选项。
假设以下场景:我有一个 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);
}
}
有几种方法可以做到这一点,您选择哪一种取决于您要测试的内容。
您可以模拟 IPhone 接口(就像您在注释掉的代码中所做的那样。
您可以子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
,那么您将不得不使用 Phone
或 Phone
的子 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>()
作为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);
或者我们可以创建 IPhone
、IDevice
然后有:
interface IPhoneDevice : IPhone, IDevice { }
public class Phone : IPhoneDevice { ... }
我以前看到过像这样的接口继承不鼓励,所以你可能想在选择这个选项之前先研究一下。
您还可以结合这些方法,将上面示例中的 IDevice
替换为您当前的 Device
基础 class 并模拟它(免责声明:非 virtual
s).
我认为您需要回答的主要问题是您希望如何将 PhoneController
与其依赖项耦合?考虑它需要哪些具体依赖关系,以及可以用逻辑接口表达什么。答案将决定您的测试选项。