接口和包装器太多?
Too many interfaces and wrappers?
我正在慢慢开始掌握单元测试和模拟的窍门,但这是一个缓慢的过程。我已经尝试对这个 Active Directory 代码进行单元测试。这个问题与AD没有严格的关系。
class ActiveDirectoryQueryer {
DirectorySearcher mSearcher;
public ActiveDirectoryQueryer() {
var searcher = new DirectorySearcher(...);
}
public void GetAllMailEntries() {
MailEntries =
mSearcher
.FindAll()
.Select(result => result.GetDirectoryEntry())
.Select(BuildNewADUser)
.ToList();
}
static ActiveDirectoryUser BuildNewADUser(DirectoryEntry pDirectoryEntry) {
return ActiveDirectoryUser.Create(
pDirectoryEntry.Guid,
(pDirectoryEntry.Properties["name"].Value ?? "").ToString(),
(pDirectoryEntry.Properties["mail"].Value ?? "").ToString()
);
}
所以,我想对 GetAllMailEntries
方法进行单元测试。为了使用 MOQ 执行此操作,我不得不为各种 .NET 类型手动生成接口和包装器,并将上述许多对接口的引用更改为替代(如 IDirectoryEntry
)。下面的每个 IXxxx
接口都有一个关联的包装器 class XxxxWrapper
。总共我为这个测试添加了至少 12 个新的源文件。这是单元测试的结果:
[TestMethod]
public void TestGetAllMailEntries() {
var mockSearcher = new Mock<IDirectorySearcher>();
var mockResultCollection = new Mock<ISearchResultCollection>();
var mockSearchResult = new Mock<ISearchResult>();
var mockDirectoryEntry = new Mock<IDirectoryEntry>();
var mockPropertyCollection = new Mock<IPropertyCollection>();
var nameMockPropertyValueCollection = new Mock<IPropertyValueCollection>();
var mailMockPropertyValueCollection = new Mock<IPropertyValueCollection>();
const string name = "SomeNameValue";
const string mailAddress = "SomeMailAddress";
nameMockPropertyValueCollection.SetupGet(pvc => pvc.Value).Returns(name);
mailMockPropertyValueCollection.SetupGet(pvc => pvc.Value).Returns(mailAddress);
mockPropertyCollection.SetupGet(pc => pc["name"]).Returns(nameMockPropertyValueCollection.Object);
mockPropertyCollection.SetupGet(pc => pc["mail"]).Returns(mailMockPropertyValueCollection.Object);
mockDirectoryEntry.SetupGet(de => de.Properties).Returns(mockPropertyCollection.Object);
mockSearchResult.Setup(sr => sr.GetDirectoryEntry()).Returns(mockDirectoryEntry.Object);
mockResultCollection.Setup(results => results.GetEnumerator()).Returns(new List<ISearchResult> { mockSearchResult.Object }.GetEnumerator());
mockSearcher.Setup(searcher => searcher.FindAll()).Returns(mockResultCollection.Object);
var queryer = new ActiveDirectoryQueryer(mockSearcher.Object);
queryer.GetAllMailEntries();
Assert.AreEqual(1, queryer.MailEntries.Count());
var entry = queryer.MailEntries.Single();
Assert.AreEqual(name, entry.Name);
Assert.AreEqual(mailAddress, entry.EmailAddress);
}
有这么多接口和包装器class正常吗? (包装器是必需的,因为 .NET 类型无法以其他方式实现我的接口。)
我认为我的问题是过于紧密地反映了 .NET 结构。我不应该一直包装每一个 .NET 类型,直到我得到原始类型。相反,我应该抓住第一个机会尽快删除所有依赖项。在本例中,它使用 DirectorySearcher
class 和 FindAll
方法。
DirectorySearcher.FindAll
returns a SearchResultCollection
,但与其将我的 "wrapper" class 视为 .NET 类型的适配器,我应该充分利用它。
忽略 IDisposable
和其他不必要代码的实现,我的包装器看起来像这样:
public interface IDirectorySearcher : IDisposable {
ISearchResultCollection FindAll();
}
class DirectorySearcherWrapper : IDirectorySearcher {
DirectorySearcher mDirectorySearcher;
DirectorySearcherWrapper(DirectorySearcher pDirectorySearcher) {
mDirectorySearcher = pDirectorySearcher;
}
public static IDirectorySearcher Wrap(DirectorySearcher pDirectorySearcher) {
return new DirectorySearcherWrapper(pDirectorySearcher);
}
public ISearchResultCollection FindAll() {
return SearchResultCollectionWrapper.Wrap(mDirectorySearcher.FindAll());
}
}
相反,我应该借此机会停止所有依赖项。我不必 return .NET 类型甚至只是 .NET 类型的包装器,我现在可以使用此接口来 return 任何我想要的东西。 IE:如果我想从 FindAll
方法中得到的是一堆 ActiveDirectoryUser
,那么 return 就是这样。
然后我的代码变成:
public interface IDirectorySearcher : IDisposable {
IEnumerable<ActiveDirectoryUser> FindAll();
}
class DirectorySearcherWrapper : IDirectorySearcher {
DirectorySearcher mDirectorySearcher;
DirectorySearcherWrapper(DirectorySearcher pDirectorySearcher) {
mDirectorySearcher = pDirectorySearcher;
}
public static IDirectorySearcher Wrap(DirectorySearcher pDirectorySearcher) {
return new DirectorySearcherWrapper(pDirectorySearcher);
}
public IEnumerable<ActiveDirectoryUser> FindAll() {
return
mDirectorySearcher
.FindAll()
.Cast<SearchResult>()
.Select(result => result.GetDirectoryEntry())
.Select(/*BuildNewADUser*/)
.ToList();
}
}
并且 GetAllMailEntries
方法变得简单:
public void GetAllMailEntries() {
MailEntries = mSearcher.FindAll();
}
单元测试变为:
[TestMethod]
public void TestGetAllMailEntries2() {
var mockSearcher = new Mock<IDirectorySearcher>();
mockSearcher
.Setup(s => s.FindAll())
.Returns(new[] {
ActiveDirectoryUser.Create(new Guid(), "Name", "EmailAddress")
});
var queryer = new ActiveDirectoryQueryer(mockSearcher.Object);
queryer.GetAllMailEntries();
Assert.AreEqual(1, queryer.MailEntries.Count());
var entry = queryer.MailEntries.Single();
Assert.AreEqual("Name", entry.Name);
Assert.AreEqual("EmailAddress", entry.EmailAddress);
}
我正在慢慢开始掌握单元测试和模拟的窍门,但这是一个缓慢的过程。我已经尝试对这个 Active Directory 代码进行单元测试。这个问题与AD没有严格的关系。
class ActiveDirectoryQueryer {
DirectorySearcher mSearcher;
public ActiveDirectoryQueryer() {
var searcher = new DirectorySearcher(...);
}
public void GetAllMailEntries() {
MailEntries =
mSearcher
.FindAll()
.Select(result => result.GetDirectoryEntry())
.Select(BuildNewADUser)
.ToList();
}
static ActiveDirectoryUser BuildNewADUser(DirectoryEntry pDirectoryEntry) {
return ActiveDirectoryUser.Create(
pDirectoryEntry.Guid,
(pDirectoryEntry.Properties["name"].Value ?? "").ToString(),
(pDirectoryEntry.Properties["mail"].Value ?? "").ToString()
);
}
所以,我想对 GetAllMailEntries
方法进行单元测试。为了使用 MOQ 执行此操作,我不得不为各种 .NET 类型手动生成接口和包装器,并将上述许多对接口的引用更改为替代(如 IDirectoryEntry
)。下面的每个 IXxxx
接口都有一个关联的包装器 class XxxxWrapper
。总共我为这个测试添加了至少 12 个新的源文件。这是单元测试的结果:
[TestMethod]
public void TestGetAllMailEntries() {
var mockSearcher = new Mock<IDirectorySearcher>();
var mockResultCollection = new Mock<ISearchResultCollection>();
var mockSearchResult = new Mock<ISearchResult>();
var mockDirectoryEntry = new Mock<IDirectoryEntry>();
var mockPropertyCollection = new Mock<IPropertyCollection>();
var nameMockPropertyValueCollection = new Mock<IPropertyValueCollection>();
var mailMockPropertyValueCollection = new Mock<IPropertyValueCollection>();
const string name = "SomeNameValue";
const string mailAddress = "SomeMailAddress";
nameMockPropertyValueCollection.SetupGet(pvc => pvc.Value).Returns(name);
mailMockPropertyValueCollection.SetupGet(pvc => pvc.Value).Returns(mailAddress);
mockPropertyCollection.SetupGet(pc => pc["name"]).Returns(nameMockPropertyValueCollection.Object);
mockPropertyCollection.SetupGet(pc => pc["mail"]).Returns(mailMockPropertyValueCollection.Object);
mockDirectoryEntry.SetupGet(de => de.Properties).Returns(mockPropertyCollection.Object);
mockSearchResult.Setup(sr => sr.GetDirectoryEntry()).Returns(mockDirectoryEntry.Object);
mockResultCollection.Setup(results => results.GetEnumerator()).Returns(new List<ISearchResult> { mockSearchResult.Object }.GetEnumerator());
mockSearcher.Setup(searcher => searcher.FindAll()).Returns(mockResultCollection.Object);
var queryer = new ActiveDirectoryQueryer(mockSearcher.Object);
queryer.GetAllMailEntries();
Assert.AreEqual(1, queryer.MailEntries.Count());
var entry = queryer.MailEntries.Single();
Assert.AreEqual(name, entry.Name);
Assert.AreEqual(mailAddress, entry.EmailAddress);
}
有这么多接口和包装器class正常吗? (包装器是必需的,因为 .NET 类型无法以其他方式实现我的接口。)
我认为我的问题是过于紧密地反映了 .NET 结构。我不应该一直包装每一个 .NET 类型,直到我得到原始类型。相反,我应该抓住第一个机会尽快删除所有依赖项。在本例中,它使用 DirectorySearcher
class 和 FindAll
方法。
DirectorySearcher.FindAll
returns a SearchResultCollection
,但与其将我的 "wrapper" class 视为 .NET 类型的适配器,我应该充分利用它。
忽略 IDisposable
和其他不必要代码的实现,我的包装器看起来像这样:
public interface IDirectorySearcher : IDisposable {
ISearchResultCollection FindAll();
}
class DirectorySearcherWrapper : IDirectorySearcher {
DirectorySearcher mDirectorySearcher;
DirectorySearcherWrapper(DirectorySearcher pDirectorySearcher) {
mDirectorySearcher = pDirectorySearcher;
}
public static IDirectorySearcher Wrap(DirectorySearcher pDirectorySearcher) {
return new DirectorySearcherWrapper(pDirectorySearcher);
}
public ISearchResultCollection FindAll() {
return SearchResultCollectionWrapper.Wrap(mDirectorySearcher.FindAll());
}
}
相反,我应该借此机会停止所有依赖项。我不必 return .NET 类型甚至只是 .NET 类型的包装器,我现在可以使用此接口来 return 任何我想要的东西。 IE:如果我想从 FindAll
方法中得到的是一堆 ActiveDirectoryUser
,那么 return 就是这样。
然后我的代码变成:
public interface IDirectorySearcher : IDisposable {
IEnumerable<ActiveDirectoryUser> FindAll();
}
class DirectorySearcherWrapper : IDirectorySearcher {
DirectorySearcher mDirectorySearcher;
DirectorySearcherWrapper(DirectorySearcher pDirectorySearcher) {
mDirectorySearcher = pDirectorySearcher;
}
public static IDirectorySearcher Wrap(DirectorySearcher pDirectorySearcher) {
return new DirectorySearcherWrapper(pDirectorySearcher);
}
public IEnumerable<ActiveDirectoryUser> FindAll() {
return
mDirectorySearcher
.FindAll()
.Cast<SearchResult>()
.Select(result => result.GetDirectoryEntry())
.Select(/*BuildNewADUser*/)
.ToList();
}
}
并且 GetAllMailEntries
方法变得简单:
public void GetAllMailEntries() {
MailEntries = mSearcher.FindAll();
}
单元测试变为:
[TestMethod]
public void TestGetAllMailEntries2() {
var mockSearcher = new Mock<IDirectorySearcher>();
mockSearcher
.Setup(s => s.FindAll())
.Returns(new[] {
ActiveDirectoryUser.Create(new Guid(), "Name", "EmailAddress")
});
var queryer = new ActiveDirectoryQueryer(mockSearcher.Object);
queryer.GetAllMailEntries();
Assert.AreEqual(1, queryer.MailEntries.Count());
var entry = queryer.MailEntries.Single();
Assert.AreEqual("Name", entry.Name);
Assert.AreEqual("EmailAddress", entry.EmailAddress);
}