Testing Throwing Methods In Swift Actors

June 27th, 2022

⏱ Reading Time: 6 mins

In an ideal world, testing should be part of the development process of an app. The truth, however, is that testing is not always in the mix, and that’s mostly because it requires additional time; an asset that’s often missing from developers, especially when it’s necessary to meet deadlines. Or it’s not considered important enough because we all suffer from the illusion that we can predict all possible outcomes of our code.

Undeniably, testing should be a recursive task in our workflow, but leaving the general discussion aside, what I would like to focus on in this post today is something quite specific. In particular, my intension is to highlight how to test error throwing methods which belong to actors; the new type presented in WWDC 2021 that provides an isolated context and helps avoid data races when needed to support concurrency in our apps. Consider this post to be a short of a quick tip that will show you the way to avoid certain unnecessary issues if you have not written tests using actors and concurrency before.

Getting started with a usual type

To demonstrate the point of this article, let me start with a simple type that has nothing to do with concurrency and actors. That is a class that contains a stored property and a method to update the property’s value:

As you figure out from the above implementation, the input value in the set(value:) method is assigned to the userValue property if only it belongs to the closed range 1…99. In any other case, an error is thrown from the method -that’s why it’s marked with throws– and the error varies depending on the value itself.

The CustomErrors type appearing in the above code is this:

In order to ensure that the set(value:) method works as expected we are going to write a few simple unit tests. We’ll start with the next one:

After having initialized a MyType instance, we assert here that calling set(value:) with a value within the acceptable range (1…99) won’t throw an error. Running the above test succeeds just fine.

Let’s add two more tests, where we want to see if an error will be thrown in case we provide a quite low and quite high value respectively:

We can go one step further, and make sure that the correct error is thrown in each case:

Notice that in this case we use a do-catch statement. In the try block we’re providing as arguments two values that are out of the predefined acceptable range. In the catch block that we are interested in, we assert that the proper error is thrown in each test method.

All of the above tests will be successful when they’ll run. We were not expecting any surprises at all anyway, as they are intentionally simple in order to make a point.

Using an actor

To follow on the topic, let’s implement an actor with the same content as before:

As you can see, nothing changes here except for the kind of type –actor instead of a class– and the type’s name. Since all properties and methods in actors are by default isolated, unless it’s pointed otherwise, accessing either the stored property or the method through a MyActor instance is something that takes place asynchronously. Let’s see how that affects us when testing.

The first thing to take care of before making any asynchronous calls to invoke either the actor’s method or access its stored property, is to append the async keyword in the test method’s signature:

Alternatively, you can define a Task block and write any asynchronous code in its body:

Now let’s implement the same test as the first one we wrote earlier:

This attempt will crash on a wall, as Xcode will show the next error:

“Actor-isolated instance method ‘set(value:)’ can not be referenced from a non-isolated autoclosure”

As said already, set(value:) method exists in an isolated environment, so trying to access it like shown above will always fail (find out why right next). Also, we should call it prefixing it with the await keyword, indicating that this is an asynchronous call. The following however not only does not fix the problem, but adds a new one to the list:

Errors shown by Xcode

The reason for all that is that a closure with the given expression will be created automatically here even though it looks like a normal parameter currently, and that because the assert method’s parameters are marked with the @autoclosure attribute; that results to a context other than the test method’s, and async has no effect there.

Let’s go the working way now, ensuring that the above call will occur in a concurrent context and the async keyword will have an actual impact. First off, let’s call the set(value:) method in a do-catch statement. For clarification, note that we use try because it’s an error throwing method, and await because we’re making an asynchronous call:

We are expecting the value 20 given as argument not to trigger an error throw, but what exactly are we going to assert here?

There must always be something that we can measure, no matter if that’s a boolean result, an integer value or something else. In this particular example it is the userValue stored property whose value we can check against the argument we supply the set(value:) with, and therefore assert for equality.

But similarly as before, we can’t just do this:

Instead, we should access userValue asynchronously in a separate statement, and assign the fetched value to a local constant. Then, we can assert that this value is equal to 20:

And what about the catch block? Normally, we don’t expect the execution to ever get there, but just to be aware that the catch block is reached if the tested code changes in the future, we can add a XCTFail(_:) method; doing so will break the test, including a message which will appear if the catch block will get executed:

So, putting everything together, this is a working test method:

Notice the use of the await keyword twice; an occurrence for each attempt to access asynchronously an isolated property or method in the actor’s instance.

Testing asynchronously thrown errors

The previous test works for the good scenario where an acceptable value is given to the set(value:) method. But just like that, we should also add tests to make sure that errors are thrown properly for out of range values.

In order to achieve that, we should use a do-catch statement once again, focusing on the catch block this time. There, we should assert that the expected errors are thrown for each error case.

For instance, this is the test for a low value:

In the same fashion we can also assert that the error thrown for a high value is equal to .highValue:

Conclusion

Writing unit, integration and UI tests is a vital part of implementing apps, as they let us verify that our code works as intended, exposing at the same time potential bugs that we can fix before completion and delivery. Definitely nobody wants to convert end users to test users, so embrace testing and don’t just rely on trying things out manually. When it comes to testing asynchronous code, it might take sometimes a little longer code in order to prepare a test, and to adjust the way of thinking about how and what to test. In this post I just scratched that on a quite particular topic, which, even though it may look straightforward to some developers, it’s not the same for some others who just start on testing or asynchronous programming. So, I hope what you met here today to be proved useful for you.

Thank you for reading, enjoy coding! ????

Stay Up To Date

Subscribe to my newsletter and get notifiied instantly when I post something new on SerialCoder.dev.

    We respect your privacy. Unsubscribe at any time.