E2E Tests for OWIN based Web Application

Motivation

End to End tests have alluded me for a while in .Net/OWIN/WepApi/Angular/etc. Now I have finally found my solution and I thought I would share it with you. Btw please let me know if this is usefull or if you have suggestions to improve it.

Abstract

I provide you wiuth a GitHub repository containing a Unit Test Project and a sample MVC project. Summarized with buzzwords I would call this “End to End testing a self hosted OWIN/angularjs single page application web application using Selenium Browser Drivers”.

How

I created two projects A simple angularjs/OWIN application with WebApi calls and a Unit Test Class library containing a base class called OwinE2ETest which you can inherit from.

OwinE2ETest creates a self host server for a OWIN application configuration and then creates a WebDriver which can be used to query the server. This is done in before every test and after every test the server as well as the browser are disposed. note: Personally I also like to hook everything up with a CompositionContainer and also give access to the applications DbContext (when working with EF)

  • Nuget Dependencies
    • Selenium Drivers
    • Chrome Driver, Firefox Driver, PhantomJS Driver, …
    • OWIN Core
    • OWIN Self Host
    • OWIN Static file server for serving asset files

Example

I created a minimal sample web application which serves an Index.html and a WebApi endpoint. The Index.html uses angular to query the web api and shows the result within a new dom element.

using the Base class I describe further below the test class looks like this:

   [TestClass]
  public class SampleWebTest:OwinE2ETest
  {
    protected override void InitializeWebApp(Owin.IAppBuilder app)
    {
      /// first you need to serve the static files
      /// your application executes in SOLUTION_DIR/Sample.Web.Tests/bin/Debug
      /// therfore you need the ../../../ to navigate to SOLUTION_DIR
      /// I have searched alot to find a better workaround. but there is none to my knowledge.  
      /// You will also have problems if you depend on HostingEnvironment.MapPath() since
      /// it will return null because SelfHost does not configure the Hosting environment correctly. (you will have to find a solution yourself )
      /// 
      ServeVirtualDirectory(app, "", "../../../Sample.Web");
      /// here you initialize your owin app
      new Startup().Configuration(app);

    }

    [TestMethod]
    public void ApplicationBrandShouldBeCorrect()
    {
      /// find elements searches for element to appear within 5 seconds
      Browser.Manage().Timeouts().ImplicitlyWait(TimeSpan.FromSeconds(5));

      /// once the control flow is here your web application is being served and you have a web driver with which to perform queries
      /// you could also think about dropping and creating the database in Init() which would allow you to observe the db during brwoser queries
      Browser.Navigate().GoToUrl(Url);
      Browser.FindElementById("loadDataBtn").Click();

      var element = Browser.FindElementById("dataArea");
      Assert.AreEqual("a,b", element.Text);
      /// afterwards your application is destroyed as well as the browser.
    }

  }

Here is the code for my base class:

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.Owin.Hosting;
using System.Net.Http;
using System.Threading.Tasks;
using System.IO;
using OpenQA.Selenium;
using OpenQA.Selenium.PhantomJS;
using OpenQA.Selenium.Remote;
using System.Net.Sockets;
using System.Net;
using Owin;
using Microsoft.Owin;
using Microsoft.Owin.FileSystems;
using Microsoft.Owin.StaticFiles;

namespace Sample.Web.Tests
{
  /// <summary>
  /// This base class allows the tester to perform end to end tests of an OWIN web application
  /// 
  /// </summary>
  public abstract class OwinE2ETest
  {
    /// <summary>
    /// gives the test access to an automated web browser
    /// </summary>
    public RemoteWebDriver Browser { get; private set; }

    /// <summary>
    /// This method may be overriden to Generate a free url where to host 
    /// the OWIN web application 
    /// 
    /// Default implementation gets a free port number and returns http://localhost:PORTNUMBER
    /// </summary>
    /// <returns></returns>
    protected virtual string GenerateFreeUrl()
    {
      var listener = new TcpListener(IPAddress.Loopback, 0);
      listener.Start();
      var endPoint = listener.LocalEndpoint as IPEndPoint;
      var port = endPoint.Port;
      listener.Stop();
      return "http://localhost:"+port;
    }


    /// <summary>
    /// user may override to create a remote web driver other than PhantomJS 
    /// e.g. ChromeDriver, FirefoxDriver, IEDriver -> this would require further
    /// dependencies in nuget package manager
    /// </summary>
    /// <returns></returns>
    protected virtual RemoteWebDriver CreateWebDriver()
    {
      return new PhantomJSDriver();
    }

    /// <summary>
    /// Initialize method.  Subclasses may create their own initialize method which would have to call Init(). But I suggest you just override Init()
    /// </summary>
    [TestInitialize]
    public void InitializeTest()
    {
      Init();
    }

    /// <summary>
    /// Override this method and call base.Init() if you need to do further per test initialization
    /// 
    /// This method serves your web app (see InitializeWebApp)  
    /// and creates a web webdriver which the test may use.
    /// </summary>
    protected virtual void Init()
    {
      this.Url = GenerateFreeUrl();
      this.WebApplication = WebApp.Start(Url, app =>
      {
        InitializeWebApp(app);
      });
      this.Browser = CreateWebDriver();
    }

    /// <summary>
    /// implement this to generate your OWIN app.
    /// 
    /// e.g. new Startup().Configuration(app);
    /// </summary>
    /// <param name="app"></param>
    protected abstract void InitializeWebApp(Owin.IAppBuilder app);


    /// <summary>
    /// allows you to server a specific physical folder as to the specified request Path
    /// </summary>
    /// <param name="app"></param>
    /// <param name="requestPath"></param>
    /// <param name="physicalPath"></param>
    public static void ServeVirtualDirectory(IAppBuilder app, string requestPath, string physicalPath)
    {

      PathString requestPathString;
      if (string.IsNullOrWhiteSpace(requestPath)) requestPathString = PathString.Empty;
      else requestPathString = new PathString(requestPath);

      physicalPath = System.IO.Path.GetFullPath(physicalPath);
      var fileSystem = new PhysicalFileSystem(physicalPath);

      {
        var options = new FileServerOptions()
        {
          EnableDefaultFiles = true,
          RequestPath = requestPathString,
          FileSystem = fileSystem
        };
        app.UseFileServer(options);
      }
    }


    /// <summary>
    /// Per test cleanup method
    /// </summary>
    [TestCleanup]
    public void CleanupTest()
    {
      WebApplication.Dispose();
    }

    /// <summary>
    /// The Url on which the wep app is being served.
    /// </summary>
    public string Url { get; set; }

    /// <summary>
    /// the web application reference (you should not need it)
    /// </summary>
    public IDisposable WebApplication { get; set; }
  }
}

Leave a Reply

Your email address will not be published. Required fields are marked *