Crafting happiness with Free Software & Hardware

« Test Driven Development by Example » – Chapter 23. How Suite It Is

Guile Logo

Previously :

The next item on the todo list: Run multiple tests. As we have written four tests since the beginning of this serie, speaking about suite is going to be handy.

Here are the four tests:

TestCaseTest("testTemplateMethod").run().summary()
TestCaseTest("testResult").run().summary()
TestCaseTest("testFailedResultFormatting").run().summary()
TestCaseTest("testFailedResult").run().summary()

Notice that Kent has kept the testFailedResultFormatting test.
Composition (with the Composite pattern) ! This is the technique Kent had chosen to design his suite feature. He wanted to be able to treat single tests and groups of tests exactly the same.
A test suite specification was given as follow :
« We would like to be able to create a TestSuite, add a few tests to it, and then get
collective results from running it »
First of all, a test (testSuite) which brung its share of new features : the TestSuite class with a add() method, a run() method.
The result became a parameter for the run() method in the TestSuite class and in the TestCase class (it is a prerequisite to implement the Composite pattern).
Then, the tests were refactored.
That being done, Kent had to fix the four failing tests (which use the old no-argument run interface) and refactored the duplicated TestResult instantiation.

class TestSuite:
    def __init__(self):
        self.tests = []
    def add(self, test):
        self.tests.append(test)
    def run(self, result):
        for test in tests:
            test.run(result)

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

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 setUp(self):
        self.result= TestResult()
    def testTemplateMethod(self):
        test = WasRun("testMethod")
        test.run(result)
        assert("setUp testMethod tearDown " == test.log)
    def testResult(self):
        test = WasRun("testMethod")
        test.run(result)
        assert("1 run, 0 failed" == result.summary())
    def testFailedResult(self):
        test = WasRun("testBrokenMethod")
        test.run(result)
        assert("1 run, 1 failed", result.summary)
    def testFailedResultFormatting(self):
        result.testStarted()
        result.testFailed()
        assert("1 run, 1 failed" == result.summary())
    def testSuite(self):
        suite = TestSuite()
        suite.add(WasRun("testMethod"))
        suite.add(WasRun("testBrokenMethod"))
        suite.run(result)
        assert("2 run, 1 failed" == result.summary())

suite = TestSuite()
suite.add(TestCaseTest("testTemplateMethod"))
suite.add(TestCaseTest("testResult"))
suite.add(TestCaseTest("testFailedResult"))
suite.add(TestCaseTest("testFailedResultFormatting"))
suite.add(TestCaseTest("testSuite"))
result = TestResult()
suite.run(result)
print result.summary

There come my Guile version:

(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 this-test-case this-test-result)
  (test-result:test-started this-test-result)
  (for-each
   (lambda (proc)
     (with-exception-handler
	 (lambda (e)
	   (test-result:test-failed this-test-result))
       (lambda ()
	 (if (procedure? (proc this-test-case))
	     ((proc this-test-case) this-test-case)))
       #:unwind? #t))
   (list setup-proc test-proc teardown-proc)))

(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 test-suite)
  #:use-module (srfi srfi-9)
  #:use-module ((xunit-tdd test-case) #:prefix test-case:))

(define-record-type <test-suite>
  (make-test-suite tests)
  test-suite?
  (tests tests set-tests!))

(define-public (new)
  (make-test-suite '()))

(define-public (add test-suite test-case)
  (set-tests! test-suite (cons test-case (tests test-suite))))

(define-public (run test-suite test-result)
  (for-each
   (lambda (test)
     (test-case:run test test-result))
   (tests test-suite)))


(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:)
  #:use-module ((xunit-tdd test-suite) #:prefix test-suite:))

(let ([a-test-result #f])

  (define (setup _)
    (set! a-test-result (test-result:new)))

  (define (test-case-test:new test-proc)
    (test-case:new setup test-proc #f ""))


  (define (test-template-method this-test-case)
    (let ([a-test-case (was-run:new was-run:test-procedure)])
      (test-case:run a-test-case a-test-result)
      (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)])
      (test-case:run a-test-case a-test-result)
      (assert (string=? "1 run, 0 failed"
			(test-result:summary a-test-result)))))

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

  (define (test-failed-result-formatting this-test-case)
    (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))))

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

  (let ([the-test-suite (test-suite:new)]
	[the-test-result (test-result:new)])

    (test-suite:add the-test-suite (test-case-test:new test-template-method))
    (test-suite:add the-test-suite (test-case-test:new test-result))
    (test-suite:add the-test-suite (test-case-test:new test-failed-result))
    (test-suite:add the-test-suite (test-case-test:new test-failed-result-formatting))
    (test-suite:add the-test-suite (test-case-test:new test-suite))

    (test-suite:run the-test-suite the-test-result)
    (test-result:summary the-test-result)))

So this is the final chapter of this serie. As I was discovering xUnit design philosophy I did not care about functional programming as much as I thought I would. For this reason, there could be an extra chapter (or serie) in the futur where I show you my very own version of xUnit in Guile !

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