Sunday, August 28, 2005

Rake Experiences Continue

I've been working on the NanoContainer rake file recently. The development seemed to move in 3 significant directions.

  1. It was very procedural and used standard tasks exclusively.

  2. I incorporated many directory and file tasks to take advantage of only updating files when their dependant files were updated.

  3. The final (for now) version is back to being rather procedural; however, I use uptodate? often to get the benefits of the file task.

The final version is both shorter and more readable than version 2. This version is the easiest to follow because of it's procedural nature, and the use of uptodate? gives the most efficient builds.
require 'CSProjFile.rb'

def build(*relative_path)
File.join("src","build",relative_path)
end

task :default => [:compile, :test]
task :all => [:clean, :default]

task :clean do
rm_rf(build)
rm_rf("src/NanoContainer.Tests/bin")
rm_rf("src/TestComp/bin")
end

task :precompile do
mkdir_p(build) unless File.exists?(build)
def _lib(relative_path)
File.join("lib",relative_path)
end
def _precomp(files)
files.each {|f| cp(_lib(f), build) unless uptodate?(build(f), _lib(f))}
end
_precomp(%w(NUnit.Framework.dll PicoContainer.dll Castle.DynamicProxy.dll NMock.dll))
end

task :compile => :precompile do
def _compile(project)
projFile = CSProjFile.new(File.new("src/#{project}/#{project}.csproj"))
unless uptodate?("#{build(project)}.dll",projFile.files.collect {|f| "src/#{project}/#{f}" })
cd "src/#{project}"
sh projFile.create_csc("../build")
cd "../.."
end
end
%w(NanoContainer NanoContainer.Tests TestComp TestComp2 NotStartable).each {|project| _compile(project)}
end

task :pretest do
def tcVsOutput(*relative_path)
File.join("src","TestComp","bin","Debug",relative_path)
end
mkdir_p(tcVsOutput) unless File.exists?(tcVsOutput)
tcdll = "TestComp.dll"
cp(build(tcdll), tcVsOutput) unless uptodate?(tcVsOutput(tcdll), build(tcdll))
def nanoVsOutput(*relative_path)
File.join("src","NanoContainer.Tests","bin","Debug",relative_path)
end
mkdir_p(nanoVsOutput) unless File.exists?(nanoVsOutput)
def _pretest(files)
files.each {|f| cp(build(f),nanoVsOutput) unless uptodate?(nanoVsOutput(f), build(f))}
end
_pretest(%w(NMock.dll PicoContainer.dll Castle.DynamicProxy.dll NUnit.Framework.dll NanoContainer.dll NanoContainer.Tests.dll))
end

task :test => [:compile,:pretest] do
cd nanoVsOutput
sh "../../../../lib/nunit-console.exe NanoContainer.Tests.dll"
end

Thursday, August 25, 2005

Controlling Subversion with Ruby and irb

I previously blogged about Controlling Subversion with Ruby. In theory it seemed like a good idea; however, in practice I used Ruby's Interactive Ruby Shell (irb). Irb allows me to mass add or delete quickly without needing a Ruby file. If you have Ruby installed, typing irb at the command line should drop you right into irb. Once in irb, it's easy to work with subversion using Ruby:

Add all files with "?" status:
`svn st`.split(/\n/).each { |line| `svn add #{line.delete("?").lstrip}` if line[0,1] =~ /\?/ }
Delete all files with "!" status:
`svn st`.split(/\n/).each { |line| `svn rm #{line.delete("!").lstrip}` if line[0,1] =~ /\!/ }

Wednesday, August 24, 2005

Ruby C# Project File parser (CSProjFile)

In response to my blogs about using rake with .net and how to add an embedded resource, Jeremy Miller asks:

Isn't there an equivalent to the "solution" task in NAnt for Rake?
I don't see how it would be better if I had to manually specify things
like including resource files in the build script.

As far as I know, there is no built in support for .net in rake. However, a .csproj file is just an XML file and should therefore be easy to parse.

I've never done any XML parsing in Ruby so I contacted Jeremy Stell-Smith for a recommendation. His answer was REXML. REXML must have come standard in my version of Ruby because I didn't need to install anything. After about a minute looking at the tutorial I was set.

Of course, being test-driven, I started out with the tests. I didn't feel like creating a Foo.csproj, so I used the NanoContainer.csproj to create a XML string I would use for testing.
require 'test/unit'
require 'csprojfile'

class CSProjFileTest < Test::Unit::TestCase
def CSProjFileTest.NanoConfig
%q!
<VisualStudioProject>
<CSHARP
ProjectType = "Local"
ProductVersion = "7.10.3077"
SchemaVersion = "2.0"
ProjectGuid = "{10C07279-0C4B-49AC-8DA5-54062116C2ED}"
>
<Build>
<Settings
ApplicationIcon = ""
AssemblyKeyContainerName = ""
AssemblyName = "NanoContainer"
AssemblyOriginatorKeyFile = ""
DefaultClientScript = "JScript"
DefaultHTMLPageLayout = "Grid"
DefaultTargetSchema = "IE50"
DelaySign = "false"
OutputType = "Library"
PreBuildEvent = ""
PostBuildEvent = ""
RootNamespace = "NanoContainer"
RunPostBuildEvent = "OnBuildSuccess"
StartupObject = ""
>
<Config
Name = "Debug"
AllowUnsafeBlocks = "false"
BaseAddress = "285212672"
CheckForOverflowUnderflow = "false"
ConfigurationOverrideFile = ""
DefineConstants = "DEBUG;TRACE"
DocumentationFile = ""
DebugSymbols = "true"
FileAlignment = "4096"
IncrementalBuild = "false"
NoStdLib = "false"
NoWarn = ""
Optimize = "false"
OutputPath = "bin\Debug\"
RegisterForComInterop = "false"
RemoveIntegerChecks = "false"
TreatWarningsAsErrors = "false"
WarningLevel = "4"
/>
<Config
Name = "Release"
AllowUnsafeBlocks = "false"
BaseAddress = "285212672"
CheckForOverflowUnderflow = "false"
ConfigurationOverrideFile = ""
DefineConstants = "TRACE"
DocumentationFile = ""
DebugSymbols = "false"
FileAlignment = "4096"
IncrementalBuild = "false"
NoStdLib = "false"
NoWarn = ""
Optimize = "true"
OutputPath = "bin\Release\"
RegisterForComInterop = "false"
RemoveIntegerChecks = "false"
TreatWarningsAsErrors = "false"
WarningLevel = "4"
/>
</Settings>
<References>
<Reference
Name = "System"
AssemblyName = "System"
HintPath = "..\..\..\..\..\WINDOWS\Microsoft.NET\Framework\v1.1.4322\System.dll"
/>
<Reference
Name = "System.Data"
AssemblyName = "System.Data"
HintPath = "..\..\..\..\..\WINDOWS\Microsoft.NET\Framework\v1.1.4322\System.Data.dll"
/>
<Reference
Name = "System.XML"
AssemblyName = "System.Xml"
HintPath = "..\..\..\..\..\WINDOWS\Microsoft.NET\Framework\v1.1.4322\System.XML.dll"
/>
<Reference
Name = "PicoContainer"
AssemblyName = "PicoContainer"
HintPath = "..\..\lib\PicoContainer.dll"
/>
<Reference
Name = "Microsoft.JScript"
AssemblyName = "Microsoft.JScript"
HintPath = "..\..\..\..\..\WINDOWS\Microsoft.NET\Framework\v1.1.4322\Microsoft.JScript.dll"
/>
<Reference
Name = "VJSharpCodeProvider"
AssemblyName = "VJSharpCodeProvider"
HintPath = "..\..\..\..\..\WINDOWS\Microsoft.NET\Framework\v1.1.4322\VJSharpCodeProvider.DLL"
/>
</References>
</Build>
<Files>
<Include>
<File
RelPath = "AssemblyInfo.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "DefaultNanoContainer.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Attributes\AssemblyUtil.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Attributes\AttributeBasedContainerBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Attributes\ComponentAdapterType.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Attributes\DependencyInjectionType.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Attributes\RegisterWithContainerAttribute.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "IntegrationKit\ContainerBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "IntegrationKit\LifeCycleContainerBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "IntegrationKit\PicoCompositionException.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Script\AbstractFrameworkContainerBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Script\FrameworkCompiler.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Script\ScriptedContainerBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Script\CSharp\CSharpBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Script\JS\JSBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Script\JSharp\JSharpBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Script\VB\VBBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Script\Xml\ComposeMethodBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Script\Xml\Constants.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Script\Xml\ContainerStatementBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Script\Xml\XmlContainerBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "TestScripts\test.js"
BuildAction = "EmbeddedResource"
/>
<File
RelPath = "TestScripts\test.vb"
BuildAction = "EmbeddedResource"
/>
</Include>
</Files>
</CSHARP>
</VisualStudioProject>!
end

def testOutputType
projFile = CSProjFile.new(CSProjFileTest.NanoConfig)
assert_equal(projFile.output_type, "library")
end

def testAssemblyName
projFile = CSProjFile.new(CSProjFileTest.NanoConfig)
assert_equal(projFile.assembly_name, "NanoContainer")
end

def testFiles
projFile = CSProjFile.new(CSProjFileTest.NanoConfig)
expectedFiles = %w(AssemblyInfo.cs DefaultNanoContainer.cs Attributes\\AssemblyUtil.cs Attributes\\AttributeBasedContainerBuilder.cs Attributes\\ComponentAdapterType.cs Attributes\\DependencyInjectionType.cs Attributes\\RegisterWithContainerAttribute.cs IntegrationKit\\ContainerBuilder.cs IntegrationKit\\LifeCycleContainerBuilder.cs IntegrationKit\\PicoCompositionException.cs Script\\AbstractFrameworkContainerBuilder.cs Script\\FrameworkCompiler.cs Script\\ScriptedContainerBuilder.cs Script\\CSharp\\CSharpBuilder.cs Script\\JS\\JSBuilder.cs Script\\JSharp\\JSharpBuilder.cs Script\\VB\\VBBuilder.cs Script\\Xml\\ComposeMethodBuilder.cs Script\\Xml\\Constants.cs Script\\Xml\\ContainerStatementBuilder.cs Script\\Xml\\XmlContainerBuilder.cs)
assert_equal(projFile.files, expectedFiles)
end

def testReferences
projFile = CSProjFile.new(CSProjFileTest.NanoConfig)
expectedRefs = %w(System System.Data System.XML PicoContainer Microsoft.JScript VJSharpCodeProvider)
assert_equal(projFile.references, expectedRefs)
end

def testCscOutput
projFile = CSProjFile.new(CSProjFileTest.NanoConfig)
expectedCsc = "csc /out:../build/NanoContainer.dll /target:library /lib:../build /r:'System.dll;System.Data.dll;System.XML.dll;PicoContainer.dll;Microsoft.JScript.dll;VJSharpCodeProvider.dll' /res:'TestScripts\\test.js,NanoContainer.TestScripts.test.js' /res:'TestScripts\\test.vb,NanoContainer.TestScripts.test.vb' AssemblyInfo.cs DefaultNanoContainer.cs /recurse:AssemblyUtil.cs /recurse:AttributeBasedContainerBuilder.cs /recurse:ComponentAdapterType.cs /recurse:DependencyInjectionType.cs /recurse:RegisterWithContainerAttribute.cs /recurse:ContainerBuilder.cs /recurse:LifeCycleContainerBuilder.cs /recurse:PicoCompositionException.cs /recurse:AbstractFrameworkContainerBuilder.cs /recurse:FrameworkCompiler.cs /recurse:ScriptedContainerBuilder.cs /recurse:CSharpBuilder.cs /recurse:JSBuilder.cs /recurse:JSharpBuilder.cs /recurse:VBBuilder.cs /recurse:ComposeMethodBuilder.cs /recurse:Constants.cs /recurse:ContainerStatementBuilder.cs /recurse:XmlContainerBuilder.cs"
assert_equal(projFile.create_csc("../build"), expectedCsc)
end

def testEmbeddedResource
projFile = CSProjFile.new(CSProjFileTest.NanoConfig)
expectedResources = %w(TestScripts\test.js TestScripts\test.vb)
assert_equal(projFile.embedded_resources, expectedResources)
end

def testConvertToResource
projFile = CSProjFile.new(CSProjFileTest.NanoConfig)
assert_equal(projFile.convert_to_resource("a\\b.c"),"/res:'a\\b.c,NanoContainer.a.b.c'")
end
end
After getting those tests to pass I felt like my CSProjFile class was well enough tested.
require 'rexml/document'

class CSProjFile
def initialize(projFile)
@projXml = REXML::Document.new projFile
@extensions = {"library"=>"dll"}
end

def output_type
@projXml.elements["VisualStudioProject/CSHARP/Build/Settings"].attributes["OutputType"].downcase
end

def assembly_name
@projXml.elements["VisualStudioProject/CSHARP/Build/Settings"].attributes["AssemblyName"]
end

def files
result = []
path = "VisualStudioProject/CSHARP/Files/Include/File"
@projXml.elements.each(path) { |element| result << element.attributes["RelPath"] if compiledFile?(element) }
result
end

def compiledFile?(element)
element.attributes["BuildAction"]=="Compile"
end

def embedded_resources
result = []
path = "VisualStudioProject/CSHARP/Files/Include/File"
@projXml.elements.each(path) { |element| result << element.attributes["RelPath"] if embeddedResourceFile?(element) }
result
end

def embeddedResourceFile?(element)
element.attributes["BuildAction"]=="EmbeddedResource"
end

def references
result = []
@projXml.elements.each("VisualStudioProject/CSHARP/Build/References/Reference") { |element| result << element.attributes["Name"] }
result
end

def create_csc(outDir)
result = []
result << "csc"
result << "/out:#{outDir}/#{assembly_name}.#{@extensions[output_type]}"
result << "/target:#{output_type}"
result << "/lib:#{outDir}"
refs = references.collect {|ref| ref + ".dll"}
result << "/r:'#{refs.join(";")}'"
embeddedFiles = embedded_resources.collect { |item| convert_to_resource(item)}
result << embeddedFiles.join(" ")
recurseFiles = files.each { |item| item.sub!(/.+\\/,"/recurse:") }
result << recurseFiles.join(" ")
result.join(" ")
end

def convert_to_resource(item)
file = item.to_s
id = "#{assembly_name}.#{item.to_s.sub(/\\/,".")}"
"/res:'#{file},#{id}'"
end
end
So, the final (successful) test was changing my NanoContainer rakefile to use the CSProjFile class.
require 'CSProjFile.rb'

buildDir = "src/build"
nanoDll = "build/NanoContainer.dll"
nanoTestsDll = "build/NanoContainer.Tests.dll"
testCompDll = "build/TestComp.dll"
testComp2Dll = "build/TestComp2.dll"
notStartableDll = "build/NotStartable.dll"
nanoTestVsBinDir = "src/NanoContainer.Tests/bin"
nanoTestVsBinDebugDir = "src/NanoContainer.Tests/bin/Debug"
testCompBinDir = "src/TestComp/bin"
testCompBinDebugDir = "src/TestComp/bin/Debug"

task :default => [:compileInit, :compile, :test]
task :all => [:clear, :removeBuildDir, :removeVsDirs, :default]

task :clear do sh "clear" end

task :removeBuildDir => :clear do rm_rf(buildDir) end
task :removeVsDirs => :clear do
rm_rf(nanoTestVsBinDir)
rm_rf(testCompBinDir)
end

directory buildDir

task :compileInit => [buildDir] do
copyToDir(%w(lib/NUnit.Framework.dll lib/PicoContainer.dll lib/Castle.DynamicProxy.dll),buildDir)
copyToDir(%w(lib/NMock.dll lib/NUnit.Framework.dll),buildDir)
end

file nanoDll => :compileInit do compileProj("src/NanoContainer","NanoContainer.csproj") end
file nanoTestsDll => [:compileInit, nanoDll] do compileProj("src/NanoContainer.Tests","NanoContainer.Tests.csproj") end
file testCompDll => :compileInit do compileProj("src/TestComp","TestComp.csproj") end
file testComp2Dll => [:compileInit, testCompDll] do compileProj("src/TestComp2","TestComp2.csproj") end
file notStartableDll => [:compileInit, testCompDll] do compileProj("src/NotStartable","NotStartable.csproj") end
task :compile => [nanoDll,nanoTestsDll,testCompDll,testComp2Dll,notStartableDll]

directory testCompBinDir
directory testCompBinDebugDir
directory nanoTestVsBinDir
directory nanoTestVsBinDebugDir

task :testInit => [testCompBinDir,testCompBinDebugDir,nanoTestVsBinDir,nanoTestVsBinDebugDir] do
cp "src/Build/TestComp.dll", testCompBinDebugDir
copyToDir(%w(src/Build/NMock.dll src/Build/PicoContainer.dll src/Build/Castle.DynamicProxy.dll),nanoTestVsBinDebugDir)
copyToDir(%w(src/Build/NUnit.Framework.dll src/Build/NanoContainer.dll src/Build/NanoContainer.Tests.dll),nanoTestVsBinDebugDir)
end

task :test => [:compile,:testInit] do
cd nanoTestVsBinDebugDir
sh "../../../../lib/nunit-console.exe NanoContainer.Tests.dll"
end

def copyToDir(fileArray, outputDir)
fileArray.each { |file| cp file, outputDir }
end

def compileProj(workingDir, projFileName)
cd workingDir
projFile = CSProjFile.new(File.new(projFileName))
sh projFile.create_csc("../build")
cd "../.."
end


There's much room for improvement, but it work well for a first attempt. Feedback welcome.

Tuesday, August 23, 2005

Add Events to NMock

While using View Observer on my last project we needed a way to raise events for unit testing. The best solution turned out to be stubs; however, while we were evaluating options Levi Khatskevitch created a DynamicMockWithEvents class.

DynamicMockWithEvents inherits from NMock's DynamicMock, but it also contains support for raising events from a mock. To raise an event from a mock simply call the RaiseEvent method with the event name and any optional args.
public class DynamicMockWithEvents : DynamicMock
{
private const string ADD_PREFIX = "add_";
private const string REMOVE_PREFIX = "remove_";

private readonly EventHandlerList handlers;
private readonly Type mockedType;

public DynamicMockWithEvents(Type type) : base(type)
{
handlers = new EventHandlerList();
mockedType = type;
}

public override object Invoke(string methodName, params object[] args)
{
if (methodName.StartsWith(ADD_PREFIX))
{
handlers.AddHandler(GetKey(methodName, ADD_PREFIX), (Delegate) args[0]);
return null;
}
if (methodName.StartsWith(REMOVE_PREFIX))
{
handlers.RemoveHandler(GetKey(methodName, REMOVE_PREFIX), (Delegate) args[0]);
return null;
}
return base.Invoke(methodName, args);
}

private static string GetKey(string methodName, string prefix)
{
return string.Intern(methodName.Substring(prefix.Length));
}

public void RaiseEvent(string eventName, params object[] args)
{
Delegate handler = handlers[eventName];
if (handler == null)
{
if (mockedType.GetEvent(eventName) == null)
{
throw new MissingMemberException("Event " + eventName + " is not defined");
}
else if (Strict)
{
throw new ApplicationException("Event " + eventName + " is not handled");
}
}
handler.DynamicInvoke(args);
}
}

Monday, August 22, 2005

Adding an Embedded Resource to a csc command line compiled assembly

While using Rake to compile and execute NanoContainer.Tests, 4 of my tests kept failing. They were relying on 4 embedded resources that I was not embedding. In Visual Studio it's easy to embed a resource. DevHood and CodeProject both give good information on embedding using Visual Studio.net; however, I wanted to embed a resource using the command line.

MSDN2 provided some good info specific to the command line, but everything I found seemed to detail how to add a .resources or .resx file to an assembly. I needed to add .cs, .js, .java, and .vb files to the assembly. Using csc -? and resgen -? didn't seem to lead me in the right direction either.

Finally I gave up on finding the documentation I needed and starting shooting in the dark. I used /res, /win32res, and /linkresource. The tests kept failing. /res seemed like the answer, but the documentation focuses on .resources files so there was no way to be sure. Enter James Johnson's demo executable. I'd done everything I could think of and the tests were still failing. It was time to fire up an app that could show me what resources were contained in my assembly.

It turned out that my assembly was embedding the resource correctly when I used /res; however, an identifier needed to be given to match the identifier specified in the NanoContainer tests. Once I added the identifier all the tests passed.

Final correct csc (broken to multiple lines for readablity):
csc /out:../build/NanoContainer.Tests.dll /res:'TestScripts/test.cs,NanoContainer.Tests.TestScripts.test.cs' /res:'TestScripts/test.js,NanoContainer.Tests.TestScripts.test.js' /res:'TestScripts/test.java,NanoContainer.Tests.TestScripts.test.java' /res:'TestScripts/test.vb,NanoContainer.Tests.TestScripts.test.vb' /target:library /recurse:*.cs /lib:'../build' /r:'PicoContainer.dll;Microsoft.JScript.dll;VJSharpCodeProvider.dll;Castle.DynamicProxy.dll;NanoContainer.dll;NMock.dll;NUnit.Framework.dll'

Sunday, August 21, 2005

Using Rake for building and testing .net applications

The excitement concerning Ruby seems to grow on a daily basis. The Ruby on Rails framework, RubyGems, and Rake add to this excitement. Martin recently wrote a great article about Using the Rake Build Language.

Mike Ward asked me to help out with NanoContainer.net by creating a build file. This seemed like a perfect time to make use of Rake.

To get started I thought a Hello World Rake build would be appropriate.
task :helloWorld do
sh "echo HelloWorld"
end

Next I work on using Rake & csc to compile NanoContainer.net.
file "build/NanoContainer.dll" => :init do
cd "src/NanoContainer"
sh "csc /out:../build/NanoContainer.dll /target:library /recurse:*.cs /lib:../build /r:'PicoContainer.dll;Microsoft.JScript.dll;VJSharpCodeProvider.dll'"
cd "../.."
end

task :init do
buildDir = "src/build"
mkdir buildDir unless File.exists?(buildDir)

cp "lib/PicoContainer.dll", buildDir
cp "lib/Castle.DynamicProxy.dll", buildDir
cp "lib/NMock.dll", buildDir
cp "lib/NUnit.Framework.dll", buildDir
end

The "build/NanoContainer.dll" file task depends on the init task. This is expressed by:
file "build/NanoContainer.dll" => :init
The init task is used to create the build directory if it's not already been created and copy the dlls that are referenced by NanoContainer and NanoContainer.Tests. The actual sh "csc .." is fairly straight forward; however, if you need more details on csc the "csc -?" command is quite helpful.

The "build/NanoContainer.Tests.dll" file task is very similar:
file "build/NanoContainer.Tests.dll" => [:init, "build/NanoContainer.dll"] do
cd "src/NanoContainer.Tests"
sh "csc /out:../build/NanoContainer.Tests.dll /res:'TestScripts/test.cs,NanoContainer.Tests.TestScripts.test.cs' /res:'TestScripts/test.js,NanoContainer.Tests.TestScripts.test.js' /res:'TestScripts/test.java,NanoContainer.Tests.TestScripts.test.java' /res:'TestScripts/test.vb,NanoContainer.Tests.TestScripts.test.vb' /target:library /recurse:*.cs /lib:'../build' /r:'PicoContainer.dll;Microsoft.JScript.dll;VJSharpCodeProvider.dll;Castle.DynamicProxy.dll;NanoContainer.dll;NMock.dll;NUnit.Framework.dll'"
cd "../.."
end

After building NanoContainer and NanoContainer.Tests I'm ready to create a test task. NUnit provides good documentation for using the NUnit Console application; however, in my test task using the NUnit is very straight forward and easy.
task :test => :compile do
testCompVsBinDebugDir = "src/TestComp/bin/Debug"
mkdir testCompVsBinDebugDir unless File.exists?(testCompVsBinDebugDir)
cp "src/Build/TestComp.dll", testCompVsBinDebugDir

nanoTestVsBinDir = "src/NanoContainer.Tests/bin"
mkdir nanoTestVsBinDir unless File.exists?(nanoTestVsBinDir)

nanoTestVsBinDebugDir = "src/NanoContainer.Tests/bin/Debug"
mkdir nanoTestVsBinDebugDir unless File.exists?(nanoTestVsBinDebugDir)
cp "src/Build/PicoContainer.dll", nanoTestVsBinDebugDir
cp "src/Build/Castle.DynamicProxy.dll", nanoTestVsBinDebugDir
cp "src/Build/NMock.dll", nanoTestVsBinDebugDir
cp "src/Build/NUnit.Framework.dll", nanoTestVsBinDebugDir
cp "src/Build/NanoContainer.dll", nanoTestVsBinDebugDir
cp "src/Build/NanoContainer.Tests.dll", nanoTestVsBinDebugDir

cd nanoTestVsBinDebugDir
sh "../../../../lib/nunit-console.exe NanoContainer.Tests.dll"
end

The majority of the test task is about moving dlls into the NanoContainer.Tests/bin/Debug folder. This is needed because NanoContainer.Tests contains a few tests that use relative paths for testing. Eventually the tests should be refactored to remove this dependency, but I wasn't interested in taking care of this right now. Using Rake it's quite easy to move things around and that was the simplest solution.

I haven't used Rake to it's fullest. Even while I write this I can see things that I need to refactor. Unfortunately, when creating this rakefile I couldn't find much out there to help me. As more examples emerge I fully expect to see a much larger Rake adoption.

Here's my full rakefile for building NanoContainer.net
buildDir = "src/build"
nanoDll = "build/NanoContainer.dll"
nanoTestsDll = "build/NanoContainer.Tests.dll"
testCompDll = "build/TestComp.dll"
testComp2Dll = "build/TestComp2.dll"
notStartableDll = "build/NotStartable.dll"
nanoTestVsBinDir = "src/NanoContainer.Tests/bin"
nanoTestVsBinDebugDir = "src/NanoContainer.Tests/bin/Debug"
testCompBinDir = "src/TestComp/bin"
testCompBinDebugDir = "src/TestComp/bin/Debug"

task :default => [:compileInit, :compile, :test]
task :all => [:clear, :removeBuildDir, :removeVsDirs, :default]

task :clear do sh "clear" end

task :removeBuildDir => :clear do rm_rf(buildDir) end
task :removeVsDirs => :clear do
rm_rf(nanoTestVsBinDir)
rm_rf(testCompBinDir)
end

directory buildDir

task :compileInit => [buildDir] do
copyToDir(%w(lib/NUnit.Framework.dll lib/PicoContainer.dll lib/Castle.DynamicProxy.dll),buildDir)
copyToDir(%w(lib/NMock.dll lib/NUnit.Framework.dll),buildDir)
end

file nanoDll => :compileInit do
cd "src/NanoContainer"
sh "csc /out:../build/NanoContainer.dll /target:library /recurse:*.cs /lib:../build /r:'PicoContainer.dll;Microsoft.JScript.dll;VJSharpCodeProvider.dll'"
cd "../.."
end

file nanoTestsDll => [:compileInit, nanoDll] do
cd "src/NanoContainer.Tests"
sh "csc /out:../build/NanoContainer.Tests.dll /res:'TestScripts/test.cs,NanoContainer.Tests.TestScripts.test.cs' /res:'TestScripts/test.js,NanoContainer.Tests.TestScripts.test.js' /res:'TestScripts/test.java,NanoContainer.Tests.TestScripts.test.java' /res:'TestScripts/test.vb,NanoContainer.Tests.TestScripts.test.vb' /target:library /recurse:*.cs /lib:'../build' /r:'PicoContainer.dll;Microsoft.JScript.dll;VJSharpCodeProvider.dll;Castle.DynamicProxy.dll;NanoContainer.dll;NMock.dll;NUnit.Framework.dll'"
cd "../.."
end

file testCompDll => :compileInit do
cd "src/TestComp"
sh "csc /out:../build/TestComp.dll /target:library /recurse:*.cs"
cd "../.."
end

file testComp2Dll => [:compileInit, testCompDll] do
cd "src/TestComp2"
sh "csc /out:../build/TestComp2.dll /target:library /recurse:*.cs /lib:'../build' /r:'PicoContainer.dll;TestComp.dll'"
cd "../.."
end

file notStartableDll => [:compileInit, testCompDll] do
cd "src/NotStartable"
sh "csc /out:../build/NotStartable.dll /target:library /recurse:*.cs /lib:'../build' /r:TestComp.dll"
cd "../.."
end

task :compile => [nanoDll,nanoTestsDll,testCompDll,testComp2Dll,notStartableDll]

directory testCompBinDir
directory testCompBinDebugDir
directory nanoTestVsBinDir
directory nanoTestVsBinDebugDir

task :testInit => [testCompBinDir,testCompBinDebugDir,nanoTestVsBinDir,nanoTestVsBinDebugDir] do
cp "src/Build/TestComp.dll", testCompBinDebugDir
copyToDir(%w(src/Build/NMock.dll src/Build/PicoContainer.dll src/Build/Castle.DynamicProxy.dll),nanoTestVsBinDebugDir)
copyToDir(%w(src/Build/NUnit.Framework.dll src/Build/NanoContainer.dll src/Build/NanoContainer.Tests.dll),nanoTestVsBinDebugDir)
end

task :test => [:compile,:testInit] do
cd nanoTestVsBinDebugDir
sh "../../../../lib/nunit-console.exe NanoContainer.Tests.dll"
end

def copyToDir(fileArray, outputDir)
fileArray.each { |file| cp file, outputDir }
end

Tuesday, August 16, 2005

Testing View Observer Using Stubs

I recently blogged about the View Observer pattern we used at my last client. The largest obstacle in using View Observer is the claim that Testing Events is not easy. While at the client we tested View Observer 3 different ways to determine which was best. All 3 ways were good for specific reasons; however, testing View Observer seemed easiest using stubs.

The first step is creating the stub class whose state you can use for testing. Often you will want getters and setters for each property and Raise methods for each event defined in the View Interface. This can be automated with code generation.
public class StubAlbumView : IAlbumView
{
private string[] albumListBoxItems;
private string titleTextBoxText;
private string artistTextBoxText;
private string composerTextBoxText;
private bool composerTextBoxEnabled;
private bool classicalCheckBoxChecked;
private int albumListBoxSelectedIndex;

public string TitleTextBoxText
{
get { return titleTextBoxText; }
set { titleTextBoxText = value; }
}

public string[] AlbumListBoxItems
{
get { return albumListBoxItems; }
set { albumListBoxItems = value; }
}

public string ArtistTextBoxText
{
get { return artistTextBoxText; }
set { artistTextBoxText = value; }
}

public string ComposerTextBoxText
{
get { return composerTextBoxText; }
set { composerTextBoxText = value; }
}

public bool ComposerTextBoxEnabled
{
get { return composerTextBoxEnabled; }
set { composerTextBoxEnabled = value; }
}

public bool ClassicalCheckBoxChecked
{
get { return classicalCheckBoxChecked; }
set { classicalCheckBoxChecked = value; }
}

public int AlbumListBoxSelectedIndex
{
get { return albumListBoxSelectedIndex; }
set { albumListBoxSelectedIndex = value; }
}

public event UserAction ApplyButtonClick;
public event UserAction CancelButtonClick;
public event UserAction ClassicalCheckBoxCheck;
public event TextChangedUserAction ArtistTextChanged;
public event TextChangedUserAction TitleTextChanged;
public event TextChangedUserAction ComposerTextChanged;
public event IndexChangedUserAction AlbumListBoxSelectedIndexChanged;

public void RaiseApplyButtonClick()
{
ApplyButtonClick();
}

public void RaiseCancelButtonClick()
{
CancelButtonClick();
}

public void RaiseArtistTextChanged(string arg)
{
ArtistTextChanged(arg);
}

public void RaiseTitleTextChanged(string arg)
{
TitleTextChanged(arg);
}

public void RaiseComposerTextChanged(string arg)
{
ComposerTextChanged(arg);
}

public void RaiseAlbumListBoxSelectedIndexChanged(int arg)
{
AlbumListBoxSelectedIndexChanged(arg);
}

public void RaiseClassicalCheckBoxCheck()
{
ClassicalCheckBoxCheck();
}
}

The interesting thing about the observer tests is that they assert the state of the view. Therefore, the observer is unit tested based on the values of the view changing, not how it changes those values. The end result is a Observer behavioral test using the View's state that isn't tied to the implementation of the Observer.

The tests validate the state of the View after Observer creation and after each event raised from the View.

[TestFixture]
public class AlbumObserverTest
{
[Test]
public void ObserverConstructorSetsViewsValues()
{
StubAlbumView view = new StubAlbumView();
new AlbumObserver(view, createAlbums());
Assert.AreEqual(view.AlbumListBoxItems,new string[] {"Title1","Title2","Title3"});
Assert.AreEqual(view.TitleTextBoxText, "Title1");
Assert.AreEqual(view.ArtistTextBoxText, "Artist1");
Assert.AreEqual(view.ComposerTextBoxText, "Composer1");
Assert.AreEqual(view.ComposerTextBoxEnabled, true);
Assert.AreEqual(view.ClassicalCheckBoxChecked, true);
Assert.AreEqual(view.AlbumListBoxSelectedIndex, 0);
}

[Test]
public void SelectedIndexChangeUpdatesView()
{
StubAlbumView view = new StubAlbumView();
new AlbumObserver(view, createAlbums());
view.RaiseAlbumListBoxSelectedIndexChanged(1);
Assert.AreEqual(view.TitleTextBoxText, "Title2");
Assert.AreEqual(view.ArtistTextBoxText, "Artist2");
Assert.AreEqual(view.ComposerTextBoxText, "");
Assert.AreEqual(view.ComposerTextBoxEnabled, false);
Assert.AreEqual(view.ClassicalCheckBoxChecked, false);
Assert.AreEqual(view.AlbumListBoxSelectedIndex, 1);
}

[Test]
public void ClassicalCheckBoxCheckUpdatesView()
{
StubAlbumView view = new StubAlbumView();
new AlbumObserver(view, createAlbums());
view.RaiseClassicalCheckBoxCheck();
Assert.AreEqual(view.ComposerTextBoxText, "");
Assert.AreEqual(view.ComposerTextBoxEnabled, false);
Assert.AreEqual(view.ClassicalCheckBoxChecked, false);
}

[Test]
public void TitleTextChangedUpdatesView()
{
StubAlbumView view = new StubAlbumView();
new AlbumObserver(view, createAlbums());
view.RaiseTitleTextChanged("new title");
Assert.AreEqual(view.TitleTextBoxText, "new title");
}

[Test]
public void ArtistTextChangedUpdatesView()
{
StubAlbumView view = new StubAlbumView();
new AlbumObserver(view, createAlbums());
view.RaiseArtistTextChanged("new artist");
Assert.AreEqual(view.ArtistTextBoxText, "new artist");
}

[Test]
public void ComposerTextChangedUpdatesView()
{
StubAlbumView view = new StubAlbumView();
new AlbumObserver(view, createAlbums());
view.RaiseComposerTextChanged("new composer");
Assert.AreEqual(view.ComposerTextBoxText, "new composer");
}

[Test]
public void CancelUpdatesView()
{
StubAlbumView view = new StubAlbumView();
new AlbumObserver(view, createAlbums());
view.RaiseComposerTextChanged("new composer");
view.RaiseTitleTextChanged("new title");
view.RaiseArtistTextChanged("new artist");
view.RaiseClassicalCheckBoxCheck();
view.RaiseCancelButtonClick();
Assert.AreEqual(view.TitleTextBoxText, "Title1");
Assert.AreEqual(view.ArtistTextBoxText, "Artist1");
Assert.AreEqual(view.ComposerTextBoxText, "Composer1");
Assert.AreEqual(view.ComposerTextBoxEnabled, true);
Assert.AreEqual(view.ClassicalCheckBoxChecked, true);
}

[Test]
public void ApplyUpdatesModel()
{
IAlbum[] albums = createAlbums();
StubAlbumView view = new StubAlbumView();
new AlbumObserver(view, albums);
view.RaiseTitleTextChanged("new title");
view.RaiseArtistTextChanged("new artist");
view.RaiseClassicalCheckBoxCheck();
view.RaiseApplyButtonClick();
Assert.AreEqual(albums[0].Title, "new title");
Assert.AreEqual(albums[0].Artist, "new artist");
Assert.AreEqual(albums[0].Composer, "");
Assert.AreEqual(albums[0].IsClassical, false);
view.RaiseClassicalCheckBoxCheck();
view.RaiseComposerTextChanged("new composer");
view.RaiseApplyButtonClick();
Assert.AreEqual(albums[0].Composer, "new composer");
}


private IAlbum[] createAlbums()
{
return new Album[]
{
new Album("Title1",true,"Artist1","Composer1"),
new Album("Title2",false,"Artist2",""),
new Album("Title3",false,"Artist3","")
};
}
}

Saturday, August 13, 2005

Testing Events in C#

As I previously mentioned in Firing Silver Bullets I like to try things out in excess. Recently, I've been using events for Error Handling between layers, Separation of Presentation Logic, and just about anything that seemed like it might be a fit.

The most common argument you hear against events is that they are hard to test. Never being shy about a challenge I set out to dismiss this myth. My recent event experience has been in both C# 2.0 and 1.1. Depending on which version of C# I'm using the tests differ slightly.

Assume a simple class called PositiveNumber that takes an int in it's constructor and fires an Invalid event when Validate() is called if the constructor arg is not positive.

Example C# 1.1
[Test]
public void PositiveNumberDoesNotFireInvalidIfNumberIsPositive()
{
PositiveNumber num = new PositiveNumber(1);
num.Invalid+=new ValidationHandler(AssertFail);
num.Validate();
}
public void AssertFail()
{
Assert.Fail();
}

That's simple enough for me; however, testing that the event does occur is less straight forward. One option is to declare a class level bool that is set to false in the test and then set to true by the event handler. After the event is fired the bool can be tested for true. I've never been a fan of class variables in tests since they feel like global variables in a procedural application. Therefore, I actually prefer throwing a success exception (yes, I did say that).
[Test, ExpectedException(typeof(ApplicationException),"Success")]
public void PositiveNumberDoesFireInvalidIfNumberIsNotPositive()
{
PositiveNumber num = new PositiveNumber(0);
num.Invalid+=new ValidationHandler(ThrowSuccess);
num.Validate();
}
public void ThrowSuccess()
{
throw new ApplicationException("Success");
}

Perhaps a better exception than ApplicationException could be used, but you get the point. You hate it? You never imagined an exception could indicate success and "Exceptions are only for exceptional situations". Yeah, I get all that, but what's more clear than 4 lines of code that show expected behavior for an event? Read it a few more times and try to think of something more clear. Let me know if you find it.

Example C# 2.0
[Test]
public void PositiveNumberDoesNotFireInvalidIfNumberIsPositive()
{
PositiveNumber num = new PositiveNumber(1);
num.Invalid += delegate { Assert.Fail(); }
num.Validate();
}

Not much different, but Anonymous Methods do make it a bit cleaner. With the addition of Anonymous Methods I abandon the ThrowSuccess method. I could just throw the exception in the Anonymous Method; however, I can now declare the bool in the method and access it from the Anonymous Method. I'm not sure which I prefer more, but my teammates seem to prefer this method.
[Test]
public void PositiveNumberDoesFireInvalidIfNumberIsNotPositive()
{
bool methodCalled = false;
PositiveNumber num = new PositiveNumber(0);
num.Invalid += delegate { methodCalled = true; }
num.Validate();
Assert.IsTrue(methodCalled);
}

Testing the object that raises the events is fairly easy; however, testing the observers of these events can seem tough at first glance. In testing View Observer we used 3 different approaches. I'll detail those in the next few days.

Thursday, August 11, 2005

View Observer

Separation of presentation and domain can often be complex, especially concerning testing. Martin Fowler is developing a new book that provides patterns giving guidance on this topic. At my previous project we did something similar but not yet documented.

How It Works
Conceptually the view observer is quite easy. The view contains setters for any state that is dynamic and raises events in response to user actions. The views implement interfaces allowing for easy stubbing when testing observers. The observers observe the view and respond to events by changing any appropriate state and reloading the entire view.

When To Use It
View Observer is similar to both Model View Presenter (MVP) and Presentation Model (PM). Similar to MVP, the observer takes a view via constructor injection. When using MVP the most common approach to handling user interaction is to have the view call methods on the presenter. Unfortunately, the view then becomes tightly coupled to the presenter. Another option would be to add methods to the view that allow you to add delegates to the events of the view. The downside is that this introduces more logic in the view. View Observer handles user interaction issues by raising events in the view and handling them in the observers. This decreases coupling and reduces the logic in the view.

Unlike MVP but similar to PM, the observer maintains the state of the view. By maintaining the state in the observer the view can be fully refreshed after each state change. Obviously, this can be optomized if necessary, but coarse-grained refreshes reduce complexity and are recommended. Additionally, View Observer is easily testable via a xUnit tool instead of using view based testing.

Example (C#)
I'm going to use the common example Martin is using in the new book to demonstrate View Observer. The view is a simple Win Form with setters for the dynamic state and event handlers that raise new events. The View implements IAlbumView to allow easy testing using stubs or mocks.

public class AlbumForm : Form, IAlbumView
{
private ListBox albumListBox;
private Label artistLabel;
private Label titleLabel;
private TextBox artistTextBox;
private TextBox titleTextBox;
private CheckBox classicalCheckBox;
private Label composerLabel;
private TextBox composerTextBox;
private Button applyButton;
private Button cancelButton;
public event UserAction ApplyButtonClick;
public event UserAction CancelButtonClick;
public event UserAction ClassicalCheckBoxCheck;
public event TextChangedUserAction ArtistTextChanged;
public event TextChangedUserAction TitleTextChanged;
public event TextChangedUserAction ComposerTextChanged;
public event IndexChangedUserAction AlbumListBoxSelectedIndexChanged;

[Windows Form Designer generated code, dispose, & default constructor removed]

private void applyButton_Click(object sender, EventArgs e)
{
if (ApplyButtonClick!=null) { ApplyButtonClick(); }
}

private void cancelButton_Click(object sender, EventArgs e)
{
if (CancelButtonClick!=null) { CancelButtonClick(); }
}

private void albumListBox_SelectedIndexChanged(object sender, EventArgs e)
{
if (AlbumListBoxSelectedIndexChanged!=null) { AlbumListBoxSelectedIndexChanged(albumListBox.SelectedIndex); }
}

private void classicalCheckBox_CheckedChanged(object sender, EventArgs e)
{
if (ClassicalCheckBoxCheck!=null) { ClassicalCheckBoxCheck(); }
}

private void artistTextBox_TextChanged(object sender, EventArgs e)
{
if (ArtistTextChanged!=null) { ArtistTextChanged(artistTextBox.Text); }
}

private void titleTextBox_TextChanged(object sender, EventArgs e)
{
if (TitleTextChanged!=null) { TitleTextChanged(titleTextBox.Text); }
}

private void composerTextBox_TextChanged(object sender, EventArgs e)
{
if (ComposerTextChanged!=null) { ComposerTextChanged(composerTextBox.Text); }
}

public string[] AlbumListBoxItems
{
set { albumListBox.DataSource = value; }
}

public string ArtistTextBoxText
{
set { artistTextBox.Text = value; }
}

public string TitleTextBoxText
{
set { titleTextBox.Text = value; }
}

public bool ClassicalCheckBoxChecked
{
set { classicalCheckBox.Checked = value; }
}

public string ComposerTextBoxText
{
set { composerTextBox.Text = value; }
}

public int AlbumListBoxSelectedIndex
{
set { albumListBox.SelectedIndex = value; }
}

public bool ComposerTextBoxEnabled
{
set { composerTextBox.Enabled = value; }
}
}

The Observer is responsible for syncronizing the Model with the observer. The model for this example is an album.

public class Album : IAlbum
{
private string title;
private bool isClassical;
private string artist;
private string composer;

public string Title
{
get { return title; }
set { title = value; }
}

public bool IsClassical
{
get { return isClassical; }
set { isClassical = value; }
}

public string Artist
{
get { return artist; }
set { artist = value; }
}

public string Composer
{
get { return composer; }
set { composer = value; }
}
}

The Observer contains fields that maintain the state of the View's controls. It accepts the View and the Model via constructor injection. In the constructor the Observer stores the reference to both the View and the Model and subscribes to the events published by the View.

public class AlbumObserver
{
private readonly IAlbumView view;
private readonly IAlbum[] albums;
private bool isListening = true;
private string artistTextBoxText;
private string titleTextBoxText;
private bool classicalCheckBoxChecked;
private string composerTextBoxText;
private int albumListBoxSelectedIndex;

public AlbumObserver(IAlbumView view, IAlbum[] albums)
{
this.view = view;
this.albums = albums;
view.AlbumListBoxSelectedIndexChanged+=new IndexChangedUserAction(SelectedIndexChanged);
view.CancelButtonClick+=new UserAction(ReloadFromModel);
view.ClassicalCheckBoxCheck+=new UserAction(ClassicCheckChanged);
view.ApplyButtonClick+=new UserAction(SaveToModel);
view.ArtistTextChanged+=new TextChangedUserAction(ArtistTextChanged);
view.ComposerTextChanged+=new TextChangedUserAction(ComposerTextChanged);
view.TitleTextChanged+=new TextChangedUserAction(TitleTextChanged);
ReloadFromModel();
}

...
}

The Observer is responsible for reloading the View after every state change and when the Observer is constructed. Changes to the model also cause the View to be reloaded. The isListening guard clause is needed to ensure that an infinite loop does not occur when reloading the View.

public class AlbumObserver
{
private void reloadView()
{
if (!isListening)
{
return;
}
isListening = false;
view.ArtistTextBoxText = artistTextBoxText;
view.ClassicalCheckBoxChecked = classicalCheckBoxChecked;
view.ComposerTextBoxText = classicalCheckBoxChecked?composerTextBoxText:string.Empty;
view.TitleTextBoxText = titleTextBoxText;
view.AlbumListBoxItems = createAlbumStringArray();
view.AlbumListBoxSelectedIndex = albumListBoxSelectedIndex;
view.ComposerTextBoxEnabled = classicalCheckBoxChecked;
isListening = true;
}

public void ReloadFromModel()
{
IAlbum selectedAlbum = albums[albumListBoxSelectedIndex];
artistTextBoxText = selectedAlbum.Artist;
classicalCheckBoxChecked = selectedAlbum.IsClassical;
composerTextBoxText = selectedAlbum.Composer;
titleTextBoxText = selectedAlbum.Title;
reloadView();
}

private string[] createAlbumStringArray()
{
string[] result = new string[albums.Length];
for (int i=0;i<result.Length;i++)
{
result[i] = albums[i].Title;
}
return result;
}

...
}

Saving to the model is quite straight forward.

public class AlbumObserver
{
public void SaveToModel()
{
IAlbum selectedAlbum = albums[albumListBoxSelectedIndex];
selectedAlbum.Artist = artistTextBoxText;
selectedAlbum.IsClassical = classicalCheckBoxChecked;
selectedAlbum.Composer = classicalCheckBoxChecked?composerTextBoxText:string.Empty;
selectedAlbum.Title = titleTextBoxText;
ReloadFromModel();
}

...
}

Lastly, the events raised from the View change the appropriate state and cause the View to be refreshed.

public class AlbumObserver
{
public void SelectedIndexChanged(int newIndex)
{
if (isListening)
{
albumListBoxSelectedIndex = newIndex;
ReloadFromModel();
}
}

public void ClassicCheckChanged()
{
if (isListening)
{
classicalCheckBoxChecked=!classicalCheckBoxChecked;
reloadView();
}
}

public void ArtistTextChanged(string text)
{
if (isListening)
{
artistTextBoxText = text;
reloadView();
}
}

public void ComposerTextChanged(string text)
{
if (isListening)
{
composerTextBoxText = text;
reloadView();
}
}

public void TitleTextChanged(string text)
{
if (isListening)
{
titleTextBoxText = text;
reloadView();
}
}

...
}

Tuesday, August 02, 2005

SetUp and Teardown are still not good

At dinner tonight we had a wonderful discussion about testing. One topic was why SetUp and Teardown (S&T) are wrong, which I've previously blogged about. My biggest issue with S&T is that it's used to enforce DRY at the expense of readability.

Image a test that did not use S&T, it would do exactly what you wanted all in one test method (with possible helper private methods). There would be no where else to look for logic, variable declarations, or mock verifies. Wouldn't it be nice if all tests were that readable.

Unfortunately, somewhere along the way we started sacrificing readability. Also, we started blindly following rules like DRY without considering context. Sure, DRY is a great rule, but what if the duplicate code only appears within the class? What if it were only 1 line, like a variable declaration? Do you really gain anything by abstracting it to a SetUp method? Yes, you gain the ability to only make changes in one place. But, if you could make the change in one place, ensure that it changed everything correctly, and not sacrafice readability. Does this magical feature exist? Find and Replace (in this file/class only). What about more than 1 line of duplicate code? Once upon a time this was handled with private methods and the flow of a test was easy to follow. Times were simpler then, but now we do "the simplest thing that could possibly work", or do we?

Tests are procedural by naturue. Allowing developers to read tests in a procedural manner just makes sense.