A great process to know how to unit test (your Swift code)
I will show you a way to identify what you should test and some concepts to make your life a little easier when writing tests.
At some point in my career I knew how important unit tests were, but I didn’t know how to test my code. Many have faced the same issue. We read articles on how to start, but still, it’s hard to apply what we’ve learnt to our own code. We usually don’t have those functions that just sums up two parameters and returns the result, and we see it in many examples out there. Well, I came across a process that made it really easy for me.
The first thing I want to say is that you don’t need to change your architecture to start unit testing. That’s a mistake some developers make when they hear that a certain structure can make the code more testable. Then they wait until shifting to this new way of coding to start writing tests. Let’s not do that! If you want to test an existing code you will probably need to refactor some parts, but definitely not to change your architecture!
All about messages
Messages are the way objects talk to each other in an object oriented world. You pass a message to an object when you want it to execute a method. It’s the same to get a property value and that goes for everything else!
The process for knowing what to test has three simple steps and they all involve messages.
1. Identify the type of the message
There are two possible types of messages: Query and Command. It may be obvious but query is when you ask for something and a command is when you tell it to do something. A query has a return, but doesn’t change any state while a command has no return, but has some side effects.
query = returns something and changes nothing
command = returns nothing and changes something
2. Identify the origin of the message
There are three possible origins for a message: Incoming, Outgoing and Sent-to-self.
Incoming: When an object receives a message from the outside (another object).
Outgoing: When an object sends a message to the outside.
Sent-to-self: Guess what?! When an object sends a message to itself.
3. Follow this chart:
Ooh! What a great chart! Right?! Keep this around until it becomes natural to you.
Applying the proccess
First example is an adoption of Equatable.
- The function has a return and it doesn’t change any state. It’s a query!
- It is public and can be called by another object that wants to know if two
Walletare equal. Incoming!
- Incoming Query: Assert Result.
Incoming Query is pretty straightforward. You have an expected result, then you call the function and assert it if it returns what you’re expecting.
sut stands for System Under Test. It will be used in the next examples too)
Second example: After
loadView you wanna make sure your outlets were hooked right! They shouldn’t be nil at this point.
- The function has no return and after being called, the outlet should be set. Therefore it has side effects. So, it’s a command! (The side effect is direct public since the outlet is a property on the sut)
- It’s public for the module and it can be called from the outside. Incoming!
- Incoming Command: Assert direct public side effects.
Incoming Command should be easy as well. Make sure the state changes to the expected value, after calling the function.
(The documentation says we should never call
loadView directly, so we use
loadViewIfNeeded to trigger it)
Before going to the next example I just like to point out that you may have a function that’s both a query and a command. In this case you should test it for both types of messages.
In this next example we have a function that changes the label based on the parameter.
- The function has no return and has some direct public side effects. It’s a command!
- It is private! So, it can only be called from the ViewController itself. Sent-to-Self!
- Sent-to-Self command: Ignore!
It is important to define the proper access control for a function, it makes it easier to identify the possible origin. We don’t need to test it directly because we are already testing it indirectly when we test a public function that calls this private one.
It’s been easy so far, right?! Let’s see if it stays that way!
What we have now is a function that receives a value and sends it as a negative value to another object.
- The function has no return, but it doesn’t have any direct public side effect either! 🤔 What’s actually a command here is the
add(value:)on the storage object. So it’s a command!
- As stated in the first step, the origin here is outgoing since we are telling another object what to do.
- Outgoing command: Expect to send.
How do we test if a command was sent? You may try this:
However, we are not just asserting if the command was sent! We are testing that when we call
addStorage, store adds the value to its wallet property. That’s a test for the
LocalStorage itself, not for our ViewModel. In addition to duplicating code, we are totally coupled to the store implementation in our test and that’s not good. We should test our object in insolation!
WalletViewModel only needs to know about the
add(value:) function but it knows much more. To solve this problem we can create an interface for
LocalStore and make our object depend on that.
Much better! Now the dependency will be injected and our object knows nothing about the implementation! In our test we can inject a dependency that only spies on it if
addValue was actually called.
Now neither our test nor our object are coupled to
LocalStorage and we only check if
addValue was called (with the right value). We are using Dependency Injection here! I used the most simple approach, but not the best one. I advice you to read more about it and understand the other approaches.
We’ve covered the three messages we need to test and ignored the rest. However, I’d like to mention this last example to make the dependency point clear.
- Look directly to the
loadHistoryfunction, since the
viewDidLoaddoes nothing but call it. This is the declaration of it:
func loadHistory(completion: @escaping ([EntryViewModel]) -> Void)
It’s a bit trickier when working with asynchronous code, but this is also a query! The function itself doesn’t have a return, but the escaping closure will eventually be called and its parameters are the result.
- It’s sending the message to another object. Outgoing!
- Outgoing query: Ignore!
Although we should not test outgoing query, we should not depend on the implementation of our dependency! And for our tests, what if it hits the network to load the history? All other tests will need to wait until the history is fetched. That’s not good! Our unit tests need to run fast, and reaching the network can be really slow.
We need to use dependency injection again and inject a test version of
TransactionViewModel when testing our object.
TransactionHistoryViewController depend on an interface, created a test implementation for it and injected that into our tests. We will not test the outgoing query, but since it’s a dependency, we will use our test version to handle the possible returns.
I have shown you a process that I use for testing. It changed the way I test and helped make my code much more testable.
I tried to be generic enough so the knowledge could be applied to other programming languages as well. And I intentionally didn’t dive deep into some subjects, like Mocks/Stubs and Dependency Injection, because I think it would be too much.
I’m really glad I have finally finished this (long) post! It is still hard for me to write and it takes a long time for me to have a good outcome. I hope it helps and if you have any questions or feedback, leave them in the comments bellow!