trivago / Dobby

Swift helpers for mocking and stubbing
Apache License 2.0
165 stars 14 forks source link

Strange UnexpectedInteraction with (Function) #35

Closed duribreux closed 8 years ago

duribreux commented 8 years ago

Working on my unit tests I faced an explicit error: Dobby.StubError<(Swift.String, Swift.String, *******.Result<()> -> ()), ()>.UnexpectedInteraction("slug", "script", (Function))

The thing is, I already mock-ed a lot of methods looking pretty much the same without any trouble.

Relevant classes / enum:

public enum Result<T> {
    case Success(T)
    case Error(ErrorType)
}
public class MockExecutionHandler : ExecutionHandlerOperations {    
    public let startMock = Mock<(String, String, (Result<Void>) -> ())>()
    public let startStub = Stub<(String, String, (Result<Void>) -> ()), Void>()

    public func start(slug: String, script: String, completion: (Result<Void>) -> ()) {
        startMock.record((slug, script, completion))
        try! startStub.invoke(slug, script, completion)
    }
}

Test mock:

handlerMock.startStub.on(matches((self.slug, self.script, any()))) { 
    slug, script, completion in completion(Result.Success(())) // Problem?
}
handlerMock.startMock.expect(matches((self.slug, self.script, any())))

Executed code:

handler.start(crop.getSlug(), script: subscribedCrop.getScript()) {
    startResult in
        switch startResult {
        case .Success: completion(Result.Success(crop)) // case Result.Success(Void): Do something else 
        case .Error(let error): completion(Result.Error(error))
    }
}

As you can see, my stub should be typed as a Result<()> -> (), instead of that I've got a (Function) . Is it a bug or I am doing something wrong ?

PS: If I don't mock my ExecutionHandler and implement start this way the test passes:

public func start(slug: String, script: String, completion: (Result<Void>) -> ()) {
    completion(Result.Success())
}

Note: The code below is a working example of a similar code.

public let fetchAvailableCropMock = Mock<(Set<Sensor>?, (Result<Array<Crop>>) -> ())>()
public let fetchAvailableCropStub = Stub<(Set<Sensor>?, (Result<Array<Crop>>) -> ()), Void>()

public func fetchAvailableCrop(availableSensors: Set<Sensor>?, completion: (Result<Array<Crop>>) -> ()) {
    fetchAvailableCropMock.record((availableSensors, completion))
    try! fetchAvailableCropStub.invoke(availableSensors, completion)
}

And in the passing test:

self.storeClientMock().fetchAvailableCropStub.on(matches((any(), any()))) {
  sensors, completion in completion(Result.Success([self.crop]))
}
self.storeClientMock().fetchAvailableCropMock.expect(matches((any(),any())))

I don't see any relevant difference. Any idea ? Am I missing something obvious ?

hffmnn commented 8 years ago

Quickly glancing over the code I see one thing: Shouldn't the line slug, script, completion in completion(Result.Success(())) // Problem? be slug, script, completion in completion(Result.Success()) // Note the removed extra ()

duribreux commented 8 years ago

I tried both solution at some point, both giving the same output. I spent quite a lot of time poking around to find a solution and forgot to undo this.

Looking into my code I've found this mock which is working just fine.

self.clientMock().disconnectStub.on(any()) { completion in
    completion(Result.Success())
}
self.clientMock().disconnectMock.expect(any())
hffmnn commented 8 years ago

Hi @duribreux,

would be nice to see your actual (compilable) code, e.g. in your Executed code snippet: What is completion block. Where does it come from? The send Result also looks not correct (as it sends as string).

handler.start(crop.getSlug(), script: subscribedCrop.getScript()) {
    startResult in
        switch startResult {
        case .Success: completion(Result.Success(crop)) // case Result.Success(Void): Do something else 
        case .Error(let error): completion(Result.Error(error))
    }
}
duribreux commented 8 years ago

Sadly I can't share my project, it's not open source and there will be a business behind.

This is the full concerned method. Hope it can help you to understand what I'm trying to achieve.

private func startCrop(crop: Crop, completion: (Result<Crop>) -> ()) {
    if (!crop.activated()) {
        completion(Result.Error(CropException.Inactive(crop: crop)))
    } else {
        do {
            let subscribedCrop = try self.databaseCrop.retrieve(crop)
            let handler: ExecutionHandlerOperations = handlerFactory.get()

            handler.start(crop.getSlug(), script: subscribedCrop.getScript()) {
                startResult in
                switch startResult {
                case .Success: completion(Result.Success(crop))
                case .Error(let error): completion(Result.Error(error))
                }
            }
        } catch {
            completion(Result.Error(error))
        }
    }
}

Those are 2 different completion blocks. The one relative to the private function is (Result<Crop>) -> () and the mocked one relative to the startmethod is (Result<Void>) -> ()

If I don't mock my ExecutionHandler(Associated to ExecutionHandlerOperations protocol) I successfully reach my handler.start completion Success case.

On the other hand, I can see in debug mod that I jump from:

  1. handler.start(crop.getSlug(), script: subscribedCrop.getScript()) { to...
  2. } associated to the start method. And then...
  3. CRASH.

Execution doesn't seem to go into my handler.start completion block. Which would explain the stubbing exception I guess.

PS: If it's not helping I'll take the time to - try - to reproduce on a basic project.

felixvisee commented 8 years ago

Hey @duribreux, can you please verify that your expected slug and script match the actual values, for example, by invoking your stub right after having set up the behavior:

handlerMock.startStub.invoke((self.slug, self.script, { _ in }))
duribreux commented 8 years ago

I've took some times to create a sample project to hopefully being able to reproduce the problem but everything worked just fine.. The sample is a bit easier (no injection, fewer methods) but the behavior is pretty much the same.

Conclusion: There is probably a sneaky bug in my library. Since it doesn't seem related to Dobby, I guess you can close this issue. I can eventually keep you updated when I'll find the solution.

If you want to take a look, just pod install, open workspace and cmd+u

DobbySample.zip

Thanks for your time.

duribreux commented 8 years ago

Gosh I hate myself.

In my injection overriding I wrote:

binder
  .bind(ExecutionHandlerOperations.self)
  .tagged(with: APSModule.ExecutionHandlerProvider.self)
  .to(factory: MockExecutionHandler.init) // Creates a new instance when I call the get() method

Instead of:

binder
  .bind(ExecutionHandlerOperations.self)
  .tagged(with: APSModule.ExecutionHandlerProvider.self)
  .to(value: MockExecutionHandler()) // Returns the same instance

During the execution of start, handlerFactory.get() was called, creating a new instance of MockExecutionHandler which was obviously not the same object than my mock-ed one in my test causing the stub unexpected behavior...

let handler: ExecutionHandlerOperations = handlerFactory.get() // This is the freakin (Function) I guess

And yes.. that was obvious :|

Thanks for your time and work !

felixvisee commented 8 years ago

@duribreux cool! Let us know if you have any further questions :)