Testing in iOS: From Zero to Hero!

Asilbek Djamaldinov
11 min readOct 2, 2023

--

Here are my articles of the Testing Flow in iOS :

  1. Testing in iOS: From Zero to Hero! (current)
  2. Unlocking the Power of UI Testing in iOS Development
  3. Why Mocking Matters in iOS Unit Testing
  4. iOS Dev Must-Have: Snapshot Testing
  5. Why to Unit Test in Async Mode

Imagine your boss is about to give a big presentation to some super important people with deep pockets, and just as he’s about to impress them with your app, it goes haywire! 😱 That’s like baking a perfect cake, but it falls apart right before you serve it to your guests. Testing helps you avoid this “baking disaster” and ensures your boss’s presentation goes off without a hitch!

Testing in iOS

Testing in iOS is like constructing a sturdy skyscraper, and it relies on three essential building blocks:

  1. Unit Testing: This is where we ensure that each individual brick (or piece of code, mostly functions) is strong and reliable. It’s like checking that every component, like the elevators or the air conditioning, works perfectly before assembling the whole building.
  2. Integration Tests: These are like checking that different parts of our skyscraper fit together seamlessly. We examine how the elevators interact with the electrical systems, making sure everything works in harmony.
  3. UI Tests: Here, we put ourselves in the shoes of the people who will live and work in our skyscraper. We check that the windows provide a beautiful view, the doors open smoothly, and everything looks and functions as expected. It’s like ensuring the skyscraper is not just functional but also aesthetically pleasing.

By building on these three pillars of testing, we can construct iOS apps that are not only robust but also provide a fantastic user experience — just like a skyscraper that stands tall and impresses everyone who sees it.

Another essential type of test is the performance test. These tests evaluate how efficiently your app runs, similar to checking your car’s performance. They help identify any issues, like slow loading times, ensuring your app remains speedy and responsive, even during demanding tasks. But let’s leave it for a while. We will come to it in later parts, I promise.

Theory is great, but nothing beats getting your hands dirty with some real code. Let’s dive in and start writing some tests to see how it all comes together in practice. It’s where the real fun begins! 🚀

Making First “Waves”: Adding Tests to Your iOS Project

Imagine you’re starting a new project in Xcode, and you want tests from the get-go. Well, there’s this magical option when you create your project.

  1. Create Your Project: Fire up Xcode, and when you’re creating your new project, keep an eye out for that cheeky little checkbox that says, “Include Tests.”
  1. Tick That Box: Don’t be shy! Go ahead and tick that box. It’s like saying, “Yes, I want some testing goodness right here!” 🎉
  2. Xcode Does the Rest: Once you’ve checked the box, Xcode works its Abracadabra (magic — hope you get it). It creates a whole testing playground for you, complete with test files ready to roll. 🎩✨

And that’s it! You’ve added tests to your project without breaking a sweat.

Testing Waters Explored: A Dive into Tests’ Directories and Files

Let’s take a closer look at the files added by Xcode in the “YourProjectNameTests” and “YourProjectNameUITests” directory, where you’ll add files for unit and ui tests respectively for your app.

XCTestCase class

Additionally, Xcode automatically takes care of some of the heavy lifting for you. Here’s how it works in simplified terms:

// 1
import XCTest

// 2
final class MyOceanTestsTests: XCTestCase {
// Auto-generated code. We will cover it shortly.
}
  1. Import XCTest: Think of this as inviting a special guest to your app’s party. By importing XCTest, you’re telling Xcode that you want to include testing capabilities.
  2. Create Test Class and inherits from XCTestCase: Xcode goes a step further and creates a test class for you. This class is like your testing playground, ready for all your testing adventures. Your test class is not just any class; it inherits from XCTestCase. Think of this inheritance as your class learning the secret testing handshake. It means your class can now write and run tests using XCTest’s powerful tools.

So, by adding tests during app creation, Xcode sets up the stage for your testing journey, making it easier to start testing your app right from the beginning!

setUp() and tearDown()

In both unit tests and UI tests, there are special methods for setting up and tearing down the testing environment to ensure a clean and consistent state for each test. These methods play a crucial role in the testing process:

...

final class MyOceanTestsTests: XCTestCase {
// 1
override func setUp() {
// This method is called BEFORE each individual test.
}

// 2
override func tearDown() {
// This method is called AFTER each individual test.
}
}
  1. setUp(): This method is called before each individual test. It’s like preparing the stage before an actor’s performance. You can use it to initialise any necessary objects or resources needed for your test. setUpWithError(): this is the same as setUp(), but if something goes wrong during the setup, it can throw an error even before your test begins.
  2. tearDown(): Conversely, this method is called after each test has executed. It’s like clearing the stage after the act. You can use it to release any resources or clean up any changes made during the test, ensuring a clean slate for the next test. tearDownWithError(): this is the same as tearDown(), but if something goes wrong during the clean up, it can throw an error.

Methods are called every time before and after execution.

It’s like having your test’s backstage team, ensuring everything’s in order before and after the spotlight shines on your code!

A Shallow Dive into the Deep Waters of Unit Testing

Our Testing Toolkit

Before we jump into the testing waters, let’s check our gear:

  1. Xcode: Our virtual submarine for exploring the code sea.
  2. XCTest: Our trusty harpoon for tagging and capturing bugs.
  3. Coffee: Our essential underwater oxygen supply (just kidding, it’s for staying awake during late-night bug hunts). 😁

Start from small fish

Let’s dive into testing a simple and understandable function, shall we?

Imagine you have a small ViewModel, I will call it “DeepOceanViewModel”, and inside it, there’s a function called “sum.” This function takes two parameters, let’s call them “a” and “b,” and it does something really straightforward — it adds them together!

class DeepOceanViewModel {
func sum(_ a: Int, _ b: Int) -> Int {
a + b
}
}

Now, we want to test this function to make sure it’s adding correctly. We’ll create some test cases to check if the “sum” function behaves as expected.

In our “MyOceanTestsTests” class, we’ll write test methods to check different scenarios. For example, we might have a test method like this:

// 1
func test_sumPositiveNumbers() {
// 2
// given
// 3
let sut = DeepOceanViewModel() // Create an instance of your DeepOceanViewModel
let a = 3
let b = 5

// when
let result = sut.sum(a, b) // Call the sum function with 3 and 5

// 4
// then
XCTAssertEqual(result, 8, "Sum of 3 and 5 should be 8") // Check if the result is as expected
}

Let’s figure out what is happening here.

  1. When you’re naming your test functions, always kick them off with the magic word “test.” This tells the compiler, “Hey, this is a test case!” It doesn’t matter if you follow it with “_”, “Sum,” or anything else, as long as it starts with “test.” So, it’s like the secret code to let the computer know, “Get ready to run a test!”
  2. “given,” “when,” and “then” comments are a great practice for documenting your test’s structure and purpose. They make your test more readable and help others (and future you) understand what’s happening at each stage of the test.
  3. What is SUT? Here, “sut” is an abbreviation for “System Under Test,” and you’re creating an instance of your DeepOceanViewModel class to test it. In other words, “sut” represents the instance of your view model that you’re subjecting to testing within this particular test case. Using “sut” as a variable name is a common practice in unit testing to make your test code more readable and explicit. It clearly indicates which part of your code you’re focusing on during the test.
  4. “XCTAssertEqual” is specifically used to check if two values are equal. It’s a way to verify that the actual result of an operation in your test is equal to the expected result. There are different Assertions in XCTest library such as: XCTAssertTrue, XCTAssertFalse, XCTAssertNil, XCTAssertNotNil, etc.

The test function name, when properly named, serves as a clear and informative description of what the test is doing. In this case, “test_sumPositiveNumbers” can be read as:

📌 “It is a function to test the result of the sum of two positive numbers. Our system under testing is DeepOceanViewModel with given constants a = 3 and b =5. When we call function sut.sum(a, b) we get “result” and then we compare sum of a + b and the result of sut.sum(a, b)”.

In XCTest, the test function indicator uses a color code to let you know whether your test passed or failed:

  • Green Diamond: When your test runs, and all assertions within the test pass successfully, you’ll see a green diamond. This indicates that your test case has succeeded, and everything is working as expected.
  • Red Diamond: If any assertion within your test fails, the diamond turns red. This is a clear visual indicator that something went wrong during the test, and you need to investigate the issue.
    When a test fails and the diamond turns red, XCTest provides a clear and helpful message to indicate why the test didn’t succeed. This message is essential for diagnosing the issue quickly and efficiently.

For example, in our test case:

If the actual result of the multiply function is not equal to 12 (i.e., the test fails), XCTest will display the message "Multiplication of 4 and 3 should be 12" alongside the failure notification. This message serves as a diagnostic aid, letting you know what you expected and what the actual result was.

The color-coded feedback makes it easy to quickly identify the status of your tests and catch any issues early in the development process. It’s like having a traffic light for your code, telling you when it’s safe to proceed (green) and when you need to stop and fix something (red).

Let’s try another test:

class DeepOceanViewModel {
func sum(_ a: Int, _ b: Int) -> Int {
...
}

func multiply(_ a: Int, _ b: Int) -> Int {
a * b
}
}

As you understand we are going to test function “multiply(a, b)”. Here’s the corresponding unit test:

func test_multiplyPositiveNumbers() {
// given
let sut = DeepOceanViewModel() // Create an instance of your DeepOceanViewModel
let a = 4
let b = 3

// when
let result = sut.multiply(a, b) // Call the multiply function with 4 and 3

// then
XCTAssertEqual(result, 12, "Multiplication of 4 and 3 should be 12")
}

It is not so difficult, isn’t it? In fact, it’s a valuable practice that can save you a lot of time and headaches in the long run.

Diving Deep into Test Efficiency

Remember when I told you about the setup and teardown methods? Well, it’s time to put that knowledge into action.

What’s common in these two tests? You guessed it — the “System Under Test” (SUT), which is your DeepOceanViewModel. We're going to be smart about it and move the creation of the SUT into the setup method and any cleanup stuff into teardown.

Why? Because it makes our tests cleaner, less repetitive, and follows good coding practices. So, it’s like tidying up our testing room and making sure everything is in its place!

Let’s start by declaring our main character, the DeepOceanViewModel, as a variable named sut (System Under Test) right at the beginning of our test class.

final class MyOceanTestsTests: XCTestCase {
var sut: DeepOceanViewModel! // Define sut

// Other code
...
}

It’s totally safe to force-unwrap DeepOceanViewModel with ! because we're initializing it in the setUp method before every test. This makes it not only safe but also much more convenient to use.

Think of it like having your trusty equipment set up on stage before every performance. You know it’s there, ready to go, so you can confidently use it without worrying about it being absent.

final class MyOceanTestsTests: XCTestCase {
var sut: DeepOceanViewModel!

override func setUp() {
super.setUp()
sut = DeepOceanViewModel() // Initialize sut
}

override func tearDown() {
sut = nil // Clean up sut
super.tearDown()
}

...
}

In the setUp method, we'll give life to our sut by initializing it, getting it ready for action.

And when it’s time to wrap up the show, in the tearDown method, we'll bid farewell to our sut, making sure everything is tidy and cleaned up.

Remember the golden rule: We create our testing star, sut (System Under Test), right after we've set up the stage with super.setUp(). And just before the curtains close in tearDown(), we bid farewell to sut by setting it to nil. This way, sut is always ready to perform during the tests and gracefully exits when it's time to wrap things up.

This way, sut is like the star of our testing show, always ready for action and gracefully bowing out after each performance.

The final step is to make sure you remove the initialization of sut from your test methods. In other words, you don't need to create sut separately inside each test anymore because you've already set it up in the setUp method. This keeps your tests clean and follows the "DRY" (Don't Repeat Yourself) principle, making them more efficient and easier to maintain.

Conclusion

In the vast ocean of software development, testing is your trusty compass, guiding your app safely through turbulent waters. We’ve explored the importance of testing in iOS, delving into unit tests. We’ve witnessed how bugs can make a grand entrance when you least expect them, even during your boss’s crucial presentation.

We navigated the seas of XCTest, learning about unit tests, UI tests, and performance tests. Performance tests, like an app’s fitness check-up, keep it zippy and responsive, even when faced with heavy lifting.

And let’s not forget the ‘System Under Test’ (SUT), our star performer. We’ve optimized our tests by initializing SUT in setUp and cleaning up in tearDown, following the golden rule.

That’s just the beginning of our journey through the world of testing in iOS. We’ve embarked on a marine adventure together, exploring the importance of testing and diving into XCTest.

But don’t go too far! This is just the first part of our series of articles. I hope you’ve enjoyed our maritime escapade, and I look forward to seeing you in our upcoming adventures.

Stay tuned, fellow explorers, as we continue to navigate the iOS testing waters. See you soon! 👋

--

--