try-catch Blocks and Unit Tests - Swift 3.0

try-catch Blocks and Unit Tests - Swift 3.0

In the last post I wrote about implementing error handler in Swift 3.0 using try-catch block technique. In this post I will go over how to unit test it. Since we all know unit tests are awesome!

First off, let's write some fresh code on error handler. This is very basic code and nobody will ever use it in the production. Point of this code is to demonstrate how to write error handling routine and unit testing it.

Please note that this code is slightly different, albeit very similar to code from previous post. Earlier code that I had demonstrated was written without any unit testing approach in my mind, but this time I took care to remove some of the code which may not necessarily be required to prove the point of unit tests.


// Enum to maintain error conditions in the app.
enum PasswordError: Error {
    case TooShort
    case NoNumber
    case CustomMessage(message: String)
}

class PasswordChecker {

    let password: String

    init(password: String) {
        self.password = password
    }

    func checkPassword() throws -> Bool {
        // Password should not be a common word.
        guard password.lowercased() != "password" else {
            throw PasswordError.CustomMessage(message: "Common keyword used as a password")
        }
        
        // Password should have at least specific length.
        guard self.password.characters.count >= 8 else {
            throw PasswordError.TooShort
        }

        // Password should have at least one number in it
        let numberRegEx  = ".*[0-9]+.*"
        let texttest1 = NSPredicate(format:"SELF MATCHES %@", numberRegEx)
        guard texttest1.evaluate(with: password) == true else {
            throw PasswordError.NoNumber
        }
        // If no error conditions are encountered, return true.
        return true
    }
}

Now, let's write some unit tests to verify this functionality,

Let's start by adding conformance to Equatable protocol for our ErrorType, PasswordError. This will allow us to compare two ErrorTypes with XCTAssertEqual API.


// Make sure to implement PasswordError to conform to Equatable protocol. Without such conformance, XCTAssertEqual on ErrorType will throw a compiler error.

extension PasswordError: Equatable {
    public static func ==(lhs: PasswordError, rhs: PasswordError) -> Bool {
        switch (lhs, rhs) {
            case (.TooShort, .TooShort):
                return true
            case (.NoNumber, .NoNumber):
                return true
            case (.CustomMessage(_), .CustomMessage(_)):
                return true
            default:
                return false
        }
    }
}

Once this is done, we are free to write our unit tests which include, but are not limited to verifying thrown errors and return values in case we use try? operator. This operator converts thrown error into optional value. Meaning, if error is thrown while evaluating value of try? expression, value of expression is nil

import XCTest
@testable import ErrorHandlingDemo

class PasswordCheckerTest: XCTestCase {

    // The unit tests below will verify the exception that will be thrown as a part of an invalid input password.

    // When password is too short, it will throw an error of type TooShort

    func testShortPasswordException() {
        let passwordChecker = PasswordChecker(password: "sample")

        XCTAssertThrowsError(try passwordChecker.checkPassword()) { error in
            XCTAssertEqual(error as? PasswordError, PasswordError.TooShort)
        }
    }

    // When password does not contain any numeric, it will throw an error of type NoNumber

    func testNoNumberPasswordException() {
        let passwordChecker = PasswordChecker(password: "samplepasswordforus")
        XCTAssertThrowsError(try passwordChecker.checkPassword()) { error in
            XCTAssertEqual(error as? PasswordError, PasswordError.NoNumber)
        }
    }

    // When password is a common word, it will throw an error of type CustomMessage(let message)

    func testCommonPasswordException() {
        let passwordChecker = PasswordChecker(password: "password")
        XCTAssertThrowsError(try passwordChecker.checkPassword()) { error in
            guard case PasswordError.CustomMessage(let message) = error else {
                return XCTFail()
            }

            XCTAssertEqual(error as? PasswordError, PasswordError.CustomMessage(message: "Common keyword used as a password"))
            XCTAssertEqual(message, "Common keyword used as a password")
        }
    }

    // Following tests will verify the returned value by passwordChecker function. It involves valid input.

    func testValidPasswords() {
        let passwordChecker = PasswordChecker(password: "samplepasswordforus1234")
        let passwordCheckerFlag = try? passwordChecker.checkPassword()
        XCTAssertNotNil(passwordCheckerFlag)
        XCTAssertTrue(passwordCheckerFlag!)
    }

    // Methods below will verify the return value from function checkPassword which is a part of PasswordChecker class. These methods will not handle thrown error, but instead will return nil values back if exception of any type occurs.

    func testInvalidShortPasswordCheckWithoutException() {
        let passwordChecker = PasswordChecker(password: "small")
        let passwordCheckerFlag = try? passwordChecker.checkPassword()
        XCTAssertNil(passwordCheckerFlag)
    }

    func testInvalidNoNumberPasswordCheckWithoutException() {
        let passwordChecker = PasswordChecker(password: "aadadsasdasdasd")
        let passwordCheckerFlag = try? passwordChecker.checkPassword()
        XCTAssertNil(passwordCheckerFlag)
    }

    func testInvalidCommonPasswordCheckWithoutException() {
        let passwordChecker = PasswordChecker(password: "password")
        let passwordCheckerFlag = try? passwordChecker.checkPassword()
        XCTAssertNil(passwordCheckerFlag)
    }
}

This should be it. If you are too confident about your code, forget about try/try? keywords and simply use try! (Although I won't recommend it!). try! expression assumes expression will never throw an error. But on the contrary if it ever does, it will cause a runtime error and application will crash.

To try/try?/try!? I will leave that decision to you. Hint: always use try, since it gives the breakdown of errors thrown from an expression and you can use do-catch blocks to catch appropriate type of error