Skip to content

Instantly share code, notes, and snippets.

@rudrankriyam
Last active August 25, 2021 08:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rudrankriyam/7a09fefdfceea293928455853cd59c19 to your computer and use it in GitHub Desktop.
Save rudrankriyam/7a09fefdfceea293928455853cd59c19 to your computer and use it in GitHub Desktop.

Unit Testing in Swift

The first time someone hears the word testing, it feels scary. Although, tests help you be confident in the code you write and benefits you in the long term. Unit testing also helps you to foresee any problems that may come up in deployment.

Assume you’re in a team working on an app and someone mistakenly interchanges some data. That change was missed during manual testing so a user complains about the mock data in the app. Suddenly these types of messages bombard your inbox.

Imagine writing a simple unit test to check such manual errors and alert you before release. Sounds great, right?

In this introductory article about testing and unit testing, we’ll cover the following:

  • Why Should You Test?
  • What is Unit Testing?
  • What to Test?
  • Understanding XCTest and XCTestCase
  • Adding a Unit Test in Xcode
  • Another Unit Test Example
  • Tips for Naming
  • Debugging a Test
  • Enabling Code Coverage
  • Automating with Fastlane and Semaphore

Why Should You Test?

As a software developer, you want to make sure that the code you’re writing is working the way it’s supposed to. It may sound trivial, but codes break and regressions happen.

To minimise errors, reduce manual testing efforts, and give you confidence in the code, write tests for every program.This will help you with:

  • Reducing Bugs

While doing manual testing, bugs may get ignored due to human errors and get into the app's final release. Even a seemingly small thing like a typo can have adverse effects. If someone mistakenly updates the code that shouldn’t have been, writing tests help you to find them early in development. They also automate the process, and any bug ignored during manual testing can be found early in development itself and fixed.

  • Refactoring

Your aim is to isolate a particular piece of code to test separately. You may want to refactor your code in the process so it is more modular and clean. You can also edit your code to be more in line with your precise goals.

  • Thinking of Edge Cases

While you’re writing tests, you think about various cases that can occur and then write tests about them. This requires you to predict anything that can go wrong with the code and test against each one. Correcting these before they happen helps you prepare for anything and save time and data.

  • Code Regression

Regressions mean that the code you wrote that worked before, no longer does. That leads to more effort and time to fix it instead of spending your time more wisely.

After adding a completely new feature to the app, you can potentially break the existing code or features. That’s where writing tests become incredibly helpful.

Testing helps you be more confident that the new feature won’t break the existing ones and cause regression. You’ll speed up the development process and save time and money.

What is Unit Testing?

As the name suggests, this test is for a particular unit - a chunk of code that can be isolated to be tested separately. For example, you can test the network calls, the logic of caching an image, or the computed variables in the data models. You then have a predefined input and can check for the expected value and outcomes for the particular chunk of code in a test.

Apple provides us with a native framework called XCTest for unit testing. There are other open-source frameworks like Quick and Nimble, but we’ll focus on XCTest in this post.

What to Test?

Now you know that it is essential to write tests for your app to benefit from it in the longer term. But what should you test exactly? You are not alone in asking this.

If you’re just starting out, the first few tests can be about testing the core business logic of the app. One critical piece is that you manually test the app regularly to ensure it doesn’t break in production.

After that, you can set up a few more that can be related to testing the user interface. You may also want to test code that has multiple edge cases that are time-consuming to test manually.

Understanding XCTest and XCTestCase

XCTest is a framework by Apple that helps us create and run unit, performance, and UI tests. For now, we’ll focus on creating unit tests only. The framework provides us with two major classes:

  • XCTest which acts as the base class for creating, managing, and executing tests.
  • XCTestCase which is the primary class for defining test cases, test methods, and performance tests inherited from XCTest.

Each test class has a lifecycle where we may set up the initial state before running and cleaning up after the tests are completed.

XCTest and XCTestCase provide various types and instance methods, out of which setUp() and tearDown() are the two main ones.

setUp() method is used to customize the initial state before the tests run. For example, initialising a data structure in the test methods and resetting its initial state for every test case.

tearDown() method is used to clean up after the tests run. For example, to remove any references we set the instance of initialized data structure to nil.

There are two types of methods provided to us:

  • Class methods to set up the initial state and perform final cleanup for all test methods. Here, we override setUp() and tearDown() class methods, respectively.
  • Instance methods to set up the initial state and to perform cleanup for each test method. Similarly, we override setUp() and tearDown() instance methods, respectively.

Now that we have the fundamentals done, it’s time to implement it in Xcode!

Adding a Unit Test in Xcode

Whenever you create a new project in Xcode, you have the option check ‘Include Tests’. These include both unit tests and UI tests.

Including tests while creating a new project in Xcode

If you already have a project, you can add a Unit Testing Bundle to it as well. Go to File > New > Target.

Adding a Unit Testing bundle in Xcode

Select iOS ‘Unit Testing Bundle’ and then click Next.

Creating a new Unit Testing target in Xcode

When you create a new Unit Test target for your project, it consists of a template class. Let’s go over the content of the file:

setUp() instance method is run every time before a test method is run. You override it to add your own implementation. If you want to run the initial code once in a test class, override the setUp() class method instead.

Similar to the previous setUp() methods, to clean up the state after every test method and override the tearDown() instance method. For cleaning up once in the test class, override the tearDown() class method instead.

Testing Example

For this example, we’ll test TallestTowers, an app that displays the tallest towers from around the world and information about them. You can download the project here.

In TallestTowers, we display a list of the tallest towers. The most significant code to test is if the list is empty or not.

When writing a test, prefix the method with the word “test” so that Xcode understands that it is a testable function. Create a class TowerStaticTests inheriting from XCTestCase, and add the following method in TowerStaticTests:

class TowerStaticTests: XCTestCase {
  func testTallestTowersShouldNotBeEmpty() {
    XCTAssert(Tower.tallestTowers.count > 0)
  }
}

Get the static variable tallestTowers from the Tower model and assert if the count is greater than zero. If so, the test passes and displays a green check.

Another set of tests to write is for the data model of Tower. This test class is named TowerInstanceTests. The model computes the city's location using its longitude and latitude and concatenates the city and country name in a single string. The height is formatted with a short suffix for meters. Write a few unit tests to ensure each of these computed properties always returns the expected output.

Declare a subject variable of the type of Tower. In the setUp() method and initialize the variable with mock data. Finally, override the tearDown() method to set the subject as nil.

class TowerInstanceTests: XCTestCase {
  var subject: Tower!

  override func setUp() {
    subject = Tower(name: "Empire State Building", city: "New York City", country: "USA", height: 381, yearBuilt: 1931, latitude: 40.748457, longitude: -73.985525)
  }

  override func tearDown() {
    subject = nil
  }
}

By using long names for the test, we can specifically mention what the test case is about. For example, the test location should be created from latitude and longitude properties. So, we name the test case similarly using camel casing.

func testLocationShouldBeCreatedFromLatitudeAndLongitudeProperties() {
  XCTAssertEqual(subject.location.latitude, 40.748457, accuracy: 0.00001)
  XCTAssertEqual(subject.location.longitude, -73.985525, accuracy: 0.00001)
}

func testCityAndCountryShouldConcatenateCityAndCountry() {
  XCTAssertEqual(subject.CityAndCountry, "New York City, USA")
}

func testFormattedHeightIncludesUnits() {
  XCTAssertEqual(subject.formattedHeight, "381m")
}

After writing a few tests, you’ll get the feel of how to write a test and what to test.

Tips for Naming

Whenever you are writing a test, follow these best practices:

  • Prefix the method with the word “test” so that Xcode understands that it is a testable function.Write long method names.
  • Be specific. If a test fails among many, the glimpse of the name should be enough to give you an idea of what failed. For example, if testTallestTowersShouldNotBeEmpty() fails, you know because the list will be empty.

Debugging a Unit Test

You can use the standard tools for debugging offered by Xcode to debug the unit tests as well. After checking for any logical or assumption error, use test failure breakpoints to see if the test is still failing or not outputting the expected result.

Go to the breakpoint navigator and select the add button (+).

debugging a unit test by adding a breakpoint

From the dropdown, choose Test Failure Breakpoint. This sets a particular breakpoint before starting a test run.

selecting a test failure breakpoint

The breakout gets triggered whenever you run a test and the test case posts a failed assertion. This helps to know where the test failed and the execution of the tests stopped.

Breakpoint execution to show a test failure

Enabling Code Coverage

Xcode has built-in code coverage to test if your tests have reviewed the entire code. To enable this option, go to TallestTowers and click on ‘Edit Scheme’. Select the test option from the sidebar then options from the segmented control. Check ‘Gather coverage for all targets’.

Enabling code coverage in schemes

Run the tests (Command + U) again. Select ‘Report Navigator’ from the Project Navigator and click on one of the recent tests. Select the Coverage option. You’ll find all the files that have been tested with their coverage in percentage and executable lines.

Coverage statistics

As we thoroughly tested the Tower model, we can see 95.5% code coverage.

To see the code coverage in an editor, go to the Editor options and check Code Coverage.

Selectng Code coverage in the editor options

You’ll now see the lines in red that haven’t been tested yet.

Lines of code that hasn't been tested yet

Although it's nice to have good code coverage, aiming for 100% is not ideal. Full coverage means that you're writing tests for each line of code. That'll take a considerable chunk of your time, but that doesn't mean covering every test case with edges or bugs will vanish. The focus should be on testing significant components of the app first and then focus on the rest.

Automating Unit Test in CI with Fastlane and Semaphore

To automate the testing process, we’ll use fastlane which is aimed at simplifying deployment. There are various methods of installing Fastlane, and we’ll use Homebrew here. Open Terminal and run the command:

brew install fastlane

Change directory to the project and run:

fastlane init

Now, open the Fastfile located in the project folder and add the below lines to it:

lane :tests do
  run_tests(scheme: "TallestTowers")
end

Finally, to run the tests, execute the following command in Terminal:

fastlane tests

To automate the process with continuous integration, you can use Semaphore. Refer to this article on how to set it up: Build, Test, & Deploy an iOS App with CI/CD.

Conclusion

It’s hard to focus on writing tests when there’s a deadline approaching, but many realize that it is beneficial in the long run. Writing tests improves code quality, reduces bugs and regressions, and speeds up your development process over time.

Also, take some time to configure CI/CD for your apps to focus on writing code and delivering a great user experience instead of manually delivering the app every time. Go write some tests with confidence!

Have questions about this tutorial? Want to share your experience of unit testing? Reach out to us on Twitter @semaphoreci.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment