11 个你会喜欢的实用类






4.75/5 (22投票s)
模拟 Windows 用户(以及可能遇到的“有趣”的 bug)、正确释放 WCF CommunicationObject、从表达式获取属性名、获取相对路径、弱引用集合监听器、线程安全字典、枚举标志帮助、测试助手
引言
这些工具类是我近年来从互联网上收集的代码片段。我相信你在这里会找到日常使用的类。
我的目标不是创建一个库,而是将很酷的代码片段聚集在一个地方,供人们通过复制粘贴使用,而且没有任何依赖项。
如果还没这样做,我强烈建议你收集你在网上看到的很酷的代码片段,并将你的个人框架保存在代码存储库中。这会为你节省大量时间。
你们中的一些人会说不必重复造轮子,这是对的。
但关键是,有时你需要处理非常具体且简单(但不容易)的任务,而又不想承担另一个库的负担。而且,你自己最有可能想要在以后重用这些类:你知道在哪里找到它们,它们如何工作,有什么陷阱,以及你在使用它们时学到了什么。
我保留了代码中的原始注释,所以有时你或许能找到原始作者。
我想感谢那些在网上分享这些类的人,他们为我节省了大量的时间和金钱。我能做的最好的事情就是传播他们的工作。
这里展示的所有单元测试或代码片段都能通过。
目录
- 引言
- Content
- 11 个实用类
- LogonUser:如何轻松模拟用户
- PathUtil:如何从绝对文件夹获取相对路径
- LambdaExtensions:从表达式提取属性名的扩展方法
- NotifyPropertyChangedBase:LambdaExtensions 的具体应用场景
- DisposableWrapperExtension:如何有效地释放 WCF CommunicationObject
- EnumExtensions:如何轻松操作标志枚举
- ThreadSafeDictionary:如何自动添加项目,轻松实现
- BlockingStream:如何轻松模拟客户端/服务器通信,仅使用一个 Stream
- WeakCollectionChangedListener:如何停止内存泄漏,当监听器的生命周期比被监听对象短时
- WeakObservableCollection:一个具体的应用场景,被监听对象的包装器,由监听器拥有
- AssertEx:一个让你的测试更具可读性的类
- 结论:你拥有并经常使用哪些实用类?
11 个实用类
LogonUser:如何轻松模拟用户
有时,你需要以另一个用户的 Windows 凭据运行一段代码,以使用他的 ACL 权限。这称为模拟。这里有一个例子。
[TestMethod]
public void CanUndoImpersonation()
{
var oldName = Impersonation.CurrentUserName;
using(Impersonation.Impersonate(TestUser.UserName, TestUser.Password))
{
AssertEx.ShouldContains
(TestUser.UserName, Impersonation.CurrentUserName);
AssertEx.ShouldNotContains(TestUser.UserName, oldName);
}
AssertEx.ShouldContains(oldName, Impersonation.CurrentUserName);
}
其中 Impersonation.CurrentUserName
是
public static String CurrentUserName
{
get
{
return System.Security.Principal.WindowsIdentity.GetCurrent().Name;
}
}
需要注意的陷阱
我通过惨痛教训学到的,需要注意的一些“有趣”的 bug...
[TestMethod]
public void LazyAssemblyInAImpersonatedSecurityContextWithoutFileRightsThrows()
{
using(Impersonation.Impersonate(TestUser.UserName, TestUser.Password))
{
AssertEx.Throws<FileLoadException>(() =>
{
Class1 hello = new Class1();
});
}
}
在这段代码中,代码抛出异常是因为 Class1
(位于另一个程序集中)在 AssertEx.Throws
中的 lambda 表达式被“即时编译”(jit)时被加载。而这发生在使用 TestUser
的凭据下,而该用户没有读取程序集文件的权限!
这段代码运行正常
[TestMethod]
public void LoadAssemblyBeforeImpersonatingSoYouDontLazyLoadInTheWrongSecurityContext()
{
Class1 test = null;
using(Impersonation.Impersonate(TestUser.UserName, TestUser.Password))
{
AssertEx.DoesntThrow(() =>
{
Class1 hello = new Class1();
});
}
}
但还有一个惊喜等着你,我怀疑这是 .NET Framework 的一个 bug(不确定是否是设计如此)。
如果你尝试在一个用户没有读取权限的情况下加载一个程序集,那么以后你就永远无法加载它了。
[TestMethod]
public void AndYouCantEvenRetryToLoadAssemblyInTheRightContext()
{
var oldAccount = Impersonation.CurrentUserName;
using(Impersonation.Impersonate(TestUser.UserName, TestUser.Password))
{
AssertEx.Throws<FileLoadException>(() =>
{
Class1 hello = new Class1();
});
}
Assert.AreEqual(oldAccount, Impersonation.CurrentUserName);
AssertEx.Throws<FileLoadException>(() =>
{
Class1 hello = new Class1();
});
}
当我看到我之前的两个测试同时运行时都无法通过时,我发现了这个问题...
PathUtil:如何从绝对文件夹获取相对路径
这个任务非常简单... 但不容易编写!下面是如何使用它。
[TestMethod]
public void CanGetRelativePath()
{
var path = PathUtil.RelativePathTo(@"c:\toto\titi", @"c:\toto\titi\tutu.txt");
Assert.AreEqual("tutu.txt", path);
path = PathUtil.RelativePathTo(@"c:\toto\tata", @"c:\toto\titi\tutu.txt");
Assert.AreEqual(@"..\titi\tutu.txt", path);
}
是不是很酷?但要注意在路径中使用 "\" 而不是 "/"... 如果你想修复它,代码就在那里。
LambdaExtensions:从表达式提取属性名的扩展方法
这个是关于避免在代码中使用魔术字符串(magic string)来表示属性名,这样你在重构后就不会出现愚蠢的错误。
[TestMethod]
public void CanGetPropertyName()
{
TestClass test = new TestClass();
var propertyName = LambdaExtensions.GetPropertyName(() => test.ReferenceMember);
Assert.AreEqual("ReferenceMember", propertyName);
propertyName = LambdaExtensions.GetPropertyName(() => test.ValueMember);
Assert.AreEqual("ValueMember", propertyName);
}
NotifyPropertyChangedBase:LambdaExtensions 的具体应用场景
如果你正在进行 WPF 或 Silverlight 开发,我相信你很欣赏如何用代码片段声明 ViewModel
的一个属性
public class ViewModel : NotifyPropertyChangedBase
{
private int _Value;
public int Value
{
get
{
return _Value;
}
set
{
if(value != _Value)
{
_Value = value;
OnPropertyChanged(() => this.Value);
}
}
}
}
这是确保它按预期正常工作的测试
[TestMethod]
public void TypedOnPropertyChangedFireCorrectly()
{
ViewModel vm = new ViewModel();
bool fired = false;
vm.PropertyChanged += (s, a) =>
{
Assert.AreEqual("Value", a.PropertyName);
fired = true;
};
vm.Value = 5;
Assert.IsTrue(fired);
}
不满意?你想想如何轻松地在另一个依赖属性上触发 PropertyChanged
事件?看看构造函数!
public class ViewModel : NotifyPropertyChangedBase
{
public ViewModel()
{
this.AddDependency(() => this.Value, () => this.CalculatedValue);
}
private int _Value;
public int Value
{
get
{
return _Value;
}
set
{
if(value != _Value)
{
_Value = value;
OnPropertyChanged(() => this.Value);
}
}
}
public int CalculatedValue
{
get
{
return Value + 5;
}
}
}
还有这个测试
[TestMethod]
public void DependantPropertyFireCorrectly()
{
ViewModel vm = new ViewModel();
bool fired = false;
vm.PropertyChanged += (s, a) =>
{
if(a.PropertyName == "CalculatedValue")
fired = true;
};
vm.Value = 5;
Assert.IsTrue(fired);
}
DisposableWrapperExtension:如何有效地释放 WCF CommunicationObject
你说你足够了解如何使用 IDisposable
接口?那么你怎么看这个。
[TestMethod]
public void WCFDisposableProblem()
{
ServiceHost host = new ServiceHost(new TestService());
var endpoint = host.AddServiceEndpoint(typeof(ITestService),
new WSHttpBinding(), "https://:8372/");
host.Open();
var client = new ChannelFactory<ITestService>(endpoint).CreateChannel();
bool throwAtTheEndOfUsing = false;
AssertEx.Throws<EndpointNotFoundException>(() =>
{
using(((IClientChannel)client))
{
client.DoStuff();
host.Close();
throwAtTheEndOfUsing = true;
}
});
Assert.IsTrue(throwAtTheEndOfUsing);
}
这里我们在客户端通道之前关闭了 ServiceHost
,所以客户端通道在 Dispose
方法中抛出异常,而不是简单地中止...
我发誓,当你第一次发现这个行为时,你只想停止编程,然后去乡下种土豆,度过余生...
所以为了保持你作为开发者的尊严和热爱,请使用 DisposableWrapperExtension
,方法如下,感谢一位 Stack Overflow 的大神。
[TestMethod]
public void CanUseFixDisposable()
{
ServiceHost host = new ServiceHost(new TestService());
var endpoint = host.AddServiceEndpoint(typeof(ITestService),
new WSHttpBinding(), "https://:8374/");
host.Open();
var client = new ChannelFactory<ITestService>(endpoint).CreateChannel();
using(((IClientChannel)client).FixIDisposable())
{
client.DoStuff();
host.Close();
}
}
它在 ThreadPool
的线程中 Dispose
,然后 Abort
通道,这样异常就不会导致你的 AppDomain
崩溃。(这也是我血泪教训学到的...)
EnumExtensions:如何轻松操作标志枚举
如果布尔运算不适合你,那么这里有一个简单的类来操作标志 enum
,例如,使用这个 enum
[Flags]
public enum ErrorTypes
{
None = 0,
MissingPassword = 1,
MissingUsername = 2,
PasswordIncorrect = 4
}
你可以轻松地看到哪个标志是开启或关闭的
[TestMethod]
public void CanAppendRemoveAndConsultFlagEnum()
{
ErrorTypes error = ErrorTypes.MissingPassword;
Assert.IsTrue(error.Has(ErrorTypes.MissingPassword));
error = error.Append(ErrorTypes.MissingUsername);
Assert.IsTrue(error.Has(ErrorTypes.MissingPassword));
Assert.IsTrue(error.Has(ErrorTypes.MissingUsername));
Assert.IsTrue(error.Has(ErrorTypes.MissingPassword.Append
(ErrorTypes.MissingUsername)));
error = error.Remove(ErrorTypes.MissingPassword);
Assert.IsFalse(error.Has(ErrorTypes.MissingPassword));
Assert.IsTrue(error.Has(ErrorTypes.MissingUsername));
}
ThreadSafeDictionary:如何自动添加项目,轻松实现
一个线程安全的 Dictionary
,你可以自动添加/替换或删除项目(无需先调用 ContainsKey
)。
[TestMethod]
public void CanDeleteAndRemoveItems()
{
ThreadSafeDictionary<int, string> dictionary =
new ThreadSafeDictionary<int, string>();
dictionary.MergeSafe(1, "hello");
dictionary.MergeSafe(2, "hello2");
Assert.AreEqual(2, dictionary.Count);
dictionary.RemoveSafe(3);
dictionary.RemoveSafe(2);
Assert.AreEqual(1, dictionary.Count);
Assert.IsTrue(dictionary.ContainsKey(1));
}
该字典可以在多个线程之间安全共享。
BlockingStream:如何轻松模拟客户端/服务器通信,仅使用一个 Stream
这个类,来自 StackOverflow,将为你节省单元测试的时间... 特别是当你想要模拟客户端/服务器(生产者/消费者)交互,而又不需要依赖 Socket、WCF、System.Net 或 Remoting 时。
[TestMethod]
public void CanWriteThenRead()
{
BlockingStream stream = new BlockingStream();
stream.WriteByte(5);
var value = stream.ReadByte();
Assert.AreEqual(5, value);
}
[TestMethod]
public void CanTimeout()
{
BlockingStream stream = new BlockingStream();
stream.ReadTimeout = 500;
Stopwatch watcher = new Stopwatch();
watcher.Start();
var value = stream.ReadByte();
watcher.Stop();
Assert.IsTrue(500 < watcher.ElapsedMilliseconds);
Assert.AreEqual(-1, value);
}
WeakCollectionChangedListener:如何停止内存泄漏,当监听器的生命周期比被监听对象短时。
SpyObseravableCollection
是一个实现了 INotifyCollectionChanged
的类,用于使我的测试更容易阅读。
[TestMethod]
public void ListenerCanBeGarbageCollected()
{
var listener = new SpyCollectionListener();
var source = new SpyObservableCollection();
WeakCollectionChangedListener weakListener =
new WeakCollectionChangedListener(source, listener);
source.Add(4);
listener.AssertCollectionChanged();
WeakReference listenerHandle = new WeakReference(listener);
listener = null;
GC.Collect();
Assert.IsFalse(listenerHandle.IsAlive);
source.Add(6); //The weak listener unsubscribe on the next event
source.AssertHasNoListener();
}
WeakObservableCollection:一个具体的应用场景,被监听对象的包装器,由监听器拥有
WeakCollectionChangedListener
并不难使用,但我有另一个更易读的解决方案。
我只是将源集合包装在 WeakObservableCollection
中。当 WeakObservableCollection
被垃圾回收时,它所有的监听器都会从源集合中取消订阅。
[TestMethod]
public void
ListenerOfWeakObservableCollectionCanBeGarbageCollectedWhenWeakCollectionCollected()
{
var source = new SpyObservableCollection();
var wrappedSource = new WeakObservableCollection<int>(source);
var listener = new SpyCollectionListener();
wrappedSource.CollectionChanged += listener.OnCollectionChanged;
source.Add(4);
listener.AssertCollectionChanged();
WeakReference listenerHandle = new WeakReference(listener);
listener = null;
wrappedSource = null;
GC.Collect();
Assert.IsFalse(listenerHandle.IsAlive);
source.Add(6); //The weak listener unsubscribe on the next event
source.AssertHasNoListener();
}
AssertEx:一个让你的测试更具可读性的类
我在文章中使用了这个类,抱歉关于这个,我太懒了,不想写关于测试的测试(元测试!)所以我只是提醒你在哪里在文章中使用了这个类。
例如,AssertEx.DoesntThrow
[TestMethod]
public void LoadAssemblyBeforeImpersonatingSoYouDontLazyLoadInTheWrongSecurityContext()
{
Class1 test = null;
using(Impersonation.Impersonate(TestUser.UserName, TestUser.Password))
{
AssertEx.DoesntThrow(() =>
{
Class1 hello = new Class1();
});
}
}
你可能会问:这个方法有什么意义?尤其当你看到它的实现时。
public static void DoesntThrow(Action action)
{
action();
}
有两个原因
首先,它向用户准确地展示了我期望的行为。这样她/他就不会过度关注不那么重要的部分,如 Impersonation.Impersonate(TestUser.UserName, TestUser.Password)
。
其次,在这种特定情况下,使用 lambda 表达式可能会改变结果。请记住,在它之前的测试中,使用 lambda 会在 TestUser
的安全上下文中加载 Class1
的程序集。
[TestMethod]
public void LazyAssemblyInAImpersonatedSecurityContextWithoutFileRightsThrows()
{
using(Impersonation.Impersonate(TestUser.UserName, TestUser.Password))
{
AssertEx.Throws<FileLoadException>(() =>
{
Class1 hello = new Class1();
});
}
}
AssertEx.Throws
断言你的 lambda 抛出了正确的异常。
结论:你拥有并经常使用哪些实用类?
希望你会喜欢,但我很好奇你们。你最好的个人实用类是什么?