Galapagos Tech Blog

株式会社ガラパゴスのメンバーによる技術ブログです。

テスト分析とテスト設計勉強会に参加しました!(前編)

こんにちは!テストチームとのの(TW:@tono2587)です。
先日2017/02/03、「テスト分析とテスト設計勉強会」に参加してきました!
同日〜翌日のJaSSTには参加できなかったのですが、この勉強会はわたしにとってたいへん学びがありましたので、参加レポートとしてまとめたいと思います。

f:id:glpgsinc:20170210160415p:plain ふむふむふむふむ

■「テスト分析とテスト設計勉強会」

2017年2月3日(金)@日本大学駿河台キャンパス
め、めずらしいつくりの建物でしたよ(迷ってないよべつに全然)

https://connpass.com/event/47938/connpass.com

教室がけっこう埋まっていたように見えたので、参加者は多く感じました。

■参加前の気持ち

動機や興味、自分のなかでの課題や知りたかったことのイメージです。
・業務でテスト設計することが増えてきた
・あんまり上手く設計できていない気がする(レビュー戻り多い)
・絶対もっとなんかいい方法があるはずだ
・「初心者向け」←わたしのことか。呼ばれている

■勉強会の内容(前編)

□秋山さん:JSTQBのテストプロセスについて
・「プロセス」とは何か
 相互関係のある活動のセット。(活動は入力を出力に変換すること)
活動の例:<(入力)お湯にパスタを入れる>−[(活動)煮る]−<(出力)柔らかくする>
プロセスは、このような[活動]のセット
JSTQBのいう「テストプロセス」とは何か
 基本的には、
 [①テスト計画とコントロール]−[②テスト分析と設計]−[③テスト実装と実行]−[④終了基準の評価と報告]−[⑤テスト終了作業]
によって構成される。
・これらの①〜⑤のテスト活動は、開発と整合している必要がある。
 システムテストレベルでいうと、
プロジェクト計画と同時にシステムテスト計画を始め、
要求仕様→システムおよびアーキテクチャ設計仕様→コンポーネント設計仕様と並行してシステムテスト分析および設計をおこない、
…以降も開発の活動と並行して行う。

・テスト分析、テスト設計とは?
 ・テスト分析とは:テストベースを分析すること。テスト条件を識別する。
入力:テストベース…仕様書、議事録などテストをつくるもとになるもの。これはドキュメントで、レビュー済みの整ったものをつかうのがよい。
活動:テストベースとテスト目的を分析する…ユーザーはなにをしたいのか(目的)など
出力:識別されたテスト条件…いろんな粒度で定義することが望ましい。画面(粗)、画面の部品(細)、など

 ・テスト設計とは:
入力:テスト条件
活動1:テスト条件をどうやって網羅していくかなどを決めていく
活動2:テストケースをつくる。どういうテスティングで確認するのか、どういう環境でどういう期待値で…
出力:テストケース

・構造化分析と構造化設計について
 開発と同じでテストも、いきなりコードを書いていたが、ループするところとかは構造化しましょう、となっていった

『構造化分析とシステム仕様』トム・デマルコ 著 によると、
 ・分析とは、行動をとる前に実施する、問題の調査である
 分析の最も重要な生産物は「仕様書」である。分析をすることで具体的にしていき、それが「仕様」になる
 分析の後に続く行動とはそのシステムを構築することである。

 ・設計とは、手続き的な部分(順序)と、階層的な部分を決めて、構築することである
 トップダウンで分割して順番と階層を決めて、ものをつくっていく

ーー質疑応答より
・「テストは条件次第」
 テスト対象に従って、どんな要求があるのかを確認して決めていく。
 →先にものさしを決める。

・テスト条件が様々な粒度で定義することが望ましい理由は?
 人によって(立場などによって)みているものが違うので、お互いに合意をとるためにも必要になるから。
 (共通言語みたいなもの、というイメージでしっくりきました。)

□藤沢さん:テスト分析・設計について、釈然としないところ

・テスト分析も、設計も、要するにどんなことを言うのか?→わけなくてよくない?
・仕様書のコピペはばぜだめなのか
→仕様書に書いていないことをテストに入れるべきだ
・「テスト」も「分析」も曖昧だからくっつけてもわかんないのでは?
・「分析」ってなんだ?…理解できるようにわけることなんじゃないか。
*分析 のアウトプットが曖昧ではないか?どこまでやれば「分析」が終わっているのかわからない
・分析しないと困ることは?
 ・分析しないと仕様が曖昧だから、必要なテストが漏れる
 ・なにをテストしたのかわからないことになる

・藤沢さんのいまの感じの定義
分析:グループにわけてわかりやすくすること
設計:テストケースの元ネタをつくること
実装:元ネタ(設計)からテストケースを作成すること

・テスト設計をしないと困ることは?
 ・仕様書のコピペでテストケースをつくると、なんでこのテストケースなのかわかりづらいし、説明が不足する
 ・なんでこのテストで十分なのか説明できない

・分析と設計、どっちかが欠けるとかはなく、セットなのでは???
・どこからどこまでが分析/設計なの?

・結局、分析/設計はなにがしたいかようわからん!!

■前編までの気持ち

秋山さんのお話で、教科書的な「テスト分析」と「テスト設計」を知ることができました。
テスト分析やテスト設計といった言葉の意味自体ぼんやりとしていたので、お話を聞いて形がつかめてきた感じがします。
それをふまえて自分の仕事を振り返ると、テスト分析が全然できていないこと、テスト設計も上手くいっていないことがわかりました。
だからレビューの戻りが多いし、時間もかかるのだと思いました。

なんとなくつかんできたと思っていたところでしたが、藤沢さんのお話にはなんだか説得力があって、
「どこまでが分析なんだ」みたいなところはとくに(ああ…たしかに…??)と納得してしまい
(((分析)))や(((設計)))がまたもやもやしたものに…
このお話があったお陰でこのあとすごくスッと話が頭に入ってきたのですが、この時点では正直もやもやした理解しかありませんでした。

後編へつづく

UITextViewを画面いっぱいのサイズにする

御機嫌よう、ガラパゴスのおとめです。

今日は、UITextViewを、オートレイアウトを使って、キーボードを除いた画面いっぱいのサイズにしてみようと思います。UIScrollViewに入れてキーボード表示時にスクロールさせるのではなく、使える広さは全部UITextViewにします。

この記事は、Swift 3.0とXcode 8.2.1を対象としています。

Viewの作成

適当にプロジェクトを作成して画面を作ります。よくある感じのUITableViewのある画面で追加ボタンを押すと入力画面を表示するイメージでViewを作成し、Text Viewを適当にドロップしたら、Add New Constraintsで上下左右を0に設定します。

f:id:glpgsinc:20170124183807p:plain

キーボードの通知を受け取る

さて、UITextViewは画面いっぱいにしていますが、このままでは下部がキーボードに隠れてしまいます。そこで、キーボードが表示された時に大きさを調整する必要があります。

キーボードに関連する通知は、NotificationCenterを通じて取得することができます。キーボード関連の通知は以下のようになっています。

通知 呼ばれるタイミング
.UIKeyboardWillShow キーボードが表示される時
.UIKeyboardDidShow キーボードが表示された後
.UIKeyboardWillChangeFrame キーボードがViewの位置やサイズを変更する時
.UIKeyboardDidChangeFrame キーボードがViewの位置やサイズを変更した後
.UIKeyboardWillHide キーボードが非表示になる時
.UIKeyboardDidHide キーボードが非表示になった後

通知を処理するには、NotificationCenter.addObserver(...)で、通知とそれを受け取る関数を指定します。ここではViewが表示される時に呼び出します。

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    let notificationCenter = NotificationCenter.default
    notificationCenter.addObserver(self, selector: #selector(self.keyboardWillShow(notification:)), name: .UIKeyboardWillShow, object: nil)
    notificationCenter.addObserver(self, selector: #selector(self.keyboardWillChangeFrame(notification:)), name: .UIKeyboardWillChangeFrame, object: nil)
    notificationCenter.addObserver(self, selector: #selector(self.keyboardWillHide(notification:)), name: .UIKeyboardWillHide, object: nil)
}

もちろん.addObserver(...)したからにはお行儀よく.removeObserver(...)しましょう。Viewが表示される時に.addしたのですから、その反対といえばViewが消える時ですね。nameを指定しなければ、全ての通知を解除します。

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    let notificationCenter = NotificationCenter.default
    notificationCenter.removeObserver(self)
}

UITextViewの制約を変更する

今回はオートレイアウトを使っていますので、キーボードが表示される時に、下の余白をキーボードの高さに合わせることで実現します。

まず、Assistant Editorを表示して、ドキュメントアウトラインのConstraintsから.bottomをCtrlキーを押しながらEditorにドロップします。

@IBOutlet weak var todoConstraint: NSLayoutConstraint!

これでコードから制約を変更することができるようになります。では関数を実装しましょう。キーボードの高さを取得して、制約にその値を設定します。レイアウトを変更しますので、最後にUIView.layoutIfNeeded()を呼び出します。

func keyboardWillShow(notification: Notification) {
    let rect = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
    let duration: TimeInterval = notification.userInfo?[UIKeyboardAnimationDurationUserInfoKey] as! Double
    self.todoConstraint.constant = rect.height
    UIView.animate(withDuration: duration, animations: { () -> Void in
        self.view.layoutIfNeeded()
    })
}

キーボードの種類を切り替えたり変換候補が最初に表示される場合などでキーボードの高さが変わる場合でも、.UIKeyboardWillShow通知が発火します。

反対にキーボードが非表示になる際には、制約に0を入れます。

では実行してみましょう。UITextViewをタップしてみると、(スクリーンショットでは分かりにくいですが)キーボードを除いた高さになりましたね?

f:id:glpgsinc:20170124184056p:plain

さいごに

最近Swiftに入門したのですが、入門前はキーボード表示でViewのサイズを変更するなんて簡単というか自動でできそう? くらいの勢いでした。こうして記事にして見ると実際簡単そうですが、実は意外とハマったり?

さて、ガラパゴスでは人類の進化に貢献したいエンジニアを大絶賛超募集しています。皆様の応募をお待ちしています。

www.glpgs.com

では、御機嫌よう。

この記事は業務の一環として業務時間を使って書きました

Swift3.0でCloud APIを使ってOCRをしてみる

御機嫌よう、最近Swiftな世界に入門しているガラパゴスのおとめです。

今回は、SwiftからGoogle Cloud Vision APIとMicrsoft Cognitive Computer Vision APIのテキスト検出(OCR)を使ってみようと思います。

なお、この記事はSwift 3.0とXcode 8.2.1を対象にしています。

プロジェクトの作成

まず適当にプロジェクトを作ります。また、今回はAlamfireSwiftyJSONを使っていますが、これらのライブラリはCocoaPodsでインストール・管理しました(インストール方法などは省略します)。

ではサクッとViewを作ってしまいます。今回はカメラロールから選んだ画像を上に、検出したテキストを下に表示したいと思います。

カメラロールを表示するボタンは何でもいいのですが、ここではNavigation Barにおいてみました。 次に画像とテキストの表示ですが、わかりやすく半々に配置するために、Vertical Stack Viewを使ってみます。ぽいっとドラッグしたら、Viewをいっぱいに使うために、Add New Constraintsで上下左右を0に設定します。

f:id:glpgsinc:20170120161952p:plain

画像とテキストを半々に表示するために、Stack ViewDistributionFill Equallyにします。

f:id:glpgsinc:20170120162041p:plain

Vertical Stack Viewの設定が終わったら、Image ViewLabelを放り込みます。すると以下のように、自動的に半々になりましたね?

f:id:glpgsinc:20170120162141p:plain

Labelは複数行表示できるようにするために、Lines0を設定しておきます。

f:id:glpgsinc:20170120162222p:plain

これらの要素の配置が終わったら、Assistant Editorを表示して、Ctrlキーを押しながらEditorにドラッグし、@IBAction@IBOutletを設定しておきます。

// 選択した画像の表示
@IBOutlet weak var image: UIImageView!
// 検出したテキストの表示
@IBOutlet weak var text: UILabel!
// カメラロールを表示するボタンを押した時の処理
@IBAction func selectImage(_ sender: Any) {
}

カメラロールを使えるようにする

iOSシミュレータではカメラが使えないので、今回はカメラロールから選ぶことにします。iOSシミュレータを起動すると数枚の風景写真が入っていますが、適当な画像をシミュレータのウインドウにドロップするとカメラロールに登録されて選択できるようになります。

カメラロールにアクセスするには、info.plistPrivacy - Photo Library Usage Descriptionというキーを設定する必要があります(キーは自分で書かずにドロップダウンから選択します)。値にはアプリから初めてカメラロールを使うときに表示されるアクセス許可のダイアログに表示するメッセージを入れます。

f:id:glpgsinc:20170120162317p:plain

では実際にカメラロールを表示する処理を書いていきましょう。

@IBAction func selectImage(_ sender: Any) {
    if UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.photoLibrary) {
        // 前回検出したテキストをクリアする
        text.text = ""
        // 写真を選ぶ
        let picker = UIImagePickerController()
        picker.sourceType = .photoLibrary
        // 編集はしない
        picker.allowsEditing = false
        // カメラロールを表示
        picker.delegate = self
        present(picker, animated: true, completion: nil)
    }
}

次に、画像を選択した時の処理を書きます。まずはカメラロールからのメッセージを受け取れるようにするために、UIImagePickerControllerDelegateUINavigationControllerDelegateを継承します。

class ViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {

画像が選択された時にはimagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any])が呼ばれます。ここでは、選択された画像を表示して、 APIを呼び出します。

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
    let selected = info[UIImagePickerControllerOriginalImage] as! UIImage
    // 選択した画像をUIImageViewに表示する際にアスペクト比を維持する
    image.contentMode = .scaleAspectFit
    image.image = selected
    // 処理中表示にする
    text.text = "(文字を検出しています...)"
    // カメラロール非表示
    dismiss(animated: true, completion: nil)
    // Vision APIを使う
    detectText()
}

では、API呼び出しに取り掛かりましょう。

テキスト検出する

はじめに書いたように、GoogleMicrosoftの両方を試してみます。本来ならUIで切り替えられるようにしたりするべきなのですが、今回はちょっと横着して、コードで切り替えます。まずはこんな感じで使用するAPIを定義します。

enum DetectionMethod: String {
    case GOOGLE
    case MS
}
var method = DetectionMethod.GOOGLE

設定に応じてメソッドを切り替えるようにします。

func detectText() {
    switch method {
    case DetectionMethod.GOOGLE:
        detectTextGoogle()
    case DetectionMethod.MS:
        detectTextMs()
    }
}    

では、それぞれのAPIを準備、実装していきましょう。

Google Cloud Vision APIの準備

Google Cloud PlatformのコンソールからAPI Managerを開き、ライブラリからVision APIを有効にします(まだサインアップしていない方は無料トライアルを開始します)。 次に認証情報からAPIキーを作成し、キーの制限でiOSアプリを選択し、先に作成したiOSプロジェクトのBundle Identifierを登録します。 作成したAPIキーはSwiftコードで使いますが、直接書いてGithubなどにアップしてしまうと請求が大変なことになる場合がありますので取り扱いに注意してください。

Google Cloud Vision API呼び出しの実装

APIの公式な仕様はこちら

今回はPNGのみを対象にします。他のフォーマットを使いたい場合、例えばJpegならUIImageJPEGRepresentationなどを使います。 では仕様に沿ってリクエストを組み立てていきましょう。画像をbase64エンコードして、テキスト検出にはTEXT_DETECTIONを指定します。 また、リクエストヘッダに、キーの制限で指定したBundle Identifierを、URLにAPIキーを指定します。 ところで、Vision APIで指定できる画像にはサイズの制限があるのですが、ここではとりあえずリサイズなどはしないことにします(カメラで撮った写真をそのまま送るとサイズ制限をオーバーしますのでご注意ください)。

リクエストの処理には、はじめに書いたようにAlamofireを使います。リクエスト形式はJSONですので、encodingJSONEncoding.defaultを、またリクエストヘッダを送りますので、headersを指定します。そして、正常なレスポンスだけを処理したいので、.validate(statusCode: 200..<300)のように検証し、レスポンスもJSONですので.responseJSONとします。

func detectTextGoogle() {
    // 画像はbase64する
    // 仮にPNGのみ対象
    if let base64image = UIImagePNGRepresentation(image.image!)?.base64EncodedString() {
        // リクエストの作成
        // 文字検出をしたいのでtypeにはTEXT_DETECTIONを指定する
        // 画像サイズの制限があるので本当は大きすぎたらリサイズしたりする必要がある
        let request: Parameters = [
            "requests": [
                "image": [
                    "content": base64image
                ],
                "features": [
                    [
                        "type": "TEXT_DETECTION",
                        "maxResults": 1
                    ]
                ]
            ]
        ]
        // Google Cloud PlatformのAPI Managerでキーを制限している場合、リクエストヘッダのX-Ios-Bundle-Identifierに指定した値を入れる
        let httpHeader: HTTPHeaders = [
            "Content-Type": "application/json",
            "X-Ios-Bundle-Identifier": Bundle.main.bundleIdentifier ?? ""
        ]
        // googleApiKeyにGoogle Cloud PlatformのAPI Managerで取得したAPIキーを入れる
        Alamofire.request("https://vision.googleapis.com/v1/images:annotate?key=\(googleApiKey)", method: .post, parameters: request, encoding: JSONEncoding.default, headers: httpHeader).validate(statusCode: 200..<300).responseJSON { response in
            // レスポンスの処理
            self.googleResult(response: response)
        }
    }
}

さて、Vision APIの戻りを文字列に戻します。結果はJSONですので、SwiftyJSONでデコードし、["responses"][0]["textAnnotations"]["descriptsion"]に検出した文字が入っていますので、取り出してLabelに貼り付けます。

func googleResult(response: DataResponse<Any>) {
    guard let result = response.result.value else {
        // レスポンスが空っぽだったりしたら終了
        return
    }
    let json = JSON(result)
    let annotations: JSON = json["responses"][0]["textAnnotations"]
    var detectedText: String = ""
    // 結果からdescriptionを取り出して一つの文字列にする
    annotations.forEach { (_, annotation) in
        detectedText += annotation["description"].string!
    }
    // 結果を表示する
    text.text = detectedText
}

Microsoft Cognitive Computer Vision APIの準備

Microsoft Cognitive Services Computer Vision APIのコンソールからComputer Vision - Previewをサブスクライブします(まだサインアップしていない方はGet stated for freeします)。 するとキーが表示されます。このキーをSwiftコードで使います。Google Cloudと違い、こちらは請求情報を登録しなければ有料プランになる前に自動で止まりますが、やはりキーが流出すると大変なことになる場合がありますので取り扱いに注意しましょう。

Microsoft Cognitive Computer Vision APIの実装

APIの公式な仕様はこちら

(2017/01/17からエンドポイントが変わっています。旧エンドポイントは4/15で廃止になります。この記事では新エンドポイントを使用しています)

こちらもPNGのみを対象にします。MicrosoftComputer Vision APIは画像をoctet-streamで直接送り、リクエストヘッダでAPIキーを指定します。Alamofireバイナリを送信するには、upload(...)を使います。Cognitive Vision APIにも画像サイズの制限がありますが、ここでもリサイズなどは省略しています。

func detectTextMs() {
    // 仮にPNGのみ対象
    // 画像サイズの制限があるので本当は大きすぎたらリサイズしたりする必要がある
    let imageData = UIImagePNGRepresentation(image.image!)!
    // Ocp-Apim-Subscription-KeyにCognitive Serviceで取得したAPIキーを入れる
    let httpHeader: HTTPHeaders = [
        "Content-Type": "application/octet-stream",
        "Ocp-Apim-Subscription-Key": msApiKey
    ]
    // 言語に日本語を指定して向きの検出も行うようにする
    Alamofire.upload(imageData, to: "https://westus.api.cognitive.microsoft.com/vision/v1.0/ocr?language=ja&detectOrientation=true", method: .post, headers: httpHeader).validate(statusCode: 200..<300).responseJSON { response in
        self.msResult(response: response)
    }
}

さて、こちらもAPIの戻りを文字列に戻しましょう。やはり結果はJSONで、["regions"]["lines"]["words"]["text"]に検出した文字が入っています。

func msResult(response: DataResponse<Any>) {
    guard let result = response.result.value else {
        // レスポンスが空っぽだったりしたら終了
        return
    }
    let json = JSON(result)
    let regions: JSON = json["regions"]
    var detectedText: String = ""
    // 結果からテキストを取り出して一つの文字列にする
    regions.forEach { (_, region) in
        let lines = region["lines"]
        lines.forEach { (_, line) in
            let words = line["words"]
            words.forEach { (_, word) in
                detectedText += word["text"].string!
            }
        }
    }
    text.text = detectedText
}

実行してみる

では実行してみましょう。今回は適当な画像として、ガラパゴスWebサイトの企業理念のページスクリーンショットを使ってみます。シミュレータに画像をドロップしたらカメラロールから画像を選択してみましょう。

Googleのテキスト検出結果はこちら。

f:id:glpgsinc:20170120163536p:plain

(結果が二重に表示されているのはご愛嬌ですね)

そして、Microsoftの結果はこちら。

f:id:glpgsinc:20170120163609p:plain

うまくテキストが検出できましたね。

最後に

ごく簡単なテキスト検出アプリを作ってみました。テキスト以外にも顔検出やラベル付けなどもできますのでお試しあれ。

また、iOSアプリは、もうずっと以前にObjective-Cを書いたことはあったのですが、Swiftの方が書きやすいなという印象がありました。

さて、ガラパゴスでは、アプリもサーバもインフラもできちゃうエンジニアや、そのうちの一つだけでもできちゃうエンジニアを大絶賛超募集しています。皆様の応募をお待ちしています。

www.glpgs.com

では、ご機嫌よう。

この記事は業務の一環として業務時間を使って書きました