onsdag 22 februari 2012

Testa Generic Handler del #2

I förra inlägget visade jag hur en HttpHandler kan testas genom att använda HttpContextBase. Nu tänkte jag visa ett exempel på hur det i praktiken går till att testa HttpHandlern. I detta fall har jag en LoginHandler vars uppgift är att kontrollera om en användare är inloggad (finns i sessionen).

Om så är fallet skrivs följande JSON-objekt ut: {"isLoggedIn":true} 

Följande klasser används:

User.cs - Representerar en användare (inte jättemånga fält, men det behövs inte för detta demo...).
namespace Lab.Entities
{
    public class User
    {
        public string FirstName { getset; }
        public string LastName { getset; }
    }
}
LoginState.cs - Klassen som serialiseras till JSON-objekt (attributen används av Json.NET) av LoginHandler.
using Newtonsoft.Json;

namespace Lab.Entities
{
    [JsonObject]
    public class LoginState
    {
        [JsonProperty("isLoggedIn")]
        public bool IsLoggedIn { getset; }
    }
}
UserContext.cs - Statisk klass (nej, static är inte så snyggt men för demosyftet duger den) som används för att hämta aktuell användare från sessionen.
using System.Web;
 
namespace Lab.Entities
{
    public static class UserContext
    {
        public const string UserSessionKey = "UserSession";
 
        public static User Current
        {
            get
            {
                if (HasContext())
                {
                    return HttpContext.Current.Session[UserSessionKey] as User;
                }
 
                return null;
            }
            set
            {
                if (HasContext())
                {
                    HttpContext.Current.Session[UserSessionKey] = value;
                }
            }
        }
 
        private static bool HasContext()
        {
            return HttpContext.Current != null;
        }
    }
}

LoginHandler.cs - HttpHandlern som läser från sessionen och skriver ut resultatet genom att serialisera klassen LoginState. Notera att interfacet IReadOnlySessionState även har implementerats. Detta för att ASP.NET ska tillåta läsning från sessionen.
using System.Web;
using System.Web.SessionState;
using Newtonsoft.Json;
using Lab.Entities;
 
namespace Lab.Handlers
{
    public class LoginHandler : IHttpHandlerIReadOnlySessionState
    {
        private const string JsonContentType = "application/json";
 
        public void ProcessRequest(HttpContext context)
        {
            ProcessRequest(new HttpContextWrapper(context));
        }
 
        public void ProcessRequest(HttpContextBase context)
        {
            string json = JsonConvert.SerializeObject(new LoginState
            {
                IsLoggedIn = context.Session[UserContext.UserSessionKey] != null
            });
 
            HttpResponseBase response = context.Response;
            response.ContentType = JsonContentType;
            response.Write(json);
        }
 
        public bool IsReusable
        {
            get { return false; }
        }
    }
}
LoginHandlerTest.cs - Själva testklassen som verifierar att allting fungerar. Använder xUnit som testramverk samt Rhino Mocks för att enkelt skapa mock-ups av HttpContextBase, HttpSessionStateBase och HttpResponseBase. Rhino Mocks verifierar även att rätt JSON-objekt skickas ut till klienten.
using System.Web;
using Lab.Entities;
using Lab.Handlers;
using Rhino.Mocks;
using Xunit;
 
namespace Lab.Tests
{ 
    class LoginHandlerTest
    {     
        private readonly HttpContextBase _httpContext;
        private readonly HttpResponseBase _httpResponse;
        private readonly HttpSessionStateBase _httpSession;
        private readonly LoginHandler _handler;
 
        public LoginHandlerTest()
        {
            _handler = new LoginHandler();
 
            // Create stubs
            _httpContext = MockRepository.GenerateStub<HttpContextBase>();
            _httpResponse = MockRepository.GenerateStub<HttpResponseBase>();
            _httpSession = MockRepository.GenerateStub<HttpSessionStateBase>();
 
            // When calling context.Response return the httpResponse stub
            _httpContext.Expect(ctx => ctx.Response).Return(_httpResponse);
 
            // When calling context.Response return the httpSession stub
            _httpContext.Expect(ctx => ctx.Session).Return(_httpSession);
        }
 
        [Fact]
        public void Handler_will_output_true_in_json_response_when_the_user_is_logged_in()
        {
            // When we call Session[UserContext.UserSessionKey] return 'new User()'
            _httpSession[UserContext.UserSessionKey] = new User();
 
            _handler.ProcessRequest(_httpContext);
 
            // Verify that the handler outputs the correct json response
            _httpResponse.AssertWasCalled(res => res.Write("{\"isLoggedIn\":true}"));
 
            // Verify that the content type is application/json
            Assert.Equal(_httpResponse.ContentType, "application/json");
        }
 
        [Fact]
        public void Handler_will_output_false_in_json_response_when_the_user_isnt_logged_in()
        {
            // When we call Session[UserContext.UserSessionKey] return 'null'
            _httpSession[UserContext.UserSessionKey] = null;
 
            _handler.ProcessRequest(_httpContext);
 
            // Verify that the handler outputs the correct json response
            _httpResponse.AssertWasCalled(res => res.Write("{\"isLoggedIn\":false}"));
 
            // Verify that the content type is application/json
            Assert.Equal(_httpResponse.ContentType, "application/json");
        }
    }
}
Mycket kod blev det denna gång, så därför visar jag även en enkel .aspx-fil med codebehind som använder LoginHandlern.

Default.aspx - Består av tre knappar, där man kan simulera: logga in, logga ut samt anropa LoginHandlern mha AJAX för att se om användaren finns i sessionen.

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="Lab.Web.Default" %>
 
<!doctype html>
 
<html>
    <head>
        <title>Simple demo</title>
        <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
        <script>
            (function () {
                var status,
                    userLoginBtn,
                    template = 'User {{result}} logged in',
 
                getUserLoginStatus = function () {
                    $.ajax({
                        url: '/CheckLogin',
                        error: console.log
                    })
                    .done(function (result) {
                        status.html(
                            template.replace('{{result}}', result.isLoggedIn ? 'is' : '<b>is not</b>')
                        ).hide().fadeIn(1000);
                    });
                };
 
                $(function () {
                    userLoginBtn = $('#userLoginBtn');
                    status = $('#status');
 
                    userLoginBtn.on('click', getUserLoginStatus);
                });
 
            })(jQuery);
 
        </script>
    </head>
 
    <body>
        <form runat="server">            
            
            <div id="status" style="background-color:#ff8;"></div>
            
            <div>
                <asp:Button runat="server" OnClick="LoginUser" Text="Login" />
                <asp:Button runat="server" OnClick="LogOutUser" Text="Log out" />
                <input type="button" id="userLoginBtn" value="Is user logged in?" />    
            </div>
        </form>
    </body>
 
</html>


Default.aspx.cs (aka codebehind) - "Loggar in" eller "loggar ut" en användare i sessionen.
using System;
using System.Web.UI;
using Lab.Entities;
 
namespace Lab.Web
{
    public partial class Default : Page
    {
        protected void LoginUser(object sender, EventArgs e)
        {
            UserContext.Current = new User();
        }
 
        protected void LogOutUser(object sender, EventArgs e)
        {
            UserContext.Current = null;
        }
    }
}

I web.config behöver även följande stycke läggas till under system.web för att kunna anropa LoginHandlern. (Alternativet är annars att skapa upp en ashx-fil i själva webbprojektet).
      <httpHandlers>
        <add verb="*" path="CheckLogin" type="Lab.Handlers.LoginHandler, Lab.Handlers" />


Om någon vill ha hela lösningen, skriv en kommentar här eller på twitter.

/Nils

2 kommentarer: