Start a web-server for unit testing in C#

by mandel on July 15th, 2010

I’m currently heavily working on Windows (oh irony) and I was confronted with a situation in which I wanted to be able to write a unit test that will access a web-page perform certain operations and check that everything went ok. This is not the first time I have been confronted with this situation (yes, really) and last time I resorted to use the great trick from Phil Haack in which the Microsoft.VisualStudio.WebHost namespace (yes, cassini) is used. I’ve copy paste his code here so that you can easily find it (you cannot longer access it through his page).

using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Reflection;
using System.Text;
using Microsoft.VisualStudio.WebHost;
 
namespace Gazintas.Test.Common
{
        /// <summary>
        /// <para>A Web Server useful for unit tests.  Uses the same code used by the
        /// built in WebServer (formerly known as Cassini) in VisualStudio.NET 2005.
        /// Specifically, this needs a reference to WebServer.WebHost.dll located in
        /// the GAC.
        /// </para>
        /// </summary>
        /// <remarks>
        /// <para>If you unseal this class, make sure to make Dispose(bool disposing) a protected
        /// virtual method instead of private.
        /// </para>
        /// <para>
        /// For more information, check out: http://haacked.com/archive/2006/12/12/Using_WebServer.WebDev_For_Unit_Tests.aspx
        /// </para>
        /// </remarks>
        public sealed class TestWebServer : IDisposable
        {
                private bool started = false;
                private Server webServer;
                private int webServerPort;
                private string webServerVDir;
                private string sourceBinDir = AppDomain.CurrentDomain.BaseDirectory;
                private string webRoot = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "WebRoot");
                private string webBinDir = Path.Combine(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "WebRoot"), "bin");
                private string webServerUrl; //built in Start
 
                /// <summary>
                /// Initializes a new instance of the <see cref="TestWebServer"/> class on port 8085.
                /// </summary>
                public TestWebServer() : this(8085, "/")
                {
                }
 
                /// <summary>
                /// Initializes a new instance of the <see cref="TestWebServer"/> class
                /// using the specified port and virtual dir.
                /// </summary>
                /// <param name="port">The port.</param>
                /// <param name="virtualDir">The virtual dir.</param>
                public TestWebServer(int port, string virtualDir)
                {
                        this.webServerPort = port;
                        this.webServerVDir = virtualDir;
                }
 
                /// <summary>
                /// Starts the webserver and returns the URL.
                /// </summary>
                public Uri Start()
                {
                        //NOTE: WebServer.WebHost is going to load itself AGAIN into another AppDomain,
                        // and will be getting it's Assemblies from the BIN, including another copy of itself!
                        // Therefore we need to do this step FIRST because I've removed WebServer.WebHost from the GAC
                        if (!Directory.Exists(webRoot))
                                Directory.CreateDirectory(webRoot);
 
                        if (!Directory.Exists(webBinDir))
                                Directory.CreateDirectory(webBinDir);
 
                        CopyAssembliesToWebServerBinDirectory();
 
                        //Start the internal Web Server pointing to our test webroot
                        webServer = new Server(webServerPort, webServerVDir, this.webRoot);
                        webServerUrl = String.Format("http://localhost:{0}{1}", webServerPort, webServerVDir);
 
                        webServer.Start();
                        started = true;
                        Debug.WriteLine(String.Format("Web Server started on port {0} with VDir {1} in physical directory {2}", webServerPort, webServerVDir, this.webRoot));
                        return new Uri(webServerUrl);
                }
 
                private void CopyAssembliesToWebServerBinDirectory()
                {
                        foreach (string file in Directory.GetFiles(this.sourceBinDir, "*.dll"))
                        {
                                string newFile = Path.Combine(this.webBinDir, Path.GetFileName(file));
                                if (File.Exists(newFile))
                                {
                                        File.Delete(newFile);
                                }
                                File.Copy(file, newFile);
                        }
                }
 
                /// <summary>
                /// Makes a  simple GET request to the web server and returns
                /// the result as a string.
                /// </summary>
                /// <param name="page">The page.</param>
                /// <returns></returns>
                public string RequestPage(string page)
                {
                        WebClient client = new WebClient();
                        string url = new Uri(new Uri(this.webServerUrl), page).ToString();
                        using (StreamReader reader = new StreamReader(client.OpenRead(url)))
                        {
                                string result = reader.ReadToEnd();
                                return result;
                        }
                }
 
                /// <summary>
                /// Makes a  simple POST request to the web server and returns
                /// the result as a string.
                /// </summary>
                /// <param name="page">The page.</param>
                /// <param name="formParameters">
                /// The form paramater to post. Should be in the format "Name=Value&Name2=Value2&...&NameN=ValueN"
                /// For extra credit, build a version of this method that uses a NameValue collection. I use a
                /// string because it's possible you may want to post flat text.
                /// </param>
                /// <returns></returns>
                public string RequestPage(string page, string formParameters)
                {
                        HttpWebRequest request = WebRequest.Create(new Uri(new Uri(this.webServerUrl), page)) as HttpWebRequest;
 
                        if (request == null)
                                return null; //can't imagine this happening.
 
                        request.UserAgent = "Sutext UnitTest Webserver";
                        request.Timeout = 10000; //10 secs is reasonable, no?
                        request.Method = "POST";
                        request.ContentLength = formParameters.Length;
                        request.ContentType = "application/x-www-form-urlencoded; charset=utf-8";
                        request.KeepAlive = true;
 
                        using (StreamWriter myWriter = new StreamWriter(request.GetRequestStream()))
                        {
                                myWriter.Write(formParameters);
                        }
 
                        HttpWebResponse response = (HttpWebResponse)request.GetResponse();
                        if (response.StatusCode < HttpStatusCode.OK && response.StatusCode >= HttpStatusCode.Ambiguous)
                                return "Http Status" + response.StatusCode;
 
                        string responseText;
                        using (StreamReader reader = new StreamReader(response.GetResponseStream(), Encoding.UTF8))
                        {
                                responseText = reader.ReadToEnd();
                        }
 
                        return responseText;
                }
 
                /// <summary>
                /// Extracts a resources such as an html file or aspx page to the webroot directory
                /// and returns the filepath.
                /// </summary>
                /// <param name="resourceName">Name of the resource.</param>
                /// <param name="destinationFileName">Name of the destination file.</param>
                /// <returns></returns>
                public string ExtractResource(string resourceName, string destinationFileName)
                {
                        if (!started)
                                throw new InvalidOperationException("Please start the webserver before extracting resources.");
 
                        //NOTE: if you decide to drop this class into a separate assembly (for example,
                        //into a unit test helper assembly to make it more reusable),
                        //call Assembly.GetCallingAssembly() instead.
                        Assembly a = Assembly.GetExecutingAssembly();
                        string filePath;
                        using (Stream stream = a.GetManifestResourceStream(resourceName))
                        {
                                filePath = Path.Combine(this.webRoot, destinationFileName);
                                using (StreamWriter outfile = File.CreateText(filePath))
                                {
                                        using (StreamReader infile = new StreamReader(stream))
                                        {
                                                outfile.Write(infile.ReadToEnd());
                                        }
                                }
                        }
                        return filePath;
                }
 
                /// <summary>
                /// Stops this instance.
                /// </summary>
                public void Stop()
                {
                        Dispose();
                }
 
                ~TestWebServer()
                {
                        Dispose(false);
                }
 
                public void Dispose()
                {
                        Dispose(true);
                        GC.SuppressFinalize(this);
                }
 
                ///<summary>
                ///Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
                ///</summary>
                /// <remarks>
                /// If we unseal this class, make sure this is protected virtual.
                /// </remarks>
                ///<filterpriority>2</filterpriority>
                private void Dispose(bool disposing)
                {
                        if (disposing)
                        {
                                ReleaseManagedResources();
                        }
                }
 
                // Cleans up the directories we created.
                private void ReleaseManagedResources()
                {
                        if(this.webServer != null)
                        {
                                this.webServer.Stop();
                                this.webServer = null;
                                this.started = false;
                        }
 
                        if (Directory.Exists(this.webBinDir))
                                Directory.Delete(this.webBinDir, true);
 
                        if (Directory.Exists(this.webRoot))
                                Directory.Delete(this.webRoot, true);
                }
        }
}

This is a great solution if either you expect all your developers to have visual studio since I’m not sure that you are allows to distribute WebDev.WebHost.dll, in order cases, this solution is no good. This is where Mono becomes extremely useful:

/**
 * Copyright 2010 Canonical Ltd.
 * 
 * This file is part of UbuntuOne on Windows.
 * 
 * UbuntuOne on Windows is free software: you can redistribute it and/or modify		
 * it under the terms of the GNU Lesser General Public License version 		
 * as published by the Free Software Foundation.		
 *		
 * Ubuntu One on Windows is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the	
 * GNU Lesser General Public License for more details.	
 *
 * You should have received a copy of the GNU Lesser General Public License	
 * along with UbuntuOne for Windows.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * Authors: Manuel de la Peña <manuel.delapena@canonical.com>
 */
using System;
using System.IO;
using System.Net;
using System.Threading;
using Mono.WebServer;
 
namespace Canonical.UbuntuOne.Common.Tests
{
    /// <summary>
    /// This class allows to start a XSP server for testing purposes. Before the server is ran
    /// all the different properties must be set up so that the server knows where to run from an
    /// what to run.
    /// </summary>
    [Serializable]
    public class TestWebserver
    {
 
        private ApplicationServer _server;
 
        #region Property settings
 
        /// <summary>
        /// a comma separated list of virtual directory and
	/// real directory for all the applications we want to manage
	/// with this server. The virtual and real dirs. are separated
	/// by a colon. Optionally you may specify virtual host name
	/// and a port.
        /// </summary>
        public string Apps { get; set; }
 
        /// <summary>
        /// adds application definitions from all XML files
        /// found in the specified directory DIR. Files must have
	/// .webapp' extension
        /// </summary>
        public string AppConfigDir { get; set; }
 
        /// <summary>
        /// adds application definitions from the XML
	/// configuration file. See sample configuration file that");
        /// comes with the server.
        /// </summary>
        public string AppConfigFile { get; set; }
 
        /// <summary>
        /// the server changes to this directory before anything else.
        /// </summary>
        public string RootDir { get; set; }
 
        /// <summary>
        /// Gets and sets the port number in which the server will be ran.
        /// </summary>
        public int Port { get; set; }
 
        /// <summary>
        /// Gets and sets the ip of the server.
        /// </summary>
        public string Ip { get; set; }
 
        /// <summary>
        /// Gets and sets if the server should be stop when there is an exception.
        /// </summary>
        public bool NonStop { get; set; }
 
        /// <summary>
        /// Gets and sets if we want the server to be verbose.
        /// </summary>
        public bool Verbose { get; set; }
 
        /// <summary>
        /// The exception which stop the server.
        /// </summary>
        public Exception Exception { get; set; }
 
        #endregion
 
        /// <summary>
        /// Creates a new webserver that can be used for testing purposes.
        /// </summary>
        public TestWebserver()
        {
            // set the basic settings
            Port = 8080;
            Ip = "0.0.0.0";
        }
 
        public void Start()
        {
            var ipaddr = IPAddress.Parse(Ip);
 
            if (!String.IsNullOrEmpty(RootDir))
            {
                Environment.CurrentDirectory = RootDir;
            }
 
            RootDir = Directory.GetCurrentDirectory();
 
            WebSource webSource = new XSPWebSource(ipaddr, Port, false);
 
            _server = new ApplicationServer(webSource)
            {
                Verbose = Verbose,
                SingleApplication = false
            };
 
            if (Apps != null)
                _server.AddApplicationsFromCommandLine(Apps);
 
            if (AppConfigFile != null)
                // allows to load the config from a file
                _server.AddApplicationsFromConfigFile(AppConfigFile);
 
            if (AppConfigDir != null)
                // add config from a dir
                _server.AddApplicationsFromConfigDirectory(AppConfigDir);
 
            if (Apps == null && AppConfigDir == null && AppConfigFile == null)
                _server.AddApplicationsFromCommandLine("/:.");
 
            var vh = _server.GetSingleApp();
            if (vh != null)
            {
                // Redo in new domain
                vh.CreateHost(_server, webSource);
                var svr = (TestWebserver)vh.AppHost.Domain.CreateInstanceAndUnwrap(
                    GetType().Assembly.GetName().ToString(), GetType().FullName);
                webSource.Dispose();
                svr.Start();
            }
 
            if (_server.Start(!NonStop, Exception) == false)
                return;
        }
 
        /// <summary>
        /// Allows to stop the server if it is currently running.
        /// </summary>
        public void Stop()
        {
            _server.Stop();
        }
    }
}

The above class allows to start an XSP server that will host a webpage that you can use for your unit tests. This code was inspired by the XSP implementation from mono and adapted so that is will be a class that could be easily instantiated for unit testing and that can be distributes since it just depends on Mono.WebServer2.dll which has the MIT license, I hope it helps you :D