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 when someone hears the word testing, it **feels scary. Although, tests help you be confident in the code you write and benefits in the long term. Assume you’re in a team working on an app. Someone mistakenly interchanges some data, and that was missed during manual testing. A user complains about mock data in the app. And another user sends you this feedback, and such messages bombard your inbox.

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

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

  • Why to Test?
  • What to Test?
  • What is Unit Testing?
  • 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 to Test?

As a software developer, you want to make sure that the code you’re writing is working the way it is supposed to. Even though it sounds trivial, but code breaks or regression happens. Regressions mean that the code you wrote before that worked, no longer does. And that leads to more efforts to fix it instead of working on something else. To minimise them, reduce manual testing efforts and give you confidence in the code, you write tests.

  • Reduce Bugs

Bugs may pass manual testing and get into production. Even a tiny thing as a typo can have adverse effects. If someone mistakenly updates the code that shouldn’t be touched, writing tests help you to find them early in development.

  • Refactoring

You aim to isolate a particular piece of code to test individually. You may refactor your code in the process, so it is more modular, with precise goals.

  • Thinking Edge Cases

While you’re writing tests, you think about various cases that can occur and then write tests about them.

  • Regression

After adding a completely new feature to the app, you don’t want to break existing code or features. That’s where writing tests come incredibly helpful. If you’re confident that the new feature won’t break the existing ones and cause regression, you speed up the development process and save time.

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 to test exactly? A question that everyone wonders about. What to write tests about?

If you’re starting out, the first few tests can be about testing the core business logic of the app. Something critical to the app is that you manually test it every time to ensure it doesn’t break in production. After that, a few more can be related to testing the user interface. And, you may also want to test code that has multiple edge cases that are time-consuming to test manually.

What is Unit Testing?

As the name suggests, we test a particular unit - a chunk of code that can be isolated to test individually. From network calls, testing the logic of caching to computed variables in your models. We’ve a predefined input and then check for the expected value and outcomes for the particular chunk of code in a test method.

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.

Understanding XCTest and XCTestCase

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

  • XCTest - acts as the base class for creating, managing, and executing tests.
  • XCTestCase - 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 major ones.

setUp() method is used to customise the initial state before the tests run. For example, initialising a data structure in the test methods and reset 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 initialised data structure to nil.

There are two types of methods provided to us -

  • Class methods to set up 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’ve the fundamentals done, it’s time to practically implement it in Xcode!

Adding a Unit Test in Xcode

Whenever you create a new project, you’ve the option check Include Tests. These include both Unit Tests and UI Tests.

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

Select iOS Unit Testing Bundle and then click Next.

When you create a new Unit Test target for your project, it consists of a template. It is the lifecycle of the test case. 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, override the tearDown() instance method. For cleaning up once in the test class, override the tearDown() class method instead.

Testing Example

For this post, 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 tallest towers. And the most significant code to test is to check if the list is empty or not.

Whenever writing a test, we prefix the method with the word “test” so that Xcode understands that it is a testable function. We create a class TowerStaticTests inheriting from XCTestCase, and add the following method in TowerStaticTests -

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

We get the static variable tallestTowers from the Tower model and assert if the count is greater than zero. If it is, the test passes with a green check.

Another set of tests to write is for the data model of Tower. We compute location and concatenate city and country names with correctly formatted height. We'll write a few unit tests to ensure each of these computed properties always returns the expected output.

To start off, we create another class inheriting from XCTestCase called TowerInstanceTests. We declare a subject variable of the type Tower. In the setUp() method, and initialise the variable with mock data. Finally, we 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
  }
}

With long names for the test, we 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")
}

With writing few tests, we got the feel of how to write a test, and what to test!

Tips for Naming

Whenever writing a test, we prefix the method with the word “test” so that Xcode understands that it is a testable function.

Write long methods 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, we know that the list is empty.

Debugging a Unit Test

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

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

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

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.

Enabling Code Coverage

Xcode has in-build code coverage to test if your tests has covered all the code. To enable this option, go to TallestTowers and click on Edit Scheme…. Select the Test option from the sidebar and select Options from the segmented control. Check mark Gather coverage for all Targets.

Run 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 the files that have been tested, with their coverage in percentage and executable lines.

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

To see the code coverage in an editor, select the Editor options and checkmark Code Coverage.

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

Although it’s nice to have good code coverage, aiming for 100% is not ideal.

Automating Unit Test in CI with Fastlane and Semaphore

To automate the testing process, we’ll use fastlane, aimed at simplifying deployment. There are various methods of installing fastlane, and we’ll use Homebrew. 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 service. Refer to the 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 nearby, but slowly you realise that it is beneficial in the long run. Writing tests improves code quality, with lesser bugs and regressions to faster your development process over time. Go write some tests with confidence!

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.

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

Have a comment? Join the discussion on the forum

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