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()
{
StartServer(@"..\..\..\Salient.Web.HttpLib.TestSite");
}
[Test, Description("Simple Get with WebClient")]
public void Get()
{
WebClient client = new WebClient();
Uri uri = NormalizeUri("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()
{
StartServer(@"..\..\..\Salient.Web.HttpLib.TestSite");
}
[Test, Description("Simple Get with WebClient")]
public void Get()
{
WebClient client = new WebClient();
Uri uri = NormalizeUri("default.aspx");
string actual = client.DownloadString(uri);
Assert.IsTrue(actual.IndexOf("This is Default.aspx") > -1);
}
}
}
Listing 4: WebDevFixture.cs
#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
{
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()
{
int tmpPort;
_port = int.TryParse(ConfigurationManager.AppSettings["port"],
out tmpPort) ? tmpPort : _port;
_vdir = ConfigurationManager.AppSettings["vdir"] ?? "/";
_vdir = string.Format(CultureInfo.InvariantCulture,
"/{0}/", _vdir.Trim('/')).Replace("//", "/");
}
protected virtual int Port
{
get { return _port; }
}
protected virtual string VDir
{
get { return _vdir; }
}
protected Uri NormalizeUri(string pathAndQuery)
{
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));
}
protected void StartServer(string sitePath)
{
sitePath = Path.GetFullPath(sitePath);
string webDevPath = WebDevPathx86;
if (!File.Exists(webDevPath))
{
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);
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);
}
}
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: ASP.NET, Testing, Salient, CodeProject