Unit assessments must be as freed from exterior dependencies as potential. Which means that you need to have full management over every little thing that occurs in your assessments.
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 take a look at begins. You use on the database throughout your take a look at and after your take a look at the database could be thrown away.
By making your assessments not depend upon exterior state, you guarantee that your assessments are repeatable, can run in parallel and do not depend upon one take a look at operating earlier than one other take a look at.
Traditionally, one thing just like the community is especially onerous to make use of in assessments as a result of what in case your take a look at runs however you do not have a community connection, or what in case your take a look at runs throughout a time the place the server that you just’re speaking to has an outage? Your assessments would now fail though there’s nothing unsuitable along with your code. So that you need to decouple your assessments from the community in order that your assessments develop into repeatable, unbiased and run with out counting on some exterior server.
On this put up, I will discover two completely different choices with you.
One possibility is to easily mock out the networking layer completely. The opposite possibility makes use of one thing known as URLProtocol
which permits us to take full management over the requests and responses within URLSession
, which implies 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 take a look at
To be able to correctly determine how we will take a look at our code, we should always most likely outline the objects that we wish to take a look at. On this case, I wish to take a look at a fairly easy view mannequin and networking pair.
So let’s check out the view mannequin first. Here is the code that I wish to take a look at 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 assessments that I wish to write right here would affirm that calling fetchPost
would really replace my record of posts as new posts develop into obtainable.
Planning the assessments
I’d most likely name fetchPost
to guarantee that the feed state turns into a price that I count on, then I’d name it once more and return completely different posts from the community, ensuring that my feed state updates accordingly. I’d most likely additionally need to take a look at that if any error can be thrown through the fetching section, that my feed state will develop into the corresponding error sort.
So to boil that right down to a listing, this is the take a look at I’d 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 appropriately
I even have the create put up perform, which is a little bit bit shorter. It does not change the feed state.
What I’d take a look at there’s that if I create a put up with sure contents, a put up with the offered contents is definitely what’s returned from this perform.
I’ve already applied the networking layer for this view mannequin, so this 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 (knowledge, _) = strive await urlSession.knowledge(from: url)
return strive JSONDecoder().decode([Post].self, from: knowledge)
}
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 (knowledge, _) = strive await urlSession.knowledge(for: request)
return strive JSONDecoder().decode(Put up.self, from: knowledge)
}
}
In a super world, I’d have the ability to take a look at that calling fetchPosts
on my community shopper is definitely going to assemble the proper URL and that it’s going to use that URL to make a name to URLSession
. Equally for createPost
, I’d need to guarantee that the HTTP physique that I assemble is legitimate and accommodates the information that I intend to ship to the server.
There are basically two issues that we may need to take a look at right here:
- The view mannequin, ensuring that it calls the proper capabilities of the community.
- The networking shopper, ensuring that it makes the proper calls to the server.
Changing your networking layer with a mock for testing
A standard approach to take a look at code that depends on a community is to easily take away the networking portion of it altogether. As an alternative of relying on concrete networking objects, we might depend upon protocols.
Abstracting our dependencies with protocols
Here 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 an alternative 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 will probably be.
For the reason that capabilities that we have outlined are already outlined on NetworkClient
, we will replace our NetworkClient
to evolve to Networking
.
class NetworkClient: Networking {
// No modifications to the implementation
}
In our utility 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 approach to introduce testability into our codebase for the feed view mannequin.
Mocking the community in a take a look at
Now let’s go forward and write a take a look at 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 -> Put up {
return Put up(id: UUID(), contents: contents)
}
}
struct FeedViewModelTests {
@Take a look at func testFetchPosts() async throws {
let viewModel = FeedViewModel(community: MockNetworkClient())
// we will now begin testing the view mannequin
}
}
Now that we’ve got a setup that we will take a look at, 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 jot down for my put up 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 appropriately
Let’s begin including them one-by-one.
To be able to take a look at whether or not I can fetch posts, my mock community ought to most likely 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 take a look at our view mannequin to see if calling fetchPosts
will really use this record of posts and replace the feed state appropriately.
@Take a look at func testFetchPosts() async throws {
let viewModel = FeedViewModel(community: MockNetworkClient())
await viewModel.fetchPosts()
guard case .loaded(let posts) = viewModel.feedState else {
Problem.file("Feed state isn't set to .loaded")
return
}
#count on(posts.rely == 3)
}
The second take a look at would have us name fetchPosts
twice to guarantee that we replace the record of posts within the view mannequin.
To ensure that us to regulate our assessments totally, we should always most likely have a approach to inform the mock community what record of posts it ought to return once we name fetchPost
. Let’s add a property to the mock that permits us to specify a listing 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 -> Put up {
return Put up(id: UUID(), contents: contents)
}
}
And now we will write our second take a look at as follows:
@Take a look at 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 {
Problem.file("Feed state isn't set to .loaded")
return
}
#count on(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 {
Problem.file("Feed state isn't set to .loaded")
return
}
#count on(posts.rely == 1)
}
The take a look at is now extra verbose however we’re in full management over the responses that our mock community will present.
Our third take a look at for fetching posts is to guarantee that errors are dealt with appropriately. Which means that we should 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 Consequence
for this:
class MockNetworkClient: Networking {
var fetchPostsResult: Consequence<[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 will make our fetch posts calls succeed or fail as wanted within the assessments. Our assessments would now should be up to date in order that as an alternative of simply passing a listing of posts to return, we will present success with the record. Here is what that will appear to be for our first take a look at (I’m certain you may replace the longer take a look at primarily based on this instance).
@Take a look at 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 {
Problem.file("Feed state isn't set to .loaded")
return
}
#count on(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 selected failure.
@Take a look at func fetchPostsShouldUpdateWithErrors() async throws {
let shopper = MockNetworkClient()
let expectedError = NSError(area: "Take a look at", code: 1, userInfo: nil)
shopper.fetchPostsResult = .failure(expectedError)
let viewModel = FeedViewModel(community: shopper)
await viewModel.fetchPosts()
guard case .error(let error) = viewModel.feedState else {
Problem.file("Feed state isn't set to .error")
return
}
#count on(error as NSError == expectedError)
}
We now have three assessments that take a look at our view mannequin.
What’s attention-grabbing about these assessments is that all of them depend 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 count on it to create. To be able to do that we will leverage one thing known as URLProtocol
.
Mocking responses with URLProtocol
Understanding that our view mannequin works appropriately is basically good. Nevertheless, we additionally need to guarantee that the precise glue between our app and the server works appropriately. That signifies 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 will we get rid of the precise community from our networking shopper?
One strategy could possibly be to create a protocol for URLSession
and stuff every little thing out that means. It is an possibility, however it’s not one which I like. I a lot want to make use of one thing known as URLProtocol
.
After we use URLProtocol
to mock out our community, we will inform URLSession
that we must be utilizing our URLProtocol
when it is attempting to make a community request.
This enables us to take full management of the response that we’re returning and it signifies that we will guarantee that our code works without having the community. Let’s check out an instance of this.
Earlier than we implement every little thing that we want for our take a look at, let’s check out what it appears to be like prefer to outline an object that inherits from URLProtocol
. I am implementing a few primary strategies that I’ll want, however there are different strategies obtainable on an object that inherits from URLProtocol
.
I extremely advocate you check out Apple’s documentation in the event you’re all for studying about that.
Organising 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
perform, we’re presupposed to execute our faux community name and inform the shopper (which is a property that we inherit from URLProtocol
) that we completed loading our knowledge.
So the very first thing that we have to do is implement a means for a consumer of our faux community to supply a response for a given URL. Once more, there are lots of methods to go about this. I am simply going to make use of probably the most primary model that I can provide you with to guarantee that we do not get slowed down by particulars that may differ from venture to venture.
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 whenever you’re testing POST
requests.
Be aware 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 in a while in our begin loading perform we’ll pull out the related response invalidator. We have to guarantee that we synchronize this by means of a queue so we’ve got a number of assessments operating in parallel. We’d run into points with overlap.
Earlier than we implement the take a look at, let’s full our implementation of startLoading
:
class NetworkClientURLProtocol: URLProtocol {
// ...
override func startLoading() {
// be 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 {
Problem.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
#count on(validator(self.request))
// assemble our response object
guard let httpResponse = HTTPURLResponse(
url: requestURL,
statusCode: response.statusCode, httpVersion: nil,
headerFields: nil
) else {
Problem.file("Not in a position to create an HTTPURLResponse")
return
}
// obtain response from the faux community
shopper.urlProtocol(self, didReceive: httpResponse, cacheStoragePolicy: .notAllowed)
// inform the URLSession that we have "loaded" knowledge
shopper.urlProtocol(self, didLoad: response.physique)
// full the request
shopper.urlProtocolDidFinishLoading(self)
}
}
The code accommodates feedback on what we’re doing. When you may not have seen this type of code earlier than, it must be comparatively self-explanatory.
Implementing a take a look at that makes use of our URLProtocol subclass
Now that we’ve acquired startLoading
applied, let’s attempt to use this NetworkClientURLProtocol
in a take a look at…
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)
}
@Take a look at 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.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 operating on the similar time.
For that cause, I would like every of my Swift take a look at capabilities to get its personal class. This may be me being a little bit bit paranoid about issues overlapping by way of when they’re known as, however I discover that this creates a pleasant separation between each take a look at that you’ve and the precise URLProtocol
implementation that you just’re utilizing to carry out your assertions.
The purpose of this take a look at is to guarantee that after I ask my community shopper to go fetch posts, it really performs a request to the proper URL. And given a profitable response that accommodates knowledge in a format that’s anticipated from the server’s response, we’re in a position to decode the response knowledge 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 proper 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 take a look at that makes certain that we’re sending the proper request once we’re attempting to create a put up.
struct NetworkClientTests {
// ...
@Take a look at func testCreatePost() async throws {
let networkClient = makeClient(with: CreatePostProtocol.self)
// arrange anticipated knowledge
let content material = "It is a new put up"
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 primary setup
guard
let httpBody = request.streamedBody,
request.url == createPostURL,
request.httpMethod == "POST" else {
Problem.file("Request isn't a POST request or does not have a physique")
return false
}
// guarantee physique is right
do {
let decoder = JSONDecoder()
let physique = strive decoder.decode([String: String].self, from: httpBody)
return physique == ["contents": content]
} catch {
Problem.file("Request physique isn't a sound JSON object")
return false
}
},
for: createPostURL
)
// carry out community name and validate response
let put up = strive await networkClient.createPost(withContents: content material)
#count on(put up == expectedPost)
}
}
There’s various code right here, however general it follows a fairly 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 an alternative of accessing httpBody
, I am accessing streamedBody
. This isn’t a property that usually exists on URLRequest
, so let’s discuss why I want that for a second.
While you create a URLRequest
and execute that with URLSession
, the httpBody
that you just assign is transformed to a streaming physique.
So whenever you entry httpBody
within the validator closure that I’ve, it may be nil
.
As an alternative of accessing that, we have to entry the streaming physique, collect the information, and return alll knowledge.
Here 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<UInt8>.allocate(capability: bufferSize)
var knowledge = Information()
bodyStream.open()
whereas bodyStream.hasBytesAvailable {
let bytesRead = bodyStream.learn(buffer, maxLength: bufferSize)
knowledge.append(buffer, rely: bytesRead)
}
bodyStream.shut()
return knowledge
}
}
With all this in place, I will now verify that my community shopper constructs a totally right community request that’s being despatched to the server and that if the server responds with a put up like I count on, I am really in a position to deal with that.
So at this level, I’ve assessments for my view mannequin (the place I mock out your entire networking layer to guarantee that the view mannequin works appropriately) and I’ve assessments for my networking shopper to guarantee that it performs the proper requests on the right occasions.
In Abstract
Testing code that has dependencies is at all times a little bit bit difficult. When you will have 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 just’re testing. That means you may purely take a look at whether or not the code that you just’re all for testing acts as anticipated.
On this put up 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 guarantee that we may take a look at our view mannequin in isolation.
After that we additionally wished to jot down some assessments for the networking object itself. To try this, we used a URLProtocol
object. That means we may take away the dependency on the server completely and totally run our assessments in isolation. We are able to now take a look at that our networking shopper makes the proper 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 all for 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 right manually.