SwiftMockGeneratorForXcode icon indicating copy to clipboard operation
SwiftMockGeneratorForXcode copied to clipboard

Generated mocks do not handle generic functions

Open abbeyjackson opened this issue 7 years ago • 13 comments

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.

abbeyjackson avatar Feb 23 '18 21:02 abbeyjackson

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

seanhenry avatar Feb 24 '18 01:02 seanhenry

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?

abbeyjackson avatar Feb 24 '18 03:02 abbeyjackson

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, ()))
    }
}

seanhenry avatar Feb 24 '18 04:02 seanhenry

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.

abbeyjackson avatar Feb 26 '18 20:02 abbeyjackson

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

seanhenry avatar Feb 26 '18 21:02 seanhenry

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.

seanhenry avatar Mar 03 '18 05:03 seanhenry

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 avatar Mar 07 '18 17:03 abbeyjackson

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)
        }
    }```

abbeyjackson avatar Mar 07 '18 20:03 abbeyjackson

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.

seanhenry avatar Mar 07 '18 22:03 seanhenry

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

abbeyjackson avatar Mar 08 '18 16:03 abbeyjackson

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.

seanhenry avatar Mar 15 '18 07:03 seanhenry

fantastic thank you!

abbeyjackson avatar Apr 02 '18 15:04 abbeyjackson

Any news on this issue?

fousa avatar Mar 02 '21 11:03 fousa