To gain a better understanding of the context, begin by reading the contents of the xUnit/ch18 directory, where you will find the relevant code and tests that illustrate the concepts discussed in this chapter.
It is possible to find all the source code of this exercise here.

Exercise:

The last task to complete is creating a Create TestSuite from a TestCase class — that is, constructing a suite automatically from a test class.

Epoch 1

Step1: Add a little test.

The first step is often the hardest for those approaching the TDD style. As Kent Beck writes in the book, we have to imagine the perfect interface — even if it doesn’t exist yet — for the operation we’re about to implement. We’re telling ourselves a story about how the operation will look from the outside.

We image a TestSuite constructor takes in input the TestCase class.

def testSuiteFromTestCase(self):
    suite = TestSuite(TestCaseTest)
    suite.run(self.result)
    assert("7 run, 0 failed" == self.result.summary())

Step2: Run all tests and fail

When we run all the tests, Python tells us: TestSuite.__init__() takes 1 positional argument but 2 were given. This means that the TestSuite constructor doesn’t yet handle a TestCase class name as an input argument. To fix this, we can modify the constructor to accept the class name as a parameter:

def __init__(self, testCase):
    self.tests = []
    self.testCase = testCase

With this change in place, we’ve broken the testSuite test, since we’re no longer passing the TestCase class name to the constructor.

Step3: Make changes and run tests

I don’t like the code in its current form. I prefer to keep the constructor clean, so we apply a small change instead.

def __init__(self, testCaseClass=None):
    self.tests = []
    self.testCaseClass = testCaseClass

    if testCaseClass:
        self.testCaseClass = testCaseClass

With this change, we can choose whether or not to pass the testCaseClass as an input argument. When it is provided, we can parse the class and extract all the tests to add to the suite. The idea is that, given a test case class, we look for all methods whose names start with the word 'test'.

def loadTestsFromTestCase(self, testCaseClass):
    self.testCaseClass = testCaseClass
    
    test_methods = [method for method in dir(self.testCaseClass) 
                    if method.startswith('test') and callable(getattr(self.testCaseClass, method))]

    for method_name in test_methods:
        test_instance = self.testCaseClass(method_name)
        self.add(test_instance)

The output is the following:

testTemplateMethod: 			    1 run, 0 failed
testResult: 				        1 run, 0 failed
testFailedResultFormatting: 		1 run, 0 failed
testFailedResult: 			        1 run, 0 failed
testSetUpError: 			        1 run, 0 failed
testTearDownWithTestMethodError: 	1 run, 0 failed
testSuite: 				            1 run, 0 failed
maximum recursion depth exceeded
testSuiteFromTestCase: 			    1 run, 1 failed

The problem is that the test testSuiteFromTestCase is being called recursively. To fix this, we need to rename it to suiteFromTestCaseTest.

Step4: Run the tests and succeed

Running the tests:

suite = TestSuite()

suite.add(TestCaseTest("testTemplateMethod"))
suite.add(TestCaseTest("testResult"))
suite.add(TestCaseTest("testFailedResultFormatting"))
suite.add(TestCaseTest("testFailedResult"))
suite.add(TestCaseTest("testSetUpError"))
suite.add(TestCaseTest("testTearDownWithTestMethodError"))
suite.add(TestCaseTest("testSuite"))
suite.add(TestCaseTest("suiteFromTestCaseTest"))

result = TestResult()
suite.run(result=result)

print(result.summary())

We have the following output:

8 run, 0 failed

Now we can update the code that executes the test to utilize the TestSuite.

suite = TestSuite(TestCaseTest)
result = TestResult()
suite.run(result=result)
print(result.summary())

Remember that to test the new test case, it must be added separately.

suite = TestSuite(TestCaseTest)
result = TestResult()
suite.add(TestCaseTest("suiteFromTestCaseTest"))
suite.run(result=result)
print(result.summary())

Epoch 2

I’m not comfortable with adding suiteFromTestCaseTest separately; I would prefer to find a better approach.

Step1: Add a little test.

Renaming it to testSuiteFromTestCase brings back the recursion issue.

Step2: Run all tests and fail

if __name__ == "__main__":
    suite = TestSuite(TestCaseTest)
    result = TestResult()
    suite.run(result=result)
    print(result.summary())
maximum recursion depth exceeded
8 run, 1 failed

Step3: Make a change

The problem we face with the loadTestsFromTestCase method is that it automatically adds all methods starting with "test" from a given TestCase class. This creates a potential infinite recursion when we have a test method that itself creates a TestSuite from the same TestCase class.

The key insight is that rather than implementing complex recursion detection logic, we can solve this problem through better design of our test structure. By creating a dedicated SampleTestCaseForSuiteTest class that serves as a simple, controlled test fixture, we achieve separation of concerns. This dedicated test class contains only the methods needed to verify the TestSuite functionality, without any methods that would trigger the creation of another TestSuite from the original test class. This is a clean approach that doesn’t require any complex tracking mechanisms, global variables, or recursion detection algorithms.

class SampleTestCaseForSuiteTest(TestCase):
    pass

The test now is the following

def testSuiteFromTestCase(self):
    suite = TestSuite(SampleTestCaseForSuiteTest)
    suite.run(self.result)
    assert("0 run, 0 failed" == self.result.summary())

Step4: Run the tests and succeed

Running the TestSuite by passing TestCaseTest as an argument.

suite = TestSuite(TestCaseTest)
result = TestResult()
suite.run(result=result)
print(result.summary())

we have

8 run, 0 failed