中年エンジニアの開発と生活の日々

中年エンジニアがソフトウェア開発や日々の生活で得た知見の備忘録

Turnip の RSpec で関数を定義して呼び出すには

現在、Turnip と RSpec を組み合わせて Ruby でテストスクリプトを作成しているのですが、共通な処理を関数にまとめる方法についてまとめてみました。

Turnip については以下のサイトが詳しいです

Rubyist Magazine - エンドツーエンドテストの自動化は Cucumber から Turnip へ

Turnipでは Feature ファイルと Stepファイルに分かれていて、Step ファイルは RSpec でテストスクリプトを記述していきます。

RSpecについては以下のサイトが詳しいです relishapp.com

テストスクリプトを書いていると実感するのですが、同じ処理をパラメータを変えて繰り返すことが多いです。そのため、同じ処理を関数として定義して呼び出さないと見通しが悪いですし、メンテナンスも大変です。ところが、通常の Ruby のやり方ではうまくいきません。

たとえば、Stepファイルに以下のようにして関数を定義して、Step の中で呼びだしてみます。

  def putsHogehoge
    puts "hogehoge"
  end

step "Step1" do
  putsHogehoge
end

通常の Ruby でしたら、"Step1" の中で putsHogehoge がコールされてコンソールに “hogehgoe” と表示されますが、Turnip で Feature ファイルからこのStep1を呼び出すと以下のようなエラーメッセージが表示されます。

Failures:

  1) Test Sample Given GetParam
     Failure/Error: putHogehoge

     NameError:
       undefined local variable or method `putsHogehoge' for #<RSpec::ExampleGroups::Test::Sample:0x007fb676a58ac8>
     # spec/steps/sample_steps.rb:13:in `block in <top (required)>'
     # ./vendor/bundle/ruby/2.3.0/gems/turnip-2.1.1/lib/turnip/execute.rb:25:in `step'
     # ./spec/features/login.feature:5:in `run_step'
     # ./vendor/bundle/ruby/2.3.0/gems/turnip-2.1.1/lib/turnip/rspec.rb:44:in `instance_eval'
     # ./vendor/bundle/ruby/2.3.0/gems/turnip-2.1.1/lib/turnip/rspec.rb:44:in `run_step'
     # ./spec/features/sample.feature:6:in `block (4 levels) in run_feature'
     # ./spec/features/sample.feature:5:in `each'
     # ./spec/features/sample.feature:5:in `block (3 levels) in run_feature'
     # ./spec/features/sample.feature:5:in `Step1'

Step1 は Turnip のフレームワークから呼び出されているようで、すぐ上に定義されている putsHogehoge という関数が見えないようです。

解決策

以下のサイトを参考に Turnip で拡張されている箇所のつじつまを合わせたら、Helper Method を呼び出すことができました。 relishapp.com

  • Spec フォルダに helpers.rb を作成します。別に stepsディレクトリに作成してもよい気もしますが、Step ではなく、Helper なので一つ上のディレクトリに格納しています。
  • helpers.rb に Helpers Module を定義して、その中に関数を定義します
# coding: utf-8

module Helpers
  def putsHogehoge
    puts "hogehgoe"
  end
end
  • spec_helper.rb に require ‘./spec/helpers’ を追加します。
require 'rubygems'
require 'selenium-webdriver'
require 'appium_lib'
require 'bundler/setup'
require './spec/helpers'  # <- 追加
  • spec_helper.rb の RSpec configure に c.include Helpers を追加。これにより、Helpers Module が各 Step でロードされます。
RSpec.configure do |c|
  c.include Helpers # <- 追加

  c.before(:each) {
    @driver = Appium::Driver.new(desired_caps).start_driver
    @driver.manage.timeouts.implicit_wait = 5
    Appium.promote_appium_methods Object
  }
  c.after(:each) {
    @driver.quit
  }
end

こうすることによって、Turnip の Step ファイルから自分で定義した関数を呼び出せるようになりました。

Appium のテストスクリプトで長めの Sleep を入れる

本日は小ネタです。

内部の実装上の都合で、Appium のテストスクリプト (Rubyで書いてます) の中で長めの Wait を入れる必要がありました。テストスクリプトの中で

  sleep 90

とか、長めの Sleep を入れたところ、テストスクリプトの実行時に以下のようなエラーメッセージが出ました。

     Failure/Error: @driver.quit
     
     Selenium::WebDriver::Error::NoSuchDriverError:
       A session is either terminated or not started

なにもしてないのに、なぜ故に@driver.quit がこけるのだ。すでにセッションが終了しているだと…

エラーの原因

Appium のログを見たところ以下のような記載がありました。

[BaseDriver] Shutting down because we waited 60 seconds for a command
[Appium] Closing session, cause was 'New Command Timeout of 60 seconds expired. Try customizing the timeout using the 'newCommandTimeout' desired capability'

どうも、コマンドのタイムアウトが60秒でそれを超えるとセッションが閉じるようです。何もしていないのに @driver.quit が失敗したのではなく、何もしなかったので @driver.quit が失敗してしまったというわけです。

解決策

ログの通りに、appium 起動時の設定オプションの desired capability に newCommandTimeout=120 と設定したら、90秒の Sleep を挟んでも問題なくテストスクリプトが動作しました。

Circle CI でバックグラウンド処理を行う

CI におけるバックグラウンドプロセスの実行について

Circle CI でバックグラウンドプロセスを起動したいことがままあります。たとえば、自動検証を行う際に appium のサーバープロセスを起動するなどです。

普通、Unix系のコマンドではコマンドの後ろに ‘&’ をつけて、実行するとバックグラウンド実行になります。

> appium &

しかしながら、circle.yml にコマンドとして以下のように書くと…

test:
  pre:
    - appium&

実行時に次のような警告が出てきます。

Probable error. It looks like you’re trying to run a command in the background by using ‘&’ and not the ‘background: true’ option. If so, the process will die quickly from the hangup (HUP) signal. See the the documentation on backgrounding.

と、言うわけで、Circle CI 本家のドキュメントを見てみます。

circleci.com

このドキュメントによると、バックグラウンド処理は & を使うのではなく、yml の記法で background オプションを true にせよとのことです。また、必要に応じてバックグラウンドプロセスが安定するまでスリープを入れてもよいとのことです。

そんなわけで、以下のように circle.yml に記載することによって、appium のサーバーがバックグラウンドプロセスとして起動することができます。

test:
  pre:
    - appium:
        background: true
    - sleep 10

バックグラウンドプロセスの標準出力をみるには

Circle CI ではバックグラウンドプロセスの標準出力とエラー出力をビルド後に確認することができます。通常では Circle CI の仮想マシンのログなどを引き上げるときは artifacts としてファイルパスを circle.yml に記載する必要がありますが、バックグラウンドプロセスの標準出力とエラー出力については自動的に artifacts として待避されます。ビルド、自動テスト中にエラーが出た場合にも後からログとして確認することができます。

Circle CI の結果画面で artifacts タブを選択すると以下のような表示になります。 f:id:munacky:20170524183848p:plain

このファイル群の stdout から始まるのが、標準出力、stderr から始まるのがエラー出力になります。

自分はこの機能を知らず、わざわざ、出力先を自前でリダイレクトして、リダイレクトしたファイルを artifacts に登録して出力結果を引き上げていました。とほほ。

Ruby の空白の洗礼を受けたお話

最近、AppiumにTurnipとRSpecを組み合わせてテストスクリプトを書き始めています。

今までのエンジニア人生でRubyを本格的に開発に使用するのが初めてなので、Rubyのエキスパートから見たらどうしょうもないところでつまづいてしまいました。 自分の恥を晒すついでに、忘れないために記事にしています。

それは適当に書いた1行のコードから始まった

今回、つまづいたのは以下のコード。myArrayのサイズが4かどうかを判定するテストスクリプトのつもりで書きました

expect (myArray.size).to eq 4

実行すると以下のようなエラーメッセージが

     Failure/Error: expect (myArray.size).to eq 4

     NoMethodError:
       undefined method `to' for 4:Fixnum

Fixnumには to というメソッドはないそうですよ。

勝手な解釈で試行錯誤を続ける

「あれ、おかしいのう、もしかしてexpectに数値渡すと .to eq で評価できないのか?」

と早合点したのが運の尽き。expectの引数に色々なオブジェクトを設定したりしても解決しません。30分以上試行錯誤を繰り返してようやく

「おりょ、my.Array.size の to が expect より先に評価されてるんじゃね?」

ということに気がつきました。コードをもう一度よく見てみると…

expect (myArray.size).to eq 4

「あー、expectと括弧の間にスペースが紛れてる〜」

と、いうことに気がつきました。そんなわけで、以下のようにexpectと括弧の間のスペースを削除したら動作するようになりました。

expect(myArray.size).to eq 4

まとめ

Ruby は関数呼び出しに必要な括弧をスペースで省略できる言語のため、関数呼び出しの際のスペースが重要な意味を持ちます。 これはなるべく括弧を書きたくないという思想が生み出した言語仕様でとても気に入っています。

今まで使ってきたC++JavaJavaScriptなどの言語では関数名と括弧の間のスペースは無視されていたため、あまり気にしなかったのですが、Ruby ではキッチリと意識をして空白を使わないといけないようです。

自動署名のXcodeプロジェクトをfastlaneでビルドする

自動署名とは

iOS アプリの開発においてXcode8 から導入された機能で、アプリケーションをビルド&アーカイブする際に選択した使用方法に合わせた署名をつけてくれます。 通常、署名の種類は開発用(Development)、検証用(Adhoc)、リリース用の3種類の署名があります。

自動署名機能を使用するかどうかはXcodeプロジェクトの設定ファイル(.xcodeproj)に格納されます。

自動署名で困ること

fastlane を使用して CI 環境でビルドする際にはビルドするブランチによって、使用する署名を指定する機会が多いです(例:develop ブランチなら Adhoc 用の署名をつけて、masterブランチなら Release用の署名をつけるとか )

この時にプロジェクトの設定が自動署名になっていると fastlane を使用して署名の種類を指定してビルドするとプロジェクトの設定とfastlaneの設定が食い違い、ビルドエラーになってしまいます。

解決策

自動署名機能を使わないという極端な解決策もあるのですが、せっかくの機能を使わないと勿体無いし開発者の開発効率を CI のために落とすのも本末転倒なので、以下のように運用しています。

  • fastlaneを使用してビルドを行う場合、最初にプロジェクトの設定をマニュアル署名に設定する
  • ビルド&アーカイブ処理を実行
  • ビルド&アーカイブが終わったらプロジェクトの設定を自動署名に戻す

xcodeproj ファイルは XML ファイルなので頑張れば自分で解釈して書き換えも頑張ればできるのですが、fastlaneにはプロジェクトの自動署名設定を書き換えるためのコマンドがあります。disable_automatic_code_signing と enable_automatic_code_signing です。引数にxcodeprojファイルのパスを指定して設定を変更するプロジェクトを指定します。

例えば、検証用(AdHoc)の ipa ファイルを作成する場合は以下のようになります。 xcargs の引数は各自の署名を識別できる正しい値を使用してください

  lane :beta do
    #真っ先にプロジェクトの自動署名をオフにする
    disable_automatic_code_signing(path: "XXXX.xcodeproj")
    sigh(adhoc: true)
    gym(
      clean: true, 
      workspace: 'XXXX.xcworkspace', 
      scheme: 'XXXXXX',
      configuration: 'Release',
      xcargs: "CODE_SIGN_IDENTITY='XXXX' PROVISIONING_PROFILE_SPECIFIER='XXXXXXXXXXXX'"
      ) 
    #ビルドが終わったら自動署名を元に戻す
    enable_automatic_code_signing(path: "MyApp.xcodeproj")
  end

おまけ

本来なら fastlane 側でもう少し柔軟に対応できないかとも思うのですが、今の所はこの方法で乗り切るしか無いようです。

限られた工数でテストケースの品質を上げる

最近、検証エンジニアも兼務すること多く、2年くらい前からテスト設計からテストの実行までやるようになりました。 その際に、見極めが難しいのが、テストケースをどこまで掘り下げて書くかと言うことでした。

基本

おおよそのテスト設計の教科書には以下のように書かれています。

  • 誰が読んでも同じ手順を再現できるように作る
  • 誰が実行しても、実行結果を判定できるように作る

最初は教科書通りにテストケースを書こうとしていたのですが、なんと言っても時間がかかるし、プラットフォームやブラウザやOSのバージョンによって手順が変わったりするので、現実的に上記を100%実行するのは不可能に近いし、工数をかけてまでやることではないという結論にいたっています。

テストケースを作成するに当たって重視すること

限られた工数を使って、有効なテストケースを作成しようとする場合、教科書の中から優先度の低いところをそぎ落としていきます。

  1. OS やブラウザなど、業界標準の知識に依存する手順については省略する
  2. 仕様書を見ればわかる手順は省略する

そして、テストの仕様に対するカバー率は100%を目指します。仕様に書かれているが検証されない機能をゼロにするためです。 そんなの当たり前じゃん、と思う方もいるかもしれませんが、仕様書も完璧に書かれているわけではないケースが多いため、希に抜けることがあったりします…

そんなわけで、結論としてはテストケースの項目を作成するときは気合いを入れる。テスト項目の中身を記載する場合、ある程度読み手を限定して手を抜けることろは手を抜くとメリハリをつけています。

テストケースを書く際に気をつけていること

自分がテストケースを書くときに気をつけていることは大体以下のようなことです。

  • 読み手の仕様に対する理解率を想定しておく
  • 手順を詳細にすることに力を注ぐより、カバー率を上げることに注力する
  • なるべくほかのドキュメントに書いてあることは書かない

ドキュメントというモノは誰かに向けて書いているわけで、読み手の理解率を想定しておくことがとても重要です。 国内の同じ拠点で開発している気心の知れたメンバー向けにオフショアベンダー向けに作るようなテスト手順書を作成するのはオーバースペックな上に、読み手にとっても読みにくいモノになるでしょう。

appium-desktop を Android アプリで使ってみる

先日 appium-desktop を紹介しましたが、Android 編のリクエストがありましたので、追加して見ました。

android で appium-desktop を使う利点

android については 古い desktop 版の appium Ver.15.x でも Inspector 機能を使うには支障がなかったため、appium-desktop 必須という訳ではありません。 しかしながら、設定パラメータの与え方などは appium-desktop の方がコマンドライン版に近いため、appium-desktopの方が断然オススメです。

アプリケーションの起動からサーバーの起動まで

アプリケーションの起動からサーバーの起動までは全く iOS と同じです。

Session を起動する時の注意

iOSアプリでは事前に Simulator を起動しておく必要はありませんが、Androidアプリを動作させる時はコマンドライン版と同じように、エミュレータをあらかじめ起動しておく必要があります。 エミュレータコマンドラインからでも Android Studioから起動しても同じように動作します。

Desired Capability

Desired Capability はコマンドライン版と同じオプションを指定しておく必要があります。 - platformVersion - 起動してあるエミュレータとバージョンを合わせておかないとセッション開始時にエラーになります - deviceName - avd の名称と合わせる必要はないようですが、無いとエラーになりました - app - apkのパス。Android アプリはリリース版もエミュレータで起動できるのが嬉しいです。 - appPackage - パッケージ名 - appActivity - メインクラスの名称

{
  "platformName": "Android",
  "platformVersion": "7.0",
  "deviceName": "Nexus 5",
  "app": "/Users/muneakiosawa/MyAndroidApp/app/build/outputs/apk/app-release.apk",
  "appPackage": "com.MyApp",
  "appActivity": ".MainClass"
}

Android アプリ向けにappiumでテストスクリプトを書かないといけない方は appium-desktop を是非とも使って見ましょう。