In the realm of software development, unit testing stands as a fundamental pillar to ensuring the quality and reliability of your work. Among the many languages you could use for this, Swift is an excellent choice, and it’s why the Swift Unit Testing essentials are something every developer should be aware of. And, when used properly, a robust testing workflow is the pillar that sustains the team and even your reputation. But, for many developers, building a testing workflow has always been an extra effort that it’s not worth their time.
Sadly, many developers choose to forgo developing a test workflow because of laziness, lack of knowledge, or an unreasonably tight schedule. And although some of you might think you can get away with it, you will inevitably pay the price. So be it with increased technical debt, a bad reputation during deployments, and long nights of login into work to fix some significant issue in your app that you overlooked, not relying on tests will cost you.
I think it’s time we do something about it.
This article aims to get you familiarized with the basic concepts of Unit Testing and get you comfortable with its implementation. You should be able to answer questions like “What is Unit Testing?”, “Why is Unit Testing important?”, “What are Unit Testing best practices?” by the end of this article. Additionally, you will have a running, testable sample project containing everything you need to be empowered to make your code fully testable and production ready.
This article is intended for Swift developers. However, if you are dipping your toes into Swift, I recommend you familiarize yourself with the language before continuing. You can do that by checking my other articles on the topic.
With that out of the way, let’s get into it.
What is Unit Testing?
Unit Tests refer to a software testing strategy by which individual code units are tested and validated with a defined set of behaviors and expectations. These units of test can include groups of one or more modules, dependencies, and services together with associated control data.
Let me illustrate.
Imagine you have a calculator app. Inside the app, many classes do various things, like handling user interactions, performing calculations, and maybe even making remote requests to a service.
To perform a unit test in this app would mean isolating the logic inside an individual set of operations that make a logical group and performing validations based on certain assumptions.
This means that if we were given the task to test the reliability of the calculating part of the app, we would need to work with the class that holds all the logic responsible for performing the mathematical operations.
For us Swift developers, XCode offers us the XCTest framework to design and perform our Unit Tests.
There are, of course, other approaches to app testing. UI Testing, Integration Testing, Performance Testing, and Coverage Testing, to name a few.
If you want to explore them further, you can read my other article about UI testing vs. Unit Testing here.
Building a Unit Test Workflow in Swift
To aid you in understanding the intricacies of building tests in Swift, I have taken the liberty of creating a simple calculator app that will serve as the groundwork and playground for the exercises that follow in the article. Feel free to check out the project in my repository here and explore it to your heart’s content.
Alright, now proceed to the SwiftTestingSampleTests folder in the project.
The code contained here comes by default when you create a project in XCode. It serves as a great starting point to make your tests.
You have a method that allows you to set up the test workflow, a method to teardown and flush all resources after the test finishes, and an example test case you can use as a template.
Now, to create your first test workflow, we need to do some setup first.
Start by adding the following property to the SwiftTestingSampleTests.swift file.
// Reference of the view controller that will be subjected to tests
var sut: ViewController!
This property will be a reference (System Under Test) to the view controller you will test.
Continue by changing the setupWithError() and the tearDownWithError() methods.
override func setUpWithError() throws {
ย ย // Put setup code here. This method is called before the invocation of each test method in the class.
ย ย // Retrieve a reference of the main storyboard
ย ย let storyboard = UIStoryboard(name: "Main", bundle: nil)
ย ย // Initialize the view controller to be tested
ย ย sut = storyboard.instantiateInitialViewController() as? ViewController
ย ย // Load the view controller if needed
ย ย sut.loadViewIfNeeded()
}
override func tearDownWithError() throws {
ย ย // Put teardown code here. This method is called after the invocation of each test method in the class.
ย ย // Flush references
ย ย sut = nil
}
As you can see, the view controller is initialized during setup, and its views are loaded. Meanwhile, all references are flushed during teardown, clearing all resources and resetting the test state. This process is essential to ensure that all test cases are consistent, avoiding issues with the app state or global variables.
Adding a Test Case
Next, create a new test case and name it testResultsAreCalculatedProperly.
func testResultsAreCalculatedProperly() throws {
}
Notice that our test case starts with the word “test.” This is intentional, as all test cases must begin with this word, or XCode won’t recognize them.
Now, add the following code to this test case.
func testResultsAreCalculatedProperly() throws {
// Wait for 1 second
sleep(1)
ย ย // Retrieve and validate if the element exists and is setup
let display = try XCTUnwrap(sut.display, "Display text field not properly setup")
// Set initial values for the test
ย ย display.text = "5+5"
sut.numberArray = [5, 5]
sut.operation = "+"
// Wait for 1 second
sleep(1)
// Trigger operation
sut.calculateResult()
// Validate expectation
XCTAssertEqual(display.text, "10", "Results are not being calculated properly!")
// Wait for 1 second
sleep(1)
}
Here, we are retrieving the elements and setting values necessary to establish an initial consistent state. Then the operation to be tested is triggered to engage the logic with the set state. Finally, we assert our expectations and validate the output.
There are many different kinds of assertions at your disposal.
- XCTAssertNill / XCTAssertNotNil
- XCTAssertTrue / XCTAssertFalse
- XCTAssertThrowsError / XCTAssertNoThrow
- XCTAssertGreaterThan / XCTAssertLessThan
- XCTAssertIdentical / XCTAssertNotIdentical
All these follow a similar format and allow you to evaluate assumptions about the output of your operations.
You can find more on the official Apple documentation here.
This flow is the basis of all Unit tests and can be expanded to any unit of work you might need.
Additionally, notice the calls to the sleep() method. These are here to make the test more visible to us, but they are unnecessary and would hinder the performance of large workflows of tests.
Unit Testing Best Practices
There are many ways to go about making your code testable and reliable. But not all ways are the best way. That is why following the best practices in Unit Testing is essential. Here are the most important.
Your tests should be efficient.
It is imperative that your tests run as quickly as possible. This fact should become evident when your test workflow grows beyond just a few dozen tests and you hook it up to a continuous integration workflow. If your tests take seconds to run, that time will compound and take productive time away from your team.
Your tests should be readable and as simple as possible.
Just like any critical code in your project, you must make an effort to make your tests readable and digestible.
Test code is one of the most maintained code bases in projects, and when the complexity of projects increases, so does the temptation to make complex tests. You must avoid this because faulty tests will lead to poor quality and technical debt.
Your tests should not contain business logic.
When designing test cases for a specific workflow, it is tempting to include part of the business logic in the test case. This is a bad idea since it leads to a conflict of context and bad test designs where we end up injecting functionality into our tests instead of validating them.
Remember, the goal is to isolate the code we need to test and focus on providing the necessary input to achieve a consistent, expected outcome. Use stubs and mocks to set behavior expectations and validate the results.
Your tests should be deterministic.
This one is pretty self-explanatory. Your tests should produce the same outcome every time. Avoid dealing with states and resources that depend on user input or network. Don’t generate input randomly; make sure to design your tests in a DRY way.
Why is Automated Testing Important?
The main reason why testing is crucial is that it allows you to ensure that each operation behaves as it should and is not influenced or dependent on other factors. And if all functions behave as they should, then the whole project should too, unless the project itself is not well orchestrated, then hey, you found an issue that you must address anyway.
The objective of the unit test is to emulate the operation of the application in a controlled and granular way to determine if the application will respond as expected with valid input and handle invalid input graciously.
As we know, modern applications are designed to allow the user to perform operations and reach places in multiple ways. For example, you can get to the orders page on Amazon through your profile page, the settings, the history, or even a product page. This is, of course, intentionally designed to facilitate user interactions. However, each of these avenues contains intricacies that can affect the application’s state, causing problems.
Beyond Swift Unit Testing Essentials
Indeed, maintaining a mobile project, especially in terms of testing, can be quite complex. There’s no denying that ensuring your code is robust and functional is crucial. However, it doesn’t have to be as daunting as it sounds.
One of the most challenging aspects can be the added workload of maintaining and running tests. But if approached correctly, it can become an integral part of your development workflow rather than a hindrance.
Remember, opting out of unit testing entirely can lead to even more significant issues down the line. While it may seem like an extra effort now, developing a robust testing workflow will pay dividends in the future by saving you time and stress when debugging.
And if you’re finding the prospect of building a testing workflow daunting, remember that you’re not alone. Many developers initially find this overwhelming. However, with the help of resources like this Swift Unit Testing essentials’ guide, you can build a strong foundation and gradually enhance your testing capabilities.
So, keep learning, keep testing, and remember that the effort you put into creating solid unit tests today will lead to more stable and reliable applications tomorrow.
This post was originally written for and published by Waldo.com
Leave a Reply