Problem: How to TDD async/await ?
Solution: Stub the response, async function needs to know it's result before call
IMPORTANT: This tutorial is only available for iOS 15+
It will use the async/await integration in URLSession
And the AsyncImage
SwiftUI view to load the image
async/await it new to the in iOS ecosystem
Will learn here how to TDD it
Will use the Dog API to test our logic
It's a simple API no need for private key
First we will get dogs form the API
Second download dogs images from returned image url response.
Here is the result of our implementation
First the list of behaviour to test
======= LOAD DOGS [ ] On init view model, status is idle [ ] On load use the dog API url to get dogs images [ ] Load dogs update status to loading [ ] If Load dogs failed delivers .connectivity error [ ] Display an error message (text) [ ] User can retry to load (button) [ ] Load dogs succeed with 200 response [ ] Parsing succeed delivers dogs [ ] Extract dogs names from returned url [ ] Parsing failed delivers .invalidData error [ ] Display an error message (text) [ ] User can retry to load (button)
Start with this first requirement
It ensure that nothing happen on start
======= LOAD DOGS [ ] On init view model, status is idle
As usual write the test first
func test_init_statusIdle() {
let sut = DogsViewModel()
XCTAssertEqual(sut.status, .idle)
}
- Need to create the
DogsViewModel
- Need to create the status enum and variable
Fix the error for Xcode to compile
class DogsViewModel: ObservableObject {
enum Status {
case idle
}
@Published var status = Status.idle
}
Run tests
They are passing
Can mark this requirement as done.
========= LOAD DOGS [✅] On init view model, status is idle
Continue with this requirement for
========= LOAD DOGS [ ] On load use the dog API url to get dogs images
As usual write the test first
func test_loadDogs_performARequestWithDogsURLRequest() async {
let dogsURLRequest = URLRequest(url: URL(string: "https://dog.ceo/api/breeds/image/random/3")!) // 1
let urlSessionSpy = URLSessionAsyncAwaitSpy(result: .failure(.connectivity)) // 2
let networkService = NetworkService(session: urlSessionSpy) // 3
let dogServiceSpy = DogService(networkService: networkService) // 4
let sut = DogViewModelSpy(dogService: dogServiceSpy) // 5
try? await sut.loadDogs() // 6
XCTAssertEqual(urlSessionSpy.requests, [dogsURLRequest]) // 7
}
- Url that returns 3 dogs in a random order.
- First we need to create a spy that will use our mock of the URLSession async function. We inject (stub) the response of API (need to complete with error or success. The function must complete or you will be blocked be the
await
that will never return). - The
NetworkService
will request dogs to the API and will return response asData
. We inject oururlSessionSpy
to able to control the result. - The
DogService
will turnData
into a rich model dogs - The
DogViewModel
will use the service to get dogs - Use the SUT that perform an async loading of dogs waiting for the response
- After load complete, check that the right URL has been used
First we need a mock to test the URLSession
For this async function
Start creating the protocol
It that looks like the signature of this function
protocol AsyncAwait {
func data(request: URLRequest) async throws -> (Data, URLResponse)
}
Implement the mock to URLSession
extension URLSession: AsyncAwait {
func data(request: URLRequest) async throws -> (Data, URLResponse) {
try await self.data(for: request)
}
}
We can now create URLSessionAsyncAwaitSpy
private class URLSessionAsyncAwaitSpy: AsyncAwait {
var requests = [URLRequest]()
let result: Result<(Data, URLResponse), NetworkService.Error>
internal init(result: Result<(Data, URLResponse), NetworkService.Error>) {
self.result = result
}
func data(request: URLRequest) async throws -> (Data, URLResponse) {
self.requests.append(request) // 1
return try result.get() // 2
}
}
- We accumulate all requests for this function calls (Spy)
- We return the result that was injected (Stub)
Second create the NetworkService
class NetworkService {
private let session: AsyncAwait
init(session: AsyncAwait = URLSession.shared) {
self.session = session
}
enum Error: Swift.Error {
case connectivity
}
func perform(request: URLRequest) async throws -> (Data, URLResponse) {
do {
let (data, response) = try await session.data(request: request)
return (data, response)
} catch {
throw error
}
}
}
Third create the DogService
class DogService {
private let networkService: NetworkService
private let loadDogsRequest = URLRequest(url: URL(string: "https://any-url.com")!) // 1
init(networkService: NetworkService) {
self.networkService = networkService
}
func loadDogs() async throws {
do {
_ = try await networkService.perform(request: loadDogsRequest)
} catch {
throw error
}
}
}
- Enter a wrong URL to see a failed test first
Fourth update DogsViewModel
injecting the DogService
and create the loadDogs()
async function
class DogsViewModel: ObservableObject {
...
private let dogService: DogService
init(dogService: DogService) {
self.dogService = dogService
}
func loadDogs() async throws {
do {
try await dogService.loadDogs()
} catch {
throw error
}
}
Run the test and it fails
XCTAssertEqual failed: ("[any-url.com]") is not equal to ("[dog.ceo/api/breeds/image/random/3]")
Let’s make it pass using the right URL
class DogService {
...
private let loadDogsRequest = URLRequest(url: URL(string: "https://dog.ceo/api/breeds/image/random/3")!)
...
}
The setup to init the SUT is a bit verbose Let's create an helper function to avoid to always repeat
// MARK: - Helpers
private func makeSUT(
loadDogsResult: Result<(Data, URLResponse), NetworkService.Error> = .failure(.connectivity)) // 1
-> (sut: DogsViewModel, urlSessionSpy: URLSessionAsyncAwaitSpy) {
let urlSessionSpy = URLSessionAsyncAwaitSpy(result: loadDogsResult)
let networkService = NetworkService(session: urlSessionSpy)
let dogService = DogService(networkService: networkService)
let sut = DogsViewModel(dogService: dogService)
return (sut, urlSessionSpy)
}
- Inject the mock result of loading dogs on SUT creation. Add a default value to hide details for tests where the result in not relevant.
Refactor the 2 tests with the makeSUT() function
func test_init_statusIdle() {
let (sut, _) = makeSUT()
XCTAssertEqual(sut.status, .idle)
}
func test_loadDogs_performARequestWithDogsURLRequest() async {
let dogsURLRequest = URLRequest(url: URL(string: "https://dog.ceo/api/breeds/image/random/3")!)
let (sut, urlSessionSpy) = makeSUT()
try? await sut.loadDogs()
XCTAssertEqual(urlSessionSpy.requests, [dogsURLRequest])
}
Run tests
They are passing
Continue with this requirement for
========= LOAD DOGS [ ] Load dogs update status to loading
As usual write the test first
func test_loadDogs_statusLoading() async {
let (sut, _) = makeSUT(loadDogsResult: .failure(.connectivity))
try? await sut.loadDogs()
XCTAssertEqual(sut.messages, [.status(.loading)]) // 1
}
- Needs to create the messages property that will capture all status values update.
Fix the error for Xcode to compile
First create a DogsViewModelSpy
It captures the status updates
private class DogsViewModelSpy: DogsViewModel {
enum Message: Equatable {
static func == (lhs: Message, rhs: Message) -> Bool {
switch (lhs, rhs) {
case (status(let status1), status(let status2)):
return status1 == status2 ? true : false
}
}
case status(DogsViewModel.Status)
}
var messages = [Message]()
override var status: DogsViewModel.Status {
didSet { messages.append(.status(status)) }
}
}
Update the DogsViewModel
with the loading status
Make it Equatable
to able to compare
class DogsViewModel: ObservableObject {
enum Status: Equatable { // 1
static func == (lhs: DogsViewModel.Status, rhs: DogsViewModel.Status) -> Bool {
switch (lhs, rhs) {
case (.idle, .idle), (.loading, .loading):
return true
default:
return false
}
}
case idle
case loading // 2
}
...
}
- Make the enum
Status
conform toEquatable
, to be able to compare them - Add the
loading
case
Run the test and it fails
XCTAssertEqual failed: ("[]") is not equal to ("[AsyncAwaitTests.AsyncAwaitTests.(unknown context at $1038858a0).DogsViewModelSpy.Message.status(AsyncAwait.DogsViewModel.Status.loading)]")
Let’s make it pass on updating the status to loading
class DogsViewModel: ObservableObject {
...
func loadDogs() async throws {
status = .loading 👈
...
Run tests
They are passing
Can mark this requirement as done
========= LOAD DOGS [✅] Load dogs update status to loading
Continue with this requirement for error on request API
========= LOAD DOGS [ ] If Load dogs failed delivers .connectivity error
As usual write the test first
func test_load_deliversConnectivityErrorOnNetworkError() async throws {
let connectivityError = NetworkService.Error.connectivity
let (sut, _) = makeSUT(loadDogsResult: .failure(connectivityError))
do {
try await sut.loadDogs()
XCTFail("Expected to failed") // 1
} catch {
XCTAssertEqual(error as? NetworkService.Error, connectivityError)
}
}
- We expect a failure here. If request succeed the test must failed.
Run tests
They are passing
With async
function you always need to complete
The await
keyword will block us until success or throws error
And to be able to write previous test
I always complete SUT with a .connectivity error
Can mark this requirement as done.
========= LOAD DOGS [✅] If Load dogs failed delivers .connectivity error
Continue with this requirement for display an error message on network error
========= LOAD DOGS [ ] Display an error message (text) on .connectivity error
As usual write the test first
func test_load_onRequestError_displayMessageInfo() async {
let (sut, _) = makeSUT(loadDogsResult: .failure(.connectivity))
do {
try await sut.loadDogs()
XCTFail("Expected to failed")
} catch {
// 1
XCTAssertEqual(sut.error, .loadDogs, "Expect error update or the alert will not be displayed")
// 2
XCTAssertEqual(sut.alertTitle(), "Error loading dogs", "Expected a title or the alert will be displayed without it")
}
}
- Need to create the
error
property that is responsible to display an alert on screen - Need to create the function
alertTitle()
that return the title of the alert for.connectivity
error
Fix the error for Xcode to compile
class DogsViewModel: ObservableObject {
...
enum Error: String, Swift.Error, Identifiable {
case loadDogs
var id: String {
self.rawValue
}
}
...
var error: Error?
...
func loadDogs() async throws {
status = .loading
do {
try await dogService.loadDogs()
} catch let error as NetworkService.Error {
switch error {
case .connectivity:
self.error = .loadDogs 👈
}
throw error
}
}
func alertTitle() -> String {
switch error {
case .loadDogs:
return "Error loading dogs"
case .none:
return ""
}
}
}
Run tests
They are passing
Can mark this requirement as done.
========= LOAD DOGS [✅] Display an error message (text) on .connectivity error
Continue with this requirement for retry on load error
========= LOAD DOGS [ ] User can retry to load (button) on .connectivity error
As usual write the test first
func test_retryLoadDogs_loadDogs() async {
let (sut, _) = makeSUT(loadDogsResult: .failure(.connectivity))
await sut.retryLoadDogs() // 1
XCTAssertEqual(sut.messages, [.status(.loading)])
}
- Need to create the
retryLoadDogs()
function
Fix the error for Xcode to compile
class DogsViewModel: ObservableObject {
...
func retryLoadDogs() async {
try? await loadDogs()
}
...
Run tests
They are passing
Can mark this requirement as done
========= LOAD DOGS [✅] User can retry to load (button) on .connectivity error
Continue with this requirement for succeeded to load but return an empty JSON
========= LOAD DOGS [ ] Load dogs succeed with 200 response
As usual write the test first
func test_loadHereos_deliversSuccess200HTTPResponseWithEmptyDogsJSON() async throws {
let emptyDogsJSON = Data("{\"message\": []}".utf8)
let expectedEmptyDogs = [DogUI]() // 1
let (sut, _) = makeSUT(loadDogsResult: .success((emptyDogsJSON, httpResponse(with: 200)))) // 2
try? await sut.loadDogs()
waitForUICompletion() // 3
XCTAssertEqual(sut.messages, [.status(.loading), .status(.loaded(dogs: expectedEmptyDogs))]) // 4
}
- Create the
DogUI
model that represent the dog requirement to display the UI. - Create the
httpResponse(with:)
helper function that simulate an http response. - Create the
waitForUICompletion()
helper function that wait until the async update of UI completes. - Add the
.loaded(dogs:)
status case.
First the DogUI
model
struct DogUI {
}
Second create the httpResponse(with:)
and waitForUICompletion()
helpers functions
class AsyncAwaitTests: XCTestCase {
...
// MARK: - Helpers
...
private func anyURL() -> URL {
return URL(string: "https://any-url.com")!
}
private func httpResponse(with statusCode: Int) -> HTTPURLResponse {
return HTTPURLResponse(url: anyURL(), statusCode: statusCode, httpVersion: nil, headerFields: nil)!
}
private func waitForUICompletion() {
let expectation = self.expectation(description: "Expect that ui update on the main thread completes")
DispatchQueue.main.async {
expectation.fulfill()
}
wait(for: [expectation], timeout: 1)
}
}
Fourth add the .loaded(dogs:)
status case
class DogsViewModel: ObservableObject {
enum Status: Equatable {
static func == (lhs: DogsViewModel.Status, rhs: DogsViewModel.Status) -> Bool {
switch (lhs, rhs) {
case (.idle, .idle), (.loading, .loading):
return true
case (.loaded(dogs: let dogs1), .loaded(dogs: let dogs2)): 👈
return dogs1 == dogs2 ? true : false // 1
default:
return false
}
}
case idle
case loading
case loaded(dogs: [DogUI]) 👈
}
...
}
- The DogUI needs to conform to
Equatable
to be able to comparestruct DogUI: Equatable { ... }
Run the test and it fails
The status .loaded has never been called
XCTAssertEqual failed: ("[AsyncAwaitTests.AsyncAwaitTests.(unknown context at $10c899490).DogsViewModelSpy.Message.status(AsyncAwait.DogsViewModel.Status.loading)]") is not equal to ("[AsyncAwaitTests.AsyncAwaitTests.(unknown context at $10c899490).DogsViewModelSpy.Message.status(AsyncAwait.DogsViewModel.Status.loading), AsyncAwaitTests.AsyncAwaitTests.(unknown context at $10c899490).DogsViewModelSpy.Message.status(AsyncAwait.DogsViewModel.Status.loaded(dogs: []))]")
Let’s make it pass
Start by update the status with the loaded dogs
class DogsViewModel: ObservableObject {
...
func loadDogs() async throws {
...
do {
let dogs = try await dogService.loadDogs()
DispatchQueue.main.async {
self.status = .loaded(dogs: dogs)
}
} catch let error as NetworkService.Error {
...
}
}
...
}
After handle the response
Check the status code 200
Parse the object using Decodable
class DogService {
...
func loadDogs() async throws -> [DogUI] {
do {
_ = try await networkService.perform(request: loadDogsRequest)
let (data, response) = try await networkService.perform(request: loadDogsRequest)
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
let decoder = JSONDecoder()
do {
let dogsApi = try decoder.decode(DogsApi.self, from: data) // 1
let dogs = dogsApi.message.map{ DogUI(url: URL(string: $0)) } // 2
return dogs
}
catch {
}
}
} catch {
throw error
}
return []
}
}
- Need to create the
DogApi
model - Add the
url
property to theDogUI
model
First create the DogApi
model
struct DogsApi: Decodable {
let message: [String]
}
Second add the url
property to the DogUI
model
struct DogUI: Equatable {
let url: URL?
}
Run tests
They are passing
Can mark this requirement as done
========= LOAD DOGS [✅] Load dogs succeed with 200 response
Continue with this requirement for decodable parsing succeeded
========= LOAD DOGS [ ] Parsing succeed delivers dogs
As usual write the test first
func test_loadHereos_deliversDogsOnSuccess200HTTPResponse() async throws {
let validDogsJSON = Data("{\"message\": [\"https://images.dog.ceo/breeds/husky/n02110185_10116.jpg\"]}".utf8)
let expectedDogs = DogUI(url: URL(string: "https://images.dog.ceo/breeds/husky/n02110185_10116.jpg")!)
let (sut, _) = makeSUT(loadDogsResult: .success((validDogsJSON, httpResponse(with: 200))))
do {
try await sut.loadDogs()
waitForUICompletion()
XCTAssertEqual(sut.status, .loaded(dogs: [expectedDogs]))
} catch {
XCTFail("Expected to succeeded, got error \(error) instead")
}
}
Run tests
They are passing
Can mark this requirement as done
========= LOAD DOGS [✅] Load dogs succeed with 200 response
Continue with this requirement for dogs names extract from url
========= LOAD DOGS [ ] Extract dogs names from returned url
As usual write the test first
func test_dogsName_extractDogsNamesFromPicturesURL() async throws {
let pictureURL1 = URL(string: "https://images.dog.ceo/breeds/setter-english/n02100735_10038.jpg")!
let pictureURL2 = URL(string: "https://images.dog.ceo/breeds/komondor/n02105505_1657.jpg")!
let dog1 = DogUI(url: pictureURL1)
let dog2 = DogUI(url: pictureURL2)
let expectDogsName1 = "Setter English"
let expectDogsName2 = "Komondor"
XCTAssertEqual(dog1.name, expectDogsName1) // 1
XCTAssertEqual(dog2.name, expectDogsName2) // 1
}
- Needs to create the
name
property
Fix the error for Xcode to compile
struct DogUI: Equatable {
let url: URL?
👇
var name: String {
if let urlString = url?.absoluteString {
return urlString
.components(separatedBy: "/")[4]
.replacingOccurrences(of: "-", with: " ")
.capitalized
} else {
return "Unknown"
}
}
}
Run tests
They are passing
Can mark this requirement as done.
========= LOAD DOGS [✅] Extract dogs names from returned url
Continue with this requirement for Decodable
failure
========= LOAD DOGS [ ] Parsing failed delivers .invalidData error
As usual write the test first
func test_loadHereos_deliversErrorOn200HTTPResponseWithInvalidData() async throws {
let expectedError = NetworkService.Error.invalidData // 1
let invalidDogsJSON = Data("".utf8)
let (sut, _) = makeSUT(loadDogsResult: .success((invalidDogsJSON, httpResponse(with: 200))))
do {
try await sut.loadDogs()
XCTFail("Expected to failed the JSON cannot be decoded")
} catch {
XCTAssertEqual(error as? NetworkService.Error, expectedError)
}
}
- Needs to create the .invalidData error case
Fix the error for Xcode to compile
class NetworkService {
...
enum Error: Swift.Error {
case connectivity
case invalidData 👈
}
...
}
Run the test and it fails
Handle the new .invalidData
case
class DogsViewModel: ObservableObject {
...
func loadDogs() async throws {
...
switch error {
case .connectivity:
self.error = .loadDogs
case .invalidData: 👈
break
}
...
}
}
Send .invalidData
error on decode failure
class DogService {
...
func loadDogs() async throws -> [DogUI] {
...
do {
let dogsApi = try decoder.decode(DogsApi.self, from: data)
let dogs = dogsApi.message.map{ DogUI(url: URL(string: $0)) }
return dogs
} catch {
throw NetworkService.Error.invalidData 👈
}
...
}
}
Run tests
They are passing
Can mark this requirement as done
========= LOAD DOGS [✅] Parsing failed delivers .invalidData error
Continue with this requirement for message error on .invalidData error
========= LOAD DOGS [ ] Display an error message (text) on .invalidData error
As usual write the test first
func test_load_displayErrorMessageoOnInvalidDataError() async throws {
let expectedErrorText = "Error parsing dogs"
let (sut, _) = makeSUT(loadDogsResult: .failure(.invalidData))
XCTAssertEqual(sut.alertTitle(), "", "Expected to get an empty string as default value")
do {
try await sut.loadDogs()
XCTFail("Expected to failed")
} catch {
XCTAssertEqual(sut.error, .parsingDogs) // 1
XCTAssertEqual(sut.alertTitle(), expectedErrorText)
}
}
- Needs to create the .parsingDogs error in the view model
Fix the error for Xcode to compile
Add the .parsingDogs error
The function loadDogs
must handle this new case
Update the alertTitle()
function with this new case
class DogsViewModel: ObservableObject {
...
enum Error: String, Swift.Error, Identifiable {
case loadDogs
case parsingDogs 👈
...
func loadDogs() async throws {
switch error {
case .connectivity:
self.error = .loadDogs
case .invalidData:
self.error = .parsingDogs 👈
}
}
...
func alertTitle() -> String {
switch error {
case .loadDogs:
return "Error loading dogs"
case .parsingDogs: 👈
return "Error parsing dogs"
case .none:
return ""
}
}
}
Run tests
They are passing
Can mark this requirement as done.
========= LOAD DOGS [✅] Display an error message (text) on .invalidData error
Continue with this requirement for retry on .invalidData
error
========= LOAD DOGS [ ] User can retry to load (button) on .invalidData error
As usual write the test first
func test_loadHereos_onParsingError_userCanRetryToLoad() async {
let (sut, _) = makeSUT(loadDogsResult: .failure(.invalidData))
await sut.retryLoadDogs()
XCTAssertEqual(sut.messages, [.status(.loading)])
}
Run tests
They are passing
The retryLoadDogs()
function already exist
Can mark this requirement as done.
========= LOAD DOGS [✅] User can retry to load (button) on .invalidData error
Now let's create the view that implement the view model
struct DogsView: View {
@ObservedObject var viewModel: DogsViewModel
var body: some View {
NavigationView {
ZStack {
switch viewModel.status {
case .idle:
EmptyView()
case .loading:
ProgressView()
case .loaded(dogs: let dogs):
ZStack {
Color(UIColor.secondarySystemBackground)
.edgesIgnoringSafeArea(.all)
ScrollView {
LazyVStack(spacing: 34) {
ForEach(dogs, id: \.url) { dog in
VStack(spacing: 0) {
AsyncImage(url: dog.url) { phase in
if let image = phase.image {
image // Displays the loaded image.
.resizable()
.scaledToFill()
.frame(width: UIScreen.main.bounds.width - 32, height: UIScreen.main.bounds.width, alignment: .center)
.clipped()
} else if phase.error != nil {
Color.red // Indicates an error.
.frame(width: UIScreen.main.bounds.width - 32, height: UIScreen.main.bounds.width, alignment: .center)
} else {
ZStack { // Acts as a placeholder.
Color(UIColor.tertiarySystemBackground)
.frame(width: UIScreen.main.bounds.width - 32, height: UIScreen.main.bounds.width, alignment: .center)
ProgressView()
.scaleEffect(4)
.progressViewStyle(CircularProgressViewStyle(tint: SwiftUI.Color(UIColor.label).opacity(0.5)))
}
}
}
Text(dog.name)
.font(.title)
.foregroundColor(SwiftUI.Color.primary)
.padding()
}
.background(SwiftUI.Color(UIColor.tertiarySystemBackground))
.cornerRadius(20)
.shadow(color: Color.black.opacity(0.25), radius: 20, x: 0, y: 3)
}
}
}
.padding(.top, 32)
}
}
}
.navigationTitle("Dogs")
}
.task {
try? await viewModel.loadDogs()
}
.alert(item: $viewModel.error) { _ in
return Alert(
title: Text(viewModel.alertTitle()),
message: nil,
dismissButton: .default(Text("RETRY")) {
Task {
await viewModel.retryLoadDogs()
}
})
}
}
}
To preview the result let's create a fake view model
It will first simulate a .connectivity network error
Second an .invalidData parsing error
Third a success response with 3 dogs
Start creating setting the preview
struct DogsView_Previews: PreviewProvider {
static var previews: some View {
DogsView(viewModel: fakeViewModel()) // 1
}
}
- Needs to create this
fakeViewModel
that doesn't exist
Create the fakeViewModel
static func fakeViewModel() -> DogsViewModel {
let urlSessionSpy = URLSessionAsyncAwaitSpy( // 1
results:
[
.failure(.connectivity),
.failure(.invalidData),
.success((validData(), validResponse())) // 2
]
)
let networkService = NetworkService(session: urlSessionSpy)
let service = DogService(networkService: networkService)
return DogsViewModel(dogService: service)
}
- Need to create the
URLSessionAsyncAwaitSpy
that will handle all fake results API - Need to create
validData()
andvalidResponse()
that will simulate the fake valid response
First create the URLSessionAsyncAwaitSpy
private class URLSessionAsyncAwaitSpy: AsyncAwait {
var results: [Result<(Data, URLResponse), NetworkService.Error>]
internal init(results: [Result<(Data, URLResponse), NetworkService.Error>]) {
self.results = results
}
func data(request: URLRequest) async throws -> (Data, URLResponse) {
return try results.removeFirst().get() // 1
}
}
- Use the
removeFirst()
function to return the result and will be ready for the next one
Second create validData()
and validResponse()
static func validData() -> Data {
let dogsJSON =
[
"message":
[
"https://images.dog.ceo/breeds/setter-english/n02100735_10038.jpg",
"https://images.dog.ceo/breeds/retriever-golden/n02099601_6331.jpg",
"https://images.dog.ceo/breeds/komondor/n02105505_1657.jpg"
]
]
do {
return try JSONSerialization.data(withJSONObject: dogsJSON)
} catch {
assertionFailure("Expected to get a valid JSON, got error \(error) instead")
return Data()
}
}
static func validResponse() -> HTTPURLResponse {
HTTPURLResponse(url: URL(string: "https://any-url.com")!, statusCode: 200, httpVersion: nil, headerFields: nil)!
}
Run the preview
First we failed as expected with a .connectivity error
With the expected message create when TDD the view model
Tap the RETRY button the and the .invalidData error appear
Tap the RETRY button and the success with valid data will be displayed
I hope you enjoy this TDD journey with async/await
async/await can be tested using stub (inject result)
Because it needs how it will complete before method execution
Or you will always be block by await
If never completes with success or error
That's it !!