Open joaopedrok opened 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)
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.
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:
isPath("foo/bar")
instead of isPath("/foo/bar")
so when you send a request to http://server.com/foo/bar
it doesn't match the condition isPath("foo/bar")
so the stub is not triggeredsendRequest(url, completion: nil)
or similar, then it's implemented so that it doesn't send the request at all?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…
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
Ok. I will provide one.
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?
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
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 Presenterstate
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:
state
variable (provided it's not final and is dynamically dispatched, e.g. adding @objc
so you can observe it using the KVO mechanism), by using self.keyValueObservingExpectation(for:,keyPath:, expectedValue: )
to create the expectationexpectation(for:, evaluatedWith: handler: )
, which use an NSPredicate
, then call self.waitForExpectations(timeout: …)
to wait for your state
to change to the expected value and make your NSPredicate
turn true.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?
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
New Issue Checklist
OHHTTPStubs
for your project sectionEnvironment
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)