You probably now how to add static test cases to a SenTestingKit-based unit test. You create a subclass of SenTestCase and add instance methods with their names prefixed with “test”.
When the unit test runs it builds the test classes and then introspects each one to dynamically find all such named instance methods. Those are then executed one after the other.
Usually you get by with this technique but there might be scenarios where you would want to be able to dynamically add test methods based on some external information, like a property list. Let me show you how.
I developed this technique when I built a unit test for DTCoreText where I wanted to be able to have the test case look into the octest bundle and dynamically create one test case per file found. If you run the MacUnitTest in the DTCoreText project you see this:
In this screenshot only the first unit test testPixelsVersusPoints is a traditional static one. All the others with the “test_” prefix are dynamically added from the html files found in the test bundle.
To achieve this you need to make use of the dynamic nature of Objective-C and add one instance method per test case. The best place for that is the +initialize method which gets called in a thread-safe way before the class is used the first time.
Example
To demonstrate the technique I created a DynamicUnitTest project which you can find in my Examples GitHub repository. I created an empty project, added an iOS unit test target and added a TestCases.plist which contains a dynamic list of names of test cases. I leave it to your imagination what you would want to use to supply the information: a plist, a list of files, etc.
I already added a reference to DTFoundation as a sub-project and added the libDTFoundation for iOS to the Linking phase. I also added a recursive reference to the User Header Search Path so that Xcode can find the NSObject+DTRuntime.h header. This contains a method for dynamically adding a block as an instance method.
The setup in the unit test has become quite simple:
+ (void)initialize { // Load test case list NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestCases" ofType:@"plist"]; NSArray *testCases = [NSArray arrayWithContentsOfFile:path]; for (NSString *oneCase in testCases) { // prepend test_ so that it gets found by introspection NSString *selectorName = [@"test_" stringByAppendingString:oneCase]; [UnitTest addInstanceMethodWithSelectorName:selectorName block:^(UnitTest *myself) { [myself internalTestCase:oneCase]; }]; } } - (void)internalTestCase:(NSString *)testCase { // this performs the actual test } |
The internalTestCase: method is the one that does the actual work. You can have any shape or form of parameters there because the block that is added for each new instance method can capture any sort of parameters and pass these onto the internal function. In this example it only transfers the name of the test case but this could also be some values from the TestCases property list.
This is how the output looks if you execute the unit test. Indeed the items from the plist turned into dynamic test cases.
This is an extra simple example. Of course the names of the selectors can only contain characters that are valid for selectors, e.g. no whitespace.
Conclusion
In addition to static test cases you might have this technique also allows to add dynamically created test cases to your unit test classes. This allows for having dozens or hundreds of individual cases without having to create a static instance method for each and every one.
Categories: Recipes
I am new to ios development. If you have demonstrated this cool solution with a practical problem that would be awesome.
That is very nice! I use similar technique to inject our existing C++ based tests written using CPPUnit as testing framework to Xcode. But it would be nice to make this dynamic list appear on the list in Test action configuration. Did You find a solution for that as well? I am struggling with that and i found out so far that the list actually gets populated during running tests. But not before. Any ideas?