Using the composite pattern to represent tests and suites

When writing unit tests, it's recommended to group together tests that are closely related to one other. We call a series of tests a suite, which can contain subsuites. Whole suites, subsuites, and individual tests can be run. These features makes the composite pattern very suitable for organizing the tests in suites, the suite itself being the composite.

First, we start with the base protocol, Testable. The only method it needs is the run method, to execute the contents of the test, and we'll use the errors thrown to indicate failure:

protocol Testable {
func run() throws
}

The following is the UnitTest class, which provides a simple wrapper around a name and a block that can be executed:

class UnitTest: Testable {
let name: String
private let block: TestImplementation

init(name: String, block: @escaping TestImplementation) {
self.name = name
self.block = block
}

func run() throws {
try self.block()
print("UnitTest \(name) ran successfully")
}
}

Now, we can implement the composite, TestSuite. In the composite pattern, the composite wraps many components, and behaves as a single one. TestSuite wraps many Testable objects, and also behaves like a single Testable object through the implementation of the Testable protocol:

class TestSuite: Testable {

let name: String
private(set) var testables: [Testable]

init(name: String, _ testables: [Testable] = []) {
self.name = name
self.testables = testables
}

func add(_ testable: Testable) {
testables.append(testable)
}

func run() throws {
print("Suite \(name) started")
let errors = testables.compactMap { (testable) -> Error? in
do {
try testable.run()
} catch let e {
return e
}
return nil
}

if errors.count > 0 {
throw Errors(errors: errors)
}
print("Suite \(name) ran successfully")
}

struct Errors: Error {
let errors: [Error]
}
}

The code here should be pretty straightforward. TestSuite, the composite, allows you to add Testable objects onto it. When calling the run method on the TestSuite, this will execute all Testable objects, and if the Testable itself is a TestSuite, the composite will in turn recursively run more tests. 

It's time to put this testing framework to good use:

let testSuite = TestSuite(name: "Top Level Suite")
testSuite.add(UnitTest(name: "First Test") {})
testSuite.add(UnitTest(name: "Second Test") {})
testSuite.add(
TestSuite(name: "ChildSuite", [
UnitTest(name: "Child 1") {},
UnitTest(name: "Child 2") {}
])
)

try? testSuite.run()

This code will produce the following output:

Suite Top Level Suite started
UnitTest First Test ran successfully
UnitTest Second Test ran successfully
Suite ChildSuite started
UnitTest Child 1 ran successfully
UnitTest Child 2 ran successfully
Suite ChildSuite ran successfully
Suite Top Level Suite ran successfully

The composite pattern is particularly suitable for representing systems where complex components behave the same as simple ones, such as filesystems, testing hierarchies, and other tree-like problems.