How To Test fatalError In Swift
Are you using fatalError functions to enforce the flow of your application and you’re keen to test it? Let’s find out a safe way to do it.
Introduction
Swift has several ways to manage failures. One of them is the function fatalError
. Since we should cover our production code with tests as much as possible, we need also a way to test the fatalError
. By default, there are no ways to handle it. If we test a function with a fatalError
, our tests would crash.
In this article, we are going to see a safe way to test if a fatalError
is called in our production code.
Happy Reading!
Replace default fatalError
By default, we cannot test if a method calls a fatalError
since we don’t have ways to wrap the failure and the tests would crash. For this reason, we need a way to handle the fatalError
function to prevent any crashes in our tests. To achieve it, we should write a custom fatalError
function.
If we have a look at the Swift interface for the function fatalError
, we would find this:
|
|
Our goal is replacing this function with a custom one. Therefore, we must declare a new function with the same signature in our application. The important thing is declaring it at the top-level. It means that it mustn’t be inside any classes/structs/enums.
We can start creating a new file called FatalErrorUtil.swift
. Then, at the top-level of this file we can add our new fatalError
method:
|
|
With this new method, every time we use fatalError
in our code, the compiler will use this function instead of the default one of Swift.
At this point, we need to add an implementation. The strategy for this implementation is using the default Swift fatalError
implementation for the production code and a custom one for unit test.
We can use a struct FatalErrorUtil
which will provide the implementation of fatalError
:
|
|
- Closure which provides the implementation of
fatalError
. By default, it uses the one provided by Swift. - Default
fatalError
implementation provided by Swift. - Static method to replace the
fatalError
implementation with a custom one. We’ll see later how to use it for unit tests. - Restores the
fatalError
implementation with the default one. We’ll need it later for the unit tests.
The next step is using this struct in our custom fatalError
function:
|
|
So far so good. If we use fatalError
in our production code, we can expect the same behaviour of the one provided by Swift.
Unit test handling
In the previous section, we introduced a new fatalError
implementation. Now, we are able to handle it for the unit tests.
We can start adding a new file XCTestCase+FatalError.swift
in the project target of our unit tests to extend XCTestCase
.
The target membership of the file should be something similar to this:
In this XCTestCase
extension, we can add a method expectFatalError
which will wrap the fatalError
failure:
|
|
With this method, we can wrap fatalError
and test if the method under test calls a fatalError
function with an expected message. We’ll see later how to use it.
Inside this method, we must replace the default fatalError
implementation with a mock to test if it’s called with an expected message:
|
|
If the compiler doesn’t find FatalErrorUtil.replaceFatalError
, it means that you must add @testable import <ProjectName>
at the beginning of the file.
We need expectation
because it’s an asynchronous test.
As we may have noticed in the example above, we call a method unreachable
which is this one:
|
|
fatalError
is a function which returns Never
. It means that this function will never finish its execution since will be stopped by either a failure or a thrown error. In this case, a normal fatalError
would never complete since it lets crash the application. Therefore, we must simulate a never returning function. We can do it with an infinite loop—like in method unreachable
. If we use the approach of an infinite loop, we should call RunLoop.current.run()
to let execute any services attached to the main thread. You can read more details about this method here.
Then, we must execute the testcase
closure in a background thread, since the main one will be blocked by the infinite loop:
|
|
Finally, we must handle the expectation
completion to test the fatalError
message and restore the default fatalError
implementation:
|
|
We’ve just finished the implementation of our XCTestCase
extension and it should be like this:
|
|
Test example
We’ve completed everything to test our fatalError
calls.
Now, we can see a plain example to understand how to use the new method expectFatalError
.
Let’s consider the following Handler
class:
|
|
We call a fatalError
if the argument of handle
is less than 6.
We can test this method like this:
|
|
This test would fail if either the method handle
doesn’t call the fatalError
or expectedMessage
is not the same message of the fatalError
.
Conclusion
In this article, we focused on the fatalError
but we can use a similar approach also for other failure methods like assert
and preconditionFailure
. You can have a look here for more details.