Unit assessments must be as freed from exterior dependencies as doable. Which means that you need to have full management over all the things that occurs in your assessments.
For instance, should 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 might be thrown away.
By making your assessments not rely upon exterior state, you make it possible for your assessments are repeatable, can run in parallel and do not rely upon one check working earlier than one other check.
Traditionally, one thing just like the community is especially laborious to make use of in assessments 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 just’re speaking to has an outage? Your assessments would now fail regardless that there’s nothing improper along with your code. So that you need to decouple your assessments from the community in order that your assessments develop into repeatable, impartial and run with out counting on some exterior server.
On this publish, I’ll discover two completely different choices with you.
One possibility is to easily mock out the networking layer fully. 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 will really make our assessments work with out a community connection and with out eradicating URLSession
from our assessments.
Defining the code that we need to check
With the intention to correctly work out how we will check our code, we must always in all probability outline the objects that we want to check. On this case, I want 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 want to check for my view mannequin.
@Observable
class FeedViewModel {
var feedState: FeedState = .notLoaded
non-public let community: NetworkClient
init(community: NetworkClient) {
self.community = community
}
func fetchPosts() async {
feedState = .loading
do {
let posts = attempt await community.fetchPosts()
feedState = .loaded(posts)
} catch {
feedState = .error(error)
}
}
func createPost(withContents contents: String) async throws -> Publish {
return attempt await community.createPost(withContents: contents)
}
}
In essence, the assessments that I want to write right here would verify that calling fetchPost
would really replace my listing of posts as new posts develop into out there.
Planning the assessments
I’d in all probability name fetchPost
to make it possible for the feed state turns into a price that I anticipate, then I’d name it once more and return completely different posts from the community, ensuring that my feed state updates accordingly. I’d in all probability additionally need to check that if any error could be thrown through the fetching section, that my feed state will develop into the corresponding error sort.
So to boil that all the way down to an inventory, here is the check I’d write:
- Make it possible for I can fetch posts
- Make it possible for posts get up to date if the community returns new posts
- Make it possible for errors are dealt with appropriately
I even have the create publish operate, which is somewhat bit shorter. It does not change the feed state.
What I’d check there’s that if I create a publish with sure contents, a publish with the offered contents is definitely what’s returned from this operate.
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, _) = attempt await urlSession.information(from: url)
return attempt JSONDecoder().decode([Post].self, from: information)
}
func createPost(withContents contents: String) async throws -> Publish {
let url = baseURL.appending(path: "create-post")
var request = URLRequest(url: url)
request.httpMethod = "POST"
let physique = ["contents": contents]
request.httpBody = attempt JSONEncoder().encode(physique)
let (information, _) = attempt await urlSession.information(for: request)
return attempt JSONDecoder().decode(Publish.self, from: information)
}
}
In a great world, I’d be capable of check that calling fetchPosts
on my community shopper 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’d need to make it possible for the HTTP physique that I assemble is legitimate and incorporates the information 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 shopper, ensuring that it makes the right calls to the server.
Changing your networking layer with a mock for testing
A typical method 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 rely 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 -> Publish
}
@Observable
class FeedViewModel {
var feedState: FeedState = .notLoaded
non-public 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 shopper, we rely on the Networking
protocol. The Networking
protocol defines which capabilities we will name and what the return varieties for these capabilities can be.
Because the capabilities that we have outlined are already outlined on NetworkClient
, we will replace our NetworkClient
to adapt to Networking
.
class NetworkClient: Networking {
// No adjustments to the implementation
}
In our software code, we will just about use this community shopper passage to our feed view mannequin and nothing would actually change. It is a actually low-key method 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 will begin testing it.
class MockNetworkClient: Networking {
func fetchPosts() async throws -> [Post] {
return []
}
func createPost(withContents contents: String) async throws -> Publish {
return Publish(id: UUID(), contents: contents)
}
}
struct FeedViewModelTests {
@Check func testFetchPosts() async throws {
let viewModel = FeedViewModel(community: MockNetworkClient())
// we will now begin testing the view mannequin
}
}
Now that we have now a setup that we will check, it is time to take one other have a look at our testing targets for the view mannequin. These testing targets are what is going on to drive our selections for what we’ll put in our MockNetworkClient
.
Writing our assessments
These are the assessments that I wished to write down for my publish fetching logic:
- Make it possible for I can fetch posts
- Make it possible for posts get up to date if the community returns new posts
- Make it possible for errors are dealt with appropriately
Let’s begin including them one-by-one.
With the intention 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 will check our view mannequin to see if calling fetchPosts
will really use this listing of posts and replace the feed state appropriately.
@Check func testFetchPosts() async throws {
let viewModel = FeedViewModel(community: MockNetworkClient())
await viewModel.fetchPosts()
guard case .loaded(let posts) = viewModel.feedState else {
Situation.file("Feed state is just not set to .loaded")
return
}
#anticipate(posts.rely == 3)
}
The second check would have us name fetchPosts
twice to make it possible for we replace the listing of posts within the view mannequin.
To ensure that us to manage our assessments absolutely, we must always in all probability have a method to inform the mock community what listing of posts it ought to return after we name fetchPost
. Let’s add a property to the mock that enables us to specify an inventory of posts to return from inside our assessments:
class MockNetworkClient: Networking {
var postsToReturn: [Post] = []
func fetchPosts() async throws -> [Post] {
return postsToReturn
}
func createPost(withContents contents: String) async throws -> Publish {
return Publish(id: UUID(), contents: contents)
}
}
And now we will write our second check as follows:
@Check func fetchPostsShouldUpdateWithNewResponses() async throws {
let shopper = MockNetworkClient()
shopper.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: shopper)
await viewModel.fetchPosts()
guard case .loaded(let posts) = viewModel.feedState else {
Situation.file("Feed state is just not set to .loaded")
return
}
#anticipate(posts.rely == 3)
shopper.postsToReturn = [
Post(id: UUID(), contents: "This is a new post")
]
await viewModel.fetchPosts()
guard case .loaded(let posts) = viewModel.feedState else {
Situation.file("Feed state is just not set to .loaded")
return
}
#anticipate(posts.rely == 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 make it possible for errors are dealt with appropriately. Which means that we must always apply one other replace to our mock. The aim is to permit us to outline whether or not our name to fetchPosts
ought to return an inventory 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 attempt fetchPostsResult.get()
}
func createPost(withContents contents: String) async throws -> Publish {
return Publish(id: UUID(), contents: contents)
}
}
Now we will make our fetch posts calls succeed or fail as wanted within the assessments. Our assessments would now have to be up to date in order that as a substitute of simply passing an inventory of posts to return, we will present success with the listing. This is what that may appear like for our first check (I’m positive you possibly can replace the longer check primarily based on this instance).
@Check func testFetchPosts() async throws {
let shopper = MockNetworkClient()
shopper.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: shopper)
await viewModel.fetchPosts()
guard case .loaded(let posts) = viewModel.feedState else {
Situation.file("Feed state is just not set to .loaded")
return
}
#anticipate(posts.rely == 3)
}
Information that we will present successful or failure for our assessments. We are able to really go on forward and inform our assessments to throw a particular failure.
@Check func fetchPostsShouldUpdateWithErrors() async throws {
let shopper = MockNetworkClient()
let expectedError = NSError(area: "Check", code: 1, userInfo: nil)
shopper.fetchPostsResult = .failure(expectedError)
let viewModel = FeedViewModel(community: shopper)
await viewModel.fetchPosts()
guard case .error(let error) = viewModel.feedState else {
Situation.file("Feed state is just not set to .error")
return
}
#anticipate(error as NSError == expectedError)
}
We now have three assessments that check our view mannequin.
What’s attention-grabbing about these assessments is that all of them rely upon a mock community. Which means that we’re not counting on a community connection. However this additionally does not imply that our view mannequin and community shopper are going to work appropriately.
We’ve not examined that our precise networking implementation goes to assemble the precise requests that we anticipate it to create. With the intention to do that we will leverage one thing referred to as URLProtocol
.
Mocking responses with URLProtocol
Realizing that our view mannequin works appropriately is admittedly good. Nonetheless, we additionally need to make it possible for the precise glue between our app and the server works appropriately. That implies that we must be testing our community shopper in addition to the view mannequin.
We all know that we should not be counting on the community in our unit assessments. So how can we eradicate the precise community from our networking shopper?
One method might be to create a protocol for URLSession
and stuff all the things out that means. It is an possibility, however it’s not one which I like. I a lot desire to make use of one thing referred to as URLProtocol
.
Once we use URLProtocol
to mock out our community, we will 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 implies that we will make it possible for our code works without having the community. Let’s check out an instance of this.
Earlier than we implement all the things 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 primary 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 should you’re fascinated about studying about that.
Establishing ur URLProtocol subclass
For the assessments 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 will carry out our faux request right here
}
}
Within the startLoading
operate, we’re purported to execute our faux community name and inform the shopper (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 means for a person of our faux community to offer 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 primary model that I can provide you with to make it possible for we do not get slowed down by particulars that can range from mission to mission.
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
leads to the anticipated URLRequest
being constructed by the networking layer. That is significantly helpful while you’re testing POST
requests.
Observe that we have to make our responses and validators objects static. That is as a result of we will not 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 afterward in our begin loading operate we’ll pull out the related response invalidator. We have to make it possible for we synchronize this by way of a queue so we have now a number of assessments working 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() {
// make sure that we're good to...
guard let shopper = self.shopper,
let requestURL = self.request.url,
let validator = validators[requestURL],
let response = responses[requestURL]
else {
Situation.file("Tried to carry out a URL Request that does not have a validator and/or response")
return
}
// validate that the request is as anticipated
#anticipate(validator(self.request))
// assemble our response object
guard let httpResponse = HTTPURLResponse(
url: requestURL,
statusCode: response.statusCode, httpVersion: nil,
headerFields: nil
) else {
Situation.file("Not capable of create an HTTPURLResponse")
return
}
// obtain response from the faux community
shopper.urlProtocol(self, didReceive: httpResponse, cacheStoragePolicy: .notAllowed)
// inform the URLSession that we have "loaded" information
shopper.urlProtocol(self, didLoad: response.physique)
// full the request
shopper.urlProtocolDidFinishLoading(self)
}
}
The code incorporates feedback on what we’re doing. Whilst you may not 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 bought startLoading
applied, let’s try 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 = attempt 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 = attempt await networkClient.fetchPosts()
#anticipate(posts.rely > 0)
}
}
The very first thing I am doing on this code is creating a brand new subclass of my NetworkClientProtocol
. The rationale I am doing that’s as a result of I might need a number of assessments working on the identical time.
For that motive, I need every of my Swift check capabilities to get its personal class. This is perhaps me being somewhat 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 got and the precise URLProtocol
implementation that you just’re utilizing to carry out your assertions.
The aim of this check is to make it possible for after I ask my community shopper 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 capable of decode the response information into an inventory 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 now 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 will write a check that makes positive that we’re sending the right request after we’re making an attempt to create a publish.
struct NetworkClientTests {
// ...
@Check func testCreatePost() async throws {
let networkClient = makeClient(with: CreatePostProtocol.self)
// arrange anticipated information
let content material = "It is a new publish"
let expectedPost = Publish(id: UUID(), contents: content material)
let returnData = attempt 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 primary setup
guard
let httpBody = request.streamedBody,
request.url == createPostURL,
request.httpMethod == "POST" else {
Situation.file("Request is just not a POST request or does not have a physique")
return false
}
// guarantee physique is appropriate
do {
let decoder = JSONDecoder()
let physique = attempt decoder.decode([String: String].self, from: httpBody)
return physique == ["contents": content]
} catch {
Situation.file("Request physique is just not a sound JSON object")
return false
}
},
for: createPostURL
)
// carry out community name and validate response
let publish = attempt await networkClient.createPost(withContents: content material)
#anticipate(publish == expectedPost)
}
}
There’s various code right here, however general it follows a reasonably comparable 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.
Whenever you create a URLRequest
and execute that with URLSession
, the httpBody
that you just assign is transformed to a streaming physique.
So while you entry httpBody
within the validator closure that I’ve, it will be nil
.
As a substitute of accessing that, we have to entry the streaming physique, collect the information, 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, rely: bytesRead)
}
bodyStream.shut()
return information
}
}
With all this in place, I can now test that my community shopper constructs a totally appropriate community request that’s being despatched to the server and that if the server responds with a publish like I anticipate, I am really capable of deal with that.
So at this level, I’ve assessments for my view mannequin (the place I mock out your complete networking layer to make it possible for the view mannequin works appropriately) and I’ve assessments for my networking shopper to make it possible for it performs the right requests on the appropriate occasions.
In Abstract
Testing code that has dependencies is all the time somewhat bit difficult. When you will have a dependency you will need to mock it out, stub it out, take away it or in any other case disguise it from the code that you just’re testing. That means you possibly can purely check whether or not the code that you just’re fascinated about testing acts as anticipated.
On this publish we checked out a view mannequin and networking object the place the view mannequin will depend on the community. We mocked out the networking object to make it possible for we might check our view mannequin in isolation.
After that we additionally wished to write down some assessments for the networking object itself. To do this, we used a URLProtocol
object. That means we might take away the dependency on the server fully and absolutely run our assessments in isolation. We are able to now check that our networking shopper makes the right requests and handles responses appropriately as nicely.
Which means that we now have end-to-end testing for a view mannequin and networking shopper in place.
I don’t typically leverage URLProtocol
in my unit assessments; it’s primarily in advanced POST
requests or flows that I’m fascinated about testing my networking layer this deeply. For easy requests I are likely to run my app with Proxyman connected and I’ll confirm that my requests are appropriate manually.