在 class 中的所有方法上使用 Virtual 关键字的后果?
Consequences of using Virtual keyword on all methods in a class?
我是 TDD 的新手,我正在使用 Moq
作为我的模拟框架。
我正在尝试检查我的 class 中是否调用了某个方法。
class 没有实现任何接口。
var mockFooSaverService = new Mock<FooSaverService>();
mockFooSaverService.Verify(service => service.Save(mockNewFoo.Object));
为了完成这项工作,我发现 here 我必须将 Save()
方法作为 Virtual
方法。
问题:
为了使其可测试而对 class 中的所有方法使用 Virtual
关键字会产生什么后果?
TL;DR
根据评论,需要 virtual 关键字表明您的 class 层次结构耦合得太紧,您应该应用 SOLID principles 将它们彼此分离。这具有使 class 层次结构更易于单元测试的 "happy" 副作用,因为可以通过接口抽象模拟依赖项。
更详细
需要将所有 public 方法虚拟化以允许 Moq 覆盖它们通常表示关注点分离或 class 耦合气味。
例如this scenario needed virtual methods 因为 class under test 有多个问题,并且需要模拟一个方法并在同一被测系统中实际调用另一个方法。
根据@JonSkeet 的评论,将依赖项抽象为接口是常见的 SOLID 最佳实践。就目前而言,你的 class 被测试(我可以称之为 "Controller" 吗?)依赖于具体的 FooSaverService
来保存 Foos。
通过应用Dependency Inversion Principle,可以通过将FooSaverService
的外部有用的方法、属性和事件抽象到一个接口(IFooSaverService
)来放松这种耦合,然后
FooSaverService
实施 IFooSaverService
Controller
仅依赖于 IFooSaverService
(显然,可能还有其他优化,例如使 IFooSaverService
通用,例如 ISaverService<Foo>
但不在此处的范围内)
回复:Mock<Foo>
- 需要模拟简单数据存储 classes(POCO、实体、DTO 等)的情况并不常见 - 因为这些通常会保留存储在其中的数据,并且可以直接在单元测试中进行推理。
回答你关于 Virtual
的问题(希望现在不太相关):
- 您正在破坏(多态)Open and Closed Principle - 它邀请其他人在没有为此特意设计的情况下覆盖行为 - 可能会产生意想不到的后果。
- 根据 Henk 的评论,管理 virtual method table
会对性能产生很小的影响
代码示例
如果将所有这些放在一起,您将得到一个 class 层次结构,如下所示:
// Foo is assumed to be an entity / POCO
public class Foo
{
public string Name { get; set; }
public DateTime ExpiryDate { get; set; }
}
// Decouple the Saver Service dependency via an interface
public interface IFooSaverService
{
void Save(Foo aFoo);
}
// Implementation
public class FooSaverService : IFooSaverService
{
public void Save(Foo aFoo)
{
// Persist this via ORM, Web Service, or ADO etc etc.
}
// Other non public methods here are implementation detail and not relevant to consumers
}
// Class consuming the FooSaverService
public class FooController
{
private readonly IFooSaverService _fooSaverService;
// You'll typically use dependency injection here to provide the dependency
public FooController(IFooSaverService fooSaverService)
{
_fooSaverService = fooSaverService;
}
public void PersistTheFoo(Foo fooToBeSaved)
{
if (fooToBeSaved == null) throw new ArgumentNullException("fooToBeSaved");
if (fooToBeSaved.ExpiryDate.Year > 2015)
{
_fooSaverService.Save(fooToBeSaved);
}
}
}
然后您将能够测试具有 IFooSaverService
依赖项的 class,如下所示:
[TestFixture]
public class FooControllerTests
{
[Test]
public void PersistingNullFooMustThrow()
{
var systemUnderTest = new FooController(new Mock<IFooSaverService>().Object);
Assert.Throws<ArgumentNullException>(() => systemUnderTest.PersistTheFoo(null));
}
[Test]
public void EnsureOldFoosAreNotSaved()
{
var mockFooSaver = new Mock<IFooSaverService>();
var systemUnderTest = new FooController(mockFooSaver.Object);
systemUnderTest.PersistTheFoo(new Foo{Name = "Old Foo", ExpiryDate = new DateTime(1999,1,1)});
mockFooSaver.Verify(m => m.Save(It.IsAny<Foo>()), Times.Never);
}
[Test]
public void EnsureNewFoosAreSaved()
{
var mockFooSaver = new Mock<IFooSaverService>();
var systemUnderTest = new FooController(mockFooSaver.Object);
systemUnderTest.PersistTheFoo(new Foo { Name = "New Foo", ExpiryDate = new DateTime(2038, 1, 1) });
mockFooSaver.Verify(m => m.Save(It.IsAny<Foo>()), Times.Once);
}
}
TL;DR;
另一个好的答案是 - 使 classes 可扩展,并提供虚方法(即扩展它们的可能性)是 class 的 "feature"。并且此功能需要像其他任何功能一样得到支持和测试。
可以在 Eric Lippert's blog 上阅读更好的解释。
我是 TDD 的新手,我正在使用 Moq
作为我的模拟框架。
我正在尝试检查我的 class 中是否调用了某个方法。
class 没有实现任何接口。
var mockFooSaverService = new Mock<FooSaverService>();
mockFooSaverService.Verify(service => service.Save(mockNewFoo.Object));
为了完成这项工作,我发现 here 我必须将 Save()
方法作为 Virtual
方法。
问题:
为了使其可测试而对 class 中的所有方法使用 Virtual
关键字会产生什么后果?
TL;DR
根据评论,需要 virtual 关键字表明您的 class 层次结构耦合得太紧,您应该应用 SOLID principles 将它们彼此分离。这具有使 class 层次结构更易于单元测试的 "happy" 副作用,因为可以通过接口抽象模拟依赖项。
更详细
需要将所有 public 方法虚拟化以允许 Moq 覆盖它们通常表示关注点分离或 class 耦合气味。 例如this scenario needed virtual methods 因为 class under test 有多个问题,并且需要模拟一个方法并在同一被测系统中实际调用另一个方法。
根据@JonSkeet 的评论,将依赖项抽象为接口是常见的 SOLID 最佳实践。就目前而言,你的 class 被测试(我可以称之为 "Controller" 吗?)依赖于具体的 FooSaverService
来保存 Foos。
通过应用Dependency Inversion Principle,可以通过将FooSaverService
的外部有用的方法、属性和事件抽象到一个接口(IFooSaverService
)来放松这种耦合,然后
FooSaverService
实施IFooSaverService
Controller
仅依赖于IFooSaverService
(显然,可能还有其他优化,例如使 IFooSaverService
通用,例如 ISaverService<Foo>
但不在此处的范围内)
回复:Mock<Foo>
- 需要模拟简单数据存储 classes(POCO、实体、DTO 等)的情况并不常见 - 因为这些通常会保留存储在其中的数据,并且可以直接在单元测试中进行推理。
回答你关于 Virtual
的问题(希望现在不太相关):
- 您正在破坏(多态)Open and Closed Principle - 它邀请其他人在没有为此特意设计的情况下覆盖行为 - 可能会产生意想不到的后果。
- 根据 Henk 的评论,管理 virtual method table 会对性能产生很小的影响
代码示例
如果将所有这些放在一起,您将得到一个 class 层次结构,如下所示:
// Foo is assumed to be an entity / POCO
public class Foo
{
public string Name { get; set; }
public DateTime ExpiryDate { get; set; }
}
// Decouple the Saver Service dependency via an interface
public interface IFooSaverService
{
void Save(Foo aFoo);
}
// Implementation
public class FooSaverService : IFooSaverService
{
public void Save(Foo aFoo)
{
// Persist this via ORM, Web Service, or ADO etc etc.
}
// Other non public methods here are implementation detail and not relevant to consumers
}
// Class consuming the FooSaverService
public class FooController
{
private readonly IFooSaverService _fooSaverService;
// You'll typically use dependency injection here to provide the dependency
public FooController(IFooSaverService fooSaverService)
{
_fooSaverService = fooSaverService;
}
public void PersistTheFoo(Foo fooToBeSaved)
{
if (fooToBeSaved == null) throw new ArgumentNullException("fooToBeSaved");
if (fooToBeSaved.ExpiryDate.Year > 2015)
{
_fooSaverService.Save(fooToBeSaved);
}
}
}
然后您将能够测试具有 IFooSaverService
依赖项的 class,如下所示:
[TestFixture]
public class FooControllerTests
{
[Test]
public void PersistingNullFooMustThrow()
{
var systemUnderTest = new FooController(new Mock<IFooSaverService>().Object);
Assert.Throws<ArgumentNullException>(() => systemUnderTest.PersistTheFoo(null));
}
[Test]
public void EnsureOldFoosAreNotSaved()
{
var mockFooSaver = new Mock<IFooSaverService>();
var systemUnderTest = new FooController(mockFooSaver.Object);
systemUnderTest.PersistTheFoo(new Foo{Name = "Old Foo", ExpiryDate = new DateTime(1999,1,1)});
mockFooSaver.Verify(m => m.Save(It.IsAny<Foo>()), Times.Never);
}
[Test]
public void EnsureNewFoosAreSaved()
{
var mockFooSaver = new Mock<IFooSaverService>();
var systemUnderTest = new FooController(mockFooSaver.Object);
systemUnderTest.PersistTheFoo(new Foo { Name = "New Foo", ExpiryDate = new DateTime(2038, 1, 1) });
mockFooSaver.Verify(m => m.Save(It.IsAny<Foo>()), Times.Once);
}
}
TL;DR; 另一个好的答案是 - 使 classes 可扩展,并提供虚方法(即扩展它们的可能性)是 class 的 "feature"。并且此功能需要像其他任何功能一样得到支持和测试。
可以在 Eric Lippert's blog 上阅读更好的解释。