Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A General Purpose Visual Studio 2008 Development Server (WebDev.WebServer.exe) Test Fixture

0.00/5 (No votes)
18 Apr 2010 1  
Use Visual Studio 2008 Development Server (WebDev.WebServer.exe) in automated testing frameworks

UPDATE: This post is valid as reference material but the code presented has been redesigned and is available here.


While I generally use CassiniDev for smoke testing, sometimes a quick and simple way to run some tests against a site using the built in server comes in handy.

Spinning up an instance of WebDev.WebServer.exe is fairly simple.

The executable can be found at C:\Program Files\Common Files\Microsoft Shared\DevServer\9.0\WebDev.WebServer.exe (Program Files (x86) for x64 Windows).

If you invoke the .exe with no arguments, you will see the usage document listed below.

Listing 1: WebDev.WebServer Usage

---------------------------
ASP.NET Development Server
---------------------------
ASP.NET Development Server Usage:
 WebDev.WebServer /port:<port number> /path:<physical path> [/vpath:<virtual path>]

	port number:
		[Optional] An unused port number between 1 and 65535.
		The default is 80 (usable if you do not also have 
				IIS listening on the same port).

	physical path:
		A valid directory name where the Web application is rooted.

	virtual path:
		[Optional] The virtual path or application root 
				in the form of '/<app name>'.
		The default is simply '/'.

Example:
WebDev.WebServer /port:8080 /path:"c:\inetpub\wwwroot\MyApp" /vpath:"/MyApp"

You can then access the Web application using a URL of the form:
	http://localhost:8080/MyApp

With this information in hand, you can spin up an instance of webdev from within a test fixture.

The problem you will find is that if an instance of webdev is already running on the specified port, a program crash dialog will be presented. The existing instance is still available to serve requests to your tests but the crash dialog is a serious usability issue that should be dealt with.

A suitable strategy for ensuring an existing instance is not already running on the desired port is to query the Windows Management API (WMI) and return the command line that started each instance, comparing it to the command line arguments that you would like to use in spinning up a new instance. If an instance is already running, there is no need to start another.

So, with these concerns managed, the final requirements seem to be:

  • Determine location of WebDev.WebServer.exe.
  • Provide the port on which to serve the site.
  • Provide the virtual directory in which to root the site.
  • Provide a means of determining if an instance of WebDev.WebServer.exe is already running on the desired port.
  • Provide a means of starting, if necessary, an instance of WebDev.WebServer.exe at the physical path of the site to serve..

With these requirements in mind, I have constructed an abstract class, WebDevFixture.cs, that is suitable for use with any testing framework.

This class encapsulates all requirements listed.

The minimum usage requirement is to call StartServer with an absolute or relative path pointing to the directory of the site to be served.

The default Port and VDir can be changed by either overriding the readonly properties in the derived class or adding an appSettings entry in an optional app.config.

As a convenience, the NormalizeUri method will accept a relative URL fragment and return an absolute URI.

Listing 2 provides an example of an NUnit TestFixture derived from WebDevFixture using default values.

Listing 3 provides an example using overridden properties to supply the Port and VDir.

Listing 2: Usage with NUnit - Default Port and VDir

using System;
using System.Net;
using NUnit.Framework;

namespace Salient.Web.HttpLib.Tests
{
    [TestFixture]
    public class NUnitWebDevFixture : WebDevFixture
    {
        [TestFixtureSetUp]
        public void TestFixtureSetUp()
        {
            // default WebDevFixture port = 9999
            // default WebDevFixture vdir = '/'
            // thus the site under test will be found at http://localhost:9999/

            // the path to the website under test can be absolute or relative

            StartServer(@"..\..\..\Salient.Web.HttpLib.TestSite");
        }

        [Test, Description("Simple Get with WebClient")]
        public void Get()
        {
            WebClient client = new WebClient();
            Uri uri = NormalizeUri("default.aspx"); // http://localhost:9999/default.aspx
            
            string actual = client.DownloadString(uri);
            Assert.IsTrue(actual.IndexOf("This is Default.aspx") > -1);
        }
    }
}

Listing 3: Usage with NUnit - Specific Port and VDir

using System;
using System.Net;
using NUnit.Framework;

namespace Salient.Web.HttpLib.Tests
{
    [TestFixture]
    public class NUnitWebDevFixture2 : WebDevFixture
    {
        protected override int Port
        {
            get { return 12345; }
        }
        protected override string VDir
        {
            get { return "/myapp"; }
        }

        [TestFixtureSetUp]
        public void TestFixtureSetUp()
        {
            // the site under test will be found at http://localhost:12345/myapp
            StartServer(@"..\..\..\Salient.Web.HttpLib.TestSite");
        }

        [Test, Description("Simple Get with WebClient")]
        public void Get()
        {
            WebClient client = new WebClient();

            // http://localhost:12345/myapp/default.aspx
            Uri uri = NormalizeUri("default.aspx"); 		

            string actual = client.DownloadString(uri);
            Assert.IsTrue(actual.IndexOf("This is Default.aspx") > -1);
        }
    }
}

Listing 4: WebDevFixture.cs

// /*!
//  * Project: Salient.Web.HttpLib
//  * http://salient.codeplex.com
//  * Date: April 11 2010 
//  */

#region

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Management;

#endregion

namespace Salient.Web.HttpLib
{
    /// <summary>
    /// A general purpose Visual Studio 2008 Development Server 
    /// (WebDev.WebServer.exe) test fixture
    /// </summary>
    public abstract class WebDevFixture
    {
        private const string WebDevPathx64 =
            @"C:\Program Files (x86)\Common Files\
		Microsoft Shared\DevServer\9.0\WebDev.WebServer.exe";

        private const string WebDevPathx86 =
            @"C:\Program Files\Common Files\
		Microsoft Shared\DevServer\9.0\WebDev.WebServer.exe";

        private readonly int _port = 9999;
        private readonly string _vdir = "/";

        protected WebDevFixture()
        {
            // set testing port - default 9999 - 
            // can be changed by adding an appSettings 'port'
            // note: appSetting is optional, will just default to 9999
            int tmpPort;
            _port = int.TryParse(ConfigurationManager.AppSettings["port"], 
			out tmpPort) ? tmpPort : _port;

            // set vdir - default '/' - can be changed by adding an appSettings 'vdir'
            // note: appSetting is optional, will just default to '/'

            _vdir = ConfigurationManager.AppSettings["vdir"] ?? "/";

            // do some munging to ensure that the vdir is going to work out for us
            _vdir = string.Format(CultureInfo.InvariantCulture, 
			"/{0}/", _vdir.Trim('/')).Replace("//", "/");
        }

        // these can optionally be overriden in app.config 
        // using similarly named appSettings. e.g. 'port' and 'vdir'
        // or overridden by derived class
        protected virtual int Port
        {
            get { return _port; }
        }

        protected virtual string VDir
        {
            get { return _vdir; }
        }

        /// <summary>
        /// Returns an absolute Uri rooted on the running server.
        /// e.g. NormalizeUrl("default.aspx") will return 
        /// 'http://localhost:9999/default.aspx'
        /// </summary>
        /// <param name="pathAndQuery">path and query relative to app vdir</param>
        /// <returns></returns>
        protected Uri NormalizeUri(string pathAndQuery)
        {
            // do some munging to ensure that the vdir is going to work out for us
            string vdir = string.Format(CultureInfo.InvariantCulture, 
		"/{0}/", VDir.Trim('/')).Replace("//", "/");
            return
                new Uri(string.Format(CultureInfo.InvariantCulture, 
		"http://localhost:{0}{1}{2}", Port, vdir, pathAndQuery));
        }

        /// <summary>
        /// Ensure that an instance of WebDev.WebServer.exe is 
        /// running and serving the target site. 
        /// </summary>
        /// <param name="sitePath">path of the website to serve, 
        /// can be absolute or relative</param>
        protected void StartServer(string sitePath)
        {
            sitePath = Path.GetFullPath(sitePath);

            // try x86 first
            string webDevPath = WebDevPathx86;
            if (!File.Exists(webDevPath))
            {
                // then x64
                webDevPath = WebDevPathx64;
                if (!File.Exists(webDevPath))
                {
                    throw new FileNotFoundException("Cannot find WebDev.WebServer.exe");
                }
            }

            string devServerCmdLine = String.Format(CultureInfo.InvariantCulture,
                          "/port:{0} /path:\"{1}\" /vpath:\"{2}\"", Port, sitePath,
                          VDir);

            // check to see if we already have a server running
            bool running = false;
            foreach (string cmdLine in GetCommandLines("WebDev.WebServer.exe"))
            {
                if (cmdLine.EndsWith
		(devServerCmdLine, StringComparison.OrdinalIgnoreCase))
                {
                    running = true;
                    break;
                }
            }

            if (!running)
            {
                Process.Start(webDevPath, devServerCmdLine);
            }
        }

        /// <summary>
        /// Using WMI to fetch the command line that started all instances of a process
        /// </summary>
        /// <param name="processName">Image name, e.g. WebDev.WebServer.exe</param>
        /// <returns></returns>
        /// adapted from http://stackoverflow.com/questions/504208/
        /// how-to-read-command-line-arguments-of-another-process-in-c/504378%23504378
        /// original code by http://stackoverflow.com/users/61396/xcud
        private static IEnumerable<string> GetCommandLines(string processName)
        {
            List<string> results = new List<string>();

            string wmiQuery = string.Format(CultureInfo.InvariantCulture,
                 "select CommandLine from Win32_Process where Name='{0}'", processName);

            using (ManagementObjectSearcher searcher = 
			new ManagementObjectSearcher(wmiQuery))
            {
                using (ManagementObjectCollection retObjectCollection = searcher.Get())
                {
                    foreach (ManagementObject retObject in retObjectCollection)
                    {
                        results.Add((string) retObject["CommandLine"]);
                    }
                }
            }
            return results;
        }
    }
}

A complete solution utilizing this fixture can be found here.

As always, you can get the latest source and tests @ http://salient.codeplex.com.


Technorati tags: , , ,

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here