昨天安装了Microsoft Expression Studio 4以后,今天上班使用Visual Studio 2008打开一个包含了单元测试项目的解决方案发现单元测试项目无法正常显示,有个加号,就是点不开,项目图标也变成了一个文件夹。上天下地中文英文的网站都翻了个遍,总之很辛苦的花费了2个小时也没找到答案,最后凭着开发人员的直觉搞定了,解决方案如下:
在VIsual Studio安装目录(我的是C:\Program Files (x86)\Microsoft Visual Studio 9.0\)找到Common7\IDE目录,里面会有2个文件夹ProjectTemplates和ProjectTemplatesCache,这两个文件夹的子目录结构是一样的,但是一个是zip包,一个是解压后的文件夹。在这两个项目文件夹找到子目录CSharp\Test,里面会有按语言分类的子文件夹中文2052、英文1033。中文的Visual Studio使用2052,英文使用1033文件夹。如果这里没有的话,从正确安装的系统中拷贝一份过来就可以了。
我的问题比较奇特,能够创建测试项目,但是不能正常显示。因为我的安装比较奇特,中文Visual Studio 2008安装在英文Winodws系统上,所以我在这两个ProjectTemplates目录下,都新建了一个1033目录,拷贝一份模板文件,就可以成功显示了。
下面的例子来至Asp.Net MVC 2的项目模板。
首先是一个实体类:
实体类
[PropertiesMustMatch("Password", "ConfirmPassword", ErrorMessage = "The password and confirmation password do not match.")]
public class RegisterModel
{
[Required]
[DisplayName("User name")]
public string UserName { get; set; }
[Required]
[DataType(DataType.EmailAddress)]
[DisplayName("Email address")]
public string Email { get; set; }
[Required]
[ValidatePasswordLength]
[DataType(DataType.Password)]
[DisplayName("Password")]
public string Password { get; set; }
[Required]
[DataType(DataType.Password)]
[DisplayName("Confirm password")]
public string ConfirmPassword { get; set; }
}
然后是Action:
代码
[HttpPost]
public ActionResult Register(RegisterModel model)
{
if (ModelState.IsValid)
{
// Attempt to register the user
MembershipCreateStatus createStatus = MembershipService.CreateUser(model.UserName, model.Password, model.Email);
if (createStatus == MembershipCreateStatus.Success)
{
FormsService.SignIn(model.UserName, false /* createPersistentCookie */);
return RedirectToAction("Index", "Home");
}
else
{
ModelState.AddModelError("", AccountValidation.ErrorCodeToString(createStatus));
}
}
// If we got this far, something failed, redisplay form
ViewData["PasswordLength"] = MembershipService.MinPasswordLength;
return View(model);
}
如果你对这个Action写单元测试,你会发现没办法测试输入不完整的情况,比如下面的代码:
单元测试
public void RegisterTest()
{
AccountController target = new AccountController()
RegisterModel model = new RegisterModel();
ActionResult actual;
actual = target.Register(model);
var view = actual as ViewResult;
Assert.IsNotNull(view);
Assert.IsNotNull(view.ViewData.ModelState["PasswordLength"]);
}
这个单元测试不会跑完,因为注册的时候由于用户名是null,会抛出异常。因为这个判断:
解决办法有2个,第一是编写单元测试的人知道模型是否正确,只要改变ModelState.ISValid的值即可。如下代码:
方法一
///Register 的测试
///</summary>
[TestMethod()]
public void RegisterTest()
{
AccountController target = new AccountController()
RegisterModel model = new RegisterModel();
ActionResult actual;
target.ViewData.ModelState.AddModelError("", "模型没有正确赋值");
actual = target.Register(model);
var view = actual as ViewResult;
Assert.IsNotNull(view);
Assert.IsNotNull(view.ViewData.ModelState["PasswordLength"]);
}
另外一种思路就是主动验证模型,并把验证结果添加到ModelState集合中去,这也就是Asp.Net MVC框架内部所作的工作。如果使用.Net 4.0,在System.ComponentModel.DataAnnotations命名空间中新增了ValidationContext类,可以在任何需要的地方对模型进行验证。这样只需要简单的写个帮助类,在单元测试中手动调用这个方法即可验证模型:
模型验证帮助类
{
var context = new ValidationContext(model, null, null);
var results = new List<ValidationResult>();
Validator.TryValidateObject(model, context, results, true);
foreach (var result in results)
{
var name = result.MemberNames.First();
modelState.AddModelError(name, result.ErrorMessage);
}
}
protected internal bool TryValidateModel(object model); 这个方法对正确的模型,返回true,错误的返回false。这个方法并不是公开的,可以公开一个新的方法调用它。
{
return TryValidateModel(model);
}
最终代码
public void RegisterTest()
{
AccountController target = new AccountController()
RegisterModel model = new RegisterModel();
ActionResult actual;
target.InvokeValidateModel(model);
actual = target.Register(model);
var view = actual as ViewResult;
Assert.IsNotNull(view);
Assert.IsNotNull(view.ViewData.ModelState["PasswordLength"]);
}
通过上面两种办法,现在我们可以正确的测试包含了ModelState.IsValid代码的Action方法了。
实际上,如果你的mscorlib.dll版本不是2.0.50727.4952的话,一切都很美好,网上很多很多的教程教你如何进入.Net Framework源代码调试。比如这个。
但是,当你的mscorlib.dll版本是2.0.50727.4952的话,恶梦来了。无论你如何修改配置,都会得到一个“没有可用于当前位置的源代码”的提示信息。经过我一个下午的努力搜索,对照了很多FAQ(比如这个)努力修改配置,最后的出结论就是微软提供的2.0.50727.4952版本的pdb文件有问题,其大小只有763KB,正常应该在8M多(上一个版本)。通过查看Wiki上的.Net版本历史记录,发现2.0.50727.4952版本是因为打了一个补丁KB983590,很自然的就想到把这个补丁卸载,让版本回滚到2.0.50727.4927。卸载后需要重启电脑。
重启电脑后打开VS2008,打开项目,F5启动调试程序,如果按照前面的配置,会自动下载mscorlib.dll的pdb文件。在我的电脑上,这个自动下载的pdb文件不管用,单步进入Console.WriteLine方法,仍然提示没有pdb文件,在堆栈窗口中手动加载pdb文件以后又再次下载了一次,不过这次在缓存文件夹中没有看到下载的影子。pdb下载完毕以后,由于是正确的pdb文件,立即就开始下载源代码了,就能进入Console.WriteLine方法了。真是千辛万苦啊。
今天,想使用自定义的类型作为Dictionary<TKey, TValue> 的键类型,不可避免的要使用到这个接口。如果另外定义一个实现类,使用者必须首先知道有这个类存在,这样调用方才会得到正确的结果。自然想到的办法就是这个自定义类型主动实现IEqualityComparer<T>接口,但是Dictionary<TKey, TValue> 类型并不会去检查这个键对象有没有实现IEqualityComparer<T>接口。
如果构造Dictionary<TKey, TValue> 泛型集合的实例对象时候,没有提供一个实现了IEqualityComparer<T>接口的实例对象,Dictionary<TKey, TValue>默认使用EqualityComparer<T> 泛型类的 Default属性。看看IEqualityComparer<T>接口MSDN的说明:使用此接口,可以实现集合的自定义相等比较。也就是说,对于类型 T,您可以创建自己的相等定义,并指定该定义可与接受 IEqualityComparer<T>泛型接口的集合类型一起使用。在 .NET Framework 中,Dictionary<TKey, TValue> 泛型集合类型的构造函数接受此接口。继续看EqualityComparer<T> 泛型类Default属性的MSDN的说明:Default 属性检查类型 T 是否实现此 System.IEquatable<T> 泛型接口,如果实现,该属性将返回一个使用该实现的 EqualityComparer<T>。否则,它返回一个使用 T 提供的 Object.Equals 和 Object.GetHashCode 的重写的 EqualityComparer<T>。
看到上面的说明,我天真的以为实现了System.IEquatable<T> 泛型接口就可以在Dictionary<TKey, TValue> 泛型集合的键中使用自己定义的类了。结果发现无论如何都不能使程序按照设想中的运行。最后发现MSDN中关于System.IEquatable<T>的实现者说明:如果实现 IEquatable<T>,还应重写 Object.Equals(Object) 和 GetHashCode 的基类实现,以便其行为与 IEquatable<T>.Equals 方法的行为一致。尝试着重写了这两个方法,一切都OK了。后来查看了.Net Framework源代码才明白了,实际上,对于EqualityComparer<T> 泛型类来说前面这个重写是必须的,因为前面说的“Default 属性检查类型 T 是否实现此 System.IEquatable<T> 泛型接口,如果实现,该属性将返回一个使用该实现的 EqualityComparer<T>”,实际上这个未公开的 System.IEquatable<T>实现内部其实使用的还是Object.Equals(Object)方法来做比较。
这就比较郁闷了,对于Dictionary<TKey, TValue>来说,实现System.IEquatable<T>其实没什么用,始终还是要重写 Object.Equals(Object) 和 GetHashCode。
有关的文章可以看这两篇:
1、http://www.joycode.com/vbcti/archive/2009/02/20/115473.joy
2、http://www.cnblogs.com/ldp615/archive/2009/09/05/1560791.html
公司的爬虫在爬取个别网站内容时候发现目标服务器对访问频率做了限制,这样只能限制爬虫访问的频率,造成了信息的滞后和数量始终上不去。于是就研究能不能让访问请求依次通过不同的IP地址访问目标服务器来增大访问的频率。让爬虫在不同的服务器上运行,这是另外一种解决思路,涉及到分布式爬虫的很多问题,不在本文讨论范围内。
Google搜索了半天,找到了一些蛛丝马迹,发现Socket.Bind方法理论上可行。接下来就想如何干涉HttpWebRequest的这个过程。一路查看HttpWebRequest的源代码,果然在ServicePoint中找到了BindIPEndPointDelegate属性,这个属性是一个名为BindIPEndPoint的代理,签名如下:
通过设置这个属性,可以在发出连接的时候绑定客户端发出连接所使用的IP地址。
一个简单的示例程序:
代码
2 {
3 IPEndPoint m_OutIPEndPoint;
4 MyWebClient(IPEndPoint outIp)
5 {
6 if (outIp == null)
7 throw new ArgumentNullException("outIp");
8
9 m_OutIPEndPoint = outIp;
10 CachePolicy = new RequestCachePolicy(RequestCacheLevel.NoCacheNoStore);
11 }
12
13 protected override WebRequest GetWebRequest(Uri address)
14 {
15 var request = (HttpWebRequest)base.GetWebRequest(address);
16 request.ServicePoint.BindIPEndPointDelegate = (servicePoint, remoteEndPoint, retryCount) =>
17 {
18 return m_OutIPEndPoint;
19 };
20 return request;
21 }
22
23 public static void Run()
24 {
25 MyWebClient client2 = new MyWebClient(new IPEndPoint(IPAddress.Parse("192.168.1.105"), 0));
26 Console.WriteLine(client2.DownloadString("http://192.168.1.15:8000/Default.asp"));
27 client2.Dispose();
28 Thread.Sleep(ServicePointManager.MaxServicePointIdleTime);
29 MyWebClient client = new MyWebClient(new IPEndPoint(IPAddress.Parse("192.168.1.204"), 0));
30 Console.WriteLine(client.DownloadString("http://192.168.1.15:8000/"));
31 client.Dispose();
32 }
33 }
我在本机的网卡绑定了2个IP地址,192.168.1.105和192.168.1.204.而http://192.168.1.15:8000/Default.asp是我写的一个测试页面,只输出客户端请求的IP地址。请注意,其中有一行Thread.Sleep(ServicePointManager.MaxServicePointIdleTime);如果没有加入这行,你会得到两行192.168.1.105的结果,在代理那行(return m_OutIPEndPoint;)加入断点会发现只命中了一次。加入这行以后会得到192.168.1.105和192.168.1.204这个期望的结果.
原因就得从ServicePointManager说起,这个类缓存了ServicePoint,代表了到目标的连接。只要这个连接已经存在,出去的IP地址就已经确定了,所以不会再调用BindIPEndPointDelegate来获取出去的IP地址。加入了Thread.Sleep这行代码,只是等待这个连接释放,这样再次访问页面时候就会重新建立连接,从新绑定出去的IP地址了。这样做只是为了验证本文的目的。
当然这个示例程序没有办法真正工作,因为爬虫不可能休眠这么长时间,也不可能把ServicePointManager.MaxServicePointIdleTime设置为很短的时间,虽然实现了目的,但是降低了效率。这个示例程序只是揭示了使用不同IP地址访问目标服务器的可能性和实现途径。