-
Notifications
You must be signed in to change notification settings - Fork 2k
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鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] Refactoring Moya's stubbing API #1502
base: master
Are you sure you want to change the base?
Conversation
// TODO: - Not sure how this would affect sample data | ||
// public var sampleData: Data { | ||
// return target.sampleData | ||
// } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is also the only thing I haven't been able to get around with this PR. I don't use the MultiTarget
so I don't really understand it's purpose.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this is tricky. I will think about this one, but two ideas that come to my mind right now are:
- Create two MultiTargets, one that stubs and one that does not (like with target types)
- If target is not a Testing one, use empty data/crash, but this is not the best solution I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR is very low priority IMO. It won't make it in Moya 12.0 for sure and I'd like a few Moya members to discuss if they even like this change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Revisited this today... I have a feeling it "just works" now that I understand what MultiTarget
is doing.
If the multi-target type is a TestTargetType
the user will have to specify where to get sampleData
from.
Following the pattern of the existing properties, they would have to extract the associated value of the target
case. Then they would have to cast to TestTargetType
to access the underlying sampleData
.
If casting fails they still have to return something because sampleData
is non-optional. Really hard to go unnoticed unless you're already writing bad code to begin with.
So as the other properties are doing, they would unwrap the associated value of the enum and return the data from the nested target.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@SD10 Sorry, I'm not sure I follow... Are you saying that we won't provide any support for MultiTarget
with TestTargetType
so the users would have to do it themselves?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sunshinejr I wouldn't say "we won't provide any support" but I was going to suggest that they extend MultiTarget
to conform to TestTargetType
inside their test target. We could add a MultiTestTarget
if you believe this is expecting too much.
I guess the latter also has some added type safety, because we can ensure that the associated value in the enum case is a TestTargetType
as well
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@SD10 Oh, sorry if it sounded like that, I was just trying to understand if that's what you would do. I don't think there is a 100% good solution for this, MultiTestTarget
is okay, but still doesn't cover all cases (like mixed targets & test targets). I wonder if making few examples instead would be enough... What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MultiTestTarget
would cover all cases because the enum associated value would have to be TestTargetType
as well.
An edge case would be a MultiTarget
with an inner TestTargetType
. Stubbing would be determined by the type of the root target, MultiTarget
, so it would never stub.
We could document this. Even provide an assertion/error if the inner target is a TestTargetType
but MultiTarget
is not? The only way to fully prevent this is to check every target at runtime by trying to cast it to TestTargetType
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Honestly, the edge case allows you to opt in or out of testing by wrapping your TargetType
or TestTargetType
. It's valuable behavior in a way 馃槀
Let's sit on this a bit more for me to explore casting. To be honest, I really liked the code separation that the where
clause provides, but if I can protect people from their own design decisions then that should be explored.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Realized last night in a dream that nothing can be done with casting. The MultiTarget
abstracts away any sense of an underlying TargetType
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the idea in general. I think it provides a less involved approach.
I couldn't exactly understand where you define the headers - since I noticed you removed those everywhere.
Also have a few styling nits / thoughts I'd love some comments on :)
Great work!
notifyPluginsOfImpendingStub(for: request, target: target) | ||
let plugins = self.plugins | ||
let stub: () -> Void = createStubFunction(cancellableToken, forTarget: target, withCompletion: completion, endpoint: endpoint, plugins: plugins, request: request) | ||
switch stubBehavior { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mind breaking this up to logical portions with some newlines? (before the switch, before and after the notifyPlugins, etc)
It's a bit hard to chew down this way
case .delayed(let delay): | ||
let killTimeOffset = Int64(CDouble(delay) * CDouble(NSEC_PER_SEC)) | ||
let killTime = DispatchTime.now() + Double(killTimeOffset) / Double(NSEC_PER_SEC) | ||
(callbackQueue ?? DispatchQueue.main).asyncAfter(deadline: killTime) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should be able to do do .asyncAfter(deadline: .now() + delay) {
IIRC
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this should work as well. Good clean up 馃憤
/// Creates a function which, when called, executes the appropriate stubbing behavior for the given parameters. | ||
public final func createStubFunction(_ token: CancellableToken, forTarget target: Target, withCompletion completion: @escaping Moya.Completion, endpoint: Endpoint<Target>, plugins: [PluginType], request: URLRequest) -> (() -> Void) { // swiftlint:disable:this function_parameter_count | ||
return { | ||
if token.isCancelled { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would read better as
guard !token.isCancelled else {
self.cancelCompletion(completion, target: target)
return
}
IMO
let expectedData = "sample data".data(using: .utf8)! | ||
expect(target.sampleData).to(equal(expectedData)) | ||
} | ||
// it("uses correct sample data") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it's commented out, just remove it :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I commented it out temporarily. I should've added a TODO. I don't think I should be reducing test coverage, but rather fix whatever tests I break 馃槄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding a TODO is always good :) But if your PR is WIP and you remember to fix this later than its fine :)
|
||
/// Return a response after a delay. | ||
case delayed(seconds: TimeInterval) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really great ideas here ! Super nice work @SD10 .
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the review!
SwiftLint found issuesWarnings
Generated by 馃毇 Danger |
I'm tagging #1539 because I think it's an issue that will help me better analyze potential cons with this approach. |
Revisited this today with an even better understanding of Moya's internals. Currently, to stub a request in Moya you have to configure the stubbed request in 3 different places:
I think the drawback of this approach is it requires so much configuration in many different places throughtout the library. This also results in intertwining testing logic with the business logic of your codebase. Furthermore, I think this alludes to the behavior that people really want: to customize behavior per So, this PR aims to address this issue by making all these things configurable in the |
@SD10 I know that we are still trying to pass this one, so please rebase it when you find time so we can review it again. |
Hey @SD10, just wanted to chime in as an outsider as I've been using Moya since v7.0 and love it. I think this would really be a worthwhile change. I've been reading through the issues and I believe this would address #1126 so I thought I'd link it here as well, but correct me if I'm wrong? One thing I wanted to add, I am big on unit testing my frameworks and have been writing tests for the network layer for a while, so having the stubs really makes this easy; however if it was possible to only include the stubs and Happy to provide help on this matter and jump more into the open source waters. |
@gligorkot Thanks for the feedback 馃挴 This does in fact address #1126 and I agree it's a worthwhile change. It allows greater flexibility, a better separation of concerns, and would allow us to remove I do plan on fixing this up, however, I just started a new job so my time for open source has significantly decreased. Over the next month, I will be moving our apps networking layer to Moya, so after that, I will look at this again. If the Moya community accepts this proposal, I don't see this change being made until Moya 13. In the meantime, if you want to move your tests to the test target this work around could work? // In your app target
extension TargetType {
var data: Data { return Data() }
}
// In your test target
extension YourObjectThatConformsToTargetType {
var data: Data {
// return your real data here
}
} This should work because the extension on your object will override the implementation provided on the |
@SD10 thanks for getting back to me and providing the workaround, however I just tried it and the extension implementation didn't override the |
@gligorkot This will definitely be in Moya 13.0 if the community agrees it's a good approach. Regarding your work around, I tried the exact same thing yesterday and figured out you can't change the dynamic dispatch for the // In your test target
let endpointClosure: (MyTargetType) -> Endpoint = { target in
return Endpoint(...
} |
I like the idea of stripping down the stub configuration to only a specific target type and its 2 properties. As I see it, there are 2 main usages of request stubbing: creating unit tests, and having a placeholder content for a soon-to-be-live route of our favorite API. For the first case, this solution is functionnal: just replace As of the second case, it's more a boilerplate. Let's say you start with a target without stub, and you want to support a new route that will be released soon:
Finally, repeat again when a new route comes up. So great solution for unit tests, not so great for temporary stubbing. Speaking of temporary stubbing, could you consider making the |
Hey @amaurydavid, thanks so much for taking the time to review this 鉂わ笍 Unfortunately I can only give a brief response.
I think this is a really good way of summarizing things 馃憤
It is a bit concerning that you would need to replace
Good point, but I think there's always some overhead around mocking an API. I would probably leverage some compiler directives so that the
The idea of combining the two is interesting! I briefly reviewed that issue but would have to explore the implications of this further. Thanks for giving me a bunch of things to think about! 馃憤 |
Hello :) Is there any update on this matter? The subject is opened since more than 1 year now :| |
Summary
This is purely experimental but I was dabbling with the idea of defining the stubbing API in terms of
TestTargetType
as opposed to configuring aMoyaProvider
.@Moya/contributors I would appreciate if any of you could review this and provide feedback 馃檹
How?
We add a protocol that allows Moya users to provide the sample response and stub behavior:
Then internally, we use a
where
clause onMoyaProvider
to determine if theTarget
we are handling is capable of stubbing requests.It all comes down to this method:
When
Target
!=TestTargetType
this version is called:When
Target
==TestTargetType
this version is called:Pros:
stubClosure
from MoyaProvider because you can control behavior inTestTargetType
MoyaProvider+Stubbing.swift
TestTargetType
need not be concerned with anything related to stubbing and perform no checks regarding stubbing at runtimeCons:
Final thoughts
This breaks a lot of tests, I have not fixed them because it will take some work and I didn't want to start if people think this may not be a good change for the project. However, I think that fixing those tests would help me better understand any drawbacks to this approach.