How to better structure XCTestCase for iOS testing

Check out a technique to make your test structure much cleaner

Henry Morbin

💻 Software Engineer @ Faster Platform (Remote Config, Experiments and Pushes) at iFood

Test code is just as important as production code. Therefore, I will share with you a technique to make your test structure much cleaner.

We always look for better and more efficient ways to solve the problems we face.

It is with this mindset that I want to start my text. I'm not here to say which is right or wrong. But I want to show that there are other ways to write your tests. Mainly with regard to declaring your System Under Test (which we will call SUT from now on) and other dependencies.

Starting with testing

I remember when I started writing tests. It's funny, because even if you've been programming for that platform for a while, it feels like you're starting all over again. There is a lack of pattern in the test code, you don't know the test anti-patterns and any validation you do seems to be enough. You are satisfied with very little.

And, to be quite honest, in the beginning there is no problem. For those just starting out, this step towards quality is already a big step forward. If what is written is well written or if the validation covers everything it should cover, ***in the beginning*** it doesn't matter much... at least there's a test. 😆

I've been on iFood for just over 6 months. It doesn't seem like much, but it's enough to learn a lot of cool things! I joined the Platforms team, a heterogeneous team responsible for proprietary Analytics, Remote Config, Push Notification tools, among others.

As iOS Engineer, I have a lot of interaction with the other teams responsible for the Features of our application. And it was in one of these interactions, more specifically reviewing a colleague's code, that I discovered that there is a much better way to declare the SUT and other XCTestCase dependencies. It's a little-known and non-intuitive detail, but it's available in Apple's documentation.

override func setUp()

You probably know and implement the method setUp that Apple makes available. You put relevant code in it that is common to all tests, such as property initialization, mainly to ensure that they are clean and isolated so as not to affect the results of other tests, etc. Right?

If your answer was yes, don't worry, you're not wrong. Until the writing of this text I used it exactly the same way.

It is essential to have our objects clean and isolated, as mentioned previously, so that they do not impact the results of other tests. When you declare their initialization in the method setUp from XCTestCase you consequently get this result, as this method is invoked before each of the test methods run. That is, the setUp Solve the problem. No big deal.

Now here's a bucket of cold water: you don't need to implement the setUp. You don't need to initialize your properties inside itYou don't need to leave them as var. You don't need to declare them as optional or do force unwrapApple guarantees isolation of properties declared in the scope of your test class when inheriting from XCTestCase. Want to see?

Image taken from the book Design Data Intensive Applications — by Martin Kleppmann (Author)

XCTestCase

When you declare a property in the scope of the test class and already assign a value to it directly in your declaration, Apple initializes the class, along with its properties, and injects this newly instantiated version of the class into each of the tests that run on it. separate processes.

Yes that's right. Each test runs in its own thread. Therefore, if you have a test that changes the state of the SUT, or of any other collaborator (Spy, Stub, etc.), this will not be reflected in the other tests. Let's see some examples to understand better?

I see in the example test that the variable we called sut starts with 0 (zero). In all tests we are increasing it by +1, however in all tests we are also checking whether the variable is equal to 1. In other words, the increase in one test does not reflect the state of the variables in the others.

Isolation is confirmed. The code is smaller and much cleaner. You will better realize the benefits when you start refactoring your tests.

And what would the first example look like without the setUp? Since in that case we have a scenario where the sut has dependencies on other objects that must already be instantiated before the sut.

You can see here that the collaborators have become constants (let) and removed the force unwrap of all properties, including the sut. The latter became a variable lazy so that when it is consumed in tests it is instantiated with all its dependencies resolved.

You might be wondering, but then what the hell is that thing for? setUp that Apple provides?

The answer is simple: it does exactly what you thought it did. Implement commands that are common to all tests in the respective sheet and that need to be executed before each test runs.

The only difference is that property initialization does not need to be part of this list of responsibilities. This is not the scope of setUp. But other things could be, for example:

In this test scenarios, if we don't create the file first we will have two consequences:

1. A false/positive in the first (testWriteContentWithoutOptions), as it will write the contents to the file, but we could not guarantee that the file was actually overwritten as it says in the test description.

two . And a failure in the second (testWriteContentWithoutOverwriting), because as there will not be an original file, the attempt to write new content will be successful, causing it to fail in the verification phase with the comparison of contents.

Bonus

And here's an extra bonus: in case you didn't know, there are 2 setUps available for you to implement in XCTestCase, a setUp instance (non-static) and class (static).

The difference between them is quite simple. The first, more commonly used, is invoked before each of the test methods in the respective sheet, while the second runs only once and before all tests begin.

Remembering that on the other hand we also have the tearDown, which follows the same dynamic. One tearDown instance (non-static), which is invoked after each test method execution, and a tearDown (static), which runs after all tests on that sheet have been executed.

To finish

I wanted to address these details that I consider important about XCTestCase: isolation of class properties, functioning of the two setUp and both tearDown, and how/when to use each. As I already mentioned, you will notice how they make the test sheet cleaner and simpler to understand. Besides, the less code the better.

The best code is the one that didn't need to be written.

If you want to know more or have any suggestions, send them in the comments.

Was this content useful to you?
YesNo

Related posts