[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/