Unit exams needs to be as freed from exterior dependencies as attainable. Which means you need to have full management over all the pieces that occurs in your exams.
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 may be thrown away.
By making your exams not depend upon exterior state, you make it possible for your exams are repeatable, can run in parallel and do not depend upon one take a look at working earlier than one other take a look at.
Traditionally, one thing just like the community is especially arduous to make use of in exams 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 exams would now fail regardless that there’s nothing incorrect along with your code. So that you need to decouple your exams from the community in order that your exams turn out to be 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 known as URLProtocol
which permits us to take full management over the requests and responses inside URLSession
, which implies we will truly make our exams work with no community connection and with out eradicating URLSession
from our exams.
Defining the code that we need to take a look at
To be able to correctly work out how we’ll take a look at our code, we must always in all probability 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. This 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 = attempt await community.fetchPosts()
feedState = .loaded(posts)
} catch {
feedState = .error(error)
}
}
func createPost(withContents contents: String) async throws -> Put up {
return attempt await community.createPost(withContents: contents)
}
}
In essence, the exams that I wish to write right here would verify that calling fetchPost
would truly replace my record of posts as new posts turn out to be accessible.
Planning the exams
I’d in all probability name fetchPost
to make it possible for the feed state turns into a worth that I count on, then I’d name it once more and return totally different posts from the community, ensuring that my feed state updates accordingly. I’d in all probability additionally need to take a look at that if any error could be thrown in the course of the fetching part, that my feed state will turn out to be the corresponding error sort.
So to boil that right down to a listing, this is the take a look at I’d write:
- Guarantee that I can fetch posts
- Guarantee that posts get up to date if the community returns new posts
- Guarantee that errors are dealt with accurately
I even have the create submit perform, which is a bit of bit shorter. It would not change the feed state.
What I’d take a look at there’s that if I create a submit with sure contents, a submit with the supplied contents is definitely what’s returned from this perform.
I’ve already carried out 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, _) = attempt await urlSession.knowledge(from: url)
return attempt 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 = attempt JSONEncoder().encode(physique)
let (knowledge, _) = attempt await urlSession.knowledge(for: request)
return attempt JSONDecoder().decode(Put up.self, from: knowledge)
}
}
In a perfect world, I’d have the ability to take a look at 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’d need to make it possible for the HTTP physique that I assemble is legitimate and accommodates the information that I intend to ship to the server.
There are primarily two issues that we may need to take a look at 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 solution to take a look at 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’d 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 an alternative of relying on a community consumer, we rely on the Networking
protocol. The Networking
protocol defines which capabilities we will name and what the return sorts 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 consumer passage to our feed view mannequin and nothing would actually change. It is a actually low-key solution 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 {
@Check 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 take 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 exams
These are the exams that I needed to put in writing for my submit fetching logic:
- Guarantee that I can fetch posts
- Guarantee that posts get up to date if the community returns new posts
- Guarantee that errors are dealt with accurately
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 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 take a look at our view mannequin to see if calling fetchPosts
will truly use this record 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.report("Feed state shouldn't be set to .loaded")
return
}
#count on(posts.depend == 3)
}
The second take a look at would have us name fetchPosts
twice to make it possible for we replace the record of posts within the view mannequin.
To ensure that us to regulate our exams absolutely, we must always in all probability have a solution to inform the mock community what record of posts it ought to return after 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 exams:
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:
@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.report("Feed state shouldn't be 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.report("Feed state shouldn't be set to .loaded")
return
}
#count on(posts.depend == 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 make it possible for errors are dealt with accurately. Which means 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 a listing of posts or throw an error. We will use End result
for this:
class MockNetworkClient: Networking {
var fetchPostsResult: End result = .success([])
func fetchPosts() async throws -> [Post] {
return attempt 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 exams. Our exams would now have to be up to date in order that as an alternative of simply passing a listing of posts to return, we’ll present success with the record. This is what that may appear like for our first take a look at (I’m certain you’ll be able to replace the longer take a look at 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.report("Feed state shouldn't be set to .loaded")
return
}
#count on(posts.depend == 3)
}
Knowledge that we will present successful or failure for our exams. We will truly go on forward and inform our exams to throw a particular 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.report("Feed state shouldn't be set to .error")
return
}
#count on(error as NSError == expectedError)
}
We now have three exams that take a look at our view mannequin.
What’s attention-grabbing about these exams 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 have 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
Realizing that our view mannequin works accurately is admittedly good. Nonetheless, we additionally need to make it possible for the precise glue between our app and the server works accurately. That implies that we needs to 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 exams. So how can we get rid of the precise community from our networking consumer?
One strategy may very well be to create a protocol for URLSession
and stuff all the pieces out that manner. It is an possibility, but it surely’s not one which I like. I a lot desire to make use of one thing known as URLProtocol
.
Once we use URLProtocol
to mock out our community, we will inform URLSession
that we needs to 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 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 pieces that we’d like for our take a look at, let’s check out what it appears prefer to outline an object that inherits from URLProtocol
. I am implementing a few fundamental strategies that I’ll want, however there are different strategies accessible on an object that inherits from URLProtocol
.
I extremely suggest you check out Apple’s documentation in the event you’re inquisitive about studying about that.
Establishing ur URLProtocol subclass
For the exams 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 pretend request right here
}
}
Within the startLoading
perform, we’re imagined to execute our pretend community name and inform the consumer (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 manner for a consumer of our pretend 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 essentially the most fundamental model that I can give you 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: Knowledge
}
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 notably helpful while you’re testing POST
requests.
Notice 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’ll 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 make it possible for we synchronize this by a queue so we’ve got a number of exams working 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 certain 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.report("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.report("Not capable of create an HTTPURLResponse")
return
}
// obtain response from the pretend community
consumer.urlProtocol(self, didReceive: httpResponse, cacheStoragePolicy: .notAllowed)
// inform the URLSession that we have "loaded" knowledge
consumer.urlProtocol(self, didLoad: response.physique)
// full the request
consumer.urlProtocolDidFinishLoading(self)
}
}
The code accommodates feedback on what we’re doing. Whilst you may not have seen this sort of code earlier than, it needs to be comparatively self-explanatory.
Implementing a take a look at that makes use of our URLProtocol subclass
Now that we’ve bought startLoading
carried out, let’s try to use this NetworkClientURLProtocol
in a take a look at…
class FetchPostsProtocol: NetworkClientURLProtocol { }
struct NetworkClientTests {
func makeClient(with protocolClass: NetworkClientURLProtocol.Kind) -> 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()
#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 may need a number of exams working on the similar time.
For that purpose, I need every of my Swift take a look at capabilities to get its personal class. This could be me being a bit of bit paranoid about issues overlapping when it comes to when they’re known as, however I discover that this creates a pleasant separation between each take a look at that you’ve got and the precise URLProtocol
implementation that you just’re utilizing to carry out your assertions.
The aim of this take a look at is to make it possible for once I ask my community consumer to go fetch posts, it truly performs a request to the right URL. And given a profitable response that accommodates knowledge in a format that’s anticipated from the server’s response, we’re capable of decode the response knowledge into a listing of posts.
We’re primarily 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 will write a take a look at that makes certain that we’re sending the right request after we’re attempting to create a submit.
struct NetworkClientTests {
// ...
@Check func testCreatePost() async throws {
let networkClient = makeClient(with: CreatePostProtocol.self)
// arrange anticipated knowledge
let content material = "It is a new submit"
let expectedPost = Put up(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 fundamental setup
guard
let httpBody = request.streamedBody,
request.url == createPostURL,
request.httpMethod == "POST" else {
Problem.report("Request shouldn't be a POST request or would 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 {
Problem.report("Request physique shouldn't be a legitimate JSON object")
return false
}
},
for: createPostURL
)
// carry out community name and validate response
let submit = attempt await networkClient.createPost(withContents: content material)
#count on(submit == expectedPost)
}
}
There’s various code right here, however total 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 inside 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 discuss 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
inside 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 information, and return alll knowledge.
This is the implementation of the streamedBody
property that I added in an extension to URLRequest
:
extension URLRequest {
var streamedBody: Knowledge? {
guard let bodyStream = httpBodyStream else { return nil }
let bufferSize = 1024
let buffer = UnsafeMutablePointer.allocate(capability: bufferSize)
var knowledge = Knowledge()
bodyStream.open()
whereas bodyStream.hasBytesAvailable {
let bytesRead = bodyStream.learn(buffer, maxLength: bufferSize)
knowledge.append(buffer, depend: bytesRead)
}
bodyStream.shut()
return knowledge
}
}
With all this in place, I will now verify 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 truly capable of deal with that.
So at this level, I’ve exams for my view mannequin (the place I mock out all the networking layer to make it possible for the view mannequin works accurately) and I’ve exams for my networking consumer to make it possible for it performs the right requests on the appropriate occasions.
In Abstract
Testing code that has dependencies is at all times a bit of 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 conceal it from the code that you just’re testing. That manner you’ll be able to purely take a look at whether or not the code that you just’re inquisitive about testing acts as anticipated.
On this submit 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 may take a look at our view mannequin in isolation.
After that we additionally needed to put in writing some exams for the networking object itself. To do this, we used a URLProtocol
object. That manner we may take away the dependency on the server completely and absolutely run our exams in isolation. We will now take a look at that our networking consumer makes the right requests and handles responses accurately as properly.
Which means we now have end-to-end testing for a view mannequin and networking consumer in place.
I don’t usually leverage URLProtocol
in my unit exams; it’s primarily in complicated POST
requests or flows that I’m inquisitive 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.