AuthorizedHandler
这个例子就复杂些了,并且直接来源于老赵以前的某个项目的代码——当然现在为了示例进行了简化和改造。在项目中我们往往要编写一些Handler来处理客户端的请求,而同时Handler需要对客户端进行身份验证及基于角色的授权,只有特定角色的客户才能访问Handler的主体逻辑,否则便抛出异常。而这样的逻辑有其固有的结构,因此我们这类Handler编写一个公用的父类,这样我们便可使用“模板方法”的形式来补充具体逻辑了。这个父类的实现如下:
public abstract class AuthorizedHandler : IHttpHandler
{
public bool IsReusable { get { return false; } }
void IHttpHandler.ProcessRequest(HttpContext context)
{
this.ProcessRequest(new HttpContextWrapper(context));
}
internal void ProcessRequest(HttpContextBase context)
{
if (!context.User.Identity.IsAuthenticated)
{
throw new UnauthorizedAccessException();
}
foreach (var role in this.AuthorizedRoles)
{
if (context.User.IsInRole(role))
{
this.ProcessRequestCore(context);
return;
}
}
throw new UnauthorizedAccessException();
}
protected internal abstract void ProcessRequestCore(HttpContextBase context);
protected internal abstract IEnumerable<string> AuthorizedRoles { get; }
}
一般来说,我们会在IHttpHandler.ProcessRequest方法中进行逻辑实现,但是我们现在直接把方法调用转发给接受HttpContextBase作为参数的ProcessRequest方法重载。HttpContextBase是一个抽象类型,这便是我们的测试目标。这个方法首先判断用户是否经过认证,然后再将用户的角色,与AuthorizedRoles抽象属性中表示的合法角色进行匹配,如果匹配成功则调用ProcessRequestCore抽象方法,而无论是用户认证还是授权失败,都会抛出UnauthorizedAccessException异常。
这里有一个题外话:不知您是否注意到,这里没有private方法,所有的方法都有internal修饰。这么做的原因完全是为了进行单元测试。由于private方法无法被外部项目调用,因此我们只能使用internal作为修饰符,再为程序集加上InternalVisibleToAttribute标记,把所有的internal成员向测试项目开放。当然,此时程序集内部就能够随意调用那些方法了——还好,都是自家人,注意点便是了。
这段逻辑需要测试的环节比较多,我们依次看一下:
[TestMethod()]
[ExpectedException(typeof(UnauthorizedAccessException))]
public void ProcessRequestTest_Nonauthenticated_Request()
{
Mock<HttpContextBase> mockContext = new Mock<HttpContextBase>(MockBehavior.Strict);
mockContext.Setup(c => c.User.Identity.IsAuthenticated).Returns(false);
Mock<AuthorizedHandler> mockHandler = new Mock<AuthorizedHandler> { CallBase = true };
mockHandler.Setup(h => h.ProcessRequestCore(It.IsAny<HttpContextBase>()))
.Throws(new Exception("ProcessRequestCore should not be called."));
mockHandler.Setup(h => h.AuthorizedRoles)
.Throws(new Exception("AuthorizedRoles should not be accessed."));
mockHandler.Object.ProcessRequest(mockContext.Object);
}
这是对没有通过身份验证的请求的回应,我们设置HttpContext.User.Identity.IsAuthenticated属性为false,并且声明不能碰触到ProcessRequestCore和AuthroizedRoles属性。在这样的情况下,我们自然期望抛出UnauthorizedAccessException。
[TestMethod()]
[ExpectedException(typeof(UnauthorizedAccessException))]
public void ProcessRequestTest_Nonauthorized_Request()
{
Mock<HttpContextBase> mockContext = new Mock<HttpContextBase>(MockBehavior.Strict);
mockContext.Setup(c => c.User.Identity.IsAuthenticated).Returns(true);
mockContext.Setup(c => c.User.IsInRole(It.IsAny<string>()))
.Returns(false).Verifiable();
Mock<AuthorizedHandler> mockHandler = new Mock<AuthorizedHandler> { CallBase = true };
mockHandler.Setup(c => c.ProcessRequestCore(It.IsAny<HttpContextBase>()))
.Throws(new Exception("ProcessRequestCore should not be called."));
mockHandler.Setup(c => c.AuthorizedRoles)
.Returns(new string[] { "admin", "user" }).Verifiable();
try
{
mockHandler.Object.ProcessRequest(mockContext.Object);
}
catch
{
throw;
}
finally
{
mockContext.Verify();
mockHandler.Verify();
}
}
这是测试身份验证通过,而基于角色的授权失败时的情况。我们把IsAuthenticated设为true,并且要求IsInRole方法在“接受到任何string类型参数”的时候都返回false,而最后再“象征性”地设置AuthorizedRoles所返回的内容。这个测试的期望是抛出UnauthorizedAccessException,不过值得注意的是,我们的代码还有其他要求,那就是要求IsInRole和AuthorizedRoles一定要调用过——您明白了吗?这就是为什么对Mock对象追加Verifiable和Verify方法,并且使用try/catch/finally的缘故。
[TestMethod()]
public void ProcessRequestTest_Authorized_Request()
{
Mock<HttpContextBase> mockContext = new Mock<HttpContextBase>(MockBehavior.Strict);
mockContext.Setup(c => c.User.Identity.IsAuthenticated).Returns(true);
mockContext.Setup(c => c.User.IsInRole(It.IsAny<string>())).Returns(false);
mockContext.Setup(c => c.User.IsInRole("user")).Returns(true).Verifiable();
Mock<AuthorizedHandler> mockHandler = new Mock<AuthorizedHandler> { CallBase = true };
mockHandler.Setup(c => c.ProcessRequestCore(It.IsAny<HttpContextBase>()))
.AtMostOnce().Verifiable();
mockHandler.Setup(c => c.AuthorizedRoles).Returns(new string[] { "admin", "user" })
.Verifiable();
mockHandler.Object.ProcessRequest(mockContext.Object);
mockHandler.Verify();
mockContext.Verify();
}
最后的测试自然是正常流程的测试。在这里我们要检验的是正常情况下ProcessRequestCore是否“被调用,而且只被调用了一次”。如果您能够理解前两个测试,这个测试应该也同样简单才是。