iT邦幫忙

2023 iThome 鐵人賽

DAY 13
1
Mobile Development

在 iOS 專案上加上 Unit testing - 因為 You need testing系列 第 13

D13 - 在 iOS 專案加上測試-You need testing {台股小工具 app-股票紀錄轉換par2}

  • 分享至 

  • xImage
  •  

測試項目

  • stockID, stockName, tradingShares, tradingCostPerShare 其中有一個為空時,得到 Error
  • stockID = “0050”, stockName = “元大50”, tradingShares = “1000”, tradingCostPerShare = “a” 時,得到 Error
  • stockID = “0050”, stockName = “元大50”, tradingShares = “a”, tradingCostPerShare = “130” 時,得到 Error
  • stockID = “0050”, stockName = “元大50”, tradingShares = “-1000”, tradingCostPerShare = “130” 時,得到 Error
  • stockID = “0050”, stockName = “元大50”, tradingShares = “1000”, tradingCostPerShare = “-130” 時,得到 Error
  • stockID = “0050”, stockName = “元大50”, tradingShares = “1000”, tradingCostPerShare = “130” 時,得到 StockRecordUtility 物件,且此物件的每個屬性和輸入的值是相對應的。也就是 tradingShares = “1000” 時 stockRecordUtility.tradingShares = 1000

第一個項目通過之後,我們繼續進行後續的測項,先從 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,所以測試失敗

https://ithelp.ithome.com.tw/upload/images/20230924/20140622juPOGtAnmq.png

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: 跑測試,而且前面的測項也要通過

https://ithelp.ithome.com.tw/upload/images/20230924/201406220CtCj5J4p6.png


測項3 tradingCostPerShare = “a” 時,得到 Error

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: 跑測試

https://ithelp.ithome.com.tw/upload/images/20230924/20140622myxCuTJHCf.png

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: 跑測試,而且前面的測項也要通過

https://ithelp.ithome.com.tw/upload/images/20230924/20140622SqFeBuVEym.png


測項4 tradingShares 小於等於 0 時,得到 Error

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

https://ithelp.ithome.com.tw/upload/images/20230924/20140622ATwHQbSIvy.png

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: 跑測試,而且前面的測項也要通過

https://ithelp.ithome.com.tw/upload/images/20230924/20140622mP0wyi5dXl.png


測項4 costPerShare 小於等於 0 時,得到 Error

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

https://ithelp.ithome.com.tw/upload/images/20230924/20140622V6I9hrjhZg.png

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: 跑測試,而且前面的測項也要通過

https://ithelp.ithome.com.tw/upload/images/20230924/20140622nMRmnO3xDB.png


測項6 每個 Record 的 property 都要正確

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

https://ithelp.ithome.com.tw/upload/images/20230924/201406223ksrcsxVGn.png

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 上的宣告,是在不同物件

https://ithelp.ithome.com.tw/upload/images/20230924/20140622olP5cMak3S.png

這時候,就需要更改 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 參數宣告

https://ithelp.ithome.com.tw/upload/images/20230924/20140622Sc1mKN3Awn.png

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 發揮功能了。他在我們實作錯誤的時候,先幫我們擋下來,不讓我們繼續開發,或者把錯誤的程式碼發佈上去。

https://ithelp.ithome.com.tw/upload/images/20230924/20140622F3n03LusU1.png

測試為什麼可以減少你的加班?如下圖,你可以清楚的看到,Unit testing 會告訴你測試項目的值,以及你的實作跑出來的值是什麼。

測試失敗原因:130 不等於 130000,而 130000 才是對的。

https://ithelp.ithome.com.tw/upload/images/20230924/20140622M0PO4BV0O2.png

所以問題在最後的 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!

https://ithelp.ithome.com.tw/upload/images/20230924/20140622O8O2g0KapN.png

全部過關,上面的 spec 都在測項裡,而測項全通過,現在的這份程式碼,在開發者的心中,應該是很有信心的,可以在這時候下 git commit 了。


上一篇
D12 - 在 iOS 專案加上測試-You need testing {台股小工具 app-股票紀錄轉換par1}
下一篇
D14 - 在 iOS 專案加上測試-You need testing {台股小工具 app-加上日期}
系列文
在 iOS 專案上加上 Unit testing - 因為 You need testing32
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言