使用没有接口的模拟的 C# 单元测试
C# unit testing using mocks without interfaces
为了正确地对我的一些 classes 进行单元测试,我需要模拟被测试的主要 class 使用的 class 对象。这是一个简单的过程,如果所有正在使用的对象都实现了接口 and 我只需要使用该接口的方法。然而,问题来了:
接口是关于其他开发人员在 class 中应该期待什么的契约。因此所有接口方法、属性等都是public。任何好的代码也应该有好的封装。因此,我不想在 class public 中创建所有方法,因此不在接口中声明它。结果我无法模拟和设置我的主要 class.
中使用的这些内部方法
我研究的其他选项是使用抽象 class,它可以有不同的访问修饰符,我可以在其中使用正确的方法,内部或私有等。但是因为我想要 class被模拟为可用的是供其他开发人员使用的 public 属性 接口类型,我需要它是一个接口,但现在它不再兼容,因为我的主要 class 无法调用内部方法不再定义为接口。第 22 条军规。(如果您想知道,对方法使用 virtual 会遇到与使用 abstract class 相同的问题)。
我搜索过有关 C# 模拟的类似问题,但没有找到与我相同的问题。并且请不要对“C# 不是用于测试的好语言”之类的东西抱有哲理。这不是答案。
有什么想法吗?
我添加了这个代码示例,以便更容易看到上述问题。
[assembly: InternalsVisibleTo("MyService.Tests")]
public interface IMyService
{
MethodABC()
...
}
public class MyService : IMyService
{
public void MethodABC()
{
...
}
internal void Initialize()
{
...
}
...
}
public sealed partial class MyMain
{
public IMyService Service { get; private set; }
private MyService _service;
...
private void SomeMethod()
{
// This method is in interface and can be used outside the project when this assembly is referenced
_service.MethodABC()
...
// This method is and internal method inside the class not to be seen or used outside the project.
// 1) When running this method through unit test that mocked the IMyService will fail here since initialize don't exist which is correct.
// 2) Mocking the MyService will require "virtual" to be added to all methods used and don't provide a interface/template for a developers if
// they want to swap the service out.
// 3) Changing the interface to abstract class and mocking that allows for making this an internal method and also set it up in mock to do
// something for unit test and provides contract other developers can implement and pass in to use a different service, but requires
// "override" for all methods to be used from "outside" this assembly. This is best solution but require changes to code for sole purpose
// of testing which is an anti-pattern.
_service.Initialize()
...
}
}
// Unit test method in test project
[TestClass]
public class MyMainTests
{
private Mock<IMyService> _myServiceMock = new Mock<IMyService>();
[TestMethod]
public void MyMain_Test_SomeMethod()
{
...
SomeMethod()
...
}
}
不幸的是,似乎没有一种干净的方法可以做到这一点。为了使用 Moq 创建模拟,它需要是接口、抽象 class 或虚拟方法。
- 接口的封装不能低于 public。使用内部接口仍然会强制您创建“Public”方法。
- 虚拟方法允许使用访问修饰符,但不提供可注入对象和合同,以供其他使用主要 class 的开发人员使用的合同 class。
- 理想的解决方案不需要仅仅为了使其可单元测试而更改代码。不幸的是,这似乎是不可能的。
这让我想到了一个抽象 class,它可以提供一个可以像接口一样处理的模板(半接口),但需要对所有合同方法进行“覆盖”,但至少将允许正确的访问修饰符对于方法。
这仍然不利于干净的代码,因为我需要将代码添加到我的所有方法中,其唯一目的是使其可进行单元测试。
这是 Microsoft 可以研究的新 .Net C# 功能。我会检查他们是否已经有此功能请求。
接口测试没有意义。界面没有说明“它应该做什么”。当我需要用接口测试一些东西时,我在我的 NUnit 测试中制作 MockClass class。此 class 仅适用于少数测试,并且是内部的。如果你的测试 class 和你的测试有相同的命名空间,那么应该有足够的内部空间。所以不是public。但是你仍然不能测试任何私有方法。
有时这很烦人,但我无法在测试中编写漂亮的代码。但这并不奇怪。
我明白了只测试 public 方法和属性的意义,但有时这种限制毫无意义,而且使用接口只是为了支持单元测试。
我的解决方法是继承我正在测试的 class,添加 public 访问方法,然后从中调用受保护的基 class 成员。
要考虑的一个选项是在 internal interface
上指定内部操作,然后您可以使用它来模拟这些操作。根据您的示例,您可以添加:
internal interface IInitialize
{
void Initialize();
}
然后在 class 中与 public 界面一起实现:
public class MyService : IMyService, IInitialize
然后您的消费 class 可以根据需要使用接口:
public sealed partial class MyMain
{
public MyMain(IMyService myService)
{
Service = myService;
}
public IMyService Service { get; }
public void SomeMethod()
{
(Service as IInitialize)?.Initialize();
Service.MethodABC();
}
}
现在在单元测试中,您可以利用 Moq 中的 As<TInterface>()
方法来处理多个接口(阅读 the docs):
[Fact]
public void Test1()
{
Mock<IMyService> myServiceMock = new Mock<IMyService>();
Mock<IInitialize> myServiceInitializeMock = myServiceMock.As<IInitialize>();
//myServiceMock.Setup(s => s.MethodABC()).Whatever
//myServiceInitializeMock.Setup(s => s.Initialize()).Whatever
MyMain myMain = new MyMain(myServiceMock.Object);
myMain.SomeMethod();
myServiceMock.Verify(s => s.MethodABC(), Times.Once);
myServiceInitializeMock.Verify(s => s.Initialize(), Times.Once);
}
请注意 As<TInterface>()
上的以下备注:
This method can only be called before the first use of the mock
Moq.Mock`1.Object property, at which point the runtime type has
already been generated and no more interfaces can be added to it.
另请注意,使用As<TInterface>()
还需要以下属性,以允许模拟代理访问实现内部接口:
[assembly:InternalsVisibleTo("DynamicProxyGenAssembly2")]
为了正确地对我的一些 classes 进行单元测试,我需要模拟被测试的主要 class 使用的 class 对象。这是一个简单的过程,如果所有正在使用的对象都实现了接口 and 我只需要使用该接口的方法。然而,问题来了:
接口是关于其他开发人员在 class 中应该期待什么的契约。因此所有接口方法、属性等都是public。任何好的代码也应该有好的封装。因此,我不想在 class public 中创建所有方法,因此不在接口中声明它。结果我无法模拟和设置我的主要 class.
中使用的这些内部方法我研究的其他选项是使用抽象 class,它可以有不同的访问修饰符,我可以在其中使用正确的方法,内部或私有等。但是因为我想要 class被模拟为可用的是供其他开发人员使用的 public 属性 接口类型,我需要它是一个接口,但现在它不再兼容,因为我的主要 class 无法调用内部方法不再定义为接口。第 22 条军规。(如果您想知道,对方法使用 virtual 会遇到与使用 abstract class 相同的问题)。
我搜索过有关 C# 模拟的类似问题,但没有找到与我相同的问题。并且请不要对“C# 不是用于测试的好语言”之类的东西抱有哲理。这不是答案。
有什么想法吗?
我添加了这个代码示例,以便更容易看到上述问题。
[assembly: InternalsVisibleTo("MyService.Tests")]
public interface IMyService
{
MethodABC()
...
}
public class MyService : IMyService
{
public void MethodABC()
{
...
}
internal void Initialize()
{
...
}
...
}
public sealed partial class MyMain
{
public IMyService Service { get; private set; }
private MyService _service;
...
private void SomeMethod()
{
// This method is in interface and can be used outside the project when this assembly is referenced
_service.MethodABC()
...
// This method is and internal method inside the class not to be seen or used outside the project.
// 1) When running this method through unit test that mocked the IMyService will fail here since initialize don't exist which is correct.
// 2) Mocking the MyService will require "virtual" to be added to all methods used and don't provide a interface/template for a developers if
// they want to swap the service out.
// 3) Changing the interface to abstract class and mocking that allows for making this an internal method and also set it up in mock to do
// something for unit test and provides contract other developers can implement and pass in to use a different service, but requires
// "override" for all methods to be used from "outside" this assembly. This is best solution but require changes to code for sole purpose
// of testing which is an anti-pattern.
_service.Initialize()
...
}
}
// Unit test method in test project
[TestClass]
public class MyMainTests
{
private Mock<IMyService> _myServiceMock = new Mock<IMyService>();
[TestMethod]
public void MyMain_Test_SomeMethod()
{
...
SomeMethod()
...
}
}
不幸的是,似乎没有一种干净的方法可以做到这一点。为了使用 Moq 创建模拟,它需要是接口、抽象 class 或虚拟方法。
- 接口的封装不能低于 public。使用内部接口仍然会强制您创建“Public”方法。
- 虚拟方法允许使用访问修饰符,但不提供可注入对象和合同,以供其他使用主要 class 的开发人员使用的合同 class。
- 理想的解决方案不需要仅仅为了使其可单元测试而更改代码。不幸的是,这似乎是不可能的。
这让我想到了一个抽象 class,它可以提供一个可以像接口一样处理的模板(半接口),但需要对所有合同方法进行“覆盖”,但至少将允许正确的访问修饰符对于方法。
这仍然不利于干净的代码,因为我需要将代码添加到我的所有方法中,其唯一目的是使其可进行单元测试。
这是 Microsoft 可以研究的新 .Net C# 功能。我会检查他们是否已经有此功能请求。
接口测试没有意义。界面没有说明“它应该做什么”。当我需要用接口测试一些东西时,我在我的 NUnit 测试中制作 MockClass class。此 class 仅适用于少数测试,并且是内部的。如果你的测试 class 和你的测试有相同的命名空间,那么应该有足够的内部空间。所以不是public。但是你仍然不能测试任何私有方法。
有时这很烦人,但我无法在测试中编写漂亮的代码。但这并不奇怪。
我明白了只测试 public 方法和属性的意义,但有时这种限制毫无意义,而且使用接口只是为了支持单元测试。
我的解决方法是继承我正在测试的 class,添加 public 访问方法,然后从中调用受保护的基 class 成员。
要考虑的一个选项是在 internal interface
上指定内部操作,然后您可以使用它来模拟这些操作。根据您的示例,您可以添加:
internal interface IInitialize
{
void Initialize();
}
然后在 class 中与 public 界面一起实现:
public class MyService : IMyService, IInitialize
然后您的消费 class 可以根据需要使用接口:
public sealed partial class MyMain
{
public MyMain(IMyService myService)
{
Service = myService;
}
public IMyService Service { get; }
public void SomeMethod()
{
(Service as IInitialize)?.Initialize();
Service.MethodABC();
}
}
现在在单元测试中,您可以利用 Moq 中的 As<TInterface>()
方法来处理多个接口(阅读 the docs):
[Fact]
public void Test1()
{
Mock<IMyService> myServiceMock = new Mock<IMyService>();
Mock<IInitialize> myServiceInitializeMock = myServiceMock.As<IInitialize>();
//myServiceMock.Setup(s => s.MethodABC()).Whatever
//myServiceInitializeMock.Setup(s => s.Initialize()).Whatever
MyMain myMain = new MyMain(myServiceMock.Object);
myMain.SomeMethod();
myServiceMock.Verify(s => s.MethodABC(), Times.Once);
myServiceInitializeMock.Verify(s => s.Initialize(), Times.Once);
}
请注意 As<TInterface>()
上的以下备注:
This method can only be called before the first use of the mock Moq.Mock`1.Object property, at which point the runtime type has already been generated and no more interfaces can be added to it.
另请注意,使用As<TInterface>()
还需要以下属性,以允许模拟代理访问实现内部接口:
[assembly:InternalsVisibleTo("DynamicProxyGenAssembly2")]