|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionTesting ASP.NET web applications can be a painful process. Wouldn't it be nice to have a unit testing framework that covered both the API and the web UI testing in an integrated fashion? In this article, we'll walk through the integration of NUnit and Ruby unit tests - not only how the code was written, but also the thought process behind it. Picking a Test FrameworkNUnit is a great framework for .NET API-level unit testing, and is pretty widely accepted and supported. It even has Visual Studio integration using the TestDriven.NET VS add-in. A free, open-source product, and a clear winner for our API testing framework. (See the section towards the end, on other unit testing frameworks.) The problem is that it doesn't really help us when it comes to web UI. There are projects like NUnitAsp that aid in such testing, but when you're writing custom server controls, NUnitAsp leaves a lot to be desired and can cause a lot of work. Other UI testing tools (like Rational Robot) are expensive, and can require a lot of training to get it right. We don't have that sort of time or money. That's where Ruby and Watir come in. Ruby is an interpreted, object-oriented scripting language that's pretty quick to learn and use. It has its own notion of unit testing. There's a library for Ruby called Watir (Web Application Testing In Ruby) that enhances Ruby unit testing and makes web UI testing super simple. Both Ruby and Watir are free, open-source products like NUnit, and with the low learning curve, they can't be beat. The only issue with Watir is that it will only test your web app in Internet Explorer, but we can live with that. Let's go with a Ruby/Watir solution for the UI testing. Setting Up the EnvironmentThe rest of this article assumes you have:
I won't go into the installation instructions here; there are plenty of docs on the respective sites to explain that. The versions of the above packages aren't super important. This should work for any of them. I'll be using NUnit 2.1, Ruby 1.8.2, and Watir 1.3.1 for the purposes of this article, but again, later versions should be fine. Where We're GoingWe have two disparate unit test frameworks - NUnit and Ruby - and we want to see integrated testing and results from them. Assuming you're going to make your developers not only write tests but run them to ensure they're not breaking your build when they check changes in (you are testing before you check your code in, right?), you don't want to be wasting time running a bunch of tests in different frameworks and manually trying to aggregate the results - that's error prone and time consuming. The tests should be easy to write, and should be runnable from a single place so when all the lights are green, we know all the tests passed. We need to integrate NUnit and Ruby unit tests into a nice, compatible framework. Since it would be nice to have the Visual Studio integration capability that TestDriven.NET provides as well as have the ability to use the other NUnit-based test runners (I like seeing my tests in the NUnit GUI and it doesn't hurt that NAnt supports NUnit testing), we'll integrate Ruby/Watir into NUnit instead of the other way around. Running a Ruby/Watir TestRuby unit tests are pretty simple. Below is a sample test script with a single test: require 'test/unit'
require 'test/unit/assertions'
class RubyTest < Test::Unit::TestCase
def test_Valid
assert(true, 'This unit test should always pass.')
end
end
Key things we notice are:
Other interesting things about Ruby and its unit test framework are (not necessarily illustrated by the example, but that will be factored into our integration):
require 'watir'
require 'test/unit'
require 'test/unit/assertions'
include Watir
class WatirTest < Test::Unit::TestCase
def setup
@ie = IE.new
@ie.goto("http://www.google.com")
end
def test_Valid
assert(@ie.contains_text('Google'),
'This unit test should always pass.')
end
def teardown
@ie.close
end
end
What are the differences between the Ruby test and the Watir test?
Again, I won't go into the more detailed aspects of Ruby and Watir testing - there are plenty of sample documents out there on that. Suffice to say that the Ruby/Watir test framework gives us a pretty decent entity to integrate with, and includes a few things as noted above that we should remember during that integration effort. RequirementsGiven what we know about NUnit and Ruby testing, let's lay out our requirements:
Given those requirements, how do we want to integrate? ArchitectureHere's how we're going to do this: Following the NUnit pattern, we'll add some custom attributes that indicate an NUnit test is a Ruby or Watir test. We'll embed all of the Ruby/Watir scripts and their support files as resources in the NUnit test assembly to ease distribution. Finally, we'll create a test executor that will use these attributes to extract the scripts, fire up the Ruby interpreter, execute our tests, parse the results, and convert them into something NUnit understands. That means we have to get the attribute information from our NUnit test assembly at runtime from our test executor. Rather than having a base class that we have to derive our test fixtures from (that's pretty intrusive, isn't it?), we'll use Reflection and the ability to walk the stack to determine the test we're running so we know where to read the custom attributes from. That allows us to use a sort of visitor pattern to enable the functionality, which is particularly helpful for folks who want to perform the integration without having to modify their chain of inheritance. Instead, we latch onto the side. As long as we write our Ruby tests with the assumption that any support files will be in the same folder as the executing test script (or in a subfolder thereof), this will be a snap. Custom AttributesThe custom attributes are pretty simple. We'll need three:
Writing custom attributes is pretty straightforward. The
Minus comments, the using System;
namespace Paraesthesia.Test.Ruby{
[AttributeUsage(AttributeTargets.Method)]
public class RubyTestAttribute : System.Attribute {
private string _assemblyName = "";
private string _embeddedScriptPath = "";
private string _testMethod = "";
public string AssemblyName {
get {
return _assemblyName;
}
set {
if(value == null){
_assemblyName = "";
}
else{
_assemblyName = value;
}
}
}
public string EmbeddedScriptPath {
get {
return _embeddedScriptPath;
}
}
public string TestMethod {
get {
return _testMethod;
}
}
public RubyTestAttribute(string embeddedScriptPath,
string testMethod) {
// Validate parameters here -
// code omitted from example for easier reading
this._embeddedScriptPath = embeddedScriptPath;
this._testMethod = testMethod;
}
public RubyTestAttribute(string embeddedScriptPath,
string testMethod, string assemblyName) :
this(embeddedScriptPath, testMethod){
// Validate parameters here -
// code omitted from example for easier reading
this._assemblyName = assemblyName;
}
}
}
Not too bad, right? We've got appropriate constructors for the attribute allowing for the optional specification of the assembly name, and we've got properties for each incoming parameter. Cool. The using System;
namespace Paraesthesia.Test.Ruby{
[AttributeUsage(AttributeTargets.Method)]
public class WatirTestAttribute : RubyTestAttribute {
public WatirTestAttribute(string embeddedScriptPath,
string testMethod) :
base(embeddedScriptPath, testMethod){}
public WatirTestAttribute(string embeddedScriptPath,
string testMethod, string assemblyName) :
base(embeddedScriptPath, testMethod, assemblyName){}
}
}
Now, we can put these attributes on an NUnit test method to indicate we want a Ruby or Watir test to run. We'll do that in a while; it won't do us any good right now because we don't have anything that knows what to do with the attributes. That's the test executor. The last attribute we need is the using System;
namespace Paraesthesia.Test.Ruby {
[AttributeUsage(AttributeTargets.Method, AllowMultiple=true)]
public class RubySupportFileAttribute : Attribute {
private string _assemblyName = "";
private string _embeddedFilePath = "";
private string _targetFilename = "";
public string AssemblyName {
get {
return _assemblyName;
}
set {
if(value == null){
_assemblyName = "";
}
else{
_assemblyName = value;
}
}
}
public string EmbeddedFilePath {
get {
return _embeddedFilePath;
}
}
public string TargetFilename {
get {
return _targetFilename;
}
}
public RubySupportFileAttribute(string embeddedFilePath,
string targetFilename) {
// Validate parameters here -
// code omitted from example for easier reading
this._embeddedFilePath = embeddedFilePath;
this._targetFilename = targetFilename;
}
public RubySupportFileAttribute(string embeddedFilePath,
string targetFilename, string assemblyName) :
this(embeddedFilePath, targetFilename){
// Validate parameters here -
// code omitted from example for easier reading
this._assemblyName = assemblyName;
}
}
}
We still allow for the (Why don't we let files be above the executing script in the hierarchy? We have to "root" our temporary extracted file system somewhere, and since each test will end up getting its own copy of the extracted files, it becomes too much pain to try to dynamically calculate where each file needs to go in the temporary location in order to recreate the arbitrary file system hierarchy. Maybe, I took the easy way out, but I wasn't willing to spend time writing that code. If you want to, go for it. You'll see where to do that in the executor.) Great, now that we have our test metadata attributes, we have a way to mark an NUnit test as a Ruby test so our executor knows it has to do something. Now what? Data Structure for Test ResultsWe know we're going to need to pass Ruby test results around internally in our test executor. It also might be nice to return that data to the NUnit test that calls our executor so someone wanting to build on top of our executor doesn't have to fight us to get the test results. Ruby test results will include:
Our data class will include all of this information, plus we'll add a convenience property called using System;
namespace Paraesthesia.Test.Ruby {
public class RubyTestResult {
private int _assertions = 0;
private int _errors = 0;
private int _failures = 0;
private string _message = "";
private int _tests = 0;
public int Assertions {
get {
return _assertions;
}
}
public int Errors {
get {
return _errors;
}
}
public int Failures {
get {
return _failures;
}
}
public string Message {
get {
return _message;
}
}
public bool Success {
get {
return this.Failures == 0 && this.Errors == 0;
}
}
public int Tests {
get {
return _tests;
}
}
public RubyTestResult(int tests, int assertions,
int failures, int errors, string message) {
if(tests <= 0){
throw new ArgumentOutOfRangeException("tests",
tests,
"Number of tests contained in a" +
" RubyTestResult must be at least 1.");
}
this._tests = tests;
if(assertions < 0){
throw new ArgumentOutOfRangeException("assertions",
assertions,
"Number of assertions contained" +
" in a RubyTestResult must be at least 0.");
}
this._assertions = assertions;
if(failures < 0){
throw new ArgumentOutOfRangeException("failures",
failures,
"Number of failures contained in a" +
" RubyTestResult must be at least 0.");
}
this._failures = failures;
if(errors < 0){
throw new ArgumentOutOfRangeException("errors",
errors,
"Number of errors contained in a " +
"RubyTestResult must be at least 0.");
}
this._errors = errors;
if(message != null){
this._message = message;
}
}
}
}
Starting the RubyTestExecutorSince we're going to make our using System;
using System.CodeDom.Compiler;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
using NUnit.Framework;
The first thing we'll throw in there is a method that we'll use to extract the embedded scripts/files from an assembly into a temporary location during the execution of the test. Call that method private static void ExtractResourceToFile(Assembly assembly,
string resourcePath, string destinationPath){
// Validate parameters here -
// code omitted from example for easier reading
System.IO.Stream resStream = null;
FileStream fstm = null;
try{
// Get the stream from the assembly resource.
resStream = assembly.GetManifestResourceStream(resourcePath);
// Get a filestream to write the data to.
fstm = new FileStream(destinationPath, FileMode.CreateNew,
FileAccess.Write, FileShare.ReadWrite);
// Initialize properties for reading stream data
long numBytesToRead = resStream.Length;
int numBytesRead = 0;
int bufferSize = 1024;
byte[] bytes = new byte
With this, we can specify an assembly, an embedded resource path, and a location that the resource should be dumped to and the resource will get extracted. It's okay that we're writing errors to the console window because NUnit test runners appropriately redirect that output as needed. For example, the NUnit GUI has a special window just for The last bit of convenience we're going to want is a bit of configuration for the test framework. We can make use of the application configuration file stuff built right into .NET, so if someone specifies some particular // App settings key for whether to delete
// temp files when the test is finished.
const string SETTINGS_DELETEFILESWHENFINISHED =
"DeleteTempFilesWhenFinished";
// App settings key for display/hide
// the browser window in a WATIR test.
const string SETTINGS_SHOWBROWSERWINDOW = "ShowBrowserWindow";
// App settings key for failure output stream.
const string SETTINGS_FAILURESTREAM = "FailureStream";
// App settings key for success output stream.
const string SETTINGS_SUCCESSSTREAM = "SuccessStream";
// App settings key for success output stream.
const string SETTINGS_FAILMESSAGEFROMTESTOUTPUT =
"FailMessageFromTestOutput";
public static bool FailMessageFromTestOutput{
get {
return ParseAppSettingsBoolean(
SETTINGS_FAILMESSAGEFROMTESTOUTPUT, true);
}
}
public static bool ShowBrowserWindow{
get {
return ParseAppSettingsBoolean(
SETTINGS_SHOWBROWSERWINDOW, false);
}
}
public static bool DeleteTempFilesWhenFinished{
get {
return ParseAppSettingsBoolean(
SETTINGS_DELETEFILESWHENFINISHED, true);
}
}
public static TextWriter FailureStream{
get {
string streamName =
System.Configuration.ConfigurationSettings.
AppSettings[SETTINGS_FAILURESTREAM];
TextWriter writer = null;
switch(streamName){
case "Error":
writer = Console.Error;
break;
case "Out":
writer = Console.Out;
break;
case "None":
default:
writer = null;
break;
}
return writer;
}
}
public static TextWriter SuccessStream{
get {
string streamName =
System.Configuration.ConfigurationSettings.
AppSettings[SETTINGS_SUCCESSSTREAM];
TextWriter writer = null;
switch(streamName){
case "Error":
writer = Console.Error;
break;
case "None":
writer = null;
break;
case "Out":
default:
writer = Console.Out;
break;
}
return writer;
}
}
private static bool ParseAppSettingsBoolean(string appSettingsKey,
bool defaultValue){
bool retVal = defaultValue;
string retValStr =
System.Configuration.ConfigurationSettings.
AppSettings[appSettingsKey];
if(retValStr != null && retValStr != ""){
try{
retVal = bool.Parse(retValStr);
}
catch{
retVal = defaultValue;
}
}
return retVal;
}
How did we know we were going to need all of those configuration values? Actually, when I first wrote this, I didn't. Once I got using the executor, I found that having the above values configurable was a lot of help in debugging tests and ensuring test output was rendered correctly for a given test runner. The default values should work for most cases (you don't actually have to provide any configuration - there's a default for everything), but if you need to change something behaviorally about the executor, you can. We'll also throw in a convenience method that will take a method description and get the private static RubyTestAttribute GetRubyTestAttribute(MethodBase method){
RubyTestAttribute[] rubyTests = (RubyTestAttribute[])
method.GetCustomAttributes(typeof(RubyTestAttribute), true);
if(rubyTests == null || rubyTests.Length == 0){
return null;
}
if(rubyTests.Length > 1){
throw new NotSupportedException("Only one" +
" RubyTestAttribute per method is allowed.");
}
RubyTestAttribute rubyTest = rubyTests[0];
return rubyTest;
}
Now we can get to some of the more exciting stuff. Let's write the method that actually fires up a Ruby process, executes the test, and parses the results into one of our
Make it private static RubyTestResult RunTest(string scriptFilename,
RubyTestAttribute attrib, string workingDirectory){
// Build the basic command line
string command = "ruby.exe";
string arguments = String.Format("\"{0}\" --name={1}",
scriptFilename, attrib.TestMethod);
// WATIR-specific options
if(attrib is WatirTestAttribute){
// Determine if we show/hide the browser window
bool showBrowserWindow = RubyTestExecutor.ShowBrowserWindow;
if(!showBrowserWindow){
arguments += " -b";
}
}
// Test execution
RubyTestResult results = null;
using (Process proc = new Process()) {
// Create the process
proc.StartInfo.UseShellExecute = false;
proc.StartInfo.RedirectStandardOutput = true;
proc.StartInfo.CreateNoWindow = true;
proc.StartInfo.FileName = command;
proc.StartInfo.Arguments = arguments;
proc.StartInfo.WorkingDirectory = workingDirectory;
// Execute the process
proc.Start();
proc.WaitForExit();
// Get the output
string output = proc.StandardOutput.ReadToEnd();
// Clean up the process
proc.Close();
// Parse the results
RegexOptions options = RegexOptions.IgnoreCase | RegexOptions.Multiline;
Regex regex = new Regex(
@"(\d+)\s*tests[^\d]*(\d+)\s*assertions[^\" +
@"d]*(\d+)\s*failures[^\d]*(\d+)\s*errors",
options);
Match match = regex.Match(output);
if(match != null && match.Success){
int tests = Convert.ToInt32(match.Groups[1].Value,
System.Globalization.CultureInfo.CurrentCulture);
int assertions = Convert.ToInt32(match.Groups[2].Value,
System.Globalization.CultureInfo.CurrentCulture);
int failures = Convert.ToInt32(match.Groups[3].Value,
System.Globalization.CultureInfo.CurrentCulture);
int errors = Convert.ToInt32(match.Groups[4].Value,
System.Globalization.CultureInfo.CurrentCulture);
results = new RubyTestResult(tests, assertions,
failures, errors, output);
}
else{
results = new RubyTestResult(1, 0, 0, 1,
"*** Unable to parse output from Ruby test ***\n" + output);
}
}
return results;
}
We pass in the full path to the extracted script, the test's Now for the meat of the thing - the method that drives the process of getting the attribute information, extracting the files, running the test, and handling the results. We'll call this one public static RubyTestResult ExecuteTest(){
return ExecuteTest(true, true);
}
public static RubyTestResult ExecuteTest(bool makeAssertions,
bool displayOutput){
// Our code will go here
}
Cool. Let's write this thing. The ExecuteTest MethodThe first thing we need to do when executing the test is to find out which method is calling our test executor. Luckily, we can do that using the // Get the RubyTest attribute on the calling method
StackTrace trace = new StackTrace();
MethodBase method = null;
for(int i = 0; i < trace.FrameCount; i++){
MethodBase frameMethod = trace.GetFrame(i).GetMethod();
if(frameMethod.ReflectedType != typeof(RubyTestExecutor)){
method = frameMethod;
break;
}
}
RubyTestAttribute rubyTest = GetRubyTestAttribute(method);
if(rubyTest == null){
Assert.Fail("GetResultsFromRuby called from a method" +
" that does not have the [RubyTest]" +
" attribute in place.");
return null;
}
Why can't we just go one step up on the call stack and stop? We have that no-parameter overload, and at the time we grab the stack, if the caller used the no-parameter overload, one step up puts us at that overload, not at the method that called the This works pretty well, but it kind of stops you from having a really complex unit test scenario where you might try to define a central method that calls the Now, we need to extract the Ruby script and any associated support files into a temporary location. The // Get the configuration setting indicating
// if temp files should be deleted
bool deleteFilesWhenFinished =
RubyTestExecutor.DeleteTempFilesWhenFinished;
// Extract files and run the test
TempFileCollection tempFiles = null;
try{
// Prepare for temporary file extraction
tempFiles = new TempFileCollection();
tempFiles.KeepFiles = !deleteFilesWhenFinished;
if(!Directory.Exists(tempFiles.BasePath)){
Directory.CreateDirectory(tempFiles.BasePath);
}
// Add the rest of the method code here
}
finally{
// Delete any temporary files
if(tempFiles != null){
tempFiles.Delete();
if(Directory.Exists(tempFiles.BasePath) &&
deleteFilesWhenFinished){
Directory.Delete(tempFiles.BasePath, true);
}
tempFiles = null;
}
}
Looks good. We'll add the rest of the code to the spot noted above. First, let's extract the Ruby test script. We'll extract it to a fixed filename so there's no confusion about what the script should be called. Since it's the script that will end up getting run, it shouldn't matter what the script name is anyway as long as any support files are named correctly. // Extract the Ruby test
string rubyScriptPath = Path.GetFullPath(
Path.Combine(tempFiles.BasePath, "__RubyTestScript.rb"));
Assembly scriptAssembly = null;
if(rubyTest.AssemblyName != ""){
try{
scriptAssembly = Assembly.Load(rubyTest.AssemblyName);
}
catch{
Console.Error.WriteLine("Error loading assembly [{0}].",
rubyTest.AssemblyName);
throw;
}
}
else{
scriptAssembly = method.ReflectedType.Assembly;
}
ExtractResourceToFile(scriptAssembly,
rubyTest.EmbeddedScriptPath, rubyScriptPath);
if(!File.Exists(rubyScriptPath)){
throw new FileNotFoundException("Error extracting" +
" Ruby test script to file.", rubyScriptPath);
}
tempFiles.AddFile(rubyScriptPath, !deleteFilesWhenFinished);
The Ruby script has been extracted from the appropriate assembly, placed in the temporary directory, and added to our // Extract support files
RubySupportFileAttribute[] supportFiles =
(RubySupportFileAttribute[])
method.GetCustomAttributes(
typeof(RubySupportFileAttribute), true);
if(supportFiles != null){
foreach(RubySupportFileAttribute supportFile in supportFiles){
// Calculate the location for the support file
string supportFilePath = Path.GetFullPath(
Path.Combine(tempFiles.BasePath, supportFile.TargetFilename));
string supportFileDirectory = Path.GetDirectoryName(supportFilePath);
// Ensure the location is valid
if(supportFileDirectory.IndexOf(tempFiles.BasePath) != 0){
throw new ArgumentOutOfRangeException("TargetFilename",
supportFile.TargetFilename,
"Target location must be at or below" +
" the location of the extracted Ruby script.");
}
// Create any missing folders
if(!Directory.Exists(supportFileDirectory)){
Directory.CreateDirectory(supportFileDirectory);
}
// Get the assembly the support file is in
Assembly fileAssembly = null;
if(supportFile.AssemblyName != ""){
try{
fileAssembly = Assembly.Load(supportFile.AssemblyName);
}
catch{
Console.Error.WriteLine("Error loading" +
" assembly [{0}].", supportFile.AssemblyName);
throw;
}
}
else{
fileAssembly = method.ReflectedType.Assembly;
}
// Extract the support file
ExtractResourceToFile(fileAssembly,
supportFile.EmbeddedFilePath, supportFilePath);
tempFiles.AddFile(supportFilePath, !deleteFilesWhenFinished);
}
}
Note that we're not only extracting the support files, we're also creating any necessary directory structure as we go. Remember, we decided that any support file have to be either in the same folder or below the Ruby test script? That decision helps us here - it makes it much easier to get the temporary file hierarchy laid down. We've got the script extracted, we've got the support files extracted, let's run the test: // Run test
RubyTestResult result = RunTest(rubyScriptPath,
rubyTest, tempFiles.BasePath);
Doesn't get much easier than that. We've got our test output now, let's build and display some results. Add a constant to const string STR_RESULTS_DIVIDER = "\n----------\n";
Now, back in the // Create the test description string
string scriptDesc = String.Format("Script [{0}]; Test [{1}]",
rubyTest.EmbeddedScriptPath, rubyTest.TestMethod);
// Write output
if(displayOutput && result != null && result.Message != ""){
System.IO.TextWriter output = null;
string successMsg = "";
if(!result.Success){
output = RubyTestExecutor.FailureStream;
successMsg = "FAILED";
}
else{
output = RubyTestExecutor.SuccessStream;
successMsg = "SUCCESS";
}
if(output != null){
output.WriteLine("{0}: {1}", scriptDesc, successMsg);
output.WriteLine(result.Message);
output.WriteLine(STR_RESULTS_DIVIDER);
}
}
We build that "test description string" because it will help us when we want to not only display output but also make any assertions - this will let you know which script and which test failed. Next, assuming we've told the executor to display the output (a parameter to the The last thing we'll do is make any assertions in NUnit that we need to make (based on the parameter to // Make assertions
if(makeAssertions){
if(result == null){
Assert.Fail("Ruby test result not" +
" correctly returned from test execution.");
}
else{
string failMsg = String.Format("{0}: FAILED", scriptDesc);
if(RubyTestExecutor.FailMessageFromTestOutput){
failMsg += result.Message + STR_RESULTS_DIVIDER;
}
Assert.IsTrue(result.Success, failMsg);
}
}
// Return results
return result;
This is how we get the red or green light in NUnit to correspond to the success or failure of the Ruby test. In this case, if we didn't get any result from the test, we fail, otherwise we build a reasonable failure message and assert the success of the Ruby test. That's it! Now, it's time to put the Using the RubyTestExecutorSo you've got the The first thing you'll need to do is write your Ruby/Watir test scripts. Make sure they run in a standalone environment - this integration solution allows us to use the same scripts that will run by themselves, without any modification. You might want to create some tests that use support files as well. Your script hierarchy might look like this (note that the scripts are at the top of the hierarchy, and support files are in the same folder or below):
In my example, I've got a couple of scripts and some support files that will be used. My test scripts look pretty much like the ones we wrote earlier, but I've added a test that uses the support files: def test_RubySupportFile
assert(File.exist?("supportfile.txt"),
"supportfile.txt does not exist.")
assert(File.exist?("SubFolder1\\supportfile1.txt"),
"supportfile1.txt does not exist.")
assert(File.exist?("SubFolder1\\SubFolder2\\supportfile2.txt"),
"supportfile2.txt does not exist.")
end
That test just looks to ensure that the support files exist in the folder hierarchy - in a real-world test, you might use these support files to read data from for use in your test. For our purposes, we just want to ensure that the support files get extracted correctly in our integration framework. Once the Ruby tests run to your satisfaction, add the scripts and support files to your NUnit project as embedded resources. Also, add a reference to the assembly containing the
Finally, in your NUnit test fixture, add test methods that make use of the custom attributes we created, and have a call to the using System;
using NUnit.Framework;
using RTE = Paraesthesia.Test.Ruby.RubyTestExecutor;
namespace Paraesthesia.Test.Ruby.Test {
[TestFixture]
public class RubyTestExecutor {
[Test(Description="Verifies you may
run standard NUnit-only tests.")]
public void NUnitOnly_NoRubyAttrib(){
Assert.IsTrue(true,
"This NUnit-only test should always pass.");
}
[RubyTest("Paraesthesia.Test.Ruby.Test.
Scripts.RubyTest.rb", "test_Valid")]
[Test(Description="Verifies a valid Ruby test
will execute and allow success.")]
public void RubyTest_Valid(){
RTE.ExecuteTest();
}
[WatirTest("Paraesthesia.Test.Ruby.Test.
Scripts.WatirTest.rb", "test_Valid")]
[Test(Description="Verifies a valid WATIR test
will execute and allow success.")]
public void WatirTest_Valid(){
RTE.ExecuteTest();
}
[RubySupportFile("Paraesthesia.Test.Ruby.
Test.Scripts.supportfile.txt",
"supportfile.txt")]
[RubySupportFile("Paraesthesia.Test.Ruby.Test.
Scripts.SubFolder1.supportfile1.txt",
@"SubFolder1\supportfile1.txt")]
[RubySupportFile("Paraesthesia.Test.Ruby.Test.
Scripts.SubFolder1.SubFolder2.supportfile2.txt",
@"SubFolder1\SubFolder2\supportfile2.txt")]
[RubyTest("Paraesthesia.Test.Ruby.Test.Scripts.RubyTest.rb",
"test_RubySupportFile")]
[Test(Description="Verifies Ruby support
files can correctly be extracted.")]
public void RubySupportFile_Valid(){
RTE.ExecuteTest();
}
}
}
In this example, you can see that we have an NUnit-only test, two Ruby tests, and a Watir test, all side-by-side. The only content in the Ruby/Watir tests is a call to the
Voila! NUnit running native tests and Ruby/Watir tests side-by-side! Final TouchesI did two additional things to the What's in the PackageThe source code attached includes:
Next StepsWhat could be done to make this even better? You could...
Of course, the possibilities are endless, but what you have here should at least get you going. Other Unit Testing FrameworksAfter writing this article and hearing back from some folks, it turns out that this mechanism should work for any unit testing framework that functions similar to NUnit. For example, the Visual Studio 2005 unit testing framework can be used as well, simply by using the appropriate namespace/attributes on your unit tests, like this: using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using RTE = Paraesthesia.Test.Ruby.RubyTestExecutor;
namespace Paraesthesia.Test.Ruby.Test {
[TestClass]
public class RubyTestExecutor {
[TestMethod]
public void VS2005Only_NoRubyAttrib(){
Assert.IsTrue(true, "This VS2005-only test should always pass.");
}
[WatirTest("Paraesthesia.Test.Ruby.Test.Scripts.WatirTest.rb", "test_Valid")]
[TestMethod]
public void WatirTest_Valid(){
RTE.ExecuteTest();
}
}
}
ConclusionHopefully, this will help those of you faced with trying to automate web UI testing. I also hope you learned not only how to do the integration but why it was done this way. Happy testing! History
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||