測試項目
第一個項目通過之後,我們繼續進行後續的測項,先從 tradingCosPerShare = “a” 時報錯開始
step1: 先寫測試
func testTradingShareCannotConvertToInt() throws {
let result = StockRecordUtility().getStockRecord(stockID: "0050", stockName: "元大50", tradingSide: .buy, stockShares: "a", stockCostPerShare: "130")
switch result {
case .success(_):
XCTFail("This test case not able success")
case .failure(let failure):
let error = try XCTUnwrap(failure as? StockRecordUtility.StockRecordInputError)
XCTAssertEqual(error, StockRecordUtility.StockRecordInputError.castingError)
}
}
step2: 跑測試,這時候因為所有參數都有值,所以可以轉成物件,但在 spec 上應 return error,所以測試失敗
step3: 修改實作,讓這一測項通過
func getStockRecord(stockID: String, stockName: String, tradingSide: StockTradingInputView.TradingSide, stockShares: String, stockCostPerShare: String) -> Result<StockTradingRecord, Error> {
/// 不可以輸入空值
if stockID.isEmpty ||
stockName.isEmpty ||
stockShares.isEmpty ||
stockCostPerShare.isEmpty {
return .failure(StockRecordInputError.noValue)
}
/// 需通過 stockShares 能轉換成 Int
guard let stockSharesInt = Int(stockShares) else {
return .failure(StockRecordInputError.castingError)
}
/// 這還不是正式實作,需要寫 return 是為了讓 project 能 build 起來,只有能 build 後才能 testing
return .success(StockTradingRecord(stockID: "", stockName: "", tradingSide: .buy, tradingShares: 0, tradingAmount: 0))
}
step4: 跑測試,而且前面的測項也要通過
step1: 先寫測試
func testTradingCostPerShareConvertToInt() throws {
let result = StockRecordUtility().getStockRecord(stockID: "0050", stockName: "元大50", tradingSide: .buy, stockShares: "1000", stockCostPerShare: "a")
switch result {
case .success(_):
XCTFail("This test case not able success")
case .failure(let failure):
let error = try XCTUnwrap(failure as? StockRecordUtility.StockRecordInputError)
XCTAssertEqual(error, StockRecordUtility.StockRecordInputError.castingError)
}
}
step2: 跑測試
step3: 修改實作,讓這一測項通過
func getStockRecord(stockID: String, stockName: String, tradingSide: StockTradingInputView.TradingSide, stockShares: String, stockCostPerShare: String) -> Result<StockTradingRecord, Error> {
/// 不可以輸入空值
if stockID.isEmpty ||
stockName.isEmpty ||
stockShares.isEmpty ||
stockCostPerShare.isEmpty {
return .failure(StockRecordInputError.noValue)
}
/// 需通過 stockShares 能轉換成 Int
/// 需通過 stockCostPerShare 能轉換成 Int
guard let stockSharesInt = Int(stockShares),
let stockCostPerShare = Int(stockCostPerShare) else {
return .failure(StockRecordInputError.castingError)
}
/// 這還不是正式實作,需要寫 return 是為了讓 project 能 build 起來,只有能 build 後才能 testing
return .success(StockTradingRecord(stockID: "", stockName: "", tradingSide: .buy, tradingShares: 0, tradingAmount: 0))
}
step4: 跑測試,而且前面的測項也要通過
step1: 先寫測試
func testTradingSharesBiggerThanZero() throws {
let result = StockRecordUtility().getStockRecord(stockID: "0050", stockName: "元大50", tradingSide: .buy, stockShares: "-1000", stockCostPerShare: "130")
switch result {
case .success(_):
XCTFail("This test case not able success")
case .failure(let failure):
let error = try XCTUnwrap(failure as? StockRecordUtility.StockRecordInputError)
XCTAssertEqual(error, StockRecordUtility.StockRecordInputError.castingError)
}
}
step2: 跑測試,確定 failed
step3: 修改實作
func getStockRecord(stockID: String, stockName: String, tradingSide: StockTradingInputView.TradingSide, stockShares: String, stockCostPerShare: String) -> Result<StockTradingRecord, Error> {
/// 不可以輸入空值
if stockID.isEmpty ||
stockName.isEmpty ||
stockShares.isEmpty ||
stockCostPerShare.isEmpty {
return .failure(StockRecordInputError.noValue)
}
/// 需通過 stockShares 能轉換成 Int
/// 需通過 stockCostPerShare 能轉換成 Int
/// stockShares 需大於等於 0
guard let stockSharesInt = Int(stockShares),
let stockCostPerShare = Int(stockCostPerShare),
stockSharesInt > 0 else {
return .failure(StockRecordInputError.castingError)
}
/// 這還不是正式實作,需要寫 return 是為了讓 project 能 build 起來,只有能 build 後才能 testing
return .success(StockTradingRecord(stockID: "", stockName: "", tradingSide: .buy, tradingShares: 0, tradingAmount: 0))
}
step4: 跑測試,而且前面的測項也要通過
step1: 先寫測試
func testCostPerShareBiggerThanZero() throws {
let result = StockRecordUtility().getStockRecord(stockID: "0050", stockName: "元大50", tradingSide: .buy, stockShares: "1000", stockCostPerShare: "-130")
switch result {
case .success(_):
XCTFail("This test case not able success")
case .failure(let failure):
let error = try XCTUnwrap(failure as? StockRecordUtility.StockRecordInputError)
XCTAssertEqual(error, StockRecordUtility.StockRecordInputError.castingError)
}
}
step2: 跑測試,確定 failed
step3: 改實作
func getStockRecord(stockID: String, stockName: String, tradingSide: StockTradingInputView.TradingSide, stockShares: String, stockCostPerShare: String) -> Result<StockTradingRecord, Error> {
/// 不可以輸入空值
if stockID.isEmpty ||
stockName.isEmpty ||
stockShares.isEmpty ||
stockCostPerShare.isEmpty {
return .failure(StockRecordInputError.noValue)
}
/// 需通過 stockShares 能轉換成 Int
/// 需通過 stockCostPerShare 能轉換成 Int
/// stockShares 需大於等於 0
/// stockCostPerShare 需大於等於 0
guard let stockSharesInt = Int(stockShares),
let stockCostPerShare = Int(stockCostPerShare),
stockSharesInt > 0,
stockCostPerShareInt > 0 else {
return .failure(StockRecordInputError.castingError)
}
/// 這還不是正式實作,需要寫 return 是為了讓 project 能 build 起來,只有能 build 後才能 testing
return .success(StockTradingRecord(stockID: "", stockName: "", tradingSide: .buy, tradingShares: 0, tradingAmount: 0))
}
step4: 跑測試,而且前面的測項也要通過
step1: 先寫測試
func testStockRecordStockProperties() throws {
let result = StockRecordUtility().getStockRecord(stockID: "0050", stockName: "元大50", tradingSide: .buy, stockShares: "1000", stockCostPerShare: "130")
switch result {
case .success(let record):
XCTAssertEqual(record.stockID, "0050")
XCTAssertEqual(record.stockName, "元大50")
XCTAssertEqual(record.tradingSide, .buy)
XCTAssertEqual(record.tradingShares, 1000)
/// trading Amount 是交易總金額,所以是 1000 * 130 = 130000
XCTAssertEqual(record.tradingAmount, 130000)
case .failure(_):
XCTFail("Test failed")
}
}
step2: 跑測試,確定 failed
step3: 改實作
func getStockRecord(stockID: String, stockName: String, tradingSide: StockTradingInputView.TradingSide, stockShares: String, stockCostPerShare: String) -> Result<StockTradingRecord, Error> {
/// 不可以輸入空值
if stockID.isEmpty ||
stockName.isEmpty ||
stockShares.isEmpty ||
stockCostPerShare.isEmpty {
return .failure(StockRecordInputError.noValue)
}
/// 需通過 stockShares 能轉換成 Int
/// 需通過 stockCostPerShare 能轉換成 Int
/// stockShares 需大於等於 0
/// stockCostPerShare 需大於等於 0
guard let stockSharesInt = Int(stockShares),
let stockCostPerShareInt = Int(stockCostPerShare),
stockSharesInt > 0,
stockCostPerShareInt > 0 else {
return .failure(StockRecordInputError.castingError)
}
/// 這還不是正式實作,需要寫 return 是為了讓 project 能 build 起來,只有能 build 後才能 testing
return .success(StockTradingRecord(stockID: stockID, stockName: stockName, tradingSide: tradingSide, tradingShares: stockSharesInt, tradingAmount: stockCostPerShareInt))
}
然後,你就會看到 Error 因為 tradingSide 在 buy / sell 上的宣告,是在不同物件
這時候,就需要更改 enum TradingSide 的設計。可以有兩個方向
1 > 在 StockRecordUtility 內,進行 StockTradingInputView.TradingSide 和 StockTradingRecord.TradingSide 的轉換
2 > 將 TradingSide 從 StockRecordUtility 移到外面
我這邊的思考是,每一本交易在台股/期貨/選擇權上,都是買進/賣出的選擇,所以我這個專案想走策略2。(但事實上,策略1和策略2的走向,都只是選擇而已,不同選擇,會有不同實作而已)
step4: 將 TradingSide 移到 StockTradingRecord 和 StockTradingInputView 的外面
import Foundation
enum TradingSide: Int {
case buy = 0
case sell
}
step5: 再跑測試,會發現 failed,再修改 func 參數宣告
step6: 修改實作後
func getStockRecord(stockID: String, stockName: String, tradingSide: TradingSide, stockShares: String, stockCostPerShare: String) -> Result<StockTradingRecord, Error> {
/// 不可以輸入空值
if stockID.isEmpty ||
stockName.isEmpty ||
stockShares.isEmpty ||
stockCostPerShare.isEmpty {
return .failure(StockRecordInputError.noValue)
}
/// 需通過 stockShares 能轉換成 Int
/// 需通過 stockCostPerShare 能轉換成 Int
/// stockShares 需大於等於 0
/// stockCostPerShare 需大於等於 0
guard let stockSharesInt = Int(stockShares),
let stockCostPerShareInt = Int(stockCostPerShare),
stockSharesInt > 0,
stockCostPerShareInt > 0 else {
return .failure(StockRecordInputError.castingError)
}
/// 這還不是正式實作,需要寫 return 是為了讓 project 能 build 起來,只有能 build 後才能 testing
return .success(StockTradingRecord(stockID: stockID, stockName: stockName, tradingSide: tradingSide, tradingShares: stockSharesInt, tradingAmount: stockCostPerShareInt))
}
step7: 跑測試,確定更改後,測項都是通過的
發現前面的 error 判定都過了,但最後測 property 時沒有過。這時候,我們就可以看到 Unit testing 發揮功能了。他在我們實作錯誤的時候,先幫我們擋下來,不讓我們繼續開發,或者把錯誤的程式碼發佈上去。
測試為什麼可以減少你的加班?如下圖,你可以清楚的看到,Unit testing 會告訴你測試項目的值,以及你的實作跑出來的值是什麼。
所以問題在最後的 tradingAmount,stockCostPerShareInt 是每股價格,在最後實作就是缺了將 sharesInt * costPerShareInt,這兩個值的相乘,才是 tradingAmount。
/// 這還不是正式實作,需要寫 return 是為了讓 project 能 build 起來,只有能 build 後才能 testing
return .success(StockTradingRecord(stockID: stockID,
stockName: stockName,
tradingSide: tradingSide,
tradingShares: stockSharesInt,
tradingAmount: stockCostPerShareInt))
step8: 修改實作,將 amount 成交金額填入
func getStockRecord(stockID: String, stockName: String, tradingSide: TradingSide, stockShares: String, stockCostPerShare: String) -> Result<StockTradingRecord, Error> {
/// 不可以輸入空值
if stockID.isEmpty ||
stockName.isEmpty ||
stockShares.isEmpty ||
stockCostPerShare.isEmpty {
return .failure(StockRecordInputError.noValue)
}
/// 需通過 stockShares 能轉換成 Int
/// 需通過 stockCostPerShare 能轉換成 Int
/// stockShares 需大於等於 0
/// stockCostPerShare 需大於等於 0
guard let stockSharesInt = Int(stockShares),
let stockCostPerShareInt = Int(stockCostPerShare),
stockSharesInt > 0,
stockCostPerShareInt > 0 else {
return .failure(StockRecordInputError.castingError)
}
/// amount 成交金額 是 成交股數 * 每股價價
let amount = stockSharesInt * stockCostPerShareInt
return .success(StockTradingRecord(stockID: stockID,
stockName: stockName,
tradingSide: tradingSide,
tradingShares: stockSharesInt,
tradingAmount: amount))
}
step9: 測試,You need testing!
全部過關,上面的 spec 都在測項裡,而測項全通過,現在的這份程式碼,在開發者的心中,應該是很有信心的,可以在這時候下 git commit 了。