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 ""
}
}
-
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– IEatBagels2019年12月03日 14:00:40 +00:00Commented Dec 3, 2019 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– Vishal Sonawane2019年12月04日 05:34:20 +00:00Commented Dec 4, 2019 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– Mast ♦2019年12月04日 14:48:48 +00:00Commented Dec 4, 2019 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– Mast ♦2019年12月04日 14:50:31 +00:00Commented Dec 4, 2019 at 14:50
-
1\$\begingroup\$ @Adrian Yes it does that. Thanks \$\endgroup\$Vishal Sonawane– Vishal Sonawane2019年12月09日 04:41:48 +00:00Commented Dec 9, 2019 at 4:41
1 Answer 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.
- Utility methods aka class methods are not dependency injection friendly and that makes them more complicated to test
- 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")
}
}
-
\$\begingroup\$ Thanks @Adobels. This a clear and detailed explanation. \$\endgroup\$Vishal Sonawane– Vishal Sonawane2019年12月11日 05:51:49 +00:00Commented Dec 11, 2019 at 5:51