🎄teratailクリスマスプレゼントキャンペーン2024🎄』開催中!

\teratail特別グッズやAmazonギフトカード最大2,000円分が当たる!/

詳細はこちら
Swift

Swiftは、アップルのiOSおよびOS Xのためのプログラミング言語で、Objective-CやObjective-C++と共存することが意図されています

テスト駆動開発

テスト駆動開発は、 プログラム開発手法の一種で、 プログラムに必要な各機能をテストとして書き、 そのテストが動作する必要最低限な実装を行い コードを洗練させる、といったサイクルを繰り返す手法の事です。

Q&A

解決済

2回答

1445閲覧

Swift テストコード

imasyou718

総合スコア5

Swift

Swiftは、アップルのiOSおよびOS Xのためのプログラミング言語で、Objective-CやObjective-C++と共存することが意図されています

テスト駆動開発

テスト駆動開発は、 プログラム開発手法の一種で、 プログラムに必要な各機能をテストとして書き、 そのテストが動作する必要最低限な実装を行い コードを洗練させる、といったサイクルを繰り返す手法の事です。

0グッド

0クリップ

投稿2019/11/04 05:53

Swiftでの質問です

下記のコードのテストを行いたいですが
テストを行ったことがないので苦労してます

ResponseData のクラスにString の型を記載して、APIのテスト用JSONを作ってそれが本当にちゃんと作れているかどうかをテストすればいいと考えたのですが
どんなコードになるかいまいちピンこないです

お答えいただけたら嬉しいです!

コード import UIKit import Alamofire import Keys /// レスポンスデータ struct ResponseData: Codable { var request_id = "" var output_type = "" var converted = "" } class ViewController: UIViewController { @IBOutlet weak var inputCharacter: UITextField! @IBOutlet weak var rubyCharacter: UILabel! @IBOutlet weak var outputCharacter: UILabel! @IBOutlet weak var outputView: UIView! override func viewDidLoad() { super.viewDidLoad() // 出力先の枠線の設定 outputView.layer.borderWidth = 1.0 outputView.layer.borderColor = UIColor.red.cgColor // 枠に対して文字の大きさを自動調節 inputCharacter.adjustsFontSizeToFitWidth = true rubyCharacter.adjustsFontSizeToFitWidth = true outputCharacter.adjustsFontSizeToFitWidth = true } /// ルビ変換ボタン押下時、ルビを画面に表示するメソッド /// /// - Parameter sender: UIButton @IBAction func convertRuby(_ sender: UIButton) { guard let inputText = inputCharacter.text else { return } if inputText != "" { // レスポンスを画面に返す HttpRequest(sentence: inputText, completion: {(responseData: ResponseData) -> Void in self.outputCharacter.text = inputText self.rubyCharacter.text = responseData.converted }) } } /// ひらがな化APIにリクエスト送信する /// /// - Parameters: /// - sentence: 入力された文字 /// - completion: リクエスト送信完了 func HttpRequest(sentence: String, completion: @escaping (ResponseData)->Void) { // リクエスト情報 let url = "https://labs.goo.ne.jp/api/hiragana" let headers: HTTPHeaders = [ "Contenttype": "application/json" ] let appKey : RubyConversionAppKeys = RubyConversionAppKeys() let parameters:[String: Any] = [ "app_id":"b9596802da072f39033d9bb35a9338e5e9dcbec54c03e9061d6df38d00449967", "sentence": sentence, "output_type": "hiragana" ] // リクエスト送信 Alamofire.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: headers).responseJSON { response in guard let jsonData = response.data else { print("response err") return } let responseData = try! JSONDecoder().decode(ResponseData.self, from: jsonData) completion(responseData) print(responseData) } } // 画面タップ時、キーボードを閉じる /// - Parameter sender: Any @IBAction func downKeyboard(_ sender: Any) { inputCharacter.endEditing(true) } }

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

nakasho_dev

2019/11/04 13:12

タグにテスト駆動開発とありますがテスト駆動開発がしたいのでしょうか、それともUnitTestが書きたいのでしょうか。また、「いまいちピンこない」というのではなく、まずはご自身がどのように調べてどこまでわかってどこが分からないかを伝えたほうが具体的なアドバイスをしてもらえるかもしれません。
imasyou718

2019/11/05 05:53

UnitTestが書きたいです!
guest

回答2

0

提示されたプロダクトコードで最もテストしたい価値は、HttpRequest関数であると判断し、その関数で「Alamofireを使い通信し、返却されたJSONデータをクラスに変更して取得できること」のテストコードを書いてみました。

Alamofireは使ったことがないのですが非常にテストしづらいModuleだと感じました。
テストするためには「Method Swizzling」という技術を使い通信をテスト用データに差し替える必要があります。

上記のURLを参考にテストプロジェクト側に以下を作成しました。
参考サイトと違うのは最後の方のresponseの中身を今回のプロジェクト用のJSONデータに変更しています。

Swift

1import Foundation 2 3public class MockURLProtocol: URLProtocol { 4 5 // 引数のURLRequestを処理できる場合はtrue 6 override open class func canInit(with request:URLRequest) -> Bool { 7 return true 8 } 9 10 // URLRequestの修正が必要でなければそのまま返す。 11 override open class func canonicalRequest(for request: URLRequest) -> URLRequest { 12 return request 13 } 14 15 // 通信開始時に呼ばれるメソッド、ここに通信のモックを実装します。 16 override open func startLoading() { 17 let delay: Double = 1.0 // 通信に1秒かかるモック 18 DispatchQueue.global().asyncAfter(deadline: .now() + delay) { 19 self.client?.urlProtocol(self, didLoad: self.response!) // 結果を返す 20 self.client?.urlProtocolDidFinishLoading(self) // 通信が終了したことを伝える 21 22 // エラー時のハンドリングもこちらで可能です。 23 // self.client?.urlProtocol(self, didFailWithError: error) 24 } 25 } 26 27 // 通信停止時に呼ばれるメソッド 28 override open func stopLoading() { 29 } 30 31 private var response: Data? { 32 // URLなどでパターンマッチングすることで結果を切り替えることも出来る 33 // self.request.url 34 let json = "{\"converted\":\"てすとくどうかいはつ\",\"output_type\":\"hiragana\",\"request_id\":\"request-id\"}" 35 return json.data(using: .utf8) 36 } 37} 38

そして「Method Swizzling」を実現するためのコードを実装します。
こちらは参考サイトと同じです。

Swift

1import Foundation 2 3public extension URLSessionConfiguration { 4 5 // .defaultをモック用と入れ替えるメソッド 6 public class func setupMockDefaultSessionConfiguration() { 7 let defaultSessionConfiguration = class_getClassMethod(URLSessionConfiguration.self, #selector(getter: URLSessionConfiguration.default))! 8 let swizzledDefaultSessionConfiguration = class_getClassMethod(URLSessionConfiguration.self, #selector(getter: URLSessionConfiguration.mock))! 9 method_exchangeImplementations(defaultSessionConfiguration, swizzledDefaultSessionConfiguration) 10 } 11 12 // .defaultと入れ替えるプロパティ変数 13 @objc private dynamic class var mock: URLSessionConfiguration { 14 let configuration = self.mock 15 configuration.protocolClasses?.insert(MockURLProtocol.self, at: 0) 16 URLProtocol.registerClass(MockURLProtocol.self) 17 return configuration 18 } 19}

これでAlamofireでrequestした際に実際のサーバにはアクセスせずに、MockURLProtocolで実行した内容がResponseとして返却される準備ができました。

そこで以下のTestを作成します。

Swift

1@testable import プロジェクト名 2import XCTest 3 4class AdaptiveCardSwiftSampleTests: XCTestCase { 5 private var subject: ViewController! 6 7 override func setUp() { 8 subject = ViewController() 9 // スタブに置き換え 10 URLSessionConfiguration.setupMockDefaultSessionConfiguration() 11 } 12 13 func testExample() { 14 //非同期の待ち時間用 15 let expectation = XCTestExpectation(description: "Hoge") 16 17 subject.HttpRequest(sentence: "テスト駆動開発") { (result) in 18 //期待する返却値(MockURLProtocolで指定しているJSONデータ) 19 XCTAssertEqual(result.request_id, "request-id") 20 XCTAssertEqual(result.output_type, "hiragana") 21 XCTAssertEqual(result.converted, "てすとくどうかいはつ") 22 expectation.fulfill() 23 } 24 wait(for: [expectation], timeout: 3) 25 } 26 27} 28

今回はサーバに通信し返却されたJSONファイルをクラスに変換するというテストを行いました。
しかし、他にもテストしないといけないことがたくさんあります。
例えば以下のようなものがあります。

  • ルビ変換ボタン押下時にサーバへリクエストを投げているか
  • 正しくサーバにパラメータを渡しているか
  • サーバとの通信が失敗した場合
  • サーバから返ってきたJSONデータが想定と異なる場合
  • サーバから返ってきたデータを正しく表示できているか

ちゃんと何を確認したいか目的を持ってテストを書きましょう。
これらをテストするために色々なテストデータなどを用意する必要があります。
テスト毎にMockURLProtocolのようなクラスを作るのは大変です。
テストしたいデータだけ書き換えてスタブが作れるようにした方が楽です。
参考サイトでは[Mockingjay](https://github.com/kylef/Mockingjay)を紹介しているので、それを使うという選択肢もあります。

そして、テストがやりやすい設計にすることも大事です。
今回は簡易的に作りました。しかしテストのしやすい設計のためには、プロダクトにあったアーキテクチャを採用したり、DI(依存性の注入)やRepositoryパターンなどを実践すると良いと考えます。
iOSの設計・テストについては以下が参考になります。

投稿2019/11/06 16:51

nakasho_dev

総合スコア2655

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

imasyou718

2019/11/07 05:43

nakasho_dev様とても丁寧なご回答ありがとうございます。 すいません初学者なのでとても変な質問だと思いますが 3番目の方のコードはテストに書き込むことはわかるのですが 上二つはどこにコードを記載すればよろしいでしょうか? 現状自分が質問させてもらっているViewController.Swiftとテストを記載している RubyAppTest.Swiftというファイルのみとなっております。
imasyou718

2019/11/07 05:44

どちらに記載すればよろしいですか?
nakasho_dev

2019/11/07 11:30

RubyAppTest.Swiftのファイル内に並べて記載しても構いません。その際はimport文が重複しないように気をつけてください。また、本来であればテストプロジェクト側に別ファイルで保存したほうが良いです。
imasyou718

2019/11/08 05:08

エラーばかりが出てどう記述すればいいかわからないですすいません。
guest

0

ベストアンサー

ユニットテストはテストが正しく行われればそれでOKなのでそんなに難しく考えないでください。
こんなのでいいです。

swift

1 2// Equatableにする 3struct User: Equatable { 4 let name: String 5 let age: Int 6}

swift

1func testDecodeJSON() { 2 3 // APIから取得したものをそのまま張り付ける 4 let jsonStr = #""" 5{ 6 "name": "Name", 7 "age": 20 8} 9"""# 10 11 // 上のJSONから生成されることが期待されるUserのインスタンス 12 let expected = User(name: "Name", age: 20) 13 14 do { 15 let decoded = try JSONDecoder().decode(User.self, from: jsonStr.data(using: .utf8)!) 16 XCTAssertEqual(expected, decoded) 17 } 18 catch { 19 XCTFail("Can not decode JSON") 20 } 21} 22 23// 注意!!! エラーや例外がHttpRequest(sentence:completion:)メソッドに吸収されているため正しいテストは不可能 24// また、テストのたびにサーバーにアクセスする必要がある。 25func testSendAPI() { 26 27 let vc = ViewController() 28 29 let exp = expectation(description: "Hoge") 30 31 vc.HttpRequest(sentence: "Word") { _ in 32 exp.fulfill() 33 } 34 35 wait(for: [exp], timeout: 10.0) 36}

通信部分をテストするにはDI(Dependency Injection)やモック(mock)、スタブ(stub)などテストを容易に行えるようなコードを書く必要性が出てきます。

投稿2019/11/05 03:39

編集2019/11/05 06:24
MasakiHori

総合スコア3391

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

imasyou718

2019/11/05 06:19

func testSendAPI() { let vc = ViewController() let expectation = expectation(description: "Hoge") vc.HttpRequest(sentence: "Word") { _ in expectation.fulfill() } wait(for: [expectation], timeout: 10.0) } こちらのテストをすると let expectation = expectation(description: "Hoge") ここの部分がVariable used within its own initial value とエラーが出るのですが何が悪いんですかね?
MasakiHori

2019/11/05 06:23 編集

識別子がだめですね。書き直します。 #書き直しました。 (expectationをexpにした)
MasakiHori

2019/11/05 06:24

teratailバグってますね。 どうしよう....
imasyou718

2019/11/05 06:26

func testSendAPI() { let vc = ViewController() let exo = expectation(description: "Hoge") vc.HttpRequest(sentence: "Word") { _ in exo.fulfill() } wait(for: [exo], timeout: 10.0) } 少し書き直しましたができました! これはHttpRequestの通信の処理のテストという認識であってますか?
MasakiHori

2019/11/05 06:46

completionが呼ばれたら成功でそれ以外は失敗するテストです。それ以上でも以下でもありません。
nakasho_dev

2019/11/05 16:30

DI、モック、Stubについて触れているので、UnitTestの書き方については知っているのかと思いますが、テスト初心者に対してこれをUnitTestというのは非常に不親切ではないでしょうか。 Testの度にサーバにアクセスする必要があるのであればIntegrationTestになってしまうのではないでしょうか。 対向のサーバの状況や通信の不具合など様々な要因でテストが失敗することがあり、とてもUnitをTestしているとは思えません。
MasakiHori

2019/11/06 03:19

コメントにテストが不完全であること、正しいテストを作成するには様々な知識やコードの変更が必要なことを明記していますが、それでは問題があるということでしょうか? 問題があると思われるのでしたら、質問者さんのために正しい回答を行ってもらえませんでしょうか?
nakasho_dev

2019/11/06 17:08

問題あります。そもそも何が不完全であるかも説明していません。 提示されたテストで良いのか、と思われたらテストについて知りたい質問者の方が不幸になります。 1つ目のテストはJSONDecoderのテストとなってしまっておりプロダクトコードのテストとなりえません。用意したデータクラスがJSONと型があっているかのテストにはなるかもしれませんが、私が提示したテストでカバーされますし、テストとしての価値がほとんどありません。 2つ目のテストはリクエスト投げてエラーにならないことだけをテストしているとのことですが、レスポンスのステータスコードやボディの中身を検証したりするテストを書いたときにこのテストは無意味になりますし、そもそもそれを検証する価値があるか疑問です。 テストで何を検証すべきかについて説明せず、ただXCTestCaseをエラーが出ないように書くだけの説明ではテストを知りたい質問者に対して不親切だと考えます。 確かにHTTP通信を含むテストを書くのは面倒です。それでも質問者に間違った知識を与えて良いとは思えません。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.36%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問