I have been practicing Swift for some months now and I think I have got a good taste of some of its features. Then, some days ago, I found this gist that uses Ruby to present examples to learn Ruby in an uncommon way. It is based on the following premises:
- It should be based on examples
- Examples should be easy to read, almost in plain English
- Each example should be able to run as a test
With that in mind, I decided to try to do a similar thing in Swift. Basically, an example like this could read something like:
You can find the whole implementation as a playground in my GitHub account. Of course, you are welcome to fork it and suggest improvements. In this post, I will try to explain the main features of the language that allowed my to do this. Let’s get started!
Closures and Generics
First thing I tried to accomplish was to model examples as objects in the language. Since I want to run each example as a test, it would have to have two parts which, when executed, will be the two arguments of an assertion.
Each of the two parts of the examples are based on code so, how could I pass code to an object? The answer to this issue is to have closures as fields in the object. In this way, I can store the two closures and execute them when the test runs.
However, what should be the type of the closure? Since the example is self-contained, the closure should not receive any parameters, but it can return anything. In that case, it seems that the most suitable thing we can return is a generic type. Nevertheless, in the end we need to compare the execution of both closures is equal, so the returned type should be
Therefore, I managed to create the
SwiftExample<T : Equatable> class:
Higher order functions
We are going to need to create examples, but calling the constructor every time that we write an example would not look like writing in plain English, so I decided to add some sugar to it.
To be able to do this, I used higher order functions. Basically, a higher order function is a function that can take another function as a parameter. This is possible thanks to the fact that functions are first order citizens in Swift. Thus, I created the
thisCode function to help me create examples, receiving the first half of the example as a closure:
Since I want to run examples as tests, I created the
SwiftExamplesTest class extending the base class for unit testing in Swift. However, I don’t know beforehand how many tests I am going to have, and I don’t want to remember to add a test every time I add an example.
How can I solve that then? The answer is method swizzling. Basically, with method swizzling you can add methods to a class or replace the implementation of existing ones. Hence, every time I have a new example, I would like to add a method to the
SwiftExamplesTest class to run and test it.
This was actually the hardest part to accomplish but I finally managed to make it work with the following code:
Finally, I want to introduce each example with a small text explaining each language feature, followed by the code that shows it. In order to be able to put it all together, I decided to use extensions. This feature enables to extend any class (or even protocols) with functionality that it is lacking.
In this case, the
String class does not properly lacks what I need, but this feature helps my needs pretty well, so I did the following:
As you can see, what this extension does is to use the previously defined class method to add a new test for each example that is added. Notice that we can have a variable number of examples.
Testing in a Playground
Running unit tests is not enabled by default in a Playground, which is something that I was missing until I found this blog post that proves how to run unit tests with a little bit of boilerplate (which I am not reproducing here, you can check either the post or the playground).
Although the current implementation is pretty powerful, it still has some limitations:
- Examples under the same rationale must return the same type. The reason behind this is that functions accepting a variable number of arguments represent them as an Array, and in Swift they must be homogeneous.
- Closures with more than one line of code must explicitly call return. That is something that hinders a little bit the readability of the examples and add a bit of redundancy, but I guess it is not a big deal.
This little experiment shows the power Swift has to build a Domain-Specific Language (DSL) in a few lines of code. I didn’t even had to use features like operator overloading or protocol extensions, which gives you a lot of freedom to extend existing things to match your needs.
I hope you have enjoyed the post as much as I did when I was doing this Playground, and feel free to leave a comment below if you want to discuss anything!
Tomás Ruiz-López DEVELOPMENT
swift iOS testing playground