Unit checks must be as freed from exterior dependencies as doable. Which means you need to have full management over the whole lot that occurs in your checks.
For instance, in the event you’re working with a database, you need the database to be empty or in some predefined state earlier than your check begins. You use on the database throughout your check and after your check the database will be thrown away.
By making your checks not depend upon exterior state, you ensure that your checks are repeatable, can run in parallel and do not depend upon one check operating earlier than one other check.
Traditionally, one thing just like the community is especially onerous to make use of in checks as a result of what in case your check runs however you do not have a community connection, or what in case your check runs throughout a time the place the server that you simply’re speaking to has an outage? Your checks would now fail though there’s nothing fallacious along with your code. So that you need to decouple your checks from the community in order that your checks change into repeatable, unbiased and run with out counting on some exterior server.
On this submit, I will discover two totally different choices with you.
One possibility is to easily mock out the networking layer completely. The opposite possibility makes use of one thing referred to as URLProtocol which permits us to take full management over the requests and responses within URLSession, which suggests we are able to really make our checks work with out a community connection and with out eradicating URLSession from our checks.
Defining the code that we need to check
With a view to correctly work out how we will check our code, we must always in all probability outline the objects that we wish to check. On this case, I wish to check a reasonably easy view mannequin and networking pair.
So let’s check out the view mannequin first. This is the code that I wish to check for my view mannequin.
@Observable
class FeedViewModel {
var feedState: FeedState = .notLoaded
personal let community: NetworkClient
init(community: NetworkClient) {
self.community = community
}
func fetchPosts() async {
feedState = .loading
do {
let posts = strive await community.fetchPosts()
feedState = .loaded(posts)
} catch {
feedState = .error(error)
}
}
func createPost(withContents contents: String) async throws -> Put up {
return strive await community.createPost(withContents: contents)
}
}
In essence, the checks that I wish to write right here would affirm that calling fetchPost would really replace my checklist of posts as new posts change into out there.
Planning the checks
I might in all probability name fetchPost to ensure that the feed state turns into a price that I count on, then I might name it once more and return totally different posts from the community, ensuring that my feed state updates accordingly. I might in all probability additionally need to check that if any error could be thrown through the fetching section, that my feed state will change into the corresponding error sort.
So to boil that right down to a listing, here is the check I might write:
- Be sure that I can fetch posts
- Be sure that posts get up to date if the community returns new posts
- Be sure that errors are dealt with accurately
I even have the create submit perform, which is a little bit bit shorter. It would not change the feed state.
What I might check there may be that if I create a submit with sure contents, a submit with the offered contents is definitely what’s returned from this perform.
I’ve already applied the networking layer for this view mannequin, so here is what that appears like.
class NetworkClient {
let urlSession: URLSession
let baseURL: URL = URL(string: "https://practicalios.dev/")!
init(urlSession: URLSession) {
self.urlSession = urlSession
}
func fetchPosts() async throws -> [Post] {
let url = baseURL.appending(path: "posts")
let (information, _) = strive await urlSession.information(from: url)
return strive JSONDecoder().decode([Post].self, from: information)
}
func createPost(withContents contents: String) async throws -> Put up {
let url = baseURL.appending(path: "create-post")
var request = URLRequest(url: url)
request.httpMethod = "POST"
let physique = ["contents": contents]
request.httpBody = strive JSONEncoder().encode(physique)
let (information, _) = strive await urlSession.information(for: request)
return strive JSONDecoder().decode(Put up.self, from: information)
}
}
In a super world, I might have the ability to check that calling fetchPosts on my community consumer is definitely going to assemble the right URL and that it’s going to use that URL to make a name to URLSession. Equally for createPost, I might need to ensure that the HTTP physique that I assemble is legitimate and incorporates the info that I intend to ship to the server.
There are basically two issues that we might need to check right here:
- The view mannequin, ensuring that it calls the right capabilities of the community.
- The networking consumer, ensuring that it makes the right calls to the server.
Changing your networking layer with a mock for testing
A standard option to check code that depends on a community is to easily take away the networking portion of it altogether. As a substitute of relying on concrete networking objects, we might depend upon protocols.
Abstracting our dependencies with protocols
This is what that appears like if we apply this to our view mannequin.
protocol Networking {
func fetchPosts() async throws -> [Post]
func createPost(withContents contents: String) async throws -> Put up
}
@Observable
class FeedViewModel {
var feedState: FeedState = .notLoaded
personal let community: any Networking
init(community: any Networking) {
self.community = community
}
// capabilities are unchanged
}
The important thing factor that modified right here is that as a substitute of relying on a community consumer, we rely on the Networking protocol. The Networking protocol defines which capabilities we are able to name and what the return sorts for these capabilities will likely be.
Because the capabilities that we have outlined are already outlined on NetworkClient, we are able to replace our NetworkClient to evolve to Networking.
class NetworkClient: Networking {
// No adjustments to the implementation
}
In our software code, we are able to just about use this community consumer passage to our feed view mannequin and nothing would actually change. It is a actually low-key option to introduce testability into our codebase for the feed view mannequin.
Mocking the community in a check
Now let’s go forward and write a check that units up our feed view mannequin in order that we are able to begin testing it.
class MockNetworkClient: Networking {
func fetchPosts() async throws -> [Post] {
return []
}
func createPost(withContents contents: String) async throws -> Put up {
return Put up(id: UUID(), contents: contents)
}
}
struct FeedViewModelTests {
@Check func testFetchPosts() async throws {
let viewModel = FeedViewModel(community: MockNetworkClient())
// we are able to now begin testing the view mannequin
}
}
Now that we’ve got a setup that we are able to check, it is time to take one other have a look at our testing objectives for the view mannequin. These testing objectives are what is going on to drive our selections for what we’ll put in our MockNetworkClient.
Writing our checks
These are the checks that I needed to jot down for my submit fetching logic:
- Be sure that I can fetch posts
- Be sure that posts get up to date if the community returns new posts
- Be sure that errors are dealt with accurately
Let’s begin including them one-by-one.
With a view to check whether or not I can fetch posts, my mock community ought to in all probability return some posts:
class MockNetworkClient: Networking {
func fetchPosts() async throws -> [Post] {
return [
Post(id: UUID(), contents: "This is the first post"),
Post(id: UUID(), contents: "This is post number two"),
Post(id: UUID(), contents: "This is post number three")
]
}
// ...
}
With this in place, we are able to check our view mannequin to see if calling fetchPosts will really use this checklist of posts and replace the feed state accurately.
@Check func testFetchPosts() async throws {
let viewModel = FeedViewModel(community: MockNetworkClient())
await viewModel.fetchPosts()
guard case .loaded(let posts) = viewModel.feedState else {
Problem.document("Feed state is just not set to .loaded")
return
}
#count on(posts.depend == 3)
}
The second check would have us name fetchPosts twice to ensure that we replace the checklist of posts within the view mannequin.
To ensure that us to manage our checks absolutely, we must always in all probability have a option to inform the mock community what checklist of posts it ought to return once we name fetchPost. Let’s add a property to the mock that enables us to specify a listing of posts to return from inside our checks:
class MockNetworkClient: Networking {
var postsToReturn: [Post] = []
func fetchPosts() async throws -> [Post] {
return postsToReturn
}
func createPost(withContents contents: String) async throws -> Put up {
return Put up(id: UUID(), contents: contents)
}
}
And now we are able to write our second check as follows:
@Check func fetchPostsShouldUpdateWithNewResponses() async throws {
let consumer = MockNetworkClient()
consumer.postsToReturn = [
Post(id: UUID(), contents: "This is the first post"),
Post(id: UUID(), contents: "This is post number two"),
Post(id: UUID(), contents: "This is post number three")
]
let viewModel = FeedViewModel(community: consumer)
await viewModel.fetchPosts()
guard case .loaded(let posts) = viewModel.feedState else {
Problem.document("Feed state is just not set to .loaded")
return
}
#count on(posts.depend == 3)
consumer.postsToReturn = [
Post(id: UUID(), contents: "This is a new post")
]
await viewModel.fetchPosts()
guard case .loaded(let posts) = viewModel.feedState else {
Problem.document("Feed state is just not set to .loaded")
return
}
#count on(posts.depend == 1)
}
The check is now extra verbose however we’re in full management over the responses that our mock community will present.
Our third check for fetching posts is to ensure that errors are dealt with accurately. Which means we must always apply one other replace to our mock. The purpose is to permit us to outline whether or not our name to fetchPosts ought to return a listing of posts or throw an error. We are able to use Outcome for this:
class MockNetworkClient: Networking {
var fetchPostsResult: Outcome<[Post], Error> = .success([])
func fetchPosts() async throws -> [Post] {
return strive fetchPostsResult.get()
}
func createPost(withContents contents: String) async throws -> Put up {
return Put up(id: UUID(), contents: contents)
}
}
Now we are able to make our fetch posts calls succeed or fail as wanted within the checks. Our checks would now have to be up to date in order that as a substitute of simply passing a listing of posts to return, we will present success with the checklist. This is what that may seem like for our first check (I’m positive you’ll be able to replace the longer check primarily based on this instance).
@Check func testFetchPosts() async throws {
let consumer = MockNetworkClient()
consumer.fetchPostsResult = .success([
Post(id: UUID(), contents: "This is the first post"),
Post(id: UUID(), contents: "This is post number two"),
Post(id: UUID(), contents: "This is post number three")
])
let viewModel = FeedViewModel(community: consumer)
await viewModel.fetchPosts()
guard case .loaded(let posts) = viewModel.feedState else {
Problem.document("Feed state is just not set to .loaded")
return
}
#count on(posts.depend == 3)
}
Information that we are able to present successful or failure for our checks. We are able to really go on forward and inform our checks to throw a selected failure.
@Check func fetchPostsShouldUpdateWithErrors() async throws {
let consumer = MockNetworkClient()
let expectedError = NSError(area: "Check", code: 1, userInfo: nil)
consumer.fetchPostsResult = .failure(expectedError)
let viewModel = FeedViewModel(community: consumer)
await viewModel.fetchPosts()
guard case .error(let error) = viewModel.feedState else {
Problem.document("Feed state is just not set to .error")
return
}
#count on(error as NSError == expectedError)
}
We now have three checks that check our view mannequin.
What’s fascinating about these checks is that all of them depend upon a mock community. Which means we’re not counting on a community connection. However this additionally doesn’t suggest that our view mannequin and community consumer are going to work accurately.
We’ve not examined that our precise networking implementation goes to assemble the precise requests that we count on it to create. With a view to do that we are able to leverage one thing referred to as URLProtocol.
Mocking responses with URLProtocol
Figuring out that our view mannequin works accurately is absolutely good. Nevertheless, we additionally need to ensure that the precise glue between our app and the server works accurately. That signifies that we must be testing our community consumer in addition to the view mannequin.
We all know that we should not be counting on the community in our unit checks. So how can we get rid of the precise community from our networking consumer?
One strategy might be to create a protocol for URLSession and stuff the whole lot out that manner. It is an possibility, nevertheless it’s not one which I like. I a lot want to make use of one thing referred to as URLProtocol.
Once we use URLProtocol to mock out our community, we are able to inform URLSession that we must be utilizing our URLProtocol when it is making an attempt to make a community request.
This enables us to take full management of the response that we’re returning and it signifies that we are able to ensure that our code works with no need the community. Let’s check out an instance of this.
Earlier than we implement the whole lot that we’d like for our check, let’s check out what it seems to be wish to outline an object that inherits from URLProtocol. I am implementing a few fundamental strategies that I’ll want, however there are different strategies out there on an object that inherits from URLProtocol.
I extremely suggest you check out Apple’s documentation in the event you’re eager about studying about that.
Organising ur URLProtocol subclass
For the checks that we have an interest implementing, that is the skeleton class that I will be working from:
class NetworkClientURLProtocol: URLProtocol {
override class func canInit(with request: URLRequest) -> Bool {
return true
}
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
override func startLoading() {
// we are able to carry out our faux request right here
}
}
Within the startLoading perform, we’re imagined to execute our faux community name and inform the consumer (which is a property that we inherit from URLProtocol) that we completed loading our information.
So the very first thing that we have to do is implement a manner for a consumer of our faux community to supply a response for a given URL. Once more, there are a lot of methods to go about this. I am simply going to make use of essentially the most fundamental model that I can provide you with to ensure that we do not get slowed down by particulars that may range from undertaking to undertaking.
struct MockResponse {
let statusCode: Int
let physique: Information
}
class NetworkClientURLProtocol: URLProtocol {
// ...
static var responses: [URL: MockResponse] = [:]
static var validators: [URL: (URLRequest) -> Bool] = [:]
static let queue = DispatchQueue(label: "NetworkClientURLProtocol")
static func register(
response: MockResponse, requestValidator: @escaping (URLRequest) -> Bool, for url: URL
) {
queue.sync {
responses[url] = response
validators[url] = requestValidator
}
}
// ...
}
By including this code to my NetworkClientURLProtocol, I can register responses and a closure to validate URLRequest. This enables me to check whether or not a given URL ends in the anticipated URLRequest being constructed by the networking layer. That is notably helpful once you’re testing POST requests.
Word that we have to make our responses and validators objects static. That is as a result of we won’t entry the precise occasion of our URL protocol that we will use earlier than the request is made. So we have to register them statically after which in a while in our begin loading perform we’ll pull out the related response invalidator. We have to ensure that we synchronize this via a queue so we’ve got a number of checks operating in parallel. We would run into points with overlap.
Earlier than we implement the check, let’s full our implementation of startLoading:
class NetworkClientURLProtocol: URLProtocol {
// ...
override func startLoading() {
// be sure that we're good to...
guard let consumer = self.consumer,
let requestURL = self.request.url,
let validator = validators[requestURL],
let response = responses[requestURL]
else {
Problem.document("Tried to carry out a URL Request that does not have a validator and/or response")
return
}
// validate that the request is as anticipated
#count on(validator(self.request))
// assemble our response object
guard let httpResponse = HTTPURLResponse(
url: requestURL,
statusCode: response.statusCode, httpVersion: nil,
headerFields: nil
) else {
Problem.document("Not in a position to create an HTTPURLResponse")
return
}
// obtain response from the faux community
consumer.urlProtocol(self, didReceive: httpResponse, cacheStoragePolicy: .notAllowed)
// inform the URLSession that we have "loaded" information
consumer.urlProtocol(self, didLoad: response.physique)
// full the request
consumer.urlProtocolDidFinishLoading(self)
}
}
The code incorporates feedback on what we’re doing. When you won’t have seen this sort of code earlier than, it must be comparatively self-explanatory.
Implementing a check that makes use of our URLProtocol subclass
Now that we’ve obtained startLoading applied, let’s attempt to use this NetworkClientURLProtocol in a check…
class FetchPostsProtocol: NetworkClientURLProtocol { }
struct NetworkClientTests {
func makeClient(with protocolClass: NetworkClientURLProtocol.Sort) -> NetworkClient {
let configuration = URLSessionConfiguration.default
configuration.protocolClasses = [protocolClass]
let session = URLSession(configuration: configuration)
return NetworkClient(urlSession: session)
}
@Check func testFetchPosts() async throws {
let networkClient = makeClient(with: FetchPostsProtocol.self)
let returnData = strive JSONEncoder().encode([
Post(id: UUID(), contents: "This is the first post"),
Post(id: UUID(), contents: "This is post number two"),
Post(id: UUID(), contents: "This is post number three"),
])
let fetchPostsURL = URL(string: "https://practicalios.dev/posts")!
FetchPostsProtocol.register(
response: MockResponse(statusCode: 200, physique: returnData),
requestValidator: { request in
return request.url == fetchPostsURL
},
for: fetchPostsURL
)
let posts = strive await networkClient.fetchPosts()
#count on(posts.depend > 0)
}
}
The very first thing I am doing on this code is creating a brand new subclass of my NetworkClientProtocol. The explanation I am doing that’s as a result of I might need a number of checks operating on the similar time.
For that cause, I need every of my Swift check capabilities to get its personal class. This may be me being a little bit bit paranoid about issues overlapping when it comes to when they’re referred to as, however I discover that this creates a pleasant separation between each check that you’ve and the precise URLProtocol implementation that you simply’re utilizing to carry out your assertions.
The purpose of this check is to ensure that once I ask my community consumer to go fetch posts, it really performs a request to the right URL. And given a profitable response that incorporates information in a format that’s anticipated from the server’s response, we’re in a position to decode the response information into a listing of posts.
We’re basically changing the server on this instance, which permits us to take full management over verifying that we’re making the right request and still have full management over regardless of the server would return for that request.
Testing a POST request with URLProtocol
Now let’s see how we are able to write a check that makes positive that we’re sending the right request once we’re making an attempt to create a submit.
struct NetworkClientTests {
// ...
@Check func testCreatePost() async throws {
let networkClient = makeClient(with: CreatePostProtocol.self)
// arrange anticipated information
let content material = "It is a new submit"
let expectedPost = Put up(id: UUID(), contents: content material)
let returnData = strive JSONEncoder().encode(expectedPost)
let createPostURL = URL(string: "https://practicalios.dev/create-post")!
// register handlers
CreatePostProtocol.register(
response: MockResponse(statusCode: 200, physique: returnData),
requestValidator: { request in
// validate fundamental setup
guard
let httpBody = request.streamedBody,
request.url == createPostURL,
request.httpMethod == "POST" else {
Problem.document("Request is just not a POST request or would not have a physique")
return false
}
// guarantee physique is appropriate
do {
let decoder = JSONDecoder()
let physique = strive decoder.decode([String: String].self, from: httpBody)
return physique == ["contents": content]
} catch {
Problem.document("Request physique is just not a sound JSON object")
return false
}
},
for: createPostURL
)
// carry out community name and validate response
let submit = strive await networkClient.createPost(withContents: content material)
#count on(submit == expectedPost)
}
}
There’s numerous code right here, however total it follows a reasonably related step to earlier than. There’s one factor that I need to name your consideration to, and that’s the line the place I extract the HTTP physique from my request within the validator. As a substitute of accessing httpBody, I am accessing streamedBody. This isn’t a property that usually exists on URLRequest, so let’s speak about why I would like that for a second.
If you create a URLRequest and execute that with URLSession, the httpBody that you simply assign is transformed to a streaming physique.
So once you entry httpBody within the validator closure that I’ve, it may be nil.
As a substitute of accessing that, we have to entry the streaming physique, collect the info, and return alll information.
This is the implementation of the streamedBody property that I added in an extension to URLRequest:
extension URLRequest {
var streamedBody: Information? {
guard let bodyStream = httpBodyStream else { return nil }
let bufferSize = 1024
let buffer = UnsafeMutablePointer.allocate(capability: bufferSize)
var information = Information()
bodyStream.open()
whereas bodyStream.hasBytesAvailable {
let bytesRead = bodyStream.learn(buffer, maxLength: bufferSize)
information.append(buffer, depend: bytesRead)
}
bodyStream.shut()
return information
}
}
With all this in place, I will now examine that my community consumer constructs a totally appropriate community request that’s being despatched to the server and that if the server responds with a submit like I count on, I am really in a position to deal with that.
So at this level, I’ve checks for my view mannequin (the place I mock out the whole networking layer to ensure that the view mannequin works accurately) and I’ve checks for my networking consumer to ensure that it performs the right requests on the appropriate occasions.
In Abstract
Testing code that has dependencies is at all times a little bit bit tough. When you have got a dependency you may need to mock it out, stub it out, take away it or in any other case disguise it from the code that you simply’re testing. That manner you’ll be able to purely check whether or not the code that you simply’re eager about testing acts as anticipated.
On this submit we checked out a view mannequin and networking object the place the view mannequin relies on the community. We mocked out the networking object to ensure that we might check our view mannequin in isolation.
After that we additionally needed to jot down some checks for the networking object itself. To try this, we used a URLProtocol object. That manner we might take away the dependency on the server completely and absolutely run our checks in isolation. We are able to now check that our networking consumer makes the right requests and handles responses accurately as effectively.
Which means we now have end-to-end testing for a view mannequin and networking consumer in place.
I don’t typically leverage URLProtocol in my unit checks; it’s primarily in complicated POST requests or flows that I’m eager about testing my networking layer this deeply. For easy requests I are likely to run my app with Proxyman hooked up and I’ll confirm that my requests are appropriate manually.
