I was creating a small banner rotation script yesterday. It needed to be tiny, lightweight and easy to deploy, so I chose to do it in JScript as it is available on every Windows machine. I usually do small automation projects in
Ruby, but the machine that has to run the script does not have Ruby installed. I am a
closure freak, so I went for JScript (VBScript does not have closures).
The script has to select a random subfolder from the source location and copy all its files to the destination. I don't have much experience with Windows Scripting Host's (WSH) FileSystemObject and its File and Folder minions, so I decided it would be fun to do some learning tests and test-drive the entire implementation. WSH allows you to create XML-based .wsf files that allow you to mix scripts in several languages and reference other script files. This is all I needed to create a minimal test framework and a test runner. My
run-tests.wsf file looked initially like this:
<job>
<script type="JScript" src="test-lib.js" />
<script type="JScript" src="rotator.js" />
<script type="JScript">
//...
</script>
</job>
Note that it references
test-lib.js and
rotator.js. These are the test framework and the actual implementation. My tests go inside the inline <script> block. So I am ready for my first test. I hardcoded some assumptions about the filesystem before the action and added assertions on how I thought it would look afterwards. Here it is:
test(function(){
var folder = new Folder("data1");
assertEquals("2 files", 2, folder.Files.Count);
if (fso.FileExists("index.html"))
fso.DeleteFile("index.html");
assert("no file before copy", !fso.FileExists("index.html"));
folder.copyTo(".");
assert("file copied successfully", fso.FileExists("index.html"));
});
Folder is my wrapper around the stock Folder and File objects. The
assert() and
assertEquals() functions are all we need to verify if our program behaves according to our expectations. A test is just a closure, containing assertions. All of them are defined in
test-lib.js Here is how
assertEquals() looks like:
function assertEquals(message, expected, actual)
{
if (expected != actual)
{
throw "Expected: <" + expected.toString() + ">, actual <" + actual.toString() + ">.\n" + message;
}
}
test() gets a single parameter that it executes in a try-catch block, which takes care of error and failure reports:
function test(body)
{
try
{
body();
}
catch(error)
{
WScript.Echo("error: " + error.toString() + "\n" + error.message);
}
}
I did play around with WSH's filesystem API until I got to know it better. I experimented with stuff, deleted it or moved into the main implementation. I went in tiny steps and that saved me a lot of time when debugging cryptic "This object does not support this property or method" error messages. I did not know how to generate random numbers in JScript, but I learned that quickly with Google's help. I am particularly proud of my randomization test:
test(function(){
var folders = new Folders(".");
assertEquals("exactly two subfolders", 2, folders.count());
var random = [];
for (var i = 0; i < 20; i++)
{
random.push(folders.randomSubFolder());
}
assert("has data1", hasSubFolder("data1", random));
assert("has data2", hasSubFolder("data2", random));
});
Yes, I know that in theory it could fail without the program being wrong. It could randomly select the "data1" subfolder 20 times in a row, but it has never happened. If that happens I will crank the number up to 200.
I am done with the program. My code is stored in
rotator.js. I configure some variables with the appropriate paths in
rotator.wsf, and call the implementation I have test-driven so far:
<job>
<script type="JScript" src="rotator.js" />
<script type="JScript">
//don't forget to escape backslashes in paths e.g.
//C:\\somefolder\\somesubfolder
var sourceFolder = ".";
var destinationFolder = ".";
copyRandom(sourceFolder, destinationFolder);
</script>
</job>
I learned a lot from that experiment. I am really comfortable with the WSH filesystem API now. I liked the way I could hit
Undo when I saw an error that I could not fix in seconds. I tried different approaches to the problem -- some of them worked well, others had to go.
Notice how easy it is to grow your own test framework? I did not need fancy features like setUp and tearDown. I got away without a GUI -- the console told me everything I needed to know.
Ron Jeffries' advice from
Extreme Programming Adventures in C# proved quite true: "You don't need a framework. You need tests."
My testing "framework" was inspired by
Phlip's
NanoCppUnit defined in his recursive descent parser
TDD example.