Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generated mocks do not handle generic functions #8

Open
abbeyjackson opened this issue Feb 23, 2018 · 13 comments
Open

Generated mocks do not handle generic functions #8

abbeyjackson opened this issue Feb 23, 2018 · 13 comments

Comments

@abbeyjackson
Copy link

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
Copy link
Owner

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

@abbeyjackson
Copy link
Author

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
Copy link
Owner

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
Copy link
Author

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
Copy link
Owner

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

@seanhenry
Copy link
Owner

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
Copy link
Author

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
Copy link
Author

abbeyjackson commented Mar 7, 2018

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
Copy link
Owner

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
Copy link
Author

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
Copy link
Owner

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
Copy link
Author

fantastic thank you!

@fousa
Copy link

fousa commented Mar 2, 2021

Any news on this issue?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants