Open TheMet app in the Starter folder. CountView and the commented out lines from the final app of Lesson 3 have been removed.
Open ContentView. It instantiates TheMetStore as a @State property.
The preview instantiates ContentView, so it also uses TheMetStore, which makes network calls every time the preview refreshes.
TheMetStore instantiates TheMetService. The service is a dependency. Instead of creating it here, a small code change makes it explicit that it can be injected into the view model:
private let service: TheMetService
init(service: TheMetService = TheMetService(),
_ maxIndex: Int = 20) {
self.service = service
self.maxIndex = maxIndex
}
TheMetService contains all the networking code. To write unit tests that don’t access the network, developers often create a mock networking service to inject into the system-under-test. This could also be used to stop network access by the ContentView preview, but it’s outside the scope of this lesson.
To prevent network calls from the ContentView preview, it’s easier to create a mock view model. Head back to TheMetStore to get started.
In TheMetStore, add a subclass of TheMetStore named MockMetStore:
@Observable class MockMetStore: TheMetStore {
}
Now, what should you put into MockMetStore? Look at the published properties and methods of TheMetStore. It publishes objects, and it has a fetchObjects method that creates objects.
Quickly check ContentView to see what it uses.
Right-click store, and Edit All in Scope.
ContentView uses objects, maxIndex, and down here, it calls fetchObjects. So you need to create a mock version of fetchObjects.
In MockMetStore, override fetchObjects:
override func fetchObjects(for queryTerm: String) async throws {
}
Here’s something that was prepared earlier. Open Preview Content/MetStoreDevData and copy the objects code. Paste this into fetchObjects in MockMetStore:
objects = [
Object(
objectID: 452174,
title: "Bahram Gur Slays the Rhino-Wolf",
creditLine: "Gift of Arthur A. Houghton Jr., 1970",
objectURL: "https://www.metmuseum.org/art/collection/search/452174",
isPublicDomain: false,
primaryImageSmall: ""),
Object(
objectID: 241715,
title: "Terracotta oil lamp",
creditLine: "The Cesnola Collection, Purchased by subscription, 1874–76",
objectURL: "https://www.metmuseum.org/art/collection/search/241715",
isPublicDomain: true,
primaryImageSmall: "https://images.metmuseum.org/CRDImages/gr/web-large/DP239561.jpg"),
Object(
objectID: 452648,
title: "Gushtasp Slays the Rhino-Wolf",
creditLine: "Bequest of Monroe C. Gutman, 1974",
objectURL: "https://www.metmuseum.org/art/collection/search/452648",
isPublicDomain: true,
primaryImageSmall: "https://images.metmuseum.org/CRDImages/is/web-large/DP108572.jpg")
]
These are the same three “rhino” objects that TheMet starts with, but you set the first object’s isPublicDomain value to false and its primaryImageSmall value to "". This list looks different from the real list, so you’ll know you’re using the mock view model.
Now, back to ContentView to replace ContentView() in Preview with this:
ContentView(store: MockMetStore())
If necessary, refresh the preview. “Bahram Gur Slays the Rhino-Wolf” is on a red background, and tapping it takes you to its web page on The Met’s website. Injecting the mock view model into the preview allows you to adjust the appearance of ContentView all you want without sending network requests.
One last thing: Encapsulating presentation logic in the view model makes it easy to test the logic with unit tests instead of UI tests.
Open TheMetStore. The maxIndex property controls the size of the objects array. It’s easy to write a unit test to verify it works. Open TheMetTests in the Test navigator.
TheMet app is already imported, and the setUp and tearDown methods call the super methods. The system-under-test is TheMetStore. Declare it as an implicitly unwrapped optional:
final class TheMetTests: XCTestCase {
var sut: TheMetStore! // add this line
Then instantiate it in setUpWithError:
override func setUpWithError() throws {
try super.setUpWithError()
sut = TheMetStore() // add this line
}
And remove it in tearDownWithError:
override func tearDownWithError() throws {
sut = nil // add this line
try super.tearDownWithError()
}
Now, replace testExample with this test method:
func testMaxIndexObjectsFetched() async throws {
try await sut.fetchObjects(for: "cat")
XCTAssertLessThanOrEqual(sut.objects.count, sut.maxIndex)
}
There are a lot of cat objects in the museum’s collection. You’re checking that TheMetStore downloads at most 20.
The text runs the app, so select a simulator as the run destination.