Skip to main content

How to Write Unit Tests in Swift

Tags

How to Write Unit Tests in Swift

Writing unit testing is a cornerstone of software development.  When developing mobile or desktop applications, unit testing becomes even more critical, as there is often vital business logic that needs to be validated.  Have you ever found yourself wondering; what makes a great unit test in Swift? What key components give my application enough testing coverage to make sure that everything works and nothing regresses when new code is added to my project?  If you have asked yourself these questions then you are not alone.  In fact, that is why I wanted to write this tutorial. To provide my perspective on what makes a great unit test in Swift, and to provide some sample code of unit testing in a real world scenario. After the tutorial is finished you should know the fundamentals of how to get started writing Swift unit tests with XCTests!  Let’s jump in!


NOTE: This tutorial was created using Xcode 9.3 using Swift 4.1.  The code in this example has only been tested on macOS 10.13 and not on Linux or macOS 10.14.

 

Getting Started 🚀

First, let’s describe a scenario where you will need to do some testing in your application; The scenario is you have a eCommerce application that downloads coupon configurations from your web service and creates coupons in your app.  The coupon object in the Swift code below represents one of these coupons. When new coupons are created, the constructor runs through a validation routine to make sure the coupon is valid based upon a set of predefined business logic.  The business logic states that a coupon can be valid only if active for seven days or less and if the amount is ten dollars or less.

The scenario described above for coupons is a great example of business logic that needs to be tested.  After all, an invalid coupons could potentially be giving customers discounts if our code in our app does not work properly.  To start, let’s take a look at the coupon object. The object contains a start and end date to set the days the coupon should be running.  There also are amount and validDays fields present to describe how much the coupon should be and how long the coupon should run. Finally there is a isValid flag that is set to false to make sure that the coupon is valid before usage in the application.

class Coupon: NSObject {
 
    // The Coupon Object's Properties
    var startDate: Date?
    var endDate: Date?
    var amount: Double = 10.0
    var isValid: Bool = false
    var validDays: Int = 7
 
    // Constructor for the Coupon Object
    init(start: Date, end: Date, couponAmount: Double) {
        super.init()
        self.startDate = start
        self.endDate = end
        self.amount = couponAmount
        self.validateCoupon()
    }
 
    // Validate the Coupon
    public func validateCoupon() {
        // Make sure the coupon is 10.00 or less
        if self.amount > 10.0 {
            // return right away with isValid still false
            return
        }
        // Unwrap the start and end dates
        guard let start = self.startDate, let end = self.endDate else {
            return
        }
        // Calculate the start and end dates
        guard let dayDifference = Calendar.current.dateComponents([.day], from: start, to: end).day,
            dayDifference <= validDays else {
            // return right away with isValid still false
            return
        }
        // Make sure that isValid is set to true once all fields have been validated
        self.isValid = true
    }
 
}

Testing with XCTest 👨‍💻

Now that we have defined our coupon scenario let’s define what needs to be tested and then write our test using XCTest.  First, let’s make sure that our coupon can be created successfully by testing our success case:

// Test that the coupon can be created with a successful set of values
func testCouponSuccess() {
    guard let futureDate = Calendar.current.date(byAdding: .day, value: 5, to: dateNow) else {
        XCTFail("testCouponSuccess() failed")
        return
    }
    let coupon = Coupon(start: dateNow, end: futureDate, couponAmount: 5.0)
 
    XCTAssertTrue(coupon.isValid, "testCouponSuccess() succeeded")
}

Next, let’s validate that an invalid date range fails like we expect it to.  In the example below the date range should exceed the isValid field and cause a testing failure:

// Test that the coupon can be flagged as invalid with an extended date range
func testCouponDateFailure() {
    guard let futureDate = Calendar.current.date(byAdding: .day, value: 15, to: dateNow) else {
        XCTFail("testCouponDateFailure() failed")
        return
    }
    let coupon = Coupon(start: dateNow, end: futureDate, couponAmount: 5.0)
 
    XCTAssertFalse(coupon.isValid, "testCouponDateFailure() correctly validated")
}

Next, let’s validate that a coupon with an amount of 15.0 does in-fact fail too.  15 is greater than 10.0 in this case, so this will make sure that amount property check is valid:

// Test that the coupon can be flagged as invalid with a amount that is too large
func testCouponAmountFailure() {
    guard let futureDate = Calendar.current.date(byAdding: .day, value: 5, to: dateNow) else {
        XCTFail("testCouponSuccess() failed")
        return
    }
    let coupon = Coupon(start: dateNow, end: futureDate, couponAmount: 15.0)
 
    XCTAssertFalse(coupon.isValid, "testCouponAmountFailure() correctly validated")
}

Finally, let’s regroup on what we just learned about this test.  As you can see I identified both the failing and passing state of our logic to make sure that each scenario is covered.  When I was writing my test, I made sure that if a logical or unwrap failure occurred I marked this with as a failure too because technically this is a data error.  I tried to use common data (dateNow) between each test case to make sure that each case was creating coupons from the same base date. This help tos make sure that there are not wide variances in the data that is being tested.

 

In Summary ⌛️

Testing is a critical part of software development, no matter what industry you are in.  Making sure that there is coverage for the critical business logic that make up your application will ensure that regressions do not creep into your project as new code is added.  I hope after reading this tutorial that you know a bit more about where to test and how to test using Swift in your application. Thank you for reading. All of the code from this example will be available on my Github here.  Please leave a comment if you have any questions, comments, or concerns. Thank you!

Credits: Cover image designed by Freepik.

Member for

3 years 9 months
Matt Eaton

Long time mobile team lead with a love for network engineering, security, IoT, oss, writing, wireless, and mobile.  Avid runner and determined health nut living in the greater Chicagoland area.

Comments

Armand

Sun, 07/01/2018 - 08:39 PM

Nice article!
Finally there is a isValid flag that is set to false to make sure that the coupon is valid before usage in the application.
Shouldn't this be "the coupon is INvalid" ?

Thank you, Armand.  I did word this scenario a bit odd, but what I had meant to say was; The coupon is set as invalid until I prove that parameters passed into the constructor make the coupon valid.  Sorry about the confusion.

Richard

Sat, 07/21/2018 - 05:10 PM

Since when does the argument supplied to XCTAssertFalse describe the success message? It is specifically there to describe why the test failed.

Thank you for your feedback.  XCTAssertFalse can also describe a case where a false condition is present.  For example: "Asserts that an expression is false."  If the expression is successfully, false, this would mean the test succeeded.  

John Russell

Wed, 02/20/2019 - 03:05 PM

I'd like to point out that it's probably better to define business rules in the middle tier (web services) rather than in the UI (hard coded). If they ever change and the UI (app code) isn't changed to reflect the business rules change, we could have mayhem in our system as a whole.

John, thank you for the feedback.  This is a great point.  As much as possibly business rules should be offloaded to a place, such as a web server where they can be easily updated.  The mobile clients should also be flexible enough to reflect the updates in these business rules.

Any business rules in this post are just for the sake of example.