1
\$\begingroup\$

I am learning to write test cases in iOS using Swift. I was stuck at testing static functions from my Utilities class, so I tried to mock that class and then test the cases. Please let me know if its the correct approach or else tell me how to do that. Here is my code:

import XCTest
@testable import ConnectAndSell

class UtilitiesMock{
    func getBuildNumber() -> String{
        return Utilities.getAppBuildNumber()
    }
    func getVersionNumber() -> String{
        return Utilities.getAppVersion()
    }
}

class UtilitiesTests: XCTestCase {
    //system under test
    var sut:UtilitiesMock!

    override func setUp() {
        super.setUp()
        // Put setup code here. This method is called before the invocation of each test method in the class.
        sut = UtilitiesMock()
    }

    override func tearDown() {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
        super.tearDown()
        sut = nil
    }

    func testBuildNumber() {
        let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as! String
        XCTAssertEqual(buildNumber, sut.getBuildNumber())

    }

    func testVersionNumber() {
        let versionNumber = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String
        XCTAssertEqual(versionNumber, sut.getVersionNumber())
    }

}

EDIT: Adding Utilities class so that you guys can see what am I trying to test. It's just simple functions to verify the current build number and version number. I have taken these function just for example. My main aim is to know is it the correct approach to test static methods.

import Foundation

class Utilities {

    static func getAppVersion() -> String {
        if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
            log.debug("App verison: \(version)")
            return version
        }
        return ""
    }
    static func getAppBuildNumber() -> String {
        if let number = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
            log.debug("Build number: \(number)")
            return number
        }
        return ""
    }

}
\$\endgroup\$
  • 3
    \$\begingroup\$ Hey Vishal, without more detail about the code that's being tested we can't really help you out.. You should include your full code :) \$\endgroup\$ – IEatBagels Dec 3 '19 at 14:00
  • 1
    \$\begingroup\$ @IEatBagels Please check my edit section. My main goal is to know the approach to test static methods, regardless of what I am testing. Hope this will help you to suggest something :) \$\endgroup\$ – Vishal Sonawane Dec 4 '19 at 5:34
  • \$\begingroup\$ This is Code Review. Either you put forward the code and we can help you, or you're only interested in the rough outlines and you're on the wrong site. Hypothetical code is not something we handle well, as per our help center. \$\endgroup\$ – Mast Dec 4 '19 at 14:48
  • \$\begingroup\$ For an implementation demo, the outer function (invoking the code that does matter) doesn't have to be as strictly to the reality, but the rest does. \$\endgroup\$ – Mast Dec 4 '19 at 14:50
  • 1
    \$\begingroup\$ @Adrian Yes it does that. Thanks \$\endgroup\$ – Vishal Sonawane Dec 9 '19 at 4:41
0
\$\begingroup\$
  1. Unit Tests should not have a dependency on the application bundle at runtime which could not be injected on the init phase or by a method arguments

The given Utility class has a static methods which act like a shortcuts to longer syntax calls. This methods calls directly methods on Bundle class object which is cannot be mocked. The solution is to move Bundle -> infoDictionary outside of a method and set it as a static property. Now, in the app the dictionary can reference a dictionary from the app bundle and in test bundle will reference a mocked dictionary. The main purpose of a unit testing this methods will be to validate usage of given dictionary and keys to access demanded values.

  1. Utility methods aka class methods are not dependency injection friendly and that makes them more complicated to test
  2. UtilitiesMock is not a mock, it's more a proxy / facade / wrapper class

Solution:

protocol BundleInfoDictionaryShortcuts {
    static var bundleMainInfoDictionary: [String : Any]? { get set }
}

enum BundleInfoDictionary: String {
    case keyCFBundleShortVersionString = "CFBundleShortVersionString"
    case keyCFBundleVersion = "CFBundleVersion"
}

class Utilities: BundleInfoDictionaryShortcuts {

    static var bundleMainInfoDictionary: [String : Any]? = Bundle.main.infoDictionary

    static func getAppVersion() -> String {

        guard let version = bundleMainInfoDictionary?[BundleInfoDictionary.keyCFBundleShortVersionString.rawValue] as? String else {
            fatalError()
        }
        return version
    }

    static func getAppBuildNumber() -> String {
        guard let number = bundleMainInfoDictionary?[BundleInfoDictionary.keyCFBundleVersion.rawValue] as? String else {
            fatalError()
        }
        return number
    }
}
// File: BundleInfoDictionaryShortcutsTests.swift

class BundleInfoDictionaryShortcutsTests: XCTestCase {
        static let appVersion = "1.0.1"
        static let appBuildNumber = "120"

        override class func setUp() {
            super.setUp()
            Utilities.bundleMainInfoDictionary = [
                BundleInfoDictionary.keyCFBundleShortVersionString.rawValue: appVersion,
                BundleInfoDictionary.keyCFBundleVersion.rawValue: appBuildNumber
            ]
        }

        func testGetAppVersion() {
            XCTAssertEqual(Utilities.getAppVersion(), Self.appVersion)
        }
        func testGetAppBuildNumber() {
            XCTAssertEqual(Utilities.getAppBuildNumber(), Self.appBuildNumber)
        }
    }
// File: BundleInfoDictionaryShortcutsTests.swift

    class BundleInfoDictionaryTests: XCTestCase {
        func testKeys() {
            XCTAssertEqual(BundleInfoDictionary.keyCFBundleShortVersionString.rawValue,"CFBundleShortVersionString")
            XCTAssertEqual(BundleInfoDictionary.keyCFBundleVersion.rawValue,"CFBundleVersion")
        }
    }
|improve this answer|||||
\$\endgroup\$
  • \$\begingroup\$ Thanks @Adobels. This a clear and detailed explanation. \$\endgroup\$ – Vishal Sonawane Dec 11 '19 at 5:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.