I'm struggling with unit testing of Published object.
I have a viewmodel class as below
class MovieListViewModel {
#Published public private(set) var arrayOfMovies: [Movie] = []
#Published private var arraofFavoriteMoviesID: [FavouriteMovieID] = []
init(request: NetworkServiceProtocol) {
addSubscribers()
callServicesInSequence()
}
func addSubscribers() {
$arrayOfMovies.combineLatest($arraofFavoriteMoviesID)
.debounce(for: 0.0, scheduler: DispatchQueue.main)
.sink { [weak self] (_, _) in
self?.fetchWachedMovies()
self?.fetchTobeWatchedMovies()
self?.fetchFavoriteMovies()
}
.store(in: &subscriptions)
}
func callServicesInSequence() {/*..service request...*}
}
Here addSubscribers() listen for any changes happening in arrayOfMovies or arraofFavoriteMoviesID and works perfectly is the app.
But when I tried to mock and write unit test cases. Any chnages happening in arrayOfMovies or arraofFavoriteMoviesID does not make any effect (addSubscribers's body never get called).
Can any one please guide me what am I doing wrong while writing unit test cases for Combine/Published objects.
Please let me know if more clarification required.
Your code has two obvious dependencies: a NetworkServiceProtocol and DispatchQueue.main.
NetworkServiceProtocol is not part of the iOS SDK. I assume it is a type you created, and you pass it to the model's init, so you can substitute a testable implementation in your test cases.
However, DispatchQueue is part of the iOS SDK, and you cannot create your own testable implementation of it for use in test cases. You have only a limited ability to run the main queue, which makes it difficult to test code that depends on it.
Here are three solutions:
My favorite solution is to adopt The Composable Architecture (TCA) or a similar framework, which by design makes it easy to control dependencies and hence to test code like this.
A less invasive solution is to replace the direct use of DispatchQueue.main with a type eraser, which you pass to the model's init. Then, in tests, you can pass in a deterministic scheduler that you control in the test case. The Combine Schedulers package, for example, provides the type eraser AnyScheduler and several scheduler implementations specifically for use in testing. (TCA, mentioned above, uses this package.)
Writing your own type eraser for the Scheduler protocol is simple enough that you could do it yourself if you don't want to depend on a third-party package.
The least invasive solution is to use XCTestExpectation APIs to run the main queue in your test case. I'll demonstrate that here.
You didn't post enough code to demonstrate, so I'll use the following simple types:
struct Movie: Equatable {
let id: UUID = .init()
}
struct NetworkClient {
let fetchMovies: AnyPublisher<[Movie], Error>
}
And here is the simplified model that uses them:
class Model: ObservableObject {
#Published public private(set) var movies: [Movie] = []
#Published public private(set) var error: Error? = nil
private let client: NetworkClient
private var fetchTicket: AnyCancellable? = nil
init(client: NetworkClient) {
self.client = client
fetchMovies()
}
func fetchMovies() {
fetchTicket = client.fetchMovies
.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main)
.sink(
receiveCompletion: { [weak self] in
self?.fetchTicket = nil
if case .failure(let error) = $0 {
self?.error = error
}
},
receiveValue: { [weak self] in
self?.movies = $0
}
)
}
}
For testing, I can set up a NetworkClient where fetchMovies is a PassthroughSubject. That way my test case can decide exactly what the “network” sends and when it sends it.
To test the success case, where the network “works”, I subscribe to the model's $movies publisher and fulfill an XCTestExpectation if it publishes the correct value.
func testSuccess() throws {
let fetchMovies = PassthroughSubject<[Movie], Error>()
let client = NetworkClient(
fetchMovies: fetchMovies.eraseToAnyPublisher()
)
let model = Model(client: client)
let expectedMovies = [ Movie(), Movie() ]
let ex = expectation(description: "movies publishes correct value")
let ticket = model.$movies.sink { actualMovies in
if actualMovies == expectedMovies {
ex.fulfill()
}
}
fetchMovies.send(expectedMovies)
waitForExpectations(timeout: 2)
// Some mention of ticket here keeps the subscription alive
// during the wait for the expectation.
ticket.cancel()
}
To test the failure case, where the network “fails”, I subscribe to the $error publisher and fulfill an XCTestExpectation if it publishes the correct error code.
func testFailure() throws {
let fetchMovies = PassthroughSubject<[Movie], Error>()
let client = NetworkClient(
fetchMovies: fetchMovies.eraseToAnyPublisher()
)
let model = Model(client: client)
let expectedCode = URLError.Code.resourceUnavailable
let ex = expectation(description: "error publishes correct code")
let ticket = model.$error.sink { error in
if (error as? URLError)?.code == expectedCode {
ex.fulfill()
}
}
fetchMovies.send(completion: .failure(URLError(expectedCode)))
waitForExpectations(timeout: 2)
ticket.cancel()
}
Note though that if a test fails (for example if you change testFailure to purposely publish the wrong code), it takes 2 seconds to fail. That is annoying. These two tests are simple enough that we could rewrite them to fail quicker in the case that the wrong thing is published. But in general it might be difficult to write all test cases to “fail fast” when relying on XCTestExpectation. That is the sort of problem you can avoid by replacing the direct use of DispatchQueue with a type eraser. It lets your test case use a controllable scheduler so the test case can make time flow instantly, without any use of DispatchQueues, so you don't need to use XCTestExpectation at all.
Related
I'm struggling with unit testing of Published object.
I have a viewmodel class as below
class MovieListViewModel {
#Published public private(set) var arrayOfMovies: [Movie] = []
#Published private var arraofFavoriteMoviesID: [FavouriteMovieID] = []
init(request: NetworkServiceProtocol) {
addSubscribers()
callServicesInSequence()
}
func addSubscribers() {
$arrayOfMovies.combineLatest($arraofFavoriteMoviesID)
.debounce(for: 0.0, scheduler: DispatchQueue.main)
.sink { [weak self] (_, _) in
self?.fetchWachedMovies()
self?.fetchTobeWatchedMovies()
self?.fetchFavoriteMovies()
}
.store(in: &subscriptions)
}
func callServicesInSequence() {/*..service request...*}
}
Here addSubscribers() listen for any changes happening in arrayOfMovies or arraofFavoriteMoviesID and works perfectly is the app.
But when I tried to mock and write unit test cases. Any chnages happening in arrayOfMovies or arraofFavoriteMoviesID does not make any effect (addSubscribers's body never get called).
Can any one please guide me what am I doing wrong while writing unit test cases for Combine/Published objects.
Please let me know if more clarification required.
Your code has two obvious dependencies: a NetworkServiceProtocol and DispatchQueue.main.
NetworkServiceProtocol is not part of the iOS SDK. I assume it is a type you created, and you pass it to the model's init, so you can substitute a testable implementation in your test cases.
However, DispatchQueue is part of the iOS SDK, and you cannot create your own testable implementation of it for use in test cases. You have only a limited ability to run the main queue, which makes it difficult to test code that depends on it.
Here are three solutions:
My favorite solution is to adopt The Composable Architecture (TCA) or a similar framework, which by design makes it easy to control dependencies and hence to test code like this.
A less invasive solution is to replace the direct use of DispatchQueue.main with a type eraser, which you pass to the model's init. Then, in tests, you can pass in a deterministic scheduler that you control in the test case. The Combine Schedulers package, for example, provides the type eraser AnyScheduler and several scheduler implementations specifically for use in testing. (TCA, mentioned above, uses this package.)
Writing your own type eraser for the Scheduler protocol is simple enough that you could do it yourself if you don't want to depend on a third-party package.
The least invasive solution is to use XCTestExpectation APIs to run the main queue in your test case. I'll demonstrate that here.
You didn't post enough code to demonstrate, so I'll use the following simple types:
struct Movie: Equatable {
let id: UUID = .init()
}
struct NetworkClient {
let fetchMovies: AnyPublisher<[Movie], Error>
}
And here is the simplified model that uses them:
class Model: ObservableObject {
#Published public private(set) var movies: [Movie] = []
#Published public private(set) var error: Error? = nil
private let client: NetworkClient
private var fetchTicket: AnyCancellable? = nil
init(client: NetworkClient) {
self.client = client
fetchMovies()
}
func fetchMovies() {
fetchTicket = client.fetchMovies
.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main)
.sink(
receiveCompletion: { [weak self] in
self?.fetchTicket = nil
if case .failure(let error) = $0 {
self?.error = error
}
},
receiveValue: { [weak self] in
self?.movies = $0
}
)
}
}
For testing, I can set up a NetworkClient where fetchMovies is a PassthroughSubject. That way my test case can decide exactly what the “network” sends and when it sends it.
To test the success case, where the network “works”, I subscribe to the model's $movies publisher and fulfill an XCTestExpectation if it publishes the correct value.
func testSuccess() throws {
let fetchMovies = PassthroughSubject<[Movie], Error>()
let client = NetworkClient(
fetchMovies: fetchMovies.eraseToAnyPublisher()
)
let model = Model(client: client)
let expectedMovies = [ Movie(), Movie() ]
let ex = expectation(description: "movies publishes correct value")
let ticket = model.$movies.sink { actualMovies in
if actualMovies == expectedMovies {
ex.fulfill()
}
}
fetchMovies.send(expectedMovies)
waitForExpectations(timeout: 2)
// Some mention of ticket here keeps the subscription alive
// during the wait for the expectation.
ticket.cancel()
}
To test the failure case, where the network “fails”, I subscribe to the $error publisher and fulfill an XCTestExpectation if it publishes the correct error code.
func testFailure() throws {
let fetchMovies = PassthroughSubject<[Movie], Error>()
let client = NetworkClient(
fetchMovies: fetchMovies.eraseToAnyPublisher()
)
let model = Model(client: client)
let expectedCode = URLError.Code.resourceUnavailable
let ex = expectation(description: "error publishes correct code")
let ticket = model.$error.sink { error in
if (error as? URLError)?.code == expectedCode {
ex.fulfill()
}
}
fetchMovies.send(completion: .failure(URLError(expectedCode)))
waitForExpectations(timeout: 2)
ticket.cancel()
}
Note though that if a test fails (for example if you change testFailure to purposely publish the wrong code), it takes 2 seconds to fail. That is annoying. These two tests are simple enough that we could rewrite them to fail quicker in the case that the wrong thing is published. But in general it might be difficult to write all test cases to “fail fast” when relying on XCTestExpectation. That is the sort of problem you can avoid by replacing the direct use of DispatchQueue with a type eraser. It lets your test case use a controllable scheduler so the test case can make time flow instantly, without any use of DispatchQueues, so you don't need to use XCTestExpectation at all.
I am trying to implement upload mechanism for my application. However, I have a concurrency issue I couldn't resolve. I sent my requests using async/await with following code. In my application UploadService is creating every time an event is fired from some part of my code. As an example I creation of my UploadService in a for loop. The problem is if I do not use NSLock backend service is called multiple times (5 in this case because of loop). But if I use NSLock it never reaches the .success or .failure part because of deadlock I think. Could someone help me how to achieve without firing upload service multiple times and reaching success part of my request.
final class UploadService {
/// If I use NSLock in the commented lines it never reaches to switch result so can't do anything in success or error part.
static let locker = NSLock()
init() {
Task {
await uploadData()
}
}
func uploadData() async {
// Self.locker.lock()
let context = PersistentContainer.shared.newBackgroundContext()
// It fetches data from core data to send it in my request
guard let uploadedThing = Upload.coreDataFetch(in: context) else {
return
}
let request = UploadService(configuration: networkConfiguration)
let result = await request.uploadList(uploadedThing)
switch result {
case .success:
print("success")
case .failure(let error as NSError):
print("error happened")
}
// Self.locker.unlock()
}
}
class UploadExtension {
func createUploadService() {
for i in 0...4 {
let uploadService = UploadService()
}
}
}
A couple of observations:
Never use locks (or wait for semaphores or dispatch groups, etc.) to attempt to manage dependencies between Swift concurrency tasks. This is a concurrency system predicated upon the contract that threads can make forward progress. It cannot reason about the concurrency if you block threads with mechanisms outside of its purview.
Usually you would not create a new service for every upload. You would create one and reuse it.
E.g., either:
func createUploadService() async {
let uploadService = UploadService()
for i in 0...4 {
await uploadService.uploadData(…)
}
}
Or, more likely, if you might use this same UploadService later, do not make it a local variable at all. Give it some broader scope.
let uploadService = UploadService()
func createUploadService() async {
for i in 0...4 {
await uploadService.uploadData(…)
}
}
The above only works in simple for loop, because we could simply await the result of the prior iteration.
But what if you wanted the UploadService keep track of the prior upload request and you couldn’t just await it like above? You could keep track of the Task and have each task await the result of the previous one, e.g.,
actor UploadService {
var task: Task<Void, Never>? // change to `Task<Void, Error>` if you change it to a throwing method
func upload() {
…
task = Task { [previousTask = task] in // capture copy of previous task (if any)
_ = await previousTask?.result // wait for it to finish before starting this one
await uploadData()
}
}
}
FWIW, I made this service with some internal state an actor (to avoid races).
Since creating Task {} is part of structured concurrency it inherits environment (e.g MainThread) from the scope where it was created,try using unstructured concurrency's Task.detached to prevent it from runnning on same scope ( maybe it was called on main thread ) - with creating Task following way:
Task.detached(priority: .default) {
await uploadData()
}
Top Level Question:
I want to know how, within a retry, I can modify its source observable if it is an observable shared between multiple subscribers (in this case a BehaviorSubject/Relay).
Solution(s) I have considered:
The suggestion of using defer from this post doesn't seem to naturally port over if the source observable needs to be shared.
Use case (to fully elaborate the question)
Say I have a server connection object that, when initialized, connects to an url. Once it is created, I can also use it to get a data stream for a particular input.
class ServerConnection {
var url: URL
init(url: URL)
func getDataStream(input: String) -> Observable<Data> // the observable also errors when the instance is destroyed.
}
However, one particular url or another may be broken or overloaded. So I may want to obtain the address of a mirror and generate a new ServerConnection object. Let's say I have such a function.
// At any point in time, gets the mirror of the url with the lowest load
func getLowestLoadMirror(url: URL) -> URL {}
Ideally, I want this "mirror url" switching should be an implementation detail. The user of my code may only care about the data they receive. So we would want to encapsulate this logic in a new class:
class ServerConnectionWithMirrors {
private var currentConnection: BehaviorRelay<ServerConnection>
init(startingURL: URL)
func dataStream(for inputParams: String) -> Observable<Data>
}
// usage
let connection = ServerConnectionWithMirrors(startingURL: "www.example.com")
connection.dataStream(for: "channel1")
.subscribe { channel1Data in
// do something with channel1Data
}.disposed(by: disposeBag)
connection.dataStream(for: "channel2")
.subscribe { channel2Data in
// do something with channel2Data
}.disposed(by: disposeBag)
How should I write the dataStream() function for ServerConnectionWithMirrors? I should be using retries, but I need to ensure that the retries, when faced with a particular error (ServerOverLoadedError) update the value on the behaviorRelay.
Here is code that I have so far that demonstrates the crux at what I am trying to do. One problem is that multiple subscribers to the behaviorRelay may all update it in rapid succession when they get an error, where only one update would do.
func dataStream(for inputParams: String) -> Observable<Data> {
self.currentConnection.asObservable()
.flatMapLatest { server in
return server.getDataStream(input: inputParams)
}
.retryWhen { errors in
errors.flatMapLatest { error in
if error is ServerOverLoadedError {
self.currentConnection.accept(ServerConnection(url: getLowestLoadURL()))
} else {
return Observable.error(error)
}
}
}
}
The answer to your top level question:
I want to know how, within a retry, I can modify its source observable if it is an observable shared between multiple subscribers (in this case a BehaviorSubject/Relay).
You cannot modify a retry's source observable from within the retry. (full stop) You cannot do this whether it is shared or not. What you can do is make the source observable in such a way that it naturally updates its data for every subscription.
That is what the question you referred to is trying to explain.
func getData(from initialRequest: URLRequest) -> Observable<Data> {
return Observable.deferred {
var correctRequest = initialRequest
let correctURL = getLowestLoadMirror(url: initialRequest.url!)
correctRequest.url = correctURL
return Observable.just(correctRequest)
}
.flatMapLatest {
getDataFromServer(request: $0)
}
.retryWhen { error in
error
.do(onNext: {
guard $0 is ServerOverloadedError else { throw $0 }
})
}
}
With the above code, every time deferred is retried, it will call its closure and every time its closure is called, the URL will the lowest load will be used.
So, I have this sequence of API calls, where I fetch a employee details, then fetch the company and project details that the employee is associated with. After both fetching are complete, I combine both and publish a fetchCompleted event. I've isolated the relevant code below.
func getUserDetails() -> AnyPublisher<UserDetails, Error>
func getCompanyDetails(user: UserDetails) -> AnyPublisher<CompanyDetails, Error>
func getProjectDetails(user: UserDetails) -> AnyPublisher<ProjectDetails, Error>
If I do this,
func getCompleteUserDetails() -> AnyPublisher<UserFetchState, Never> {
let cvs = CurrentValueSubject<UserFetchState, Error>(.initial)
let companyPublisher = getUserDetails()
.flatMap { getCompanyDetails($0) }
let projectPublisher = getUserDetails()
.flatMap { getProjectDetails($0) }
companyPublisher.combineLatest(projectPublisher)
.sink { cvs.send(.fetchComplete) }
return cvs.eraseToAnyPublisher()
}
getUserDetails() will get called twice. What I need is fetch the userDetails once and with that, branch the stream into two, map it to fetch the company details and project details and re-combine both.
Is there a elegant(flatter) way to do the following.
func getCompleteUserDetails() -> AnyPublisher<UserFetchState, Never> {
let cvs = CurrentValueSubject<UserFetchState, Error>(.initial)
getUserDetails()
.sink {
let companyPublisher = getCompanyDetails($0)
let projectPublisher = getProjectDetails($0)
companyPublisher.combineLatest(projectPublisher)
.sink { cvs.send(.fetchComplete) }
}
return cvs.eraseToAnyPublisher()
}
The whole idea of Combine is that you construct a pipeline down which data flows. Actually what flows down can be a value or a completion, where a completion could be a failure (error). So:
You do not need to make a signal that the pipeline has produced its value; the arrival of that value at the end of the pipeline is that signal.
Similarly, you do not need to make a signal that the pipeline's work has completed; a publisher that has produced all the values it is going to produce produces the completion signal automatically, so the arrival of that completion at the end of the pipeline is that signal.
After all, when you receive a letter, the post office doesn't call you up on the phone and say, "You've got mail." Rather, the postman hands you the letter. You don't need to be told you've received a letter; you simply receive it.
Okay, let's demonstrate. The key to understanding your own pipeline is simply to track what kind of value is traveling down it at any given juncture. So let's construct a model pipeline that does the sort of thing you need done. I will posit three types of value:
struct User {
}
struct Project {
}
struct Company {
}
And I will imagine that it is possible to go online and fetch all of that information: the User independently, and the Project and Company based on information contained in the User. I will simulate that by providing utility functions that return publishers for each type of information; in real life these would probably be deferred futures, but I will simply use Just to keep things simple:
func makeUserFetcherPublisher() -> AnyPublisher<User,Error> {
Just(User()).setFailureType(to: Error.self).eraseToAnyPublisher()
}
func makeProjectFetcherPublisher(user:User) -> AnyPublisher<Project,Error> {
Just(Project()).setFailureType(to: Error.self).eraseToAnyPublisher()
}
func makeCompanyFetcherPublisher(user:User) -> AnyPublisher<Company,Error> {
Just(Company()).setFailureType(to: Error.self).eraseToAnyPublisher()
}
Now then, let's construct our pipeline. I take it that our goal is to produce, as the final value in the pipeline, all the information we have collected: the User, the Project, and the Company. So our final output will be a tuple of those three things. (Tuples are important when you are doing Combine stuff. Passing a tuple down the pipeline is extremely common.)
Okay, let's get started. In the beginning there is nothing, so we need an initial publisher to kick off the process. That will be our user fetcher:
let myWonderfulPipeline = self.makeUserFetcherPublisher()
What's coming out the end of that pipeline is a User. We now want to feed that User into the next two publishers, fetching the corresponding Project and Company. The way to insert a publisher into the middle of a pipeline is with flatMap. And remember, our goal is to produce the tuple of all our info. So:
let myWonderfulPipeline = self.makeUserFetcherPublisher()
// at this point, the value is a User
.flatMap { (user:User) -> AnyPublisher<(User,Project,Company), Error> in
// ?
}
// at this point, the value is a tuple: (User,Project,Company)
So what goes into flatMap, where the question mark is? Well, we must produce a publisher that produces the tuple we have promised. The tuple-making publisher par excellence is Zip. We have three values in our tuple, so this is a Zip3:
let myWonderfulPipeline = self.makeUserFetcherPublisher()
.flatMap { (user:User) -> AnyPublisher<(User,Project,Company), Error> in
// ?
let result = Publishers.Zip3(/* ? */)
return result.eraseToAnyPublisher()
}
So what are we zipping? We must zip publishers. Well, we know two of those publishers — they are the publishers we have already defined!
let myWonderfulPipeline = self.makeUserFetcherPublisher()
.flatMap { (user:User) -> AnyPublisher<(User,Project,Company), Error> in
let pub1 = self.makeProjectFetcherPublisher(user: user)
let pub2 = self.makeCompanyFetcherPublisher(user: user)
// ?
let result = Publishers.Zip3(/* ? */, pub1, pub2)
return result.eraseToAnyPublisher()
}
We're almost done! What goes in the missing slot? Remember, it must be a publisher. And what's our goal? We want to pass on the very same User that arrived from upstream. And what's the publisher that does that? It's Just! So:
let myWonderfulPipeline = self.makeUserFetcherPublisher()
.flatMap { (user:User) -> AnyPublisher<(User,Project,Company), Error> in
let pub1 = self.makeProjectFetcherPublisher(user: user)
let pub2 = self.makeCompanyFetcherPublisher(user: user)
let just = Just(user).setFailureType(to:Error.self)
let result = Publishers.Zip3(just, pub1, pub2)
return result.eraseToAnyPublisher()
}
And we're done. No muss no fuss. This is a pipeline that produces a (User,Project,Company) tuple. Whoever subscribes to this pipeline does not need some extra signal; the arrival of the tuple is the signal. And now the subscriber can do something with that info. Let's create the subscriber:
myWonderfulPipeline.sink {
completion in
if case .failure(let error) = completion {
print("error:", error)
}
} receiveValue: {
user, project, company in
print(user, project, company)
}.store(in: &self.storage)
We didn't do anything very interesting — we simply printed the tuple contents. But you see, in real life the subscriber would now do something useful with that data.
You can use the zip operator to get a Publisher which emits a value whenever both of its upstreams emitted a value and hence zip together getCompanyDetails and getProjectDetails.
You also don't need a Subject to signal the fetch being finished, you can just call map on the flatMap.
func getCompleteUserDetails() -> AnyPublisher<UserFetchState, Error> {
getUserDetails()
.flatMap { getCompanyDetails(user: $0).zip(getProjectDetails(user: $0)) }
.map { _ in UserFetchState.fetchComplete }
.eraseToAnyPublisher()
}
However, you shouldn't need a UserFetchState to signal the state of your pipeline (and especially shouldn't throw away the fetched CompanyDetails and ProjectDetails objects in the middle of your pipeline. You should simply return the fetched CompanyDetails and ProjectDetails as a result of your flatMap.
func getCompleteUserDetails() -> AnyPublisher<(CompanyDetails, ProjectDetails), Error> {
getUserDetails()
.flatMap { getCompanyDetails(user: $0).zip(getProjectDetails(user: $0)) }
.eraseToAnyPublisher()
}
I'm learning Test Driven Development in Swift. I hit a wall when I realized the delegate pattern I regularly use for asynchronous requests is difficult to test. I've learned that if something's difficult to test, the design pattern behind the implementation could probably be better. This is confusing me because I think the delegate pattern I'm using is common and I'm wondering how others have dealt with this issue.
The pattern:
I wrote a service, which executes an asynchronous request in a static function which takes a delegate instance. The delegate instance conforms to a protocol which requires implementation of a success and failure method. I've contrived an example which hits Google.com. Please ignore the Type safety issues in this example. The actual code I'm running to hit an endpoint and parse JSON is safer. I just wanted to come up with a very small snippet of code to depict the issue that's causing difficulty while testing:
protocol GoogleServiceDelegate {
func gotGoogle(str: String);
func gotError(str: String);
}
struct GoogleService {
static func getGoogle(delegate: GoogleServiceDelegate) {
let url: NSURL! = NSURL(string: "http://google.com")
NSURLSession.sharedSession().dataTaskWithURL(url) { data, response, error in
if let data = data {
let str: NSString! = NSString(data: data, encoding: NSUTF8StringEncoding)
delegate.gotGoogle(str as String)
} else {
delegate.gotError("\(error)")
}
}
}
}
Here's the test which illustrates the problem:
class AsyncTestingTests: XCTestCase {
func testExample() {
let responseExpectation = expectationWithDescription("Got google response!")
struct GoogleDelegate: GoogleServiceDelegate {
func gotGoogle(str: String) {
// expectations about response
responseExpectation.fulfill()
}
func gotError(str: String) {
// expectations about error
responseExpectation.fulfill()
}
}
let myGoogleServiceDelegate = GoogleDelegate()
GoogleService.getGoogle(myGoogleServiceDelegate)
waitForExpectationsWithTimeout(5) { _ in
print("Never got a response from Google :(")
}
}
}
The problem arises at the two .fulfill() lines. I get the following error from Xcode:
Struct declaration cannot close over value 'responseExpectation' defined in outer scope
I understand the error, but am unsure what to adjust... Is there a workaround for this which I can use in the test, or is there a better (easily testable) pattern for asynchronous callbacks than what I am attempting? If you know of a better testable solution, would you mind taking the time to write down an example?
Yes, you can not close over variables defined outside of struct, to workaround, we need to use closures/functions and pass it to the struct. Methods in struct can invoke it when they receive the response.
func testExample() {
let responseExpectation = expectationWithDescription("Got google response!")
//Let a function capture the fulfilling of the expectation
func fullFillExpectation(){
responseExpectation.fullFill()
}
struct GoogleDelegate: GoogleServiceDelegate {
var fullFiller : (()->Void)!
func gotGoogle(str: String) {
// expectations about response via invoke the closure
fullFiller()
}
func gotError(str: String) {
// expectations about error - invoke the closure
fullFiller()
}
}
//Create the delegate with full filler function.
let myGoogleServiceDelegate = GoogleDelegate(fullFiller: fullFillExpectation)
GoogleService.getGoogle(myGoogleServiceDelegate)
waitForExpectationsWithTimeout(5) { _ in
print("Never got a response from Google :(")
}
}
}
PS: I could not test this, please test and let me know.