Click here to Skip to main content
15,880,469 members
Articles / Web Development / ASP.NET

A Better Visual Studio 2008/2010 Development Server Test Fixture

Rate me:
Please Sign up or sign in to vote.
5.00/5 (6 votes)
2 Oct 2010CPOL3 min read 34K   234   12   2
Leverage the built-in development web server in testing and other scenarios.

Preface 

Presented here are 2 strategies for leveraging the Visual Studio development server, both as an out of process exe and in process library for integration testing ASP.NET based applications including Silverlight and MVC. 

The former is provided  more as an exercise in completion of a previous iteration. The latter, WebHostServer is the leanest and likely the preferred method. 

Introduction 

In a previous post, I presented a class that enables programmatic control of the Visual Studio 2008 development server, WebDev.WebServer.exe. The use case for this functionality as presented here is in the interest of testing web applications and endpoints.

Refactored

While the previous implementation is capable in the context of interactive test runners, there were a few resource management issues affecting the usability in more autonomous scenarios such as continuous integration.

Amongst these were the use of static port assignment and the lack of a means to shut down an instance, specifically the ability to start and stop an instance within the scope of a single test fixture.

In this implementation, these issues have been resolved by providing a more capable means of identifying and controlling running instances of WebDev.WebServer.exe using WMI data and the polling of port use to enable dynamic port assignment.

The only outstanding issue is the inability to remove the notification icon that is orphaned in the tray area when programmatically closing an instance.

This may be dealt with in the future as an academic, but this implementation is being provided simply as a verification that it is possible to fully control shelled instances of the executable.

There is a another way to get where we want to go, but first let's update the WebDev.WebServer.exe wrapper.

WebDevServer 

Using WebDevServer with NUnit

C#
using System.Net;
using NUnit.Framework;

namespace Salient.Excerpts
{
    [TestFixture]
    public class WebDevServerFixture : WebDevServer
    {
        [TestFixtureSetUp]
        public void TestFixtureSetUp()
        {
            StartServer(@"..\..\..\..\TestSite");

            // is the equivalent of
            // StartServer(@"..\..\..\..\TestSite",
            //    GetAvailablePort(8000, 10000, IPAddress.Loopback, true), 
       // "/", "localhost");
        }
        [TestFixtureTearDown]
        public void TestFixtureTearDown()
        {
            StopServer();
        }

        [Test]
        public void Test()
        {
            string html = new WebClient().DownloadString(NormalizeUri("Default.aspx"));
        }
    }
}

WebDevServer.cs

C#
// Project: Salient
// http://salient.codeplex.com
// Date: April 16 2010

#region

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Management;
using System.Net;
using System.Net.NetworkInformation;
using System.Text.RegularExpressions;
using System.Threading;

#endregion

namespace Salient.Excerpts
{
    /// <summary>
    /// A general purpose Visual Studio 2008 Development Server 
    /// (WebDev.WebServer.exe) test fixture.
    /// Controls a shelled instance of WebDev.WebServer.exe 
    /// so F5 debugging is not possible with this server.
    /// </summary>
    /// NOTE: code from various namespaces/classes in the 
    /// Salient project have been merged into this
    /// single class for this post in the interest of brevity.
    public class WebDevServer
    {
        public WebDevServer()
        {
        }

        public string ApplicationPath { get; private set; }
        public string HostName { get; private set; }
        public int Port { get; private set; }
        public string VirtualPath { get; private set; }

        public string RootUrl
        {
            get { return string.Format(CultureInfo.InvariantCulture, 
        "http://{0}:{1}{2}", HostName, Port, VirtualPath); }
        }

        private int ProcessId { get; set; }

        private static string ProgramFilesx86
        {
            get
            {
                if (8 == IntPtr.Size ||
                    (!String.IsNullOrEmpty(Environment.GetEnvironmentVariable
            ("PROCESSOR_ARCHITEW6432"))))
                {
                    return Environment.GetEnvironmentVariable("ProgramFiles(x86)");
                }

                return Environment.GetEnvironmentVariable("ProgramFiles");
            }
        }

        /// <summary>
        /// Combine the RootUrl of the running web application with the 
        /// relative URL specified.
        /// </summary>
        public virtual Uri NormalizeUri(string relativeUrl)
        {
            return new Uri(RootUrl + relativeUrl);
        }

        /// <summary>
        /// Will start "localhost" on first available port in the range 
        /// 8000-10000 with vpath "/"
        /// </summary>
        /// <param name="applicationPath"></param>
        public void StartServer(string applicationPath)
        {
            StartServer(applicationPath, GetAvailablePort
        (8000, 10000, IPAddress.Loopback, true), "/", "localhost");
        }

        /// <summary>
        /// </summary>
        /// <param name="applicationPath">Physical path to application.</param>
        /// <param name="port">Port to listen on.</param>
        /// <param name="virtualPath">Optional. defaults to "/"</param>
        /// <param name="hostName">Optional. 
        /// Is used to construct RootUrl. Defaults to "localhost"</param>
        public void StartServer(string applicationPath, int port, 
        string virtualPath, string hostName)
        {
            applicationPath = Path.GetFullPath(applicationPath);
            hostName = string.IsNullOrEmpty(hostName) ? "localhost" : hostName;
            virtualPath = String.Format("/{0}/", 
        (virtualPath ?? string.Empty).Trim('/')).Replace("//", "/");

            if (GetRunningInstance(applicationPath, port, virtualPath) != null)
            {
                return;
            }

            IPAddress ipAddress = IPAddress.Loopback;

            if (!IsPortAvailable(ipAddress, port))
            {
                throw new Exception(string.Format("Port {0} is in use.", port));
            }

            string arguments = String.Format
        (CultureInfo.InvariantCulture, "/port:{0} /path:\"{1}\" /vpath:\"{2}\"",
                                             port, applicationPath, virtualPath);

            using (Process proc = new Process())
            {
                proc.StartInfo = new ProcessStartInfo
                    {
                        FileName = GetWebDevExecutablePath(),
                        Arguments = arguments,
                        CreateNoWindow = true
                    };
                bool started = proc.Start();
                if (!started)
                {
                    throw new Exception("Error starting server");
                }
                ProcessId = proc.Id;
            }

            ApplicationPath = applicationPath;
            Port = port;
            VirtualPath = virtualPath;
            HostName = hostName;
        }

        /// <summary>
        /// After stopping an instance of WebDevWebServer.exe, 
        /// the orphaned tray notification icon will remain.
        /// This can be annoying but simply running your mouse over them 
        /// will dispose of it.
        /// I think there may be some Win32 voodoo to deal with this 
        /// but is a very low priority right now.
        /// </summary>
        public void StopServer()
        {
            try
            {
                WebDevServer instance = GetRunningInstance
            (ApplicationPath, Port, VirtualPath);
                if (instance != null)
                {
                    using (Process process = Process.GetProcessById(instance.ProcessId))
                    {
                        if (process.MainWindowHandle != IntPtr.Zero)
                        {
                            process.CloseMainWindow();
                        }
                        process.Kill();

                        // allow the port time to be released
                        process.WaitForExit(100);
                    }
                }
            }

                // ReSharper disable EmptyGeneralCatchClause
            catch
            // ReSharper restore EmptyGeneralCatchClause
            {
                // what can we do about spurious exceptions on Process.Kill?
            }
        }

        public void Dispose()
        {
            StopServer();
        }

        #region Instance Management


        private const string WebDevPath = 
        @"Common Files\Microsoft Shared\DevServer\9.0\WebDev.WebServer.exe";
        private static readonly Regex RxPath = 
        new Regex(@"/path:""(?<path>[^""]*)", RegexOptions.ExplicitCapture);
        private static readonly Regex RxPort = 
        new Regex(@"/port:(?<port>\d*)", RegexOptions.ExplicitCapture);
        private static readonly Regex RxVPath = 
        new Regex(@"/vpath:""(?<vpath>[^""]*)", RegexOptions.ExplicitCapture);

        /// <summary>
        /// Private ctor to build from WMI query
        /// </summary>
        private WebDevServer(string commandLine, int processId)
        {
            ProcessId = processId;
            Port = Int32.Parse(RxPort.Match(commandLine).Groups["port"].Value);
            ApplicationPath = RxPath.Match(commandLine).Groups["path"].Value;
            VirtualPath =
                String.Format("/{0}/", RxVPath.Match(commandLine).Groups
            ["vpath"].Value.Trim('/')).Replace("//", "/");
            HostName = "localhost";
        }

        public static WebDevServer GetRunningInstance(string applicationPath, 
                                   int port, string virtualPath)
        {
            return GetRunningInstances().FirstOrDefault(s =>
                string.Compare(s.ApplicationPath, applicationPath, 
            StringComparison.OrdinalIgnoreCase) == 0 &&
                string.Compare(s.VirtualPath, virtualPath, 
            StringComparison.OrdinalIgnoreCase) == 0 &&
                s.Port == port);
        }

        /// <summary>
        /// Queries WMI and builds a list of WebDevWebServer 
        /// representing all running instances.
        /// </summary>
        public static List<WebDevServer> GetRunningInstances()
        {
            List<WebDevServer> returnValue = new List<WebDevServer>();
            const string query = "select CommandLine,ProcessId from 
            Win32_Process where Name='WebDev.WebServer.EXE'";
            using (ManagementObjectSearcher searcher = 
                new ManagementObjectSearcher(query))
            {
                using (ManagementObjectCollection results = searcher.Get())
                {
                    foreach (ManagementObject process in results)
                    {
                        returnValue.Add(
                            new WebDevServer(process["CommandLine"].ToString(), 
                                int.Parse(process["ProcessId"].ToString())));
                        process.Dispose();
                    }
                }
            }
            return returnValue;
        }

        public static string GetWebDevExecutablePath()
        {
            string exePath = Path.Combine(ProgramFilesx86, WebDevPath);

            if (!File.Exists(exePath))
            {
                throw new FileNotFoundException(exePath);
            }
            return exePath;
        }

        /// <summary>
        /// Gently polls specified IP:Port to determine if it is available.
        /// </summary>
        /// <param name="ipAddress"></param>
        /// <param name="port"></param>
        public static bool IsPortAvailable(IPAddress ipAddress, int port)
        {
            bool portAvailable = false;

            for (int i = 0; i < 5; i++)
            {
                portAvailable = GetAvailablePort(port, port, ipAddress, true) == port;
                if (portAvailable)
                {
                    break;
                }
                // be a little patient and wait for the port if necessary,
                // the previous occupant may have just vacated
                Thread.Sleep(100);
            }
            return portAvailable;
        }

        /// <summary>
        /// Returns first available port on the specified IP address.
        /// The port scan excludes ports that are open on ANY loopback adapter.
        ///
        /// If the address upon which a port is requested is an 'ANY' address all
        /// ports that are open on ANY IP are excluded.
        /// </summary>
        /// <param name="rangeStart"></param>
        /// <param name="rangeEnd"></param>
        /// <param name="ip">The IP address upon which to search for available port.
        /// </param>
        /// <param name="includeIdlePorts">If true includes ports in 
        /// TIME_WAIT state in results.
        /// TIME_WAIT state is typically cool down period for recently released ports.
        /// </param>
        /// <returns></returns>
        public static int GetAvailablePort(int rangeStart, 
               int rangeEnd, IPAddress ip, bool includeIdlePorts)
        {
            IPGlobalProperties ipProps = IPGlobalProperties.GetIPGlobalProperties();

            // if the ip we want a port on is an 'any' or loopback port 
            // we need to exclude all ports that are active on any IP
            Func<IPAddress, bool> isIpAnyOrLoopBack = i => IPAddress.Any.Equals(i) ||
                                                           IPAddress.IPv6Any.Equals(i) ||
                                                           IPAddress.Loopback.Equals(i) ||
                                                           IPAddress.IPv6Loopback.
                                                               Equals(i);
            // get all active ports on specified IP.
            List<ushort> excludedPorts = new List<ushort>();

            // if a port is open on an 'any' or 'loopback' interface 
            // then include it in the excludedPorts
            excludedPorts.AddRange(from n in ipProps.GetActiveTcpConnections()
                                   where
                                       n.LocalEndPoint.Port >= rangeStart &&
                                       n.LocalEndPoint.Port <= rangeEnd && (
                                       isIpAnyOrLoopBack(ip) || 
                    n.LocalEndPoint.Address.Equals(ip) ||
                                        isIpAnyOrLoopBack(n.LocalEndPoint.Address)) &&
                                        (!includeIdlePorts || n.State 
                    != TcpState.TimeWait)
                                   select (ushort)n.LocalEndPoint.Port);

            excludedPorts.AddRange(from n in ipProps.GetActiveTcpListeners()
                                   where n.Port >= rangeStart && n.Port <= rangeEnd && (
                                   isIpAnyOrLoopBack(ip) || 
                n.Address.Equals(ip) || isIpAnyOrLoopBack(n.Address))
                                   select (ushort)n.Port);

            excludedPorts.AddRange(from n in ipProps.GetActiveUdpListeners()
                                   where n.Port >= rangeStart && n.Port <= rangeEnd && (
                                   isIpAnyOrLoopBack(ip) || 
                n.Address.Equals(ip) || isIpAnyOrLoopBack(n.Address))
                                   select (ushort)n.Port);

            excludedPorts.Sort();

            for (int port = rangeStart; port <= rangeEnd; port++)
            {
                if (!excludedPorts.Contains((ushort)port))
                {
                    return port;
                }
            }

            return 0;
        }

        #endregion
    }
}

Redesigned 

While there may be scenarios in which using an out of process executable would be the best strategy, ideally, use of an assembly referenced type in the process of the calling code, tests in this case, would provide better control of the server and eliminate much of the WMI and Process management code found in WebDevServer.

Reflecting on WebDev.WebServer.exe, it becomes clear that it is simply a wrapper for Microsoft.VisualStudio.WebHost.Server. WebHost.Server exposes all of the methods we need to capably and cleanly control an instance of a web server.

As an added bonus, since the web server is directly instantiated in code, we have the ability to F5 debug the code making the HTTP requests, our tests, and the site under test, just as we would be able to if using the development server directly from Visual Studio.

The subtle difference here is that we are in full control of the server instance, making it more suitable for headless scenarios such as in continuous integration and eliminating the necessity of juggling configurations between interactive development and check-in/test.

The bottom line is: Unless you have a compelling reason to use the out-of-process WebDevServer, you will have a better experience all around using the in-process WebHostServer.

Note: The Microsoft.VisualStudio.WebHost namespace is contained in the file WebDev.WebHost.dll. This file is in the GAC, but it is not possible to add a reference to this assembly from within Visual Studio.

To add a reference, you will need to open your .csproj file in a text editor and add the reference manually.

Look for the ItemGroup that contains the project references, and add the following element:

 For ASP.Net 2.0-3.5 using .Net Framework 3.5 

XML
<Reference Include="WebDev.WebHost, Version=9.0.0.0, 
    Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=x86">
  <Private>False</Private>
</Reference>   

 For ASP.Net 2.0-3.5 using .Net Framework 4.0 

XML
<Reference Include="WebDev.WebHost20, Version=10.0.0.0, 
    Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=x86">
  <Private>False</Private>
</Reference>   

 For ASP.Net 4.0 using .Net Framework 4.0 

XML
<Reference Include="WebDev.WebHost40, Version=10.0.0.0, 
    Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=x86">
  <Private>False</Private>
</Reference>  

You may open WebHostServer.csproj in a text editor for an example.

WebHostServer

Using WebHostServer with NUnit

C#
using System.Net;
using NUnit.Framework;

namespace Salient.Excerpts
{
    [TestFixture]
    public class WebHostServerFixture : WebHostServer
    {
        [TestFixtureSetUp]
        public void TestFixtureSetUp()
        {
            StartServer(@"..\..\..\..\TestSite");

            // is the equivalent of
            // StartServer(@"..\..\..\..\TestSite",
            // GetAvailablePort(8000, 10000, IPAddress.Loopback, true), 
            //                  "/", "localhost");
        }
        [TestFixtureTearDown]
        public void TestFixtureTearDown()
        {
            StopServer();
        }

        [Test]
        public void Test()
        {
            // while a reference to the web app under test is not necessary,
            // if you do add a reference to this test project you may F5 debug your tests.
            // if you debug this test you will break in Default.aspx.cs
            string html = new WebClient().DownloadString(NormalizeUri("Default.aspx"));
        }
    }
}

WebHostServer.cs

C#
// Project: Salient
// http://salient.codeplex.com
// Date: April 16 2010

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Threading;
using Microsoft.VisualStudio.WebHost;

namespace Salient.Excerpts
{
    /// <summary>
    /// A general purpose Microsoft.VisualStudio.WebHost.Server test fixture.
    /// WebHost.Server is the core of the Visual Studio Development Server 
    /// (WebDev.WebServer).
    ///
    /// This server is run in-process and may be used in F5 debugging.
    /// </summary>
    /// <remarks>
    /// If you are adding this source code to a new project, You will need to
    /// manually add a reference to WebDev.WebHost.dll to your project. It cannot
    /// be added from within Visual Studio.
    ///
    /// Please see the Readme.txt accompanying this code for details.
    /// </remarks>
    /// NOTE: code from various namespaces/classes in the Salient 
    /// project have been merged into this
    /// single class for this post in the interest of brevity
    public class WebHostServer
    {
        private Server _server;

        public string ApplicationPath { get; private set; }

        public string HostName { get; private set; }

        public int Port { get; private set; }

        public string VirtualPath { get; private set; }

        public string RootUrl
        {
            get { return string.Format(CultureInfo.InvariantCulture, 
        "http://{0}:{1}{2}", HostName, Port, VirtualPath); }
        }

        /// <summary>
        /// Combine the RootUrl of the running web application 
        /// with the relative URL specified.
        /// </summary>
        public virtual Uri NormalizeUri(string relativeUrl)
        {
            return new Uri(RootUrl + relativeUrl);
        }

        /// <summary>
        /// Will start "localhost" on first available port in the 
        /// range 8000-10000 with vpath "/"
        /// </summary>
        /// <param name="applicationPath"></param>
        public void StartServer(string applicationPath)
        {
            StartServer(applicationPath, GetAvailablePort
        (8000, 10000, IPAddress.Loopback, true), "/", "localhost");
        }

        /// <summary>
        /// </summary>
        /// <param name="applicationPath">Physical path to application.</param>
        /// <param name="port">Port to listen on.</param>
        /// <param name="virtualPath">Optional. defaults to "/"</param>
        /// <param name="hostName">Optional. Is used to construct 
        /// RootUrl. Defaults to "localhost"</param>
        public void StartServer(string applicationPath, int port, 
        string virtualPath, string hostName)
        {
            if (_server != null)
            {
                throw new InvalidOperationException("Server already started");
            }

            // WebHost.Server will not run on any other IP
            IPAddress ipAddress = IPAddress.Loopback;

            if(!IsPortAvailable(ipAddress, port))
            {
                throw new Exception(string.Format("Port {0} is in use.", port));
            }

            applicationPath = Path.GetFullPath(applicationPath);

            virtualPath = String.Format("/{0}/", (virtualPath ?? 
            string.Empty).Trim('/')).Replace("//", "/");

            _server = new Server(port, virtualPath, applicationPath, false, false);
            _server.Start();

            ApplicationPath = applicationPath;
            Port = port;
            VirtualPath = virtualPath;
            HostName = string.IsNullOrEmpty(hostName) ? "localhost" : hostName;
        }

        /// <summary>
        /// Stops the server.
        /// </summary>
        public void StopServer()
        {
            if (_server != null)
            {
                _server.Stop();
                _server = null;
                // allow some time to release the port
                Thread.Sleep(100);
            }
        }

        public void Dispose()
        {
            StopServer();
        }


       /// <summary>
        /// Gently polls specified IP:Port to determine if it is available.
        /// </summary>
        /// <param name="ipAddress"></param>
        /// <param name="port"></param>
        public static bool IsPortAvailable(IPAddress ipAddress, int port)
        {
            bool portAvailable = false;

            for (int i = 0; i < 5; i++)
            {
                portAvailable = GetAvailablePort(port, port, ipAddress, true) == port;
                if (portAvailable)
                {
                    break;
                }
                // be a little patient and wait for the port if necessary,
                // the previous occupant may have just vacated
                Thread.Sleep(100);
            }
            return portAvailable;
        }

        /// <summary>
        /// Returns first available port on the specified IP address.
        /// The port scan excludes ports that are open on ANY loopback adapter.
        ///
        /// If the address upon which a port is requested is an 'ANY' address all
        /// ports that are open on ANY IP are excluded.
        /// </summary>
        /// <param name="rangeStart"></param>
        /// <param name="rangeEnd"></param>
        /// <param name="ip">The IP address upon which to search for available port.
        /// </param>
        /// <param name="includeIdlePorts">If true includes ports in 
        /// TIME_WAIT state in results.
        /// TIME_WAIT state is typically cool down period for 
        /// recently released ports.</param>
        /// <returns></returns>
        public static int GetAvailablePort(int rangeStart, 
               int rangeEnd, IPAddress ip, bool includeIdlePorts)
        {
            IPGlobalProperties ipProps = IPGlobalProperties.GetIPGlobalProperties();

            // if the IP we want a port on is an 'any' or 
            // loopback port we need to exclude all ports that are active on any IP
            Func<IPAddress, bool> isIpAnyOrLoopBack = i => IPAddress.Any.Equals(i) ||
                                                           IPAddress.IPv6Any.Equals(i) ||
                                                           IPAddress.Loopback.Equals(i) ||
                                                           IPAddress.IPv6Loopback.
                                                               Equals(i);
            // get all active ports on specified IP.
            List<ushort> excludedPorts = new List<ushort>();

            // if a port is open on an 'any' or 'loopback' 
            // interface then include it in the excludedPorts
            excludedPorts.AddRange(from n in ipProps.GetActiveTcpConnections()
                                   where
                                       n.LocalEndPoint.Port >= rangeStart &&
                                       n.LocalEndPoint.Port <= rangeEnd && (
                                       isIpAnyOrLoopBack(ip) || 
                    n.LocalEndPoint.Address.Equals(ip) ||
                                        isIpAnyOrLoopBack(n.LocalEndPoint.Address)) &&
                                        (!includeIdlePorts || n.State 
                        != TcpState.TimeWait)
                                   select (ushort)n.LocalEndPoint.Port);

            excludedPorts.AddRange(from n in ipProps.GetActiveTcpListeners()
                                   where n.Port >= rangeStart && n.Port <= rangeEnd && (
                                   isIpAnyOrLoopBack(ip) || n.Address.Equals(ip) 
                    || isIpAnyOrLoopBack(n.Address))
                                   select (ushort)n.Port);

            excludedPorts.AddRange(from n in ipProps.GetActiveUdpListeners()
                                   where n.Port >= rangeStart && n.Port <= rangeEnd && (
                                   isIpAnyOrLoopBack(ip) || n.Address.Equals(ip) 
                    || isIpAnyOrLoopBack(n.Address))
                                   select (ushort)n.Port);

            excludedPorts.Sort();

            for (int port = rangeStart; port <= rangeEnd; port++)
            {
                if (!excludedPorts.Contains((ushort)port))
                {
                    return port;
                }
            }

            return 0;
        }
    }
}

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Salient Solutions
United States United States
My name is Sky Sanders and I am an end-to-end, front-to-back software solutions architect with more than 20 years experience in IT infrastructure and software development, the last 10 years being focused primarily on the Microsoft .NET platform.

My motto is 'I solve problems.' and I am currently available for hire.

I can be contacted at sky.sanders@gmail.com

Comments and Discussions

 
QuestionExcellent -- but having trouble loading a Web.Application in the WebHostServerFixture... Pin
lassesen23-Sep-13 10:20
lassesen23-Sep-13 10:20 
GeneralFileNotFoundException Pin
Graeme Foster7-Jun-11 23:26
Graeme Foster7-Jun-11 23:26 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.