This past week I started working on a new user interface framework for an application, and I wanted to do it “right” and do it with TDD. I’d tried NUnitAsp for this before and ran into problems, but I thought I’d give it another shot.
The first thing I did was to implement a base test class for creating self-contained tests using Cassini based on the ideas proposed by Scott Hanselman and expanded by Anders Noras since I needed these tests to be able to be run from a build server, and I didn’t want to worry about dependencies on iis and virtual directories, etc.
Everything started off good, but as the testing got hot and heavy, I found that I really needed to be able to manipulate items in the Session or HttpContext to setup some of my test scenarios properly. Now, I’d seen this done with a few other techniques, but none that actually used NUnitAsp and I couldn’t figure out how to make use of them.
Here’s what I ended up doing. Based on Scott and Anders examples, I realized that it would be possible to inject an aspx file into the virtual web that Cassini was creating….which meant any calls I made to it would run in the web’s application domain. So, I created a Setup.aspx file that would allow me to make requests to it and pass in a static method that I wanted to call, and as a result, any code that ran inside of it would run inside the web’s application domain and have access to the Session and HttpContext. Awesome!

Here’s the SelfContainedWebFormTestCase class abstracted from Anders’ example (code for injecting the file in bold):
1: using System;
2: using System.Diagnostics;
3: using System.IO;
4: using Cassini;
5: using NUnit.Framework;
6:
7: namespace UnitTests
8: { 9:
10: [TestFixture]
11: public class SelfContainedWebFormTestCase : NUnit.Extensions.Asp.WebFormTestCase
12: { 13: private Server webServer;
14: private int webServerPort = 8086;
15: private string webServerVDir = "/";
16: private string tempPath = AppDomain.CurrentDomain.BaseDirectory;
17: private string tempBinPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory,"bin");
18: private string webServerUrl;
19: private string aspxPath = "";
20:
21:
22: public SelfContainedWebFormTestCase(
23: string aspxPath
24: )
25: { 26: this.aspxPath = String.Format(@"..\..\..\{0}", aspxPath); 27: }
28:
29: public virtual string BaseUrl
30: { 31: get{ return webServerUrl; } 32: }
33:
34:
35: [TestFixtureSetUp]
36: public void TestFixtureSetUp()
37: { 38: Directory.CreateDirectory(tempBinPath);
39: foreach(string file in Directory.GetFiles(tempPath,"*.dll"))
40: { 41: string newFile =
42: Path.Combine(tempBinPath,Path.GetFileName(file));
43: if (File.Exists(newFile)){File.Delete(newFile);} 44: File.Copy(file,newFile);
45: }
46: foreach(string file in Directory.GetFiles(aspxPath,"*.aspx"))
47: { 48: string newFile =
49: Path.Combine(tempPath,Path.GetFileName(file));
50: if (File.Exists(newFile)){File.Delete(newFile);} 51: File.Copy(file,newFile);
52: }
53:
54:
55: //Start the internal Web Server
56: webServer = new Server(webServerPort,webServerVDir,tempPath);
57: webServerUrl = String.Format("http://localhost:{0}{1}", webServerPort, webServerVDir); 58: webServer.Start();
59:
60: // Let everyone know
61: Debug.WriteLine(
62: String.Format("Web Server started on port {0} with VDir {1} in physical directory {2}",webServerPort,webServerVDir,tempPath)); 63: }
64:
65:
66: public void InjectFile(
67: string filename
68: )
69: { 70: FileInfo fileInfo = new FileInfo(@"..\..\" + filename);
71: if ( fileInfo.Exists )
72: { 73: string file = fileInfo.FullName;
74: string newFile = Path.Combine(tempPath,Path.GetFileName(file));
75: if ( File.Exists(newFile) ){ File.Delete(newFile); } 76: File.Copy(file, newFile);
77: }
78: }
79:
80:
81: [TestFixtureTearDown]
82: new public void BaseTearDown()
83: { 84: base.BaseTearDown();
85: try
86: { 87: if ( webServer != null )
88: { 89: webServer.Stop();
90: webServer = null;
91: }
92: Directory.Delete(tempBinPath, true);
93: }
94: catch {} 95:
96: }
97: }
98: }
This is the generic Setup page that I am injecting. Query String should take the form of op=[Classname].[Static Method name]:
1: public class Setup : System.Web.UI.Page
2: { 3: protected System.Web.UI.WebControls.Label Label1;
4:
5: private void Page_Load(object sender, System.EventArgs e)
6: { 7: string op = Request.QueryString.Get("op"); 8: if ( op != null && op.Length > 0 ){ Call(op); } 9: }
10:
11:
12: public void Call(
13: string op
14: )
15: { 16: string[] expression = op.Split('.'); 17: Type t = Type.GetType(String.Format("{0}.{1}", MethodBase.GetCurrentMethod().DeclaringType.Namespace, expression[0])); 18: t.InvokeMember(expression[1], BindingFlags.Default | BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static, null, null, new object[]{}); 19: }
20:
21:
22: #region Web Form Designer generated code
23: #endregion
24: }
And a sample test case (the static method will be called by the Setup.aspx via the Browser.GetPage call on line 12):
1: public static void SessionValuesCreatedByCallToSetupPageAreUsedByPageUnderTest_Setup()
2: { 3: // This code will be run from the call to Setup.aspx and inside the web's application domain.
4: HttpContext.Current.Session["Label1"] = "Value from setup.";
5: }
6:
7:
8: [Test]
9: public virtual void SessionValuesCreatedByCallToSetupPageAreUsedByPageUnderTest()
10: { 11: this.InjectFile("Setup.aspx"); 12: Browser.GetPage(this.BaseUrl + "Setup.aspx?op=Default.SessionValuesCreatedByCallToSetupPageAreUsedByPageUnderTest_Setup");
13: Browser.GetPage(this.BaseUrl + "Default.aspx");
14: ButtonTester button1 = new ButtonTester("Button1", CurrentWebForm); 15: LabelTester label1 = new LabelTester("Label1", CurrentWebForm); 16:
17: button1.Click();
18: NUnit.Framework.Assert.AreEqual("Value from setup.", label1.Text); 19: }
Where default page has a Label and a Button. Clicking the button populates the Label text from the Session:
1: public class _Default : System.Web.UI.Page
2: { 4: protected System.Web.UI.WebControls.Button Button1;
5: protected System.Web.UI.WebControls.Label Label1;
6:
7: private void Page_Load(object sender, System.EventArgs e)
8: { 14: }
15:
16: #region Web Form Designer generated code
17: #endregion
18:
19: private void Button1_Click(object sender, System.EventArgs e)
20: { 21: this.Label1.Text = (string) Session["Label1"];
22: }
23: }
In a future post I will talk about how I used this same idea along with an MVC pattern to insert mock controllers into my pages and isolate testing of just the UI interactions.