seanhenry / SwiftMockGeneratorForXcode

An Xcode extension (plugin) to generate Swift test doubles automatically.
MIT License
748 stars 47 forks source link

[Feature request] Add expectation #31

Closed jimmya closed 4 years ago

jimmya commented 4 years ago

Hi,

First of all thanks for the great tool! In some of my code I heavily rely on PromiseKit. And while the stub completion results provide great support for working with closures etc. when working with Promises I've got some issues In ensuring all of my code is executed when making my asserts. The same applies to working with stuff that's called in UIView.animate completion blocks. Currently I'm modifying the generated mocks by adding a XCTestExpectation variable I can set that will be fulfilled when the function is called.

I think it would be great if this could be added to the default generation as it would mean I no longer have to manually modify the generated code. My custom implementation currently looks something like this:

protocol MyProtocol {

    func someFunctionThatsCalledInAnimationBlock()
}

final class MyMock: MyProtocol {
    var invokedSomeFunctionThatsCalledInAnimationBlock = false
    var invokedSomeFunctionThatsCalledInAnimationBlockCount = 0
    var someFunctionThatsCalledInAnimationBlockExpectation: XCTestExpectation?
    func someFunctionThatsCalledInAnimationBlock() {
        invokedSomeFunctionThatsCalledInAnimationBlock = true
        invokedSomeFunctionThatsCalledInAnimationBlockCount += 1
        someFunctionThatsCalledInAnimationBlockExpectation?.fulfill()
    }
}

I hope you would consider adding this.

Best regards,

Jimmy

seanhenry commented 4 years ago

Hi Jimmy,

Thanks for raising this issue and I'm pleased you're finding this tool useful. The templates I'm using are trying to follow the test doubles described in this article and I would like to keep the generated code as simple as possible so that it could be handwritten if required.

However, I think this trick might help. Create an intermediate mock which is generated by this tool. Then extend this class with your mock containing your expectation.

protocol MyProtocol {

    func someFunctionThatsCalledInAnimationBlock()
}

class _MyMock: MyProtocol {
    var invokedSomeFunctionThatsCalledInAnimationBlock = false
    var invokedSomeFunctionThatsCalledInAnimationBlockCount = 0
    var someFunctionThatsCalledInAnimationBlockExpectation: XCTestExpectation?
    func someFunctionThatsCalledInAnimationBlock() {
        invokedSomeFunctionThatsCalledInAnimationBlock = true
        invokedSomeFunctionThatsCalledInAnimationBlockCount += 1
        someFunctionThatsCalledInAnimationBlockExpectation?.fulfill()
    }
}

final class MyMock: _MyMock {
    var someFunctionThatsCalledInAnimationBlockExpectation: XCTestExpectation?
    override func someFunctionThatsCalledInAnimationBlock() {
        super. someFunctionThatsCalledInAnimationBlock()
        someFunctionThatsCalledInAnimationBlockExpectation?.fulfill()
    }
}

Whilst this will prevent you from adding the expectation after the mock is regenerated, you will still have to manually write the initial subclass. I have suggested a feature previously which was to enable custom templates. So you could copy an existing template and add your modifications as you wish. If that would solve your problem then we could change this feature request to implement that feature instead? And if it gets enough support then I will build it.

tiarnann commented 2 years ago

@seanhenry I have a feature idea that solves this and it'd likely be less effort than directly adding an expectation or custom templates.

Problem Description

The problem I'm running into is that I have a ViewModel I'd like to test. Inside the ViewModel there's a function which performs an asynchronous request.

protocol ViewModelDelegate: AnyObject {
      func didFetch(data: Data)
}

class ViewModel {
    let network: Network
    var delegate: ViewModelDelegate?

    func performNetworkRequest() {
          network.shared.performRequest({ [delegate] data in
              delegate?.didFetch(data: data)
          })
    }
}

In my test, I'd like to wait until the delegate is called to make an assertion.

class ViewModelTest: XCTestCase {
     var network: MockNetwork!
     var delegate: MockViewModelDelegate!
     var viewModel: ViewModel!

     override func setUp() {
          /*..setup..*/
     }

     func test_performNetworkRequest() {
          let testData = Data()
          network.stubbedPerformRequestResult = testData

          viewModel.performNetworkRequest()

          // I want to block here

          XCTAssertEqual(delegate.invokedDidFetchParameters.data, testData)
     }
}

Solution/Feature Idea

In addition to generating the invoked/stubbed members, we generate a "perform" member which is a closure accepting the same parameters as the method.

class MockViewModelDelegate: ViewModelDelegate {
    var performDidFetch: ((data: Data) -> Void)?

    func didFetch(data: Data) {
        performDidFetch?(data)
    }
}

This would allow for the caller to set behaviour to occur after the method is called. This could even be the use of an expectation like in Jimmy's case.

func test_performNetworkRequest() {
          let testData = Data()
          network.stubbedPerformRequestResult = testData

          let exp = expectation()
          delegate.performDidFetch = { data in
               XCTAssertEqual(data, testData)
               exp.fulfill()
          }

          viewModel.performNetworkRequest()
          waitForExpectations(timeout: 1.0)
     }

This feature is similar to Perform in SwiftyMocky. https://github.com/MakeAWishFoundation/SwiftyMocky#4-take-action-when-a-stubbed-method-is-called---perform