AliSoftware / OHHTTPStubs

Stub your network requests easily! Test your apps with fake network data and custom response time, response code and headers!
MIT License
5.03k stars 601 forks source link

Asynchronous Stup without a callback #269

Open joaopedrok opened 6 years ago

joaopedrok commented 6 years ago

New Issue Checklist

Environment

Issue Description

How can I use OHTTPStub for a method that is asynchronous but doesn't have a callback? I have research on the internet and I could not find an answer.

Complete output when you encounter the issue (if any)
[INSERT OUTPUT HERE]
AliSoftware commented 6 years ago

Hi

I don't really understand how the fact that the method you wanna call has a callback or not would change the use of OHHTTPStubs? Not sure I understand your question…

You'd still call stub(isPath("/api/v1/foo/bar")) { return OHHTTPStubs Response(…) } to create a stub the same way (see README and wiki for more info and examples, none involve the method making the request so whether the method making the request has a callback or not doesn't matter)

joaopedrok commented 6 years ago

Imagine that you are using some architectures like real MVC or MVP. Inside your presenter or controller you will have something like FooService. Also you gonna have a method like fetchFooList that will call a method from FooService.

Besides having all of this, i also want to unit test my presenter or my controller then i need to mock my requests. Those methods doesnt have any call back for ViewController. So i need to stub these requets for unit testing the behavior of my class for a success of failures.

And when i do that, it doesnt update my state. What i mean, that my completion is called before the stub.

AliSoftware commented 6 years ago

I am sorry, but I still don't see how that kind of setup would affect the way you use OHHTTPStubs?

OHHTTPStubs intercept the very low level network requests, at the OS level (using a custom NSURLProtocol interceptor). At the level where OHHTTPStubs works (the URL Loading system layer of iOS) it doesn't know nor care about who sent the request, it just intercept the request and return a stub response. Exactly like if you used a proxy server.

When you use a proxy server instead of your real server the proxy didn't know anything about your app, its architecture and if it has a completion of not. Same is true for OHHTTPStubs. Plenty of people use OHHTTPStubs with real MVC and MVP and MVVM and that works as expected no matter the architecture you have.

I think the fact that you don't have a completion has little to do with OHHTTPStubs itself. If your stubs are not working in your setup that would be for a different reason. Like:

BTW if you don't have a completion closure in your sendRequest method how do you expect your presenter layer to be updated in the first place (stub or not stub)? Since that's typically in the completion closure that you put your code to update the presentation layer initially…

AliSoftware commented 6 years ago

I think that if you could provide an example project reduced to the minimum to reproduce your use case that would help us understand what you mean exactly and what's the origin of the issue

joaopedrok commented 6 years ago

Ok. I will provide one.

joaopedrok commented 6 years ago

My unit test is like that

func testIfTheStateIsSuccessWithSuccessRequestWhenFetchProjectsIsCalled() {
        stub(condition: isPath("/api/v1/projects")) { request in
            let stubPath = OHPathForFile("ProjectsResponse.json", type(of: self))
            return fixture(filePath: stubPath!, status: 200, headers: ["Content-Type":"application/json"])
        }

        self.sut.fetchProjects()

        XCTAssertTrue(sut.state == .success(_))        
    }

And my code in my presenter

func fetchProjects() {
        self.state = .loading
        self.requestProjects(page: 1)
}
private func requestProjects(page: Int) {
        ProjectsService.fetchProjects(page: page) { (projects, responseError) in
            self.myProjectsViewController?.finishInfinityScroll()

            if let error = responseError {
                self.state = .error(error)
            } else {
                self.state = .success(projects)
            }
        }
    }

So when I run my unit test, I can see that stub is correctly but my state is never success in Unit Test, so my unit test always but the code is right.

Can you see my doubt now?

AliSoftware commented 6 years ago

Ok, I see your mistake then. As expected, it's not related to OHHTTPStubs per-se, but to asynchronous testing in general. Whether you use OHHTTPStubs or not, when you write a unit test for some asynchronous call, you have to give time for the asynchronous call to execute before doing your assertion. You typically do that by using an XCTestExpectation

See this dedicated page in the wiki explaining that

If you don't wait for an expectation before your assertion, self.sut.fetchProjects() will trigger the asynchronous code, but then immediately return (letting the asynchronous code to run in the background), and XCTAssertTrue(…) will run immediately, so without letting your asynchronous code enough time to execute, checking your condition too soon and fail. Whether that async code is a network request, a long-running operation in a background thread, or whatever, whether you use OHHTTPStubs, a proxy server, or dependency injection or whatnot, whether your code has a completion block or not…the rule is the same for all those async test, and XCTestExpectation is the solution.

Note that one main problem of your design and architecture is that it's stateful, but without a way to observe the changes of that state… which is not really MVP, as your model calling fetchProhects() does change the Presenter state directly — so those layers are not in isolation as MVP/MVC would expect. I suggest you make those layers more separate, either by introducing a completion block, or by using any reactive framework like RxSwift, or any other way, so that the brain/logic and the presentation/view are independent. You could also try to use TDD to force you to make those layers isolated from each other, so that you'll not be bitten later by a flawed architecture/design not respecting those isolation principles of all those MVC/MVP/MVVM architectures

That being said, if you still want to keep your code the way you wrote it (without any completion block and despite it mixing the layers of MVP), you can still make it work, again by using an XCTestExpectation to wait for the state to change before testing it is the correct value. And again, that technique isn't really specific to OHHTTPStubs, but to any unit test about asynchronous code in general, so you might be interested in reading more about asynchronous testing, for example in Apple's XCTestCase documentation here

So in your case, even if you decide to keep it without any completion block or any other way to be notified of the changes of state, you can still use an XCTestExpectation, you'll just not create it using expectation(description: ) and later call fulfill on it, but instead:

joaopedrok commented 6 years ago

I see your point related to my architecture and I know that is not a presenter. Is more like a controller from the real MVC that handle all the business rule. For that reason, I don't see any problem if my controller handle the state of my ViewController. But thanks for the tip.

About XCTestExpectation I have had some problem using it because there is a timeout for it. So, as the unit test increase the size, the tests start to fail because I need more time for execution.

Can you tell me why the stub response is not fast? It looks like to me that it spends almost the same time that the real network connection. Is there any response time for stub or something like that?

AliSoftware commented 6 years ago

By default the stub returns the fake response in the very next RunLoop, unless you specify a response time explicitly for the stub (see README and the wiki for details)

Since it's all using iOS URL Loading system, it behaves exactly like any normal URLSession, which runs on RunLoop events too, just returning in the very next RunLoop. Like when you use DispatchQueue.asynxAfter(0) for example, so not immediately-immediately, but in the immediate next RunLoop. If it takes too long to execute it probably means that you're blocking the thread and allowing it to run its RunLoop