Crafting happiness with Free Software & Hardware

« Test Driven Development by Example » – Chapter 22. Dealing with Failure

Guile Logo

Previously :

The next item on the todo list: Report failed tests. The last chapter let the state of the test suite in a shaky state. Indeed, the last one is still red!
According to Kent Beck, the following chapter is like going one step deeper to deal with a more specific – smaller grained – as he said. To ensure our learning base (the tests) is rock solid!

Kent properly started with a new test testFailedResultFormatting. Which was intended to force him to set in place the mecanics required to make our last, still failing, test testFailedResult. So the point here is to see the logic producing the right message when we call the right methods in the right order.
Kent add an error counter and the testFailed method to the TestResult class. Kent admitted the counting part would have been worth a test for itself.
Then a try-catch block to trigger the testFailed method.
Another interesting test, added to the todo list, would be to try to spot errors during the setup.

class TestCase:
    def __init__(self, name):
        self.name = name
    def setUp(self):
        pass
    def tearDown(self):
        pass
    def run(self):
        result = TestResult()
        result.testStarted()
        self.setUp()
        try:
            method = getattr(self, self.name)
            method()
        except:
            result.testFailed()
        self.tearDown()
        return result

class WasRun(TestCase):
    def __init__(self, name):
        TestCase.__init__(self, name)
    def setUp(self):
        self.log = "setUp "
    def testMethod(self):
        self.log = self.log + "testMethod "
    def testBrokenMethod(self):
        raise Exception
    def tearDown(self):
        self.log = self.log + "tearDown "

class TestResult:
    def __init__(self):
        self.runCount = 0
        self.errorCount = 0
    def testStarted(self):
        self.runCount = self.runCount + 1
    def testFailed(self):
        self.errorCount= self.errorCount + 1
    def summary(self):
        return "%d run, %d failed" % (self.runCount, self.failureCount)

class TestCaseTest(TestCase):
    def testTemplateMethod(self):
        test = WasRun("testMethod")
        test.run()
        assert("setUp testMethod tearDown " == test.log)
    def testResult(self):
        test = WasRun("testMethod")
        result = test.run()
        assert("1 run, 0 failed" == result.summary())
    def testFailedResult(self):
        test = WasRun("testBrokenMethod")
        result = test.run()
        assert("1 run, 1 failed", result.summary)
    def testFailedResultFormatting(self):
        result = TestResult()
        result.testStarted()
        result.testFailed()
        assert("1 run, 1 failed" == result.summary())
    TestCaseTest("testTemplateMethod").run()
    TestCaseTest("testResult").run()
    TestCaseTest("testFailedResult").run()
    TestCaseTest("testFailedResultFormatting").run()

On my side, I started with the new test. Which forced me to implement the test-failed procedure in order to compile. Just after fixing the compilation issue, I make it to pass. Baby steps are so convenient. I really like it. So with the test-failed-result-formatting to green, I had me the confidence that my notifiers were OK.
All that was left to do was to call the test-failed procedure when the test actuallu run.
With the refactoring I did in the previous chapter, I feel my code is able to catch exceptions from setup as well but! I would count multiple errors if setup and the procedure under test both fail.

(define-module (xunit-tdd test-result)
  #:use-module (srfi srfi-9))

(define-record-type <test-result>
  (make-test-result run fail)
  test-result?
  (run get-run set-run!)
  (fail get-fail set-fail!))

(define-public (new)
  (make-test-result 0 0))

(define-public (summary a-test-result)
  (format #f "~A run, ~A failed" (get-run a-test-result) (get-fail a-test-result)))

(define-public (test-started a-test-result)
  (set-run! a-test-result (+ 1 (get-run a-test-result))))

(define-public (test-failed a-test-result)
  (set-fail! a-test-result (+ 1 (get-fail a-test-result))))


(define-module (xunit-tdd test-case)
  #:use-module (srfi srfi-9)
  #:use-module ((xunit-tdd test-result) #:prefix test-result:))

(define-record-type <test-case>
  (make-test-case setup-proc test-proc teardown-proc log)
  test-case?
  (setup-proc setup-proc)
  (test-proc test-proc)
  (teardown-proc teardown-proc)
  (log log set-log!))

(define-public (new a-setup-proc a-test-proc a-teardown-proc a-log)
  (make-test-case a-setup-proc a-test-proc a-teardown-proc a-log))

(define-public (run test-case)
  (let ([result (test-result:new)])
    (test-result:test-started result)
    (for-each
     (lambda (proc)
       (with-exception-handler
	     (lambda (e)
	       (test-result:test-failed result))
	     (lambda ()
	       (if (procedure? (proc test-case))
	         ((proc test-case) test-case)))
	     #:unwind? #t))
     (list setup-proc test-proc teardown-proc))
    result))

(define-public (read-log test-case)
  (log test-case))

(define-public (append-to-log! test-case a-log)
  (set-log! test-case (string-append (log test-case) a-log)))


(define-module (xunit-tdd was-run)
  #:use-module ((xunit-tdd test-case) #:prefix test-case:))

(define-public (new proc)
  (test-case:new test-setup proc test-teardown ""))

(define (test-setup test-case)
  (test-case:append-to-log! test-case "test-setup "))

(define (test-teardown test-case)
  (test-case:append-to-log! test-case "test-teardown "))

(define-public (test-procedure test-case)
  (test-case:append-to-log! test-case "test-procedure "))

(define-public (test-broken-procedure test-case)
  (raise-exception (make-exception)))


(define-module (tests test-case-test)
  #:use-module ((rnrs) #:version (6) #:select (assert))
  #:use-module ((xunit-tdd test-case) #:prefix test-case:)
  #:use-module ((xunit-tdd was-run) #:prefix was-run:)
  #:use-module ((xunit-tdd test-result) #:prefix test-result:))

(define (test-template-method this-test-case)
  (let ([a-test-case (was-run:new was-run:test-procedure)])
    (test-case:run a-test-case)
    (assert (string=? "test-setup test-procedure test-teardown "
		      (test-case:read-log a-test-case)))))

(define (test-result this-test-case)
  (let ([a-test-case (was-run:new was-run:test-procedure)])
    (assert (string=? "1 run, 0 failed"
		              (test-result:summary (test-case:run a-test-case))))))

(define (test-failed-result this-test-case)
  (let ([a-test-case (was-run:new was-run:test-broken-procedure)])
    (assert (string=? "1 run, 1 failed"
		              (test-result:summary (test-case:run a-test-case))))))

(define (test-failed-result-formatting this-test-case)
  (let ([a-test-result (test-result:new)])
    (test-result:test-started a-test-result)
    (test-result:test-failed a-test-result)
    (assert (string=? "1 run, 1 failed"
		              (test-result:summary a-test-result)))))

(test-case:run (test-case:new #f test-template-method #f ""))
(test-case:run (test-case:new #f test-result #f ""))
(test-case:run (test-case:new #f test-failed-result #f ""))
(test-case:run (test-case:new #f test-failed-result-formatting #f ""))

I'm not sure but I think we don't need testFailedResultFormatting or test-failed-result-formatting anymore.

Thank you very much for reading this article!

Don't hesitate to give me your opinion, suggest an idea for improvement, report an error, or ask a question ! I would be so glad to discuss about the topic covered here with you ! You can reach me here.

Don't miss out on the next ones ! Either via RSS or via e-mail !

And more importantly, share this blog and tell your friends it's the best blog in the history of Free Software! No kidding!

#gnu #guile #tdd #book #english

GPG: 036B 4D54 B7B4 D6C8 DA62 2746 700F 5E0C CBB2 E2D1