typealiased / mockingbird

A Swifty mocking framework for Swift and Objective-C.
https://mockingbirdswift.com
MIT License
656 stars 81 forks source link

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

Closed Nickersoft closed 3 years ago

Nickersoft commented 3 years ago

New Issue Checklist

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

andrewchang-bird commented 3 years ago

Thanks for reporting, a few additional questions:

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).

Nickersoft commented 3 years ago

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.

andrewchang-bird commented 3 years ago

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(...)
Nickersoft commented 3 years ago

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?

andrewchang-bird commented 3 years ago

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.

Nickersoft commented 3 years ago

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