[TFUI] Re: [TDD] The feature works but the test fails
"Phlip" <[email protected]> Fri, 21 Oct 2005 22:12:52 -0700
| Newsgroups | gmane.comp.programming.test-first-user-interfaces,gmane.comp.programming.test-driven-development |
|---|---|
| Message-ID | <000f01c5d6c7$4b4dc0f0$0100a8c0@Marley> |
William Tanksley, Jr wrote:
> Robert Hanson wrote:
> > Later on, I discover that the method needs two properties; so I add a
> > new
> > property to the object, and write several more tests to exercise the new
> > functionality of the method. These new tests pass. However, the
> > original
> > tests don't set the value of the new property, so they fail.
>
> Sounds like the original tests are warning you that your objects have
> a complex, highly coupled interface.
Okay, here's the test. Note that I since fixed the problem (not because I'm
diligent, but because this thread kept the issue alive for me). The general
troubles are still illustrative.
I have a Wiki written in Ruby. Wiki source lines starting with bang ! are
special. The line !eval! ruby_source() will evaluate a line of ruby source,
within the context of the Wiki's main formatting object. That lets you
extend any page with any source you can think of. The Wiki's core code
provides many explicit and incidental methods to re-use here, including the
entire Ruby standard library.
I want to write a command generateThumbnail(), so you can write a Wiki page
with this line:
!eval! generateThumbnail('c:/path/file.png')
Maybe you have other scripts, outside the Wiki, that generated a picture.
Maybe it's a burn-down chart, or the output of a test, or whatever. That
line copies the file from your filesystem into your web site's virtual file
space, generates a smaller thumbnail copy, and inserts an <img> tag
declaring it into the Wiki's XHTML output.
I am aware everyone on this list has mocked everything from sunspot
detectors to TV remote controls. The primary reason one should mock cheap
and available things, like the file system and the database, is to help your
code decouple and clean up its logic.
In this case, the only logic is "copy the file, make a thumbnail, and insert
an <img> tag". I want the feature to work across refactors (such as merging
with generateGallery()), so I'm not going to mock the file system, or the
thumbnail program.
Here's setup:
def populateTestFolder()
test_folder = 'test_folder'
Dir.mkdir(test_folder) if not File.exist?(test_folder)
Dir.mkdir(test_folder+'/hide') if not File.exist?(test_folder+'/hide')
if make(test_folder+'/hide/penBird.png', 'images/penBird.png')
File.copy('images/penBird.png', test_folder+'/hide')
end
if make(test_folder+'/penBirdSelfPortrait.png',
'images/penBirdSelfPortrait.png')
File.copy('images/penBirdSelfPortrait.png', test_folder)
end
return test_folder
end
The Wiki always ships with at least two pictures, so I can re-use them as
test resources. I create a test folder and copy them into it. The 'if'
statements speed up the test if a previous case already created them (and
one test deletes them).
make() returns true if its first argument is older than its second, so you
need to rebuild the first argument.
Now here's the low-level test to force generateThumbnail() to behave:
def test_generateThumbnail()
xhtml = ''
test_folder = populateTestFolder()
nukeFile('images/copy_test_folder_hide_penBird.png')
nukeFile('images/thumbnail_test_folder_hide_penBird.png')
nukeFile('images/copy_test_folder_penBirdSelfPortrait.png')
nukeFile('images/thumbnail_test_folder_penBirdSelfPortrait.png')
aFormatter = WikiFormatter.new()
aFormatter.generateThumbnail(test_folder+'/penBirdSelfPortrait.png')
xhtml = aFormatter.x.contents
doc = Document.new(xhtml)
if xhtml =~ /requires the GraphicsMagick/ then
puts 'test fault'
puts xhtml
else
assert(! File.exist?('images/copy_test_folder_hide_penBird.png'))
assert(! File.exist?('images/thumbnail_test_folder_hide_penBird.png'))
assert
File.exist?('images/copy_test_folder_penBirdSelfPortrait.png')
assert
File.exist?('images/thumbnail_test_folder_penBirdSelfPortrait.png')
assert_nil XPath.first(doc, '/span/h3[text()="'+test_folder+'"]')
assert_nil XPath.first(doc,
'/span/a/img[@title="test_folder/hide/penBird.png"]')
assert_not_nil XPath.first(doc,
'/span/a/img[@title="test_folder/penBirdSelfPortrait.png"]')
selfPor = XPath.first(doc,
'/span/a/img[@title="test_folder/penBirdSelfPortrait.png"]')
assert_equal '/images/thumbnail_test_folder_penBirdSelfPortrait.png',
selfPor.attributes['src']
assert File.size(test_folder + '/penBirdSelfPortrait.png') <
File.size('images/thumbnail_test_folder_penBirdSelfPortrait.png')
assert_not_nil XPath.first(doc,
'/span/a[@href="/images/copy_test_folder_penBirdSelfPortrait.png" and
@target="_blank"]')
end
end
This test passes - we are not yet up to the situation in the subject line.
The test looks horrible, but I fear that simple fixes would only be
cosmetic. The test has to remove 4 files. If we put those lines into a
function, it would have a smelly name,
"nuke4imageFilesForTheGenerateThumbnailTest()". Not very reusable.
The test must fail gracefully, not fault, if a user runs the test on a
system that does not use GraphicsMagick. The rest of this Wiki must work
correctly, even if one minor dependency is not available.
The test has many assertions. Following the guideline "one assertion per
test case" would be an empty victory. We are not designing file systems, or
GraphicsMagick, so forcing all these assertions into separate cases, and
passing them one at a time, would force generateThumbnail() to grow with an
incrementalism it does not need.
Now here's generateThumbnail() itself:
def generateThumbnail(file)
x.span{ linkToThumbnail(file) }
end
def linkToThumbnail(file)
image_source = file.split(/[\:\\\/]/).find_all{|q|q!='.'}.join('_')
x.a_href('/images/copy_' + image_source, 'target="_blank"') do
thumbnail = '/images/thumbnail_' + image_source
x.write('<img title="')
x.write(file) # TODO x.quot
x.write('" src="')
x.write(thumbnail)
x.write('"/>')
end
copyTarget = File.join('images', 'copy_'+image_source)
thumTarget = File.join('images', 'thumbnail_'+image_source)
File.copy(file, copyTarget) if make(copyTarget, file)
createThumbnail(thumTarget, file) if make(thumTarget, file)
return nil
end
The duplicated x.write() statements could go down into a reusable x.img()
method, and the two 'make()' calls duplicate vaguely. Otherwise the methods
are short and straightforward.
I could declare the feature finished, but I'm investigating the pattern
MockTheServer, so I add a higher level test:
def test_MockServer_generateThumbnail()
writePage('WikiTestPage',
"\n\n!eval!generateThumbnail('#{@test_folder}/penBirdSelfPortrait.png')")
response = servePage('WikiTestPage')
doc = Document.new(response.body)
assert_not_nil XPath.first(doc,
'//span/a/img[@src="/images/thumbnail_test_folder_penBirdSelfPortrait.png"]')
end
That test now passes. It formerly failed because of an unrelated
typographical error in another module. Other tests also found that - when I
ran them!
That test writes a Wiki page, builds a mock server, hits the page, returns a
response, interprets its XHTML via XPath, and queries out the /span/a/img
inside it.
This is at least a mostly-harmless high-level test (regardless of minor
style flaws) because if production code fails to create the correct <img>
tag, the test will fail on a line containing an XPath expression describing
the exact
However, at failure time you might need to read the entire web page, with
all its other tags, to determine what the page actually did, in contrast to
what the test said it should do. So this is not exactly an "expressive"
test, because its failure does not produce instant evidence what failed.
There's too much code below servePage() for only generateThumbnail() to be
implicated.
Again, in this situation, write (and run) passing lower-level tests until
they throw more light on what's going on!
--
Phlip
http://www.greencheese.org/ZeekLand <-- NOT a blog!!
------------------------ Yahoo! Groups Sponsor --------------------~-->
Get Bzzzy! (real tools to help you find a job). Welcome to the Sweet Life.
http://us.click.yahoo.com/A77XvD/vlQLAA/TtwFAA/nhFolB/TM
--------------------------------------------------------------------~->
To unsubscribe, email:
TestFirstUserInterfaces-unsubscribe-hHKSG33TihhbjbujkaE4pw@public.gmane.org
Yahoo! Groups Links
<*> To visit your group on the web, go to:
http://groups.yahoo.com/group/TestFirstUserInterfaces/
<*> To unsubscribe from this group, send an email to:
TestFirstUserInterfaces-unsubscribe-hHKSG33TihhbjbujkaE4pw@public.gmane.org
<*> Your use of Yahoo! Groups is subject to:
http://docs.yahoo.com/info/terms/