[iOS] UnitTest With XCode
此篇, 將利用1個簡單的project來介紹Xcode內建的Unit Test, 該project只會做簡單的幾件事情:
(1.) 當user按下Calculate Button時計算2個輸入數字X與Y的總和, 並將結果顯示在resultLabel上.
(2.) 當user按下Request Button時發送get request, 並將response data顯示在responseTextView上.
(3.) 在viewWillAppear生命週期時清空resultLabel與responseTextView的文字

你可以在這裡下載完整的project.
此Project裡有3個主要的classes,
(1.) ViewController
(在viewWillAppear時呼叫clearData function將label與textView清空)
class ViewController: UIViewController { @IBOutlet weak var xTextField: UITextField! @IBOutlet weak var yTextField: UITextField! @IBOutlet weak var resultLabel: UILabel! @IBOutlet weak var urlTextField: UITextField! @IBOutlet weak var responseTextView: UITextView! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) self.clearData() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } func clearData(){ self.resultLabel.text = "" self.responseTextView.text = "" } @IBAction func calculateButton_Touch(_ sender: Any) { let x = Math.stringToInt(string: self.xTextField.text) let y = Math.stringToInt(string: self.yTextField.text) var outputString = "invalid input" if let x = x, let y = y { let result = Math.add(x: x, y: y) outputString = "\(result)" } self.resultLabel.text = outputString } @IBAction func requestButton_Touch(_ sender: Any) { if let url = urlTextField.text { Network.dataTask(urlString: url) { (data, response, error) in guard error == nil else { self.responseTextView.text = "\(error!.localizedDescription)" return } if let data = data { if let jsonString = String(data: data, encoding: .utf8){ DispatchQueue.main.async { self.responseTextView.text = jsonString class ViewController: UIViewController {
@IBOutlet weak var xTextField: UITextField!
@IBOutlet weak var yTextField: UITextField!
@IBOutlet weak var resultLabel: UILabel!
@IBOutlet weak var urlTextField: UITextField!
@IBOutlet weak var responseTextView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.clearData()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func clearData(){
self.resultLabel.text = ""
self.responseTextView.text = ""
}
@IBAction func calculateButton_Touch(_ sender: Any) {
let x = Math.stringToInt(string: self.xTextField.text)
let y = Math.stringToInt(string: self.yTextField.text)
var outputString = "invalid input"
if let x = x, let y = y {
let result = Math.add(x: x, y: y)
outputString = "\(result)"
}
self.resultLabel.text = outputString
}
@IBAction func requestButton_Touch(_ sender: Any) {
if let url = urlTextField.text {
Network.dataTask(urlString: url) { (data, response, error) in
guard error == nil else {
self.responseTextView.text = "\(error!.localizedDescription)"
return
}
if let data = data {
if let jsonString = String(data: data, encoding: .utf8){
DispatchQueue.main.async {
self.responseTextView.text = jsonString
}
}
}
}
}
}
}
(2.) Math
add: 將2個數字相加並回傳
stringToInt: string轉int並回傳結果, 若失敗回傳nil
class Math: NSObject {
class func add(x: Int, y: Int) -> Int {
let result = x + y
return result
}
class func stringToInt(string: String?) -> Int? {
var result:Int? = nil
if let string = string {
result = Int(string)
}
return result
}
}
(3.) Network dataTask:
發送一個request並回傳response
class Network: NSObject {
class func dataTask(urlString: String, responseHandler: @escaping (Data?, URLResponse?, Error?) -> Swift.Void) {
let url = URL(string: urlString)
if let url = url {
let request = URLRequest(url: url)
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
print("Retrieved data")
responseHandler(data, response, error)
}
task.resume()
} else {
let myError = NSError(domain:"", code: -9999, userInfo:nil)
responseHandler(nil, nil, myError)
}
}
}
建置Unit Test
首先, 如果您的project尚未加入Unit Test, 可以從File -> New -> Target 新建一個Unit Test Bundle.


完成後, 在XCode左側目錄欄會發現多了一個Tests的目錄, 且裡面已經幫你建立好了1個default的測試class.

該class的預設內容為:
import XCTest
class TestProjectTests: XCTestCase {
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func testExample() {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
func testPerformanceExample() {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
}
}
}
我們可以注意到, Unit Test用的class必須import XCTest並extend XCTestCase,
這邊有4個function,
setUp: 測試class生命週期, 在測試開始之前被呼叫
tearDown: 測試class生命週期, 在測試完成之後被呼叫
testExample: 預設的test function, 注意到所有的test function命名都必須以test開頭
testPerformanceExample: 預設的test function, 在meaure block裡面的動作皆會執行10次, 用以測試performance
接著, 我們也可以新增新的Test Class分別去測試我們的不同class
MathTest: 用來測試Math class的functions
import XCTest
class MathTest: XCTestCase {
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func testAdd() {
let a = Math.add(x: 3, y: 5)
let b = Math.add(x: 64, y: -70)
let c = Math.add(x: 999, y: 0)
XCTAssert(a == 8, "a should be 8")
XCTAssert(b == -6, "b should be -6")
XCTAssert(c == 999, "c should be 999")
}
func testStringToInt() {
let a = Math.stringToInt(string: "9")
let b = Math.stringToInt(string: "-55")
let c = Math.stringToInt(string: "sdfsks")
XCTAssert(a == 9, "a should be 8")
XCTAssert(b == -55, "b should be -55")
XCTAssert(c == nil, "c should be nil")
}
}
TestNetwork: 用來測試Network class的dataTask function, 關於網路測試是否成功有2個重點
(1.) request要有發送出去
(2.) 發送出去的url, parameters, headers是否正確
在這個例子裡面我們在response的時候檢查 response?.url?.absoluteString是否跟原本的相同.
然而, 更嚴謹的網路測試通常不會真的發送request出去, 因為這樣會影響測試時間並可能污染server端的資料, 此時就可以用Dependency Injection(DI)等方法來達到測試目的, 詳情可以參考以下連結:
The complete guide to Network Unit Testing in Swift
import XCTest
class TestNetwork: XCTestCase {
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func testDataTask() {
let urlString = "http://opendata2.epa.gov.tw/UV/UV.json"
Network.dataTask(urlString: urlString) { (data, response, error) in
let requestUrl = response?.url?.absoluteString
XCTAssert(requestUrl == urlString, "wrong URL!")
}
}
}
TestViewController: 用來測試Viewcontroller class, 因Viewcontroller在viewWillAppear裡面呼叫了clearData function, 所以我們在testClearData() 裡面測試相關元件的text是否為空.
這邊需注意:
(1.) test class的Viewcontroller需要用storyboard.instantiateViewController的方式去宣告, 否則IBOutlet元件會為nil
(2. ) 必須呼叫 vc.loadView() 才能啟動Viewcontroller的生命週期
import XCTest
class TestViewController: XCTestCase {
override func setUp() {
super.setUp()
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func testClearData() {
let storyboard = UIStoryboard(name: "Main", bundle: Bundle(for: ViewController.self))
let vc = storyboard.instantiateViewController(withIdentifier: "ViewController") as! ViewController
vc.loadView() // call life cycle
vc.clearData()
XCTAssert(vc.resultLabel!.text == "", "vc.resultLabel!.text should be empty ")
XCTAssert(vc.responseTextView!.text == "", "vc.responseTextView!.text should be empty ")
}
}
進行測試
點擊Xcode的測試按鈕就能開始測試, 結束後可以在這個頁面看到完整的測試結果, 點擊每個function的綠色勾勾也可以個別測試單個function

測試覆蓋率
(1.) 進入Edit Scheme -> Test -> Options頁面將Gather coverage for all targets選項打勾

(2.) 完成測試後就可以在這裡看到目前測試的coverage rate

Last updated