typealiased / mockingbird

A Swifty mocking framework for Swift and Objective-C.

Home Page:https://mockingbirdswift.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Receiving "Cannot infer the argument position of 'any()' " when used with optional named parameters

Nickersoft opened this issue · comments

New Issue Checklist

  • [X ] I updated my Mockingbird framework and CLI to the latest version
  • [ X] I searched for existing GitHub issues

Description

I recently upgraded my Mockingbird installation, and suddenly started receiving slews of the following error across my entire test suite when running my tests:

Cannot infer the argument position of 'any()' when used in this context

Wrap usages of 'any()' in an explicit argument position, for example:
   firstArg(any())
   secondArg(any())
   arg(any(), at: 3) (co.bird.mockingbird.TestFailure)

Almost every instance where this is occurring is a case in which the method has default parameters. For example, if the protocol is:

protocol MyServicable {
  func makeAPICall(endpoint: string, callback: (() -> Void)?)
}

and it has the extension:

extension MyServicable {
  func makeAPICall(endpoint: string, callback: (() -> Void)? = nil) {
    return self.makeAPICall(endpoint, callback);
  }
}

and it has being mocked as:

let service = mock(MyServicable.self)

service.makeAPICall("https://my.com/endpoint", callback: any())

then this error is thrown. This wasn't happening with previous versions of Mockingbird, so I'm wondering what might have changed to cause this issue.

Framework Bugs

You should be able to write a reproduction case following the example above – if needed I can set up a separate repo to demonstrate.

Environment

  • Mockingbird CLI version (mockingbird version) 0.18.1
  • Xcode and macOS version (are you running a beta?) XCode 12.5.1, macOS 11.5.2
  • Swift version (swift --version) 5.4.2
  • Installation method (CocoaPods, Carthage, from source, etc) CocoaPods
  • Unit testing framework (XCTest, Quick + Nimble, etc) Quick + Nimble
  • Does your project use .mockingbird-ignore? No
  • Are you using supporting source files? Yes

Thanks for reporting, a few additional questions:

  • Can you provide the full test callsite that’s failing (including the given or verify)?
  • What version are you upgrading from?

At a high level, the issue is that it’s choosing the Objective-C overload for given or verify over the Swift variant. For example, this can happen if the block type doesn’t match the expected one:

// Fails
given(service.makeAPICall(endpoint: "https://my.com/endpoint", callback: any())).will {
  /* no-op */
}

// Succeeds
given(service.makeAPICall(endpoint: "https://my.com/endpoint", callback: any())).will {
  endpoint, callback in /* no-op */
}

This can also happen if you’re upgrading from an older version of Mockingbird which incorrectly mocked methods defined in extensions. For example:

// Fails in 0.18 but "succeeds" in 0.16 and is a compiler error in 0.17
given(service.makeAPICall(endpoint: "https://my.com/endpoint")).will {
  /* will never be called */
}

// Succeeds
given(service.makeAPICall(endpoint: "https://my.com/endpoint", callback: any())).will {
  endpoint, callback in /* no-op */
}

Overall I agree the error message here is confusing, but I’m not sure if there’s a great alternative outside of removing the overloads (and de-unifying the syntax for Objective-C and Swift).

Hey @andrewchang-bird! Thanks for the response... I upgraded from 0.16.0 to 0.18.1. The full call site is along the lines of:

given(service.makeAPICall(endpoint: "https://my.com/endpoint", callback: any())).willReturn(.success(MyObject()))

assuming the service returns an enum of { success(val), failure(err) }. I'd prefer finding a solution within my test suite itself... removing the overloads would effectively mean refactoring my entire codebase seeing all of the optional values would then become required, and my DI uses protocols to access all the injected services.

Thanks for the additional info. I’m having trouble repro’ing the error with the following:

/* Source */
enum ServiceResult {
  case success(Bool)
  case failure(Error)
}

protocol MyServicable {
  func makeAPICall(endpoint: String, callback: (() -> Void)?) -> ServiceResult
}

extension MyServicable {
  func makeAPICall(endpoint: String, callback: (() -> Void)? = nil) -> ServiceResult {
    return self.makeAPICall(endpoint: endpoint, callback: callback)
  }
}

/* Test */
func testProtocolExtensionAny() {
    let service = mock(MyServicable.self)
    given(service.makeAPICall(endpoint: "https://my.com/endpoint", callback: any()))
      .willReturn(.success(true))
  }

What happens if you manually cast the invocation to Mockable? E.g.

given(service.makeAPICall(...) as Mockable).willReturn(...)

Hmm.. so I wasn't able to cast it because Mockable is generic and I wasn't sure what to use for type parameters. That said, I think I've uncovered the issue. In my case, my methods had multiple optional parameters in the signature, but I was only using any() on one and omitting the others. This is why I think it was prioritizing the Objective-C signature.

So if my method was actually:

func makeAPICall(endpoint: String, arg1: String? = nil, arg2: String? = nil)

I was mocking it as:

given(service.makeAPICall(endpoint: "myendpoint.com", arg2: any())).willReturn(.success(MyObject())

Changing it to

given(service.makeAPICall(endpoint: "myendpoint.com", arg1: any(), arg2: any())).willReturn(.success(MyObject())

Fixes the issue. Just to clarify, when you said that it is choosing the Objective-C overload... I noticed in the generated mocks two signatures for my methods: one that uses Swift scalars, and one that uses @autoclosure () ->. Is the @autoclosure overload the Objective-C one?

At a high level, Mockingbird generates two methods for Swift testing:

  1. A mocked method with the normal parameter types
  2. A helper method for calling given and verify in tests with @autoclosure parameter types, which returns a Mockable type

In addition, the testing API itself has overloads for given and verify to support Objective-C mocking. For example, given has:

  1. The one used for Swift types which takes in a generic Mockable type
  2. The one used for Objective-C types which takes in any return type

So using any() only for some parameters will route to the extension method which doesn’t return a Mockable type, and hence pick the Objective-C given overload.

Ah, makes sense! Cool, so I'm going to close this out seeing I managed to find a solution. Thanks for the help!