I'm really struggling to understand unit testing. I do understand the importance of TDD, but all the examples of unit testing I read about seem to be extremely simple and trivial. For example, testing to make sure a property is set or if memory is allocated to an array. Why? If I code out ..alloc] init], do I really need to make sure it works?
I'm new to development so I'm sure I'm missing something here, especially with all the craze surrounding TDD.
I think my main issue is I can't find any practical examples. Here is a method setReminderId that seems to be a good candidate for testing. What would a useful unit test look like to make sure this is working? (using OCUnit)
- (NSNumber *)setReminderId: (NSDictionary *)reminderData
{
NSNumber *currentReminderId = [[NSUserDefaults standardUserDefaults] objectForKey:#"currentReminderId"];
if (currentReminderId) {
// Increment the last reminderId
currentReminderId = #(currentReminderId.intValue + 1);
}
else {
// Set to 0 if it doesn't already exist
currentReminderId = #0;
}
// Update currentReminderId to model
[[NSUserDefaults standardUserDefaults] setObject:currentReminderId forKey:#"currentReminderId"];
return currentReminderId;
}
Update: I've improved on this answer in two ways: it's now a screencast, and I switched from property injection to constructor injection. See How to Get Started with Objective-C TDD
The tricky part is that the method has a dependency on an external object, NSUserDefaults. We don't want to use NSUserDefaults directly. Instead, we need to inject this dependency somehow, so that we can substitute a fake user defaults for testing.
There are a few different ways of doing this. One is by passing it in as an extra argument to the method. Another is to make it an instance variable of the class. And there are different ways of setting up this ivar. There's "constructor injection" where it's specified in the initializer arguments. Or there's "property injection." For standard objects from the iOS SDK, my preference is to make it a property, with a default value.
So let's start with a test that the property is, by default, NSUserDefaults. My toolset, by the way, is Xcode's built-in OCUnit, plus OCHamcrest for assertions and OCMockito for mock objects. There are other choices, but that's what I use.
First Test: User Defaults
For lack of a better name, the class will be named Example. The instance will be named sut for "system under test." The property will be named userDefaults. Here's a first test to establish what its default value should be, in ExampleTests.m:
#import <SenTestingKit/SenTestingKit.h>
#define HC_SHORTHAND
#import <OCHamcrestIOS/OCHamcrestIOS.h>
#interface ExampleTests : SenTestCase
#end
#implementation ExampleTests
- (void)testDefaultUserDefaultsShouldBeSet
{
Example *sut = [[Example alloc] init];
assertThat([sut userDefaults], is(instanceOf([NSUserDefaults class])));
}
#end
At this stage, this doesn't compile — which counts as the test failing. Look it over. If you can get your eyes to skip over the brackets and parentheses, the test should be pretty clear.
Let's write the simplest code we can to get that test to compile and run — and fail. Here's Example.h:
#import <Foundation/Foundation.h>
#interface Example : NSObject
#property (strong, nonatomic) NSUserDefaults *userDefaults;
#end
And the awe-inspiring Example.m:
#import "Example.h"
#implementation Example
#end
We need to add a line to the very beginning of ExampleTests.m:
#import "Example.h"
The test runs, and fails with the message, "Expected an instance of NSUserDefaults, but was nil". Exactly what we wanted. We have reached step 1 of our first test.
Step 2 is to write the simplest code we can to pass that test. How about this:
- (id)init
{
self = [super init];
if (self)
_userDefaults = [NSUserDefaults standardUserDefaults];
return self;
}
It passes! Step 2 is complete.
Step 3 is to refactor code to incorporate all changes, in both production code and test code. But there's really nothing to clean up yet. We are done with our first test. What do we have so far? The beginnings of a class that can access NSUserDefaults, but also have it overridden for testing.
Second Test: With no matching key, return 0
Now let's write a test for the method. What do we want it to do? If the user defaults has no matching key, we want it to return 0.
When first starting with mock objects, I recommend making them by hand at first, so that you get an idea of what they're for. Then start using a mock object framework. But I'm going to jump ahead and use OCMockito to make things faster. We add these lines to the ExampleTest.m:
#define MOCKITO_SHORTHAND
#import <OCMockitoIOS/OCMockitoIOS.h>
By default, an OCMockito-based mock object will return nil for any method. But I'll write extra code to make the expectation explicit by saying, "given that it's asked for objectForKey:#"currentReminderId", it will return nil." And given all that, we want the method to return the NSNumber 0. (I'm not going to pass an argument, because I don't know what it's for. And I'm going to name the method nextReminderId.)
- (void)testNextReminderIdWithNoCurrentReminderIdInUserDefaultsShouldReturnZero
{
Example *sut = [[Example alloc] init];
NSUserDefaults *mockUserDefaults = mock([NSUserDefaults class]);
[sut setUserDefaults:mockUserDefaults];
[given([mockUserDefaults objectForKey:#"currentReminderId"]) willReturn:nil];
assertThat([sut nextReminderId], is(equalTo(#0)));
}
This doesn't compile yet. Let's define the nextReminderId method in Example.h:
- (NSNumber *)nextReminderId;
And here's the first implementation in Example.m. I want the test to fail, so I'm going to return a bogus number:
- (NSNumber *)nextReminderId
{
return #-1;
}
The test fails with the message, "Expected <0>, but was <-1>". It's important that the test fail, because it's our way of testing the test, and ensuring that the code we write flips it from a failing state to a passing state. Step 1 is complete.
Step 2: Let's get the test test to pass. But remember, we want the simplest code that passes the test. It's going to look awfully silly.
- (NSNumber *)nextReminderId
{
return #0;
}
Amazing, it passes! But we're not done with this test yet. Now we come to Step 3: refactor. There's duplicate code in the tests. Let's pull sut, the system under test, into an ivar. We'll use the -setUp method to set it up, and -tearDown to clean it up (destroying it).
#interface ExampleTests : SenTestCase
{
Example *sut;
}
#end
#implementation ExampleTests
- (void)setUp
{
[super setUp];
sut = [[Example alloc] init];
}
- (void)tearDown
{
sut = nil;
[super tearDown];
}
- (void)testDefaultUserDefaultsShouldBeSet
{
assertThat([sut userDefaults], is(instanceOf([NSUserDefaults class])));
}
- (void)testNextReminderIdWithNoCurrentReminderIdInUserDefaultsShouldReturnZero
{
NSUserDefaults *mockUserDefaults = mock([NSUserDefaults class]);
[sut setUserDefaults:mockUserDefaults];
[given([mockUserDefaults objectForKey:#"currentReminderId"]) willReturn:nil];
assertThat([sut nextReminderId], is(equalTo(#0)));
}
#end
We run the tests again, to make sure they still pass, and they do. Refactoring should only be done in "green" or passing state. All tests should continue to pass, whether refactoring is done in the test code or the production code.
Third Test: With no matching key, store 0 in user defaults
Now let's test another requirement: the user defaults should be saved. We'll use the same conditions as the previous test. But we create a new test, instead of adding more assertions to the existing test. Ideally, each test should verify one thing, and have a good name to match.
- (void)testNextReminderIdWithNoCurrentReminderIdInUserDefaultsShouldSaveZeroInUserDefaults
{
// given
NSUserDefaults *mockUserDefaults = mock([NSUserDefaults class]);
[sut setUserDefaults:mockUserDefaults];
[given([mockUserDefaults objectForKey:#"currentReminderId"]) willReturn:nil];
// when
[sut nextReminderId];
// then
[verify(mockUserDefaults) setObject:#0 forKey:#"currentReminderId"];
}
The verify statement is the OCMockito way of saying, "This mock object should have been called this way one time." We run the tests and get a failure, "Expected 1 matching invocation, but received 0". Step 1 is complete.
Step 2: the simplest code that passes. Ready? Here goes:
- (NSNumber *)nextReminderId
{
[_userDefaults setObject:#0 forKey:#"currentReminderId"];
return #0;
}
"But why are you saving #0 in user defaults, instead of a variable with that value?" you ask. Because that's as far as we've tested. Hang on, we'll get there.
Step 3: refactor. Again, we have duplicate code in the tests. Let's pull out mockUserDefaults as an ivar.
#interface ExampleTests : SenTestCase
{
Example *sut;
NSUserDefaults *mockUserDefaults;
}
#end
The test code shows warnings, "Local declaration of 'mockUserDefaults' hides instance variable". Fix them to use the ivar. Then let's extract a helper method to establish the condition of the user defaults at the start of each test. Let's pull that nil out to a separate variable to help us with the refactoring:
NSNumber *current = nil;
mockUserDefaults = mock([NSUserDefaults class]);
[sut setUserDefaults:mockUserDefaults];
[given([mockUserDefaults objectForKey:#"currentReminderId"]) willReturn:current];
Now select the last 3 lines, context click, and select Refactor ▶ Extract. We'll make a new method called setUpUserDefaultsWithCurrentReminderId:
- (void)setUpUserDefaultsWithCurrentReminderId:(NSNumber *)current
{
mockUserDefaults = mock([NSUserDefaults class]);
[sut setUserDefaults:mockUserDefaults];
[given([mockUserDefaults objectForKey:#"currentReminderId"]) willReturn:current];
}
The test code that invokes this now looks like:
NSNumber *current = nil;
[self setUpUserDefaultsWithCurrentReminderId:current];
The only reason for that variable was to help us with the automated refactoring. Let's inline it away:
[self setUpUserDefaultsWithCurrentReminderId:nil];
Tests still pass. Since Xcode's automated refactoring didn't replace all instances of that code with a call to the new helper method, we need to do that ourselves. So now the tests look like this:
- (void)testNextReminderIdWithNoCurrentReminderIdInUserDefaultsShouldReturnZero
{
[self setUpUserDefaultsWithCurrentReminderId:nil];
assertThat([sut nextReminderId], is(equalTo(#0)));
}
- (void)testNextReminderIdWithNoCurrentReminderIdInUserDefaultsShouldSaveZeroInUserDefaults
{
// given
[self setUpUserDefaultsWithCurrentReminderId:nil];
// when
[sut nextReminderId];
// then
[verify(mockUserDefaults) setObject:#0 forKey:#"currentReminderId"];
}
See how we continually clean as we go? The tests have actually become easier to read!
Fourth Test: With matching key, return incremented value
Now we want to test that if the user defaults has some value, we return one greater. I'm going to copy and alter the "should return zero" test, using an arbitrary value of 3.
- (void)testNextReminderIdWithCurrentReminderIdInUserDefaultsShouldReturnOneGreater
{
[self setUpUserDefaultsWithCurrentReminderId:#3];
assertThat([sut nextReminderId], is(equalTo(#4)));
}
That fails, as desired: "Expected <4>, but was <0>".
Here's simple code to pass the test:
- (NSNumber *)nextReminderId
{
NSNumber *reminderId = [_userDefaults objectForKey:#"currentReminderId"];
if (reminderId)
reminderId = #([reminderId integerValue] + 1);
else
reminderId = #0;
[_userDefaults setObject:#0 forKey:#"currentReminderId"];
return reminderId;
}
Except for that setObject:#0, this is starting to look like your example. I don't see anything to refactor, yet. (There actually is, but I didn't notice until later. Let's keep going.)
Fifth Test: With matching key, store incremented value
Now we can establish one more test: given those same conditions, it should save the new reminder ID in user defaults. This is quickly done by copying the earlier test, altering it, and giving it a good name:
- (void)testNextReminderIdWithCurrentReminderIdInUserDefaultsShouldSaveOneGreaterInUserDefaults
{
// given
[self setUpUserDefaultsWithCurrentReminderId:#3];
// when
[sut nextReminderId];
// then
[verify(mockUserDefaults) setObject:#4 forKey:#"currentReminderId"];
}
That test fails, with "Expected 1 matching invocation, but received 0". To get it passing, of course, we simply change the setObject:#0 to setObject:reminderId. Everything passes. We're done!
Wait, we're not done. Step 3: Is there anything to refactor? When I first wrote this, I said, "Not really." But looking it over after watching Clean Code episode 3, I can hear Uncle Bob telling me, "How big should a function be? 4 lines is OK, maybe 5. 6 is… OK. 10 is way too big." That's at 7 lines. What did I miss? It must be violating the rule of functions by doing more than one thing.
Again, Uncle Bob: "The only way to be really be sure that a function does one thing is to extract 'til you drop." Those first 4 lines work together; they calculate the actual value. Let's select them, and Refactor ▶ Extract. Following Uncle Bob's scoping rule from episode 2, we'll give it a nice, long descriptive name since its scope of use is very limited. Here's what the automated refactoring gives us:
- (NSNumber *)determineNextReminderIdFromUserDefaults
{
NSNumber *reminderId = [_userDefaults objectForKey:#"currentReminderId"];
if (reminderId)
reminderId = #([reminderId integerValue] + 1);
else
reminderId = #0;
return reminderId;
}
- (NSNumber *)nextReminderId
{
NSNumber *reminderId;
reminderId = [self determineNextReminderIdFromUserDefaults];
[_userDefaults setObject:reminderId forKey:#"currentReminderId"];
return reminderId;
}
Let's clean that up to make it tighter:
- (NSNumber *)determineNextReminderIdFromUserDefaults
{
NSNumber *reminderId = [_userDefaults objectForKey:#"currentReminderId"];
if (reminderId)
return #([reminderId integerValue] + 1);
else
return #0;
}
- (NSNumber *)nextReminderId
{
NSNumber *reminderId = [self determineNextReminderIdFromUserDefaults];
[_userDefaults setObject:reminderId forKey:#"currentReminderId"];
return reminderId;
}
Now each method is really tight, and it's easy for anyone to read the 3 lines of the main method to see what it does. But I'm uncomfortable having that user defaults key spread across two methods. Let's extract that into a constant at the head of Example.m:
static NSString *const currentReminderIdKey = #"currentReminderId";
I'll use that constant wherever that key appears in the production code. But the test code continues to use the literals. This guards us from someone accidentally changing that constant key.
Conclusion
So there you have it. In five tests, I have TDD'd my way to the code you asked for. Hopefully it gives you a clearer idea of how to TDD, and why it's worth it. By following the 3-step waltz
Add one failing test
Write the simplest code that passes, even if it looks dumb
Refactor (both production code and test code)
you don't just end up at the same place. You end up with:
well-isolated code that supports dependency injection,
minimalist code that only implements what has been tested,
tests for each case (with the tests themselves verified),
squeaky-clean code with small, easy-to-read methods.
All these benefits will save more time than the time invested in TDD — and not just in the long term, but immediately.
For an example involving a full app, get the book Test-Driven iOS Development. Here's my review of the book.
Related
I'm new to Objective-C so please bear with me.
I have a unit test class that looks something like this:
static NSString * const kTestingUserID = #"userID";
#interface UserPreferencesTest
#property(nonatomic) UserPreferences *userPreferences;
#end
#implementation UserPreferencesTest
- (void)setUp {
[super setUp];
self.userPreferences = [[UserPreferences alloc] init];
}
- (void)testEmptyDictionary {
STAssertFalse([self.userPreferences getLockoutStatus:kTestingUserID],
#"An unset username should always return false");
}
- (void)testBlockManualEntry {
//lock the username
[self.userPreferences lockout:kTestingUserID];
STAssertTrue([self.userPreferences getLockoutStatus:kTestingUserID],
#"The account has been locked out");
}
#end
They call the following methods in the userPreferences Class:
- (void)lockout:(NSString *) userID {
NSMutableDictionary *dictionary =
[[self.defaults objectForKey:dict] mutableCopy];
dictionary[userID] = [NSNumber numberWithBool:YES];
[self.defaults setObject:[NSDictionary dictionaryWithDictionary:dictionary]
forKey:dict];
[self synchronize];
}
- (BOOL) getLockoutStatus:(NSString *) userID {
NSDictionary *dictionary = [self.defaults objectForKey:dict];
if ([dictionary objectForKey:userID])
return true;
return false;
}
If I comment out the testBlockManualEntry the test passes. If I run the two tests together the first one, testEmptyDictionary fails. The issue to my understanding is that the username is set in the 2nd test and then is found in the 1st test.
If I were to write a similar unit test in java I would use #Before in my setup and thus setup would run before each test method is called. That would ensure that the userPreferences is created each time and thus the values in that object are reset.
What am I missing here? My understanding of setup in Objective-C is that it should behave similarly (clearly it's not unless theres another bug that I'm not seeing).
Also please ignore the fact that a set would be better used in this scenario than a dict, I'm just looking (and how to fix the issue) for why the tests fail when run together.
I would suggest changing over to Xcode's XCT test framework. SN* is an old framework.
Your problem is probably coming from
#interface UserPreferencesTest
It should be
#interface UserPreferencesTest: XCTestCase
So that it inherits all the functionality of the test framework. Then it should work.
I also noticed this code:
if ([dictionary objectForKey:userID])
return true;
return false;
There is nothing wrong with this code and it will work. But from a style point of view I would change it. Firstly I always recommend ensuring that if ... statements always have {...} brackets. The reason is that it's easy to miss read a single line if and subtle bugs can appears, especially if other developers are updating your code.
The second thing is that whenever I see a return YES or return NO, I tend to look to see if they are a verbose version of what the code is trying to achieve.
In that light, I would probably code something like this:
return [dictionary objectForKey:userID] != nil;
Having said that, sometimes code is clearer when you do return YES or return NO. So it's how you feel about it.
I understand what the error is, but in this case not what is causing it. In general use it occurs maybe 1% of the time (probably less) but I have found an extreme way to cause it which I will describe below. First, I am using an in-app purchase process I found on Ray Wenderlich's site. Below are the specific pieces of concern here:
.h:
typedef void (^RequestProductsCompletionHandler)(BOOL success, NSArray * products);
#interface IAPHelper : NSObject
- (void)requestProductsWithCompletionHandler:RequestProductsCompletionHandler)completionHandler;
#end
.m
#implementation IAPHelper
{
SKProductsRequest * _productsRequest;
RequestProductsCompletionHandler _completionHandler;
}
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
NSLog(#"Loaded list of products...");
_productsRequest = nil;
NSArray * skProducts = response.products;
for (SKProduct * skProduct in skProducts) {
NSLog(#"Found product: %# %# %0.2f",
skProduct.productIdentifier,
skProduct.localizedTitle,
skProduct.price.floatValue);
}
_completionHandler(YES, skProducts); // here is where bad access occurs
_completionHandler = nil;
}
Again, 99%+ of the time this works just fine. Given how infrequent the bad access happens in regular use and it has been difficult to diagnose. However, I found an extreme way to cause the issue. The setup is "Tab 1" is a table view controller and "Tab 2" is a table view controller that uses the code from above. If I quickly switch back and forth between the two tabs I can usually cause the problem to occur anywhere from a few seconds into it to 20-30 seconds. Doesn't happen every time in this scenario but it does the vast majority. As marked above the following line gets a bad access error with Parent is Null.
_completionHandler(YES, skProducts);
To solve the issue I simple do the following:
if (_completionHandler)
{
_completionHandler(YES, skProducts);
_completionHandler = nil;
}
While that fix does work and does solve the problem I am still bothered by why this is occurring. Any thoughts as to the cause of this?
Update:
Apologies to all as I did forget to include the following in what I pasted above.
- (void)requestProductsWithCompletionHandler:(RequestProductsCompletionHandler)completionHandler {
// 1
_completionHandler = [completionHandler copy];
// 2
_productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:_productIdentifiers];
_productsRequest.delegate = self;
[_productsRequest start];
}
You need to treat your completion block as any other object when you are storing it. So if you are storing your block as a variable and then using it within a different scope from where you assign it, you need to increment the reference count by either copying it or retaining it. The simple solution is to create a strong property to store your block.
Depending on unseen bits of code, your completion handler block might not be being assigned correctly. You need to copy a block if you intend to use it outside of the scope in which it was created.
In your interface, declare your completion handler's storage attribute as "copy".
#property (nonatomic, readwrite, copy) void (^completionHandler)(BOOL, NSArray *);
If you want to control the local variable, you can synthesize the property manually in your implementation:
#synthesize completionHandler = _completionHandler;
Let's say this is my init method
- (id)initWithClient:(id <Client>)client
andDataStorage:(DataStorage *)storage
{
if (self = [super init])
{
self.client = client;
self.storage = storage;
}
return self;
}
Then I want to write a macro that somehow logs the parameters passed to a method, by wrapping the parameter with a defined macro. Is this possible in any way?
The problem is at runtime it's not possible to find out the type of a parameter passed to a method. So I'm trying to find a hack around it, and do it at compile time.
// somehow achieve this, and log the value inside the marco
#define INJECT(x) NSLog(#"%#", x)
- (id)initWithClient:(INJECT(id <Client>))client
andDataStorage:(INJECT(DataStorage *))storage
{
}
expected log in console:
id <Client>
DataStorage *
At the risk of running into what appear to be crossed wires in the comments: you can get the parameter types passed to a method at runtime.
E.g.
NSMethodSignature *signature =
[class methodSignatureForSelector:#selector(someSelector:)];
for(int argument = 2; argument < signature.numberOfArguments; argument++)
{
const char *argumentType = [signature getArgumentTypeAtIndex:argument];
// this is where it gets a bit messy...
if(!strcmp(argumentType, #encode(int))) NSLog(#"an integer");
if(!strcmp(argumentType, #encode(float))) NSLog(#"a float");
// ... etc, etc, etc ...
}
For any passed objects, use [object class] since all objects look the same at the runtime level — think of e.g. NSArray -addObject:; the runtime knows an object type will be passed in but it could be any object type.
See Apple's documentation on Type Encodings for information on what's going on there with those #encodes.
Whilst not an answer to the question as such. I would not recommend doing what you are asking about. I've seen far to much code where people have logged every single method call and argument (horribly over-complicated Java Enterprise stuff). The result has always been obscenely large logs that tell you next to nothing because of the amount of work it takes to find what you are after.
My recommendation would be that logging is important, but you should do targeted logging that clearing shows the state of relevant data at specific points which are important to understanding the flow.
Like others, I'm not sure what you are really after, or whether it is a good idea/design etc. But I wonder whether you are approaching the problem the wrong way. So let's take a look and maybe it will help you. From what I see you:
Want to find some way of obtaining the declared types of method parameters, in the form of strings, at runtime.
You are trying to tackle this by adding macros to the source. This tells me that you are not trying to do this for methods in a binary library that you are dynamically loading, but to methods in source you are compiling and are prepared to modify to achieve your goal.
Looked at that way, what is the problem? If you are prepared to add macros to your source why not simply add data declarations that contain the information you want - a mapping from a selector to an order list of parameter types as strings.
Is the issue that you want to extract the information in some automated way and were intending adding your macros by some automated process?
You can arrange for an Xcode project to run a source file through some other program by changing the file extension. Apple provide examples of using this to pre-process strings files - the files are fed through a Ruby script which produces a strings file which Xcode then handles as usual. Will that address your needs? Could you write a script/application (doesn't need to be in Ruby) which could add the information you need "on the fly" - take source in, produce modified source out which Xcode then compiles as usual? Note that the Clang compiler itself is designed to be called as a library so you can even use it to help you parse your source to extract the information you are after.
If none of those approaches suit consider that the debugger knows the correct types at runtime, and it gets those from the symbol information generated for it. There are library functions provided to help reader debugger information, so you should be able to write code which uses the same information the debugger does.
Hope those ideas help you, though I'm still not clear what you are trying or whether it makes sense!
due to objC being dynamically typed, all classes have the type id. The information about the declared types is erased. They are merely hints for the developer and to enable the compiler to do some type checking (again purely for the dev's benefit)
So while #encode works for 'primates' and structs and stuff, for classes all is equal... as there are not really object types for runtime
'Solution': Store the class names of method argumentsin a map manually and then COMBINE that info with #encode;s info to log the stuff.
working sample:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
NSDictionary *DDParamsMap(void);
NSDictionary *DDParamsMap() {
static NSDictionary *dict = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//TODO
//add all methods that are have objc classes passed
//add the classes or NSNull
dict = #{#"Test_initWithArray:data:number:": #[NSArray.class, NSData.class, NSNull.null]};
});
return dict;
}
void DDLogParamsOf(Class class, SEL sel);
void DDLogParamsOf(Class class, SEL sel) {
//
//try internal lookup first (so we get class names
//
NSString *className = #(class_getName(class));
NSString *methodName = NSStringFromSelector(sel);
NSString *key = [NSString stringWithFormat:#"%#_%#", className, methodName];
NSArray *types = DDParamsMap()[key];
//
// loop
//
NSMethodSignature *signature = [class instanceMethodSignatureForSelector:sel];
if(!signature) {
signature = [class methodSignatureForSelector:sel];
}
//if the array doesnt have the right number of values, screw it!
if(types.count != signature.numberOfArguments - 2) {
types = nil;
}
for(int argument = 2; argument < signature.numberOfArguments; argument++) {
id type = types[argument - 2];
if(type && ![type isKindOfClass:[NSNull class]]) {
NSLog(#"class is %#", type);
}
else {
const char *argumentType = [signature getArgumentTypeAtIndex:argument];
// this is where it gets a bit messy...
if(!strcmp(argumentType, #encode(int))) NSLog(#"an integer");
if(!strcmp(argumentType, #encode(float))) NSLog(#"a float");
if(!strcmp(argumentType, #encode(id))) NSLog(#"it is a class");
// ... etc, etc, etc ...
}
}
}
#define LogParams() DDLogParamsOf(self.class, _cmd);
#interface Test : NSObject
+ (void)testMethofWithFloat:(float)f;
- (id)initWithArray:(NSArray*)a
data:(NSData*)d
number:(int)i;
#end
#implementation Test
+ (void)testMethofWithFloat:(float)f {
LogParams();
}
- (id)initWithArray:(NSArray*)a
data:(NSData*)d
number:(int)i
{
LogParams();
return nil;
}
#end
int main(int argc, char *argv[]) {
#autoreleasepool {
[Test testMethofWithFloat:3.0f];
Test *t = [[Test alloc] initWithArray:#[] data:[NSMutableData data] number:1];
t = nil;
}
}
Using OCMockito and OCHamcrest, I can set up expectations on the arguments to mocked methods, thusly:
[verify(aMockObject) doSomething:allOf(is(instanceOf([NSArray class])), hasCountOf(3U), nil)];
There doesn't seem to be an equivalently simple way to do this using Kiwi. It is possible to capture arguments using a spy, something like:
KWCaptureSpy *spy = [aMockObject captureArgument:#selector(doSomething:) atIndex:0];
NSArray *capturedArray = spy.argument;
And then to check expectations on the captured object:
[[capturedArray should] haveCountOf:3U];
Is there a less clumsy way to do this in Kiwi?
(I'm aware I could probably use hamcrest matchers in here, but for the moment I'm exploring what Kiwi is capable of).
One option that I have used is stub:withBlock:
NSArray* capturedArray; // declare this as __block if needed
[aMockObject stub:#selector(doSomething:)
withBlock:^id(NSArray *params) {
capturedArray = params[0];
// this is necessary even if the doSomething method returns void
return nil;
}];
// exercise your object under test, then:
[[capturedArray should] haveCountOf:3U];
This works fine, and I find it easier to implement than the spy pattern. But your question made me wonder about expectations using message patterns. For example:
[[[aMockObject should] receive] doSomething:myArray];
[[[aMockObject should] receive] doSomething:any()];
The first example will verify that aMockObject received the doSomething: message with an argument that isEqual:myArray. The second example will simply verify that doSomething: was sent, with no expectation about the array arugment. It would be great if we can specify some type of Matcher in the message pattern, to express that we don't care what specific array instance is sent in the message, just that it has a count of 3.
I haven't found any examples of being able to do this, but it looks like there are some possibilities. To verify a message-sending expectation, Kiwi uses the KWMessagePattern class, specifically the matchesInvocation: and argumentFiltersMatchInvocationArguments: methods. This checks for three types of "argument filters":
Literal object values (such as myArray in the example above), which are compared to the actual value sent in the message using isEqual:
An object of type KWAny (such as the any() macro in the example above), which will match any argument value
Objects that satisfy [KWGenericMatchEvaluator isGenericMatcher:argumentFilter], which basically means that the object responds to matches:(id)obj
Thus, you should be able to use objects that implement matches: in message-pattern expectations to do things like verify the length of arrays sent to stubbed methods, without resorting to spys or blocks. Here's a very simple implementation: (available as a Gist)
// A reusable class that satisfies isGenericMatcher:
#interface SOHaveCountOfGenericMatcher : NSObject
- (id)initWithCount:(NSUInteger)count;
- (BOOL)matches:(id)item; // this is what KWMessagePattern looks for
#property (readonly, nonatomic) NSUInteger count;
#end
#implementation SOHaveCountOfGenericMatcher
- (id)initWithCount:(NSUInteger)count
{
if (self = [super init]) {
_count = count;
}
return self;
}
- (BOOL)matches:(id)item
{
if (![item respondsToSelector:#selector(count)])
return NO;
return [item count] == self.count;
}
#end
// Your spec:
it(#"should receive an array with count 3", ^{
NSArray* testArray = #[#"a", #"b", #"c"];
id argWithCount3 = [[SOHaveCountOfGenericMatcher alloc] initWithCount:3];
id aMockObject = [SomeObj nullMock];
[[[aMockObject should] receive] doSomething:argWithCount3];
[aMockObject doSomething:testArray];
});
It would be nice to be able to reuse Kiwi's built-in matcher classes here, but I haven't yet found out exactly how to do this.
I've got an object of type id and would like to know if it contains a value for a given keyPath:
[myObject valueForKeyPath:myKeyPath];
Now, I wrap it into a #try{ } #catch{} block to avoid exceptions when the given keypath isn't found. Is there a nicer way to do this? Check if the given keypath exists without handling exceptions?
Thanks a lot,
Stefan
You could try this:
if ([myObject respondsToSelector:NSSelectorFromString(myKeyPath)])
{
}
However, that may not correspond to the getter you have, especially if it is a boolean value. If this doesn't work for you, let me know and I'll write you up something using reflection.
For NSManagedObjects, an easy solution is to look at the object's entity description and see if there's an attribute with that key name. If there is, you can also take it to the next step and see what type of an attribute the value is.
Here's a simple method that given any NSManagedObject and any NSString as a key, will always return an NSString:
- (NSString *)valueOfItem:(NSManagedObject *)item asStringForKey:(NSString *)key {
NSEntityDescription *entity = [item entity];
NSDictionary *attributesByName = [entity attributesByName];
NSAttributeDescription *attribute = attributesByName[key];
if (!attribute) {
return #"---No Such Attribute Key---";
}
else if ([attribute attributeType] == NSUndefinedAttributeType) {
return #"---Undefined Attribute Type---";
}
else if ([attribute attributeType] == NSStringAttributeType) {
// return NSStrings as they are
return [item valueForKey:key];
}
else if ([attribute attributeType] < NSDateAttributeType) {
// this will be all of the NSNumber types
// return them as strings
return [[item valueForKey:key] stringValue];
}
// add more "else if" cases as desired for other types
else {
return #"---Unacceptable Attribute Type---";
}
}
If the key is invalid or the value can't be made into a string, the method returns an NSString error message (change those blocks to do whatever you want for those cases).
All of the NSNumber attribute types are returned as their stringValue representations. To handle other attribute types (e.g.: dates), simply add additional "else if" blocks. (see NSAttributeDescription Class Reference for more information).
If the object is a custom class of yours, you could override valueForUndefinedKey: on your object, to define what is returned when a keypath doesn't exist.
It should be possible to graft this behavior onto arbitrary classes reasonably simply. I present with confidence, but without warranty, the following code which you should be able to use to add a non-exception-throwing implementation of valueForUndefinedKey: to any class, with one, centralized line of code per class at app startup time. If you wanted to save even more code, you could make all the classes you wanted to have this behavior inherit from a common subclass of NSManagedObject and then apply this to that common class and all your subclasses would inherit the behavior. More details after, but here's the code:
Header (NSObject+ValueForUndefinedKeyAdding.h):
#interface NSObject (ValueForUndefinedKeyAdding)
+ (void)addCustomValueForUndefinedKeyImplementation: (IMP)handler;
#end
Implementation (NSObject+ValueForUndefinedKeyAdding.m):
#import "NSObject+ValueForUndefinedKeyAdding.h"
#import <objc/runtime.h>
#import <objc/message.h>
#implementation NSObject (ValueForUndefinedKeyAdding)
+ (void)addCustomValueForUndefinedKeyImplementation: (IMP)handler
{
Class clazz = self;
if (clazz == nil)
return;
if (clazz == [NSObject class] || clazz == [NSManagedObject class])
{
NSLog(#"Don't try to do this to %#; Really.", NSStringFromClass(clazz));
return;
}
SEL vfuk = #selector(valueForUndefinedKey:);
#synchronized([NSObject class])
{
Method nsoMethod = class_getInstanceMethod([NSObject class], vfuk);
Method nsmoMethod = class_getInstanceMethod([NSManagedObject class], vfuk);
Method origMethod = class_getInstanceMethod(clazz, vfuk);
if (origMethod != nsoMethod && origMethod != nsmoMethod)
{
NSLog(#"%# already has a custom %# implementation. Replacing that would likely break stuff.",
NSStringFromClass(clazz), NSStringFromSelector(vfuk));
return;
}
if(!class_addMethod(clazz, vfuk, handler, method_getTypeEncoding(nsoMethod)))
{
NSLog(#"Could not add valueForUndefinedKey: method to class: %#", NSStringFromClass(clazz));
}
}
}
#end
Then, in your AppDelegate class (or really anywhere, but it probably makes sense to put it somewhere central, so you know where to find it when you want to add or remove classes from the list) put this code which adds this functionality to classes of your choosing at startup time:
#import "MyAppDelegate.h"
#import "NSObject+ValueForUndefinedKeyAdding.h"
#import "MyOtherClass1.h"
#import "MyOtherClass2.h"
#import "MyOtherClass3.h"
static id ExceptionlessVFUKIMP(id self, SEL cmd, NSString* inKey)
{
NSLog(#"Not throwing an exception for undefined key: %# on instance of %#", inKey, [self class]);
return nil;
}
#implementation MyAppDelegate
+ (void)initialize
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[MyOtherClass1 addCustomValueForUndefinedKeyImplementation: (IMP)ExceptionlessVFUKIMP];
[MyOtherClass2 addCustomValueForUndefinedKeyImplementation: (IMP)ExceptionlessVFUKIMP];
[MyOtherClass3 addCustomValueForUndefinedKeyImplementation: (IMP)ExceptionlessVFUKIMP];
});
}
// ... rest of app delegate class ...
#end
What I'm doing here is adding a custom implementation for valueForUndefinedKey: to the classes MyOtherClass1, 2 & 3. The example implementation I've provided just NSLogs and returns nil, but you can change the implementation to do whatever you want, by changing the code in ExceptionlessVFUKIMP. If you remove the NSLog, and just return nil, I suspect you'll get what you want, based on your question.
This code NEVER swizzles methods, it only adds one if it's not there. I've put in checks to prevent this from being used on classes that already have their own custom implementations of valueForUndefinedKey: because if someone put that method in their class, there's going to be an expectation that it will continue to get called. Also note that there may be AppKit code that EXPECTS the exceptions from the NSObject/NSManagedObject implementations to be thrown. (I don't know that for sure, but it's a possibility to consider.)
A few notes:
NSManagedObject provides a custom implementation for valueForUndefinedKey: Stepping through its assembly in the debugger, all it appears to do is throw roughly the same exception with a slightly different message. Based on that 5 minute debugger investigation, I feel like it ought to be safe to use this with NSManagedObject subclasses, but I'm not 100% sure -- there could be some behavior in there that I didn't catch. Beware.
Also, as it stands, if you use this approach, you don't have a good way to know if valueForKey: is returning nil because the keyPath is valid and the state happened to be nil, or if it's returning nil because the keyPath is invalid and the grafted-on handler returned nil. To do that, you'd need to do something different, and implementation specific. (Perhaps return [NSNull null] or some other sentinel value, or set some flag in thread-local storage that you could check, but at this point is it really all that much easier than #try/#catch?) Just something to be aware of.
This appears to work pretty well for me; Hope it's useful to you.
There's no easy way to solve this. Key Value Coding (KVC) isn't intended to be used that way.
One thing is for sure: using #try-#catch is really bad since you're very likely to leak memory etc. Exceptions in ObjC / iOS are not intended for normal program flow. They're also very expensive (both throwing and setting up the #try-#catch IIRC).
If you look at the Foundation/NSKeyValueCoding.h header, the comment / documentation for
- (id)valueForKey:(NSString *)key;
clearly states which methods need to be implemented for -valueForKey: to work. This may even use direct ivar access. You would have to check each one in the order described there. You need to take the key path, split it up based on . and check each part on each subsequent object. To access ivars, you need to use the ObjC runtime. Look at objc/runtime.h.
All of this is vary hacky, though. What you probably want is for your objects to implement some formal protocol and then check -conformsToProtocol: before calling.
Are your key paths random strings or are those strings under your control? What are you trying to achieve? Are you solving the wrong problem?
I don't believe this is possible in a safe way (i.e. without mucking with -valueForUndefinedKey: or something similar on other peoples' classes). I say that because on the Mac side of things, Cocoa Bindings—which can be set to substitute a default value for invalid key paths—simply catches the exceptions that result from bad key paths. If even Apple's engineers don't have a way to test if a key path is valid without trying it and catching the exception, I have to assume that such a way doesn't exist.