Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement smarter KVC handling for class fakes #357

Merged
merged 1 commit into from
Jan 15, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 45 additions & 15 deletions Source/Doubles/CDRClassFake.mm
Original file line number Diff line number Diff line change
Expand Up @@ -32,36 +32,66 @@ - (Class)class {
return self.klass;
}

- (void)handleKVCSelector:(SEL)sel withValue:(id)value forKey:(NSString *)key {
#pragma mark - KVC Overrides

- (void)setValue:(id)value forKey:(NSString *)key {
[self handleKVCSelector:_cmd withValue:value forKey:key];
}

- (void)setValue:(id)value forKeyPath:(NSString *)key {
[self handleKVCSelector:_cmd withValue:value forKey:key];
}

- (id)valueForKey:(NSString *)key {
return [self handleKVCSelector:_cmd forKey:key];
}

- (id)valueForKeyPath:(NSString *)key {
return [self handleKVCSelector:_cmd forKey:key];
}

#pragma mark - KVC Handling Implementation

- (void)handleAllowedKVCSelector:(SEL)sel withValue:(id)value forKey:(NSString *)key {
NSMethodSignature *signature = [self.klass methodSignatureForSelector:sel];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setSelector:sel];
[invocation setArgument:&value atIndex:2];
[invocation setArgument:&key atIndex:3];
[self.cedar_double_impl record_method_invocation:invocation];
[self forwardInvocation:invocation];
}

- (void)setValue:(id)value forKey:(NSString *)key {
if ([self has_stubbed_method_for:_cmd]) {
[self handleKVCSelector:_cmd withValue:value forKey:key];
- (void)handleKVCSelector:(SEL)sel withValue:(id)value forKey:(NSString *)key {
if ([self has_stubbed_method_for:sel] || !self.requiresExplicitStubs) {
[self handleAllowedKVCSelector:sel withValue:value forKey:key];
} else {
[self setValue:value forUndefinedKey:key];
[[NSException exceptionWithName:NSInternalInconsistencyException
reason:[NSString stringWithFormat:@"Attempting to set value <%@> for key <%@>, which must be stubbed first", value, key]
userInfo:nil] raise];
}
}

- (void)setValue:(id)value forKeyPath:(NSString *)key {
if ([self has_stubbed_method_for:_cmd]) {
[self handleKVCSelector:_cmd withValue:value forKey:key];
} else {
[self setValue:value forUndefinedKey:key];
}
- (id)handleAllowedKVCSelector:(SEL)sel forKey:(NSString *)key {
NSMethodSignature *signature = [self.klass methodSignatureForSelector:sel];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setSelector:sel];
[invocation setArgument:&key atIndex:2];
[self forwardInvocation:invocation];

__unsafe_unretained id returnValue;
[invocation getReturnValue:&returnValue];

return returnValue;
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
if (self.requiresExplicitStubs) {
- (id)handleKVCSelector:(SEL)sel forKey:(NSString *)key {
if ([self has_stubbed_method_for:sel] || !self.requiresExplicitStubs) {
return [self handleAllowedKVCSelector:sel forKey:key];
} else {
[[NSException exceptionWithName:NSInternalInconsistencyException
reason:[NSString stringWithFormat:@"Attempting to set value <%@> for key <%@>, which must be stubbed first", value, key]
reason:[NSString stringWithFormat:@"Attempting to get value for key <%@>, which must be stubbed first", key]
userInfo:nil] raise];
return nil;
}
}

Expand Down
173 changes: 147 additions & 26 deletions Spec/Doubles/CDRClassFakeSpec.mm
Original file line number Diff line number Diff line change
Expand Up @@ -138,51 +138,172 @@
});
});

describe(@"using Key Value Coding to set values", ^{
describe(@"using Key Value Coding", ^{
__block ObjectWithWeakDelegate *fake;
__block id delegate;

describe(@"a nice fake", ^{
describe(@"to set values", ^{
beforeEach(^{
fake = nice_fake_for([ObjectWithWeakDelegate class]);
delegate = nice_fake_for(@protocol(ExampleDelegate));
});

it(@"should not blow up, silently failing when setValue:forKey: is invoked", ^{
[fake setValue:nice_fake_for(@protocol(ExampleDelegate)) forKey:@"delegate"];
describe(@"a nice fake", ^{
beforeEach(^{
fake = nice_fake_for([ObjectWithWeakDelegate class]);
});

fake.delegate should be_nil;
});
});
sharedExamplesFor(@"silently handling KVC setters", ^(NSDictionary *sharedContext) {
it(@"should not blow up, silently failing when setValue:forKey: is invoked", ^{
[fake setValue:nice_fake_for(@protocol(ExampleDelegate)) forKey:sharedContext[@"key"]];
fake.delegate should be_nil;
});

it(@"should not blow up, silently failing when setValue:forKeyPath: is invoked", ^{
[fake setValue:nice_fake_for(@protocol(ExampleDelegate)) forKeyPath:sharedContext[@"key"]];
fake.delegate should be_nil;
});

it(@"should record that it has received setValue:forKey:", ^{
[fake setValue:delegate forKey:sharedContext[@"key"]];
fake should have_received(@selector(setValue:forKey:)).with(delegate, sharedContext[@"key"]);
});

it(@"should record that it has received setValue:forKeyPath:", ^{
[fake setValue:delegate forKeyPath:sharedContext[@"key"]];
fake should have_received(@selector(setValue:forKeyPath:)).with(delegate, sharedContext[@"key"]);
});
});

describe(@"with a key that is KVC-compliant", ^{
itShouldBehaveLike(@"silently handling KVC setters", ^(NSMutableDictionary *context) {
context[@"key"] = @"delegate";
});
});

describe(@"a strict fake", ^{
__block id delegate;
beforeEach(^{
delegate = nice_fake_for(@protocol(ExampleDelegate));
fake = fake_for([ObjectWithWeakDelegate class]);
describe(@"with a key that is not KVC-compliant", ^{
itShouldBehaveLike(@"silently handling KVC setters", ^(NSMutableDictionary *context) {
context[@"key"] = @"bogus";
});
});
});

context(@"when not stubbed first", ^{
it(@"should blow up when setValue:forKey: is invoked", ^{
^{
describe(@"a strict fake", ^{
beforeEach(^{
fake = fake_for([ObjectWithWeakDelegate class]);
});

context(@"when not stubbed first", ^{
it(@"should blow up when setValue:forKey: is invoked", ^{
^{
[fake setValue:delegate forKey:@"delegate"];
} should raise_exception.with_name(NSInternalInconsistencyException).with_reason([NSString stringWithFormat:@"Attempting to set value <%@> for key <%@>, which must be stubbed first", delegate, @"delegate"]);
});
});

context(@"when stubbed", ^{
it(@"should happily receive -setValue:forKey:", ^{
fake stub_method(@selector(setValue:forKey:));

[fake setValue:delegate forKey:@"delegate"];
} should raise_exception.with_name(NSInternalInconsistencyException).with_reason([NSString stringWithFormat:@"Attempting to set value <%@> for key <%@>, which must be stubbed first", delegate, @"delegate"]);

fake should have_received(@selector(setValue:forKey:)).with(delegate).and_with(@"delegate");
});

it(@"should happily receive -setValue:forKeyPath:", ^{
fake stub_method(@selector(setValue:forKeyPath:));

[fake setValue:delegate forKeyPath:@"delegate"];

fake should have_received(@selector(setValue:forKeyPath:)).with(delegate).and_with(@"delegate");
});
});
});
});

describe(@"to get values", ^{
describe(@"a nice fake", ^{
beforeEach(^{
fake = nice_fake_for([ObjectWithWeakDelegate class]);
});

sharedExamplesFor(@"silently handling KVC getters", ^(NSDictionary *sharedContext) {
it(@"should not blow up, returning nil when valueForKey: is invoked", ^{
[fake valueForKey:sharedContext[@"key"]] should be_nil;
});

it(@"should not blow up, returning nil when valueForKeyPath: is invoked", ^{
[fake valueForKeyPath:sharedContext[@"key"]] should be_nil;
});

it(@"should record that it has received valueForKey:", ^{
[fake valueForKey:sharedContext[@"key"]];
fake should have_received(@selector(valueForKey:)).with(sharedContext[@"key"]);
});

it(@"should record that it has received valueForKeyPath:", ^{
[fake valueForKeyPath:sharedContext[@"key"]];
fake should have_received(@selector(valueForKeyPath:)).with(sharedContext[@"key"]);
});
});

describe(@"with a key that is KVC-compliant", ^{
itShouldBehaveLike(@"silently handling KVC getters", ^(NSMutableDictionary *context) {
context[@"key"] = @"delegate";
});
});

describe(@"with a key that is not KVC-compliant", ^{
itShouldBehaveLike(@"silently handling KVC getters", ^(NSMutableDictionary *context) {
context[@"key"] = @"bogus";
});
});
});

context(@"when stubbed", ^{
it(@"should happily receive -setValue:forKey:", ^{
fake stub_method(@selector(setValue:forKey:));
describe(@"a strict fake", ^{
beforeEach(^{
fake = fake_for([ObjectWithWeakDelegate class]);
});

context(@"when not stubbed first", ^{
it(@"should blow up when valueForKey: is invoked", ^{
^{
[fake valueForKey:@"delegate"];
} should raise_exception.with_name(NSInternalInconsistencyException).with_reason([NSString stringWithFormat:@"Attempting to get value for key <%@>, which must be stubbed first", @"delegate"]);
});
});

context(@"when stubbed with no return value", ^{
it(@"should happily receive -valueForKey: and return nil", ^{
fake stub_method(@selector(valueForKey:));

[fake valueForKey:@"delegate"];

fake should have_received(@selector(valueForKey:)).with(@"delegate");
});

it(@"should happily receive -valueForKeyPath:", ^{
fake stub_method(@selector(valueForKeyPath:));

[fake setValue:delegate forKey:@"delegate"];
[fake valueForKeyPath:@"delegate"];

fake should have_received(@selector(setValue:forKey:)).with(delegate).and_with(@"delegate");
fake should have_received(@selector(valueForKeyPath:)).with(@"delegate");
});
});

it(@"should happily receive -setValue:forKeyPath:", ^{
fake stub_method(@selector(setValue:forKeyPath:));
context(@"when stubbed with a return value", ^{
beforeEach(^{
delegate = nice_fake_for(@protocol(ExampleDelegate));
});

[fake setValue:delegate forKeyPath:@"delegate"];
it(@"should receive -valueForKey: and return the stubbed value", ^{
fake stub_method(@selector(valueForKey:)).with(@"delegate").and_return(delegate);
[fake valueForKey:@"delegate"] should be_same_instance_as(delegate);
});

fake should have_received(@selector(setValue:forKeyPath:)).with(delegate).and_with(@"delegate");
it(@"should receive -valueForKeyPath: and return the stubbed value", ^{
fake stub_method(@selector(valueForKeyPath:)).with(@"delegate").and_return(delegate);
[fake valueForKeyPath:@"delegate"] should be_same_instance_as(delegate);
});
});
});
});
Expand Down