seanhenry / SwiftMockGeneratorForXcode

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

Generated mocks do not handle generic functions #8

Open abbeyjackson opened 6 years ago

abbeyjackson commented 6 years ago

Protocols which contain generic functions can not be generated because the generic type can not be determined.

ex, this function:

`func getObjects<T: RealmSwift.Object>(_ objects: T.Type) -> Results<T>?`

Will generate as:

    var invokedGetObjects = false
    var invokedGetObjectsCount = 0
    var invokedGetObjectsParameters: (objects: T.Type, Void)?
    var invokedGetObjectsParametersList = [(objects: T.Type, Void)]()
    var stubbedGetObjectsResult: Results<T>!
    func getObjects<T: RealmSwift.Object>(_ objects: T.Type) -> Results<T>? {
        invokedGetObjects = true
        invokedGetObjectsCount += 1
        invokedGetObjectsParameters = (objects, ())
        invokedGetObjectsParametersList.append((objects, ()))
        return stubbedGetObjectsResult
    }

Where type 'T' is undeclared.

seanhenry commented 6 years ago

Thanks for raising this issue. Generic methods aren't supported yet. But I'll look at implementing this ASAP.

abbeyjackson commented 6 years ago

That would be great. I was trying to think of how to handle this myself, just modifying the generated mock but I was coming up short. What are you thinking as a possible solution?

seanhenry commented 6 years ago

It seems like we can use Any for capturing generic paramters. We can't get anymore accurate than that because a generic type might conform to a protocol with associated type requirements which would not compile.

Same goes for capturing types. T.Type is captured by Any.Type.

Returning generic types gets a little tricky but we can cast our stubbed value to the generic type in the return statement.

What do you think?

protocol AssociatedTypeProtocol {
    associatedtype A
}

protocol GenericMethod {
    func test<T>(a: T)
    func test<T: NSObject>(b: T)
    func test<T: NSObject>(c: T.Type)
    func testReturn1<T>() -> T?
    func testReturn2<T>() -> T
    func testReturn3<T: NSObject>() -> T
    func test<T: AssociatedTypeProtocol>(d: T)
}

class GenericMethodMock: GenericMethod {

    var invokedTestA = false
    var invokedTestACount = 0
    var invokedTestAParameters: (a: Any, Void)?
    var invokedTestAParametersList = [(a: Any, Void)]()

    func test<T>(a: T) {
        invokedTestA = true
        invokedTestACount += 1
        invokedTestAParameters = (a, ())
        invokedTestAParametersList.append((a, ()))
    }

    var invokedTestB = false
    var invokedTestBCount = 0
    var invokedTestBParameters: (b: Any, Void)?
    var invokedTestBParametersList = [(b: Any, Void)]()

    func test<T: NSObject>(b: T) {
        invokedTestB = true
        invokedTestBCount += 1
        invokedTestBParameters = (b, ())
        invokedTestBParametersList.append((b, ()))
    }

    var invokedTestC = false
    var invokedTestCCount = 0
    var invokedTestCParameters: (c: Any.Type, Void)?
    var invokedTestCParametersList = [(c: Any.Type, Void)]()

    func test<T: NSObject>(c: T.Type) {
        invokedTestC = true
        invokedTestCCount += 1
        invokedTestCParameters = (c, ())
        invokedTestCParametersList.append((c, ()))
    }

    var invokedTestReturn1 = false
    var invokedTestReturn1Count = 0
    var stubbedTestReturn1Result: Any!

    func testReturn1<T>() -> T? {
        invokedTestReturn1 = true
        invokedTestReturn1Count += 1
        return stubbedTestReturn1Result as? T
    }

    var invokedTestReturn2 = false
    var invokedTestReturn2Count = 0
    var stubbedTestReturn2Result: Any!

    func testReturn2<T>() -> T {
        invokedTestReturn2 = true
        invokedTestReturn2Count += 1
        return stubbedTestReturn2Result as! T
    }

    var invokedTestReturn3 = false
    var invokedTestReturn3Count = 0
    var stubbedTestReturn3Result: Any!

    func testReturn3<T: NSObject>() -> T {
        invokedTestReturn3 = true
        invokedTestReturn3Count += 1
        return stubbedTestReturn3Result as! T
    }

    var invokedTestD = false
    var invokedTestDCount = 0
    var invokedTestDParameters: (d: Any, Void)?
    var invokedTestDParametersList = [(d: Any, Void)]()

    func test<T: AssociatedTypeProtocol>(d: T) {
        invokedTestD = true
        invokedTestDCount += 1
        invokedTestDParameters = (d, ())
        invokedTestDParametersList.append((d, ()))
    }
}
abbeyjackson commented 6 years ago

I think this looks great! Thank you for being so attentive and quick to come up with a solution. Would you be able to merge the changes in? Right now I am working on switching some older mocks over to use your generator and I have one I can not yet switch without this change.

seanhenry commented 6 years ago

No problem. I’m still working on the fix but I’ll upload it as soon as it’s done.

seanhenry commented 6 years ago

I've added this feature now and uploaded the app here. One note, I couldn't support methods with where clauses due to some limitations with SourceKit but I've got a plan to support this soon.

abbeyjackson commented 6 years ago

Fantastic! Thanks for being so attentive. Your library has saved us a lot of time and made it easier to teach those that are learning about tests.

abbeyjackson commented 6 years ago

Okay so what I am seeing here is a little bit different than what you had posted above in the edge case that an array of generic type is one of the parameters or the return type also

func bar<T: Foo>(a: [T], completion: @escaping () -> Void)


    var invokedBarCount = 0
    var invokedBarParameters: (a: [T], Void)?
    var invokedBarParametersList = [(a: [T], Void)]()
    var stubbedBarCompletionResult: (Bool, Void)?
    func bar<T: Foo>(a: [T], completion: @escaping () -> Void) {
        invokedBar = true
        invokedBarCount += 1
        invokedBarParameters = (a, ())
        invokedBarParametersList.append((a, ()))
        if let result = stubbedBarCompletionResult {
            completion(result.0)
        }
    }```
seanhenry commented 6 years ago

Thanks for raising this case and I’m glad this plugin is saving you some time. The information I get from SourceKit is really basic (just a string for a parameter type) which is why my plugin won’t recognise the type within an array (and many other cases). I’m working on ditching it in favour of a custom syntax parser which I’m writing myself. But I’ll make generics my priority when it’s done.

Let’s leave this issue open and I’ll write any updates here.

abbeyjackson commented 6 years ago

Sounds good! Let me know if you need anything. For the time being I just swapped out "T" for "Any" and left a note that it needed to be done if the mock was regenerated. Works great :)

seanhenry commented 6 years ago

The release to fix your generic array is finally ready 🎉

I've replaced SourceKit with a custom parser which will hopefully speed things up going forward.

There are still some types that aren't supported yet (see the README) but I'll add support for them soon.

abbeyjackson commented 6 years ago

fantastic thank you!

fousa commented 3 years ago

Any news on this issue?