14 Dec 2021 · Software Engineering

    Unit Testing in Swift

    11 min read
    Contents

    The first time someone hears the word testing, it feels scary. Although, tests help you have more confidence in your code. They are also a benefit 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 minimize 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 feel more confident about the new feature. Of course, the goal is that the new feature should not break 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 to test exactly? A question that everyone wonders about. What to write tests about?

    If you are just starting out, the first few tests can be about testing the core business logic of the app. For example, writing tests for something you manually test in the app regularly to ensure it doesn’t break in production.

    NB: whatever tests you write, Semaphore has a neat feature that allows you to see which tests have failed, were skipped, or are too slow. Read more about Test Reports.

    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, initializing 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 practically implement it in Xcode! So, it’s time to continue with a more detailed Xcode unit testing tutorial.

    Adding a unit test in Xcode

    Whenever you create a new project, you have the option to 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 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.

    Another unit test 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 (+).

    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 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’.

    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.

    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.

    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. 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.

    Read also

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Avatar
    Writen by:
    Rudrank is an aspiring technical author and Apple platforms developer. You can mostly find him on Twitter (@rudrankriyam) or on his personal blog (rudrank.blog).