Code Reading: Shared Examples in Kiwi
Having examined the motivations for using shared examples and how they're implemented in RSpec, we're ready to examine how we can port the same functionality to Kiwi. This post takes approximately 7 minutes to read, and covers:
- How XCTest discovers instances of XCTestCase
- How Kiwi builds a suite of examples by hooking into this discovery
- How Kiwi implements shared examples
Readers familiar with using Kiwi to run Objective-C unit tests will get the most out of this post.
The Gist
Kiwi and RSpec are different beasts. RSpec is a pure testing framework that
defines its own methods for test detection, while Kiwi runs alongside XCTest.
Kiwi hooks into XCTest test detection to build example groups. Shared examples
can be implemented just as in RSpec: by storing context nodes in a global
registry and inserting them where itBehavesLike
is used in the tests. A macro
is used to ensure shared examples are registered before test execution.
Note that this post covers shared examples, a feature not yet in the official Kiwi release. See the pull request for information on when it might be available.
How XCTest discovers tests
XCTest is the framework behind the testing tools built into Xcode 5. Two classes relevant to this post are:
XCTestCase
: An individual test case.XCTestSuite
: A collection of test cases.
If you've used XCTest before, you know that in order to implement test methods,
you need to subclass XCTestCase
and write methods that begin with -test
. By
default, XCTestSuite
collects all subclasses of XCTestCase
included in the
test target. For each test method, it creates an instance of the test case and
sends it the -invokeTest
message. This executes the test case's -setUp
method, then the test method, and finally the test case's -tearDown
method.
XCTest is practically identical to SenTestingKit, the testing framework bundled with versions of Xcode 4.6 and prior–just replace
XCTestCase
withSenTestCase
, and so on, in the explanations above. In posts from now on I'll only mention the points at which they differ.
How Kiwi hooks into XCTest test detection
Normally, XCTestSuite
executes methods beginning with -test
in each
XCTestCase
. Alternatively, subclasses of XCTestCase
can override the class
method +testInvocations
to return a different set of methods to run during
the test.
In fact, this is exactly what KWSpec
does. When +testInvocations
is
called,
each KWSpec
subclass creates an instance of
KWExampleSuite
.
In order to do so, KWSpec
has the Kiwi DSL written between the SPEC_BEGIN
and
SPEC_END
macros evaluated by a builder
class
called KWExampleSuiteBuilder
. Functions like
describe
and
context
are C functions that add
nodes–that
is, blocks of code with a name
parameter–to a stack maintained by the suite.
Blocks passed to it
functions are also turned into KWItNode
objects and
added to the stack.
When the example suite is finished constructing its stack of nodes, it returns
an array of "dummy"
invocations
that match the number of KWItNode
objects in the stack. Each of these
invocations also carries a reference to the it
node
it represents, via a KWExample
object.
When the -invokeTest
method is called on each KWSpec
, Kiwi grabs the
KWExample
associated with the current
invocation
being called. It then executes the KWItNode
example
by using the -[KWContextNode performExample:withBlock:]
method.
This method uses recursion to move up the context
stack.
The recursion exits when the entire context for the it
node has been
reconstructed, and the assertions in the it
block are finally
executed.
How shared examples are injected into the context stack
The previous post in this series described how RSpec shared
examples are stored in a global registry. When RSpec encounters an
#it_behaves_like
method, it inserts the shared example as a context node in
that spot.
Kiwi shared examples are implemented in the same way. The sharedExamplesFor
method
registers an instance of KWSharedExample
in a global registry, an instance of
KWSharedExampleRegistry
. The itBehavesLike
method
inserts the shared example as a context block in the current stack.
Differences between RSpec and Kiwi implementations
Unfortunately, because Kiwi must adhere to the test execution flow imposed by
XCTest, the implementation of shared examples isn't as clean as it is in RSpec.
Whereas RSpec can load each spec file and process the shared examples in
advance, Kiwi loads test case files in an order determined by XCTestSuite
. As
a result, special precautions must be taken to ensure that shared examples are
registered before any example groups are executed.
The workaround for this limitation is to use the SHARED_EXAMPLES_BEGIN
and
SHARED_EXAMPLES_END
macros.
These define an arbitrary category and overload its +load
method.
This method is called when the class or category is added to the Objective-C
runtime. This occurs before any tests are run, so it's an adequate place to
register any shared examples:
SHARED_EXAMPLES_BEGIN(ReusableSpecHelpers)
sharedExamplesFor(@"something reusable", ^(Class describedClass) {
// ...
});
SHARED_EXAMPLES_END
Of course, no one wants more macros in Kiwi. So it's worth considering whether there's a better way of ensuring shared examples are registered before any specs are run.
This concludes our exploration of shared examples in RSpec and Kiwi. Hopefully you got as much out of reading this series as I did writing it! If you have any feedback, please contact me on Twitter.