Skip to content

Commit

Permalink
Fix wildcard arg matching for Obj-C param types (#247)
Browse files Browse the repository at this point in the history
Dynamic mocking (introduced in 0.18) changed how Objective-C parameter
types are faked for wildcard argument matching. To support Objective-C
mocking, argument matchers for `NSObjectProtocol` parameters no longer
create type facades that go through the resolution context thread, but
instead create dynamic `NSProxy` doubles.

Dynamic type facades weren’t set up to be used from Swift; this change
enables Swift to properly resolve the type facades.
  • Loading branch information
andrewchang-bird authored Jan 6, 2022
1 parent 5a5e732 commit d046f75
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 8 deletions.
8 changes: 8 additions & 0 deletions Mockingbird.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@
28C8E5DC26A64D6C00C68A1D /* MKBInvocationHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 28C8E5DA26A64D6C00C68A1D /* MKBInvocationHandler.m */; };
28D08CD62775338100AE7C39 /* OptionGroupArgumentEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28D08CD52775338100AE7C39 /* OptionGroupArgumentEncoding.swift */; };
28D08CCE2774247C00AE7C39 /* OptionalsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28D08CCD2774247C00AE7C39 /* OptionalsTests.swift */; };
28D08CD0277477F700AE7C39 /* ObjCParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28D08CCF277477F700AE7C39 /* ObjCParameters.swift */; };
28D08CD327747A4B00AE7C39 /* ObjectiveCParameterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28D08CD1277479B600AE7C39 /* ObjectiveCParameterTests.swift */; };
28DAD96E251BDD66001A0B3F /* Project.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28DAD96D251BDD66001A0B3F /* Project.swift */; };
28DDDFC126B8571D002556C7 /* DynamicCast.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28DDDFC026B8571D002556C7 /* DynamicCast.swift */; };
8356225C26A94CBE005CD5C5 /* TargetDescriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8356225B26A94CBE005CD5C5 /* TargetDescriptionTests.swift */; };
Expand Down Expand Up @@ -625,6 +627,8 @@
28C8E5DA26A64D6C00C68A1D /* MKBInvocationHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MKBInvocationHandler.m; sourceTree = "<group>"; };
28D08CD52775338100AE7C39 /* OptionGroupArgumentEncoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionGroupArgumentEncoding.swift; sourceTree = "<group>"; };
28D08CCD2774247C00AE7C39 /* OptionalsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalsTests.swift; sourceTree = "<group>"; };
28D08CCF277477F700AE7C39 /* ObjCParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjCParameters.swift; sourceTree = "<group>"; };
28D08CD1277479B600AE7C39 /* ObjectiveCParameterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectiveCParameterTests.swift; sourceTree = "<group>"; };
28DAD96D251BDD66001A0B3F /* Project.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Project.swift; sourceTree = "<group>"; };
28DDDFC026B8571D002556C7 /* DynamicCast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicCast.swift; sourceTree = "<group>"; };
8356225B26A94CBE005CD5C5 /* TargetDescriptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TargetDescriptionTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1335,6 +1339,7 @@
OBJ_169 /* KeywordArgumentNames.swift */,
28A1F3C326ADD57C002F282D /* MinimalTestTypes.swift */,
OBJ_196 /* ModuleImportCases.swift */,
28D08CCF277477F700AE7C39 /* ObjCParameters.swift */,
OBJ_198 /* OpaquelyInheritedTypes.swift */,
OBJ_199 /* Optionals.swift */,
OBJ_200 /* OverloadedMethods.swift */,
Expand Down Expand Up @@ -1526,6 +1531,7 @@
OBJ_260 /* InitializerTests.swift */,
OBJ_261 /* LastSetValueStubTests.swift */,
28719AF426B23AB200C38C2C /* ObjectiveCTests.swift */,
28D08CD1277479B600AE7C39 /* ObjectiveCParameterTests.swift */,
28D08CCD2774247C00AE7C39 /* OptionalsTests.swift */,
OBJ_262 /* OrderedVerificationTests.swift */,
OBJ_263 /* OverloadedMethodTests.swift */,
Expand Down Expand Up @@ -2194,6 +2200,7 @@
OBJ_1042 /* InitializersMockableTests.swift in Sources */,
OBJ_1043 /* InoutParametersMockableTests.swift in Sources */,
OBJ_1044 /* InoutParametersStubbableTests.swift in Sources */,
28D08CD327747A4B00AE7C39 /* ObjectiveCParameterTests.swift in Sources */,
OBJ_1045 /* KeywordArgumentNamesMockableTests.swift in Sources */,
OBJ_1046 /* KeywordArgumentNamesStubbableTests.swift in Sources */,
OBJ_1047 /* SubscriptMockableTests.swift in Sources */,
Expand Down Expand Up @@ -2250,6 +2257,7 @@
OBJ_1124 /* ChildProtocol.swift in Sources */,
OBJ_1125 /* ClassInitializers.swift in Sources */,
OBJ_1126 /* ClassOnlyProtocols.swift in Sources */,
28D08CD0277477F700AE7C39 /* ObjCParameters.swift in Sources */,
OBJ_1127 /* ClassScopedTypes.swift in Sources */,
OBJ_1128 /* ClosureParameters.swift in Sources */,
OBJ_1129 /* Collections.swift in Sources */,
Expand Down
17 changes: 16 additions & 1 deletion Sources/MockingbirdFramework/Matching/TypeFacade.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import Foundation
/// guarantees and autocompletion compared to conforming all parameter types to a common protocol.
///
/// It goes without saying that this should probably never be done in production.

private class ResolutionContext {
enum Constants {
static let contextKey = DispatchSpecificKey<ResolutionContext>()
Expand Down Expand Up @@ -105,6 +104,7 @@ func createTypeFacade<T: NSObjectProtocol>(_ value: Any?) -> T {
func resolve<T>(_ parameter: () -> T) -> ArgumentMatcher {
let resolvedValue = ResolutionContext().resolveTypeFacade(parameter)
if let matcher = resolvedValue as? ArgumentMatcher { return matcher }
if let boxedValue = resolveObjCTypeFacade(resolvedValue) { return boxedValue }
if let typedValue = resolvedValue as? T { return ArgumentMatcher(typedValue) }
return ArgumentMatcher(resolvedValue)
}
Expand All @@ -113,6 +113,7 @@ func resolve<T>(_ parameter: () -> T) -> ArgumentMatcher {
func resolve<T: Equatable>(_ parameter: () -> T) -> ArgumentMatcher {
let resolvedValue = ResolutionContext().resolveTypeFacade(parameter)
if let matcher = resolvedValue as? ArgumentMatcher { return matcher }
if let boxedValue = resolveObjCTypeFacade(resolvedValue) { return boxedValue }
if let typedValue = resolvedValue as? T { return ArgumentMatcher(typedValue) }
return ArgumentMatcher(resolvedValue)
}
Expand All @@ -121,3 +122,17 @@ func resolve<T: Equatable>(_ parameter: () -> T) -> ArgumentMatcher {
func resolve(_ parameter: @escaping () -> ArgumentMatcher) -> ArgumentMatcher {
return parameter()
}

/// Check whether this is an Objective-C type facade and try to unbox the value.
private func resolveObjCTypeFacade(_ value: Any?) -> ArgumentMatcher? {
let objectValue = value as AnyObject
if objectValue.responds(to: Selector(("mkb_isTypeFacade"))) {
let boxedObject = objectValue.perform(Selector(("mkb_boxedObject")))?.takeRetainedValue()
if let matcher = boxedObject as? ArgumentMatcher {
return ArgumentMatcher(matcher)
} else {
return ArgumentMatcher(boxedObject)
}
}
return nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@ NS_ASSUME_NONNULL_BEGIN

@interface MKBTypeFacade<T> : NSProxy

@property (nonatomic, strong, readonly) id boxedObject;
@property (nonatomic, strong, readonly) MKBConcreteMock *mock;
@property (nonatomic, strong, readonly) id mkb_boxedObject;
@property (nonatomic, strong, readonly) MKBConcreteMock *mkb_mock;

/// Used to check whether an instance is a type facade.
/// Callers can just check whether the instance responds to the `mkb_isTypeFacade` selector and
/// ignore the value of this property.
@property (nonatomic, assign, readonly) bool mkb_isTypeFacade;

- (instancetype)initWithMock:(id)mock object:(id)object NS_DESIGNATED_INITIALIZER;
- (T)fixupType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ @implementation MKBTypeFacade
- (instancetype)initWithMock:(id)mock object:(id)object
{
if (self) {
_mock = mock;
_boxedObject = object;
_mkb_mock = mock;
_mkb_boxedObject = object;
_mkb_isTypeFacade = YES;
}
return self;
}
Expand All @@ -31,11 +32,32 @@ + (id)createFromObject:(id)object
return object;
}

- (bool)isTypeFacadeSelector:(SEL)aSelector {
return aSelector == @selector(mkb_boxedObject)
|| aSelector == @selector(mkb_mock)
|| aSelector == @selector(mkb_isTypeFacade);
}

#pragma mark - NSProxy

- (void)forwardInvocation:(NSInvocation *)invocation
{
[invocation setTarget:self.mock];
if ([self isTypeFacadeSelector:invocation.selector]) {
[invocation setTarget:self];
} else {
[invocation setTarget:self.mkb_mock];
}
}

- (BOOL)respondsToSelector:(SEL)aSelector
{
return [(NSObject *)self.mkb_mock respondsToSelector:aSelector]
|| [self isTypeFacadeSelector:aSelector];
}

- (NSMethodSignature *_Nullable)methodSignatureForSelector:(SEL)aSelector
{
return [(NSObject *)self.mkb_mock methodSignatureForSelector:aSelector];
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ - (MKBArgumentMatcher *)serializeArgumentAtIndex:(NSUInteger)index
[invocation getArgument:&value atIndex:index];

// Unwrapped boxed types within type facades.
if ([[value class] isSubclassOfClass:[MKBTypeFacade class]]) {
value = ((MKBTypeFacade *)value).boxedObject;
if ([value respondsToSelector:@selector(mkb_isTypeFacade)]) {
value = ((MKBTypeFacade *)value).mkb_boxedObject;
}

// Use argument matchers directly.
Expand Down
14 changes: 14 additions & 0 deletions Sources/MockingbirdTestsHost/ObjCParameters.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// ObjCParameters.swift
// MockingbirdTestsHost
//
// Created by typealias on 12/22/21.
//

import AppKit
import Foundation

protocol ObjCParameters {
func method(value: NSViewController) -> Bool
func method(optionalValue: NSViewController?) -> Bool
}
114 changes: 114 additions & 0 deletions Tests/MockingbirdTests/Framework/ObjectiveCParameterTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
//
// ObjectiveCParameterTests.swift
// MockingbirdTestsHost
//
// Created by typealias on 12/22/21.
//

import Mockingbird
@testable import MockingbirdTestsHost
import XCTest

class ObjectiveCParameterTests: BaseTestCase {

var parametersMock: ObjCParametersMock!
var parametersInstance: ObjCParameters { parametersMock }

override func setUpWithError() throws {
self.parametersMock = mock(ObjCParameters.self)
}

func testExactParameterMatching() {
let instance = NSViewController()
given(parametersMock.method(value: instance)).willReturn(true)
XCTAssertTrue(parametersInstance.method(value: instance))
verify(parametersMock.method(value: instance)).wasCalled()
}
func testExactParameterMatching_stubbingOperator() {
let instance = NSViewController()
given(parametersMock.method(value: instance)) ~> true
XCTAssertTrue(parametersInstance.method(value: instance))
verify(parametersMock.method(value: instance)).wasCalled()
}

func testExactNilParameterMatching() {
given(parametersMock.method(optionalValue: nil)).willReturn(true)
XCTAssertTrue(parametersInstance.method(optionalValue: nil))
verify(parametersMock.method(optionalValue: nil)).wasCalled()
}
func testExactNilParameterMatching_stubbingOperator() {
given(parametersMock.method(optionalValue: nil)) ~> true
XCTAssertTrue(parametersInstance.method(optionalValue: nil))
verify(parametersMock.method(optionalValue: nil)).wasCalled()
}

func testWildcardParameterMatchingAny() {
given(parametersMock.method(value: any())).willReturn(true)
XCTAssertTrue(parametersInstance.method(value: NSViewController()))
verify(parametersMock.method(value: any())).wasCalled()
}
func testWildcardParameterMatchingAny_stubbingOperator() {
given(parametersMock.method(value: any())) ~> true
XCTAssertTrue(parametersInstance.method(value: NSViewController()))
verify(parametersMock.method(value: any())).wasCalled()
}

func testWildcardOptionalParameterMatchingAny() {
given(parametersMock.method(optionalValue: any())).willReturn(true)
XCTAssertTrue(parametersInstance.method(optionalValue: nil))
verify(parametersMock.method(optionalValue: any())).wasCalled()
}
func testWildcardOptionalParameterMatchingAny_stubbingOperator() {
given(parametersMock.method(optionalValue: any())) ~> true
XCTAssertTrue(parametersInstance.method(optionalValue: nil))
verify(parametersMock.method(optionalValue: any())).wasCalled()
}

func testWildcardParameterMatchingAnyWhere() {
let instance = NSViewController()
given(parametersMock.method(value: any(where: { $0 === instance }))).willReturn(true)
XCTAssertTrue(parametersInstance.method(value: instance))
verify(parametersMock.method(value: any(where: { $0 === instance }))).wasCalled()
}
func testWildcardParameterMatchingAnyWhere_stubbingOperator() {
let instance = NSViewController()
given(parametersMock.method(value: any(where: { $0 === instance }))) ~> true
XCTAssertTrue(parametersInstance.method(value: instance))
verify(parametersMock.method(value: any(where: { $0 === instance }))).wasCalled()
}

func testWildcardParameterMatchingNotNil() {
given(parametersMock.method(value: notNil())).willReturn(true)
XCTAssertTrue(parametersInstance.method(value: NSViewController()))
verify(parametersMock.method(value: notNil())).wasCalled()
}
func testWildcardParameterMatchingNotNil_stubbingOperator() {
given(parametersMock.method(value: notNil())) ~> true
XCTAssertTrue(parametersInstance.method(value: NSViewController()))
verify(parametersMock.method(value: notNil())).wasCalled()
}

func testWildcardOptionalParameterMatchingNotNil() {
given(parametersMock.method(optionalValue: notNil())).willReturn(true)
XCTAssertTrue(parametersInstance.method(optionalValue: NSViewController()))
verify(parametersMock.method(optionalValue: notNil())).wasCalled()
}
func testWildcardOptionalParameterMatchingNotNil_stubbingOperator() {
given(parametersMock.method(optionalValue: notNil())) ~> true
XCTAssertTrue(parametersInstance.method(optionalValue: NSViewController()))
verify(parametersMock.method(optionalValue: notNil())).wasCalled()
}

func testWildcardOptionalParameterDoesNotMatchNil() {
shouldFail {
given(self.parametersMock.method(optionalValue: notNil())).willReturn(true)
XCTAssertTrue(self.parametersInstance.method(optionalValue: nil))
}
}
func testWildcardOptionalParameterDoesNotMatchNil_stubbingOperator() {
shouldFail {
given(self.parametersMock.method(optionalValue: notNil())) ~> true
XCTAssertTrue(self.parametersInstance.method(optionalValue: nil))
}
}
}

0 comments on commit d046f75

Please sign in to comment.