woshidan's blog

あいとゆうきとITと、とっておきの話。

JAWS-UG コンテナ支部 #13 に参加しました

ちょっとバタバタしていたため遅くなってすみません。。

jawsug-container.connpass.com

上記にブログ枠で参加したので各発表についてまとめます。

AWS サービスアップデート

toricls さんの発表でした。

EKSのTokyoリージョンおめでとうごさいます!

Kubernetes関連を勉強しはじめだったのでアップデート内容についてはうなずくしかできない...という感じだったのですが、re:Inventのアップデート内容の中で、DNSについて、SQSキューなどのリソースのURIを固定にするためRoute53にCNAMEレコードをたくさん登録したことがあったのであとで試してみたいと思います。

今日から始める人のための Kubernetes on AWS ベストプラクティス 2018版

mumoshu さんの発表。

https://www.slideshare.net/mumoshu/kubernetes-on-aws-2018

まさに、これからKubernetesAWSでさわってみようかな、と考えていたので、各種ツールのアプローチやできることの差異などが非常に参考になりました。

特に前半について、KubernetesAWSで動かすための選択肢として、kops, kube-aws, EKSの選択肢があげられていたのですが

  • 連携しているツールの違い(kops: Terraform/kube-aws: CloudFormation/EKS: SDK, CLIの手作業ベース , kubeconfig(まだ貧弱))
  • サポートしているホストOSの違い(kops: いろいろ/kube-aws: CoreOs/ Container Linux)
  • ツールで扱える範囲(kops/kube-aws: マスタ+ノードの運用少し, EKS: Control-Plane*1の構築、運用)

など、一筋縄でいかなさそうです...。

AWSで仮想コンピューティングが行えるサービスとしては、Lambda・EKS・ECS(Fargate)がありますが、それらの使い分けについて触れられていたのも助かりました。

後半はまた改めてお世話になると思います。本当に濃かったです...。

Kubernetes Scheduler のカスタマイズ

チェシャ猫さんの発表。EKSの話ははじめてだったのでついていけるか不安だったのですが、ECSの復習からはじまったので非常に助かりました!

LTがわりの連続Tweetの最初のアドレスはこちら

メンバーごとのリモートで試せる開発環境を用意したい場合*2に、常にメンバーの人数分のEC2インスタンスを立ち上げていても無駄なので、もしすでにDockerのタスクが動作しているインスタンスがあり、余裕があるなら新しいタスクもそちらに配置するようにしたい、という場合にEKSではどう設定したらいいか、について*3

KubenetersのPod*4のSchedulingは、

  • 配置しようとしているPodの条件に合うNodeをフィルタリングする
  • 残ったNodeを優先度にもとづいてスコアリングする

の二段階で行われています。

EKSを利用する場合、上記を実行するSchedulerの指定が行えませんが、追加のSchedulerを指定することで追加の指定を行うことはできます。

しかし、これも追加のSchedulerをPodごとに用意する必要があったりで煩雑なので、Scheduler Extenderの利用が考えられます。

が、こちらは実行タイミングの問題でEKSのデフォルトのSchedulerでフィルタリングされたNodeを選べない、などややこしく、こういった状況にたいして Scheduling Framework が開発されていますが果たして、という内容でした。

犬でもわかった気になってしまった!!

Firecracker とは何か

理解できているか自信ないですが、今回の発表の中で一番面白かったです。

https://speakerdeck.com/pottava/what-is-firecracker

  • アカウントごとにGuest OSのリソースを確保して動かすから従来のLambda(EC2利用)はリソースの無駄な確保もあり、遅かった
  • アカウントごとではなくプロセス単位で分離できていれば十分なのでより細かくリソースを分離、既に起動しているGuest OSを利用できるので早くなる

くらいのノリで理解していました。

発表とは直接関係ないですが、あとで見返す時用にスライドの4ページの各層についてメモを置いておきます。

  • Our code ... 人間が扱いやすいようなレベルに抽象化された命令が書いてある
  • Lambda runtime ... コードの実行環境。(各種サンドボックス相対パスから見て)Lambdaは特定のバージョンのPythonが実行できるようにライブラリなどを決まったパスに入れている
  • Sandbox ... 外部から受け取ったプログラムを保護された領域で動作させて、システムが不正に操作されるのを防ぐセキュリティ・システムのことをいいます。この決まった領域内(多分usrディレクトリ以下はOKでsys以下はだめとか、そういう)にLambdaの実行環境を構築する https://blog.codecamp.jp/sandbox
  • ゲストOS ... ハイパーバイザによって実行されているOS. Macの中にWindowsが!!! とか?
  • ハイパーバイザ (hypervisor) とは、コンピュータの仮想化技術のひとつである仮想機械(バーチャルマシン)を実現するための、制御プログラム https://ja.wikipedia.org/wiki/%E3%83%8F%E3%82%A4%E3%83%91%E3%83%BC%E3%83%90%E3%82%A4%E3%82%B6
  • ホストOS ... ハイパーバイザを動かしているOS

Envoyを分かりやすく例えつつApp Meshの話をします

_mpon さんの発表。

https://speakerdeck.com/mpon/envoywofen-kariyasukuli-etutuapp-meshfalsehua-wosimasu

App Meshどころか、envoyすら初見だったのですが、nginxでできることに合わせて説明されていたのでめちゃくちゃ助かりました...。 マイクロサービス方面についてもっと調べてみようと思います。

GameDay @ re:Invent のこと

chie8842 さんの発表。

GameDayに行った際、英語がうまく話せなくても自分の担当領域の作業を見つけて実行していく様子や優勝したときに採点されたポイントに他のチームがあまり気づいていなかった話が聞いてて参考になりました。

EKSのいーかんじなサービス公開方法

sugimount さんの発表。

https://speakerdeck.com/sugimount/expose-service-eks-externaldns-acm

EKSでは外部に公開しようと思った時にELBの利用が必要で、その具体的な手順のまとめでした。お名前.comなどからドメインを予約するところから始まっていてありがたかったです。

Virtual Kubelet + Fargate + EKSでノードレス Kubernetes を夢見た話

bbrfkr さんの発表。

https://speakerdeck.com/bbrfkr/virtual-kubelet-plus-fargate-plus-eksdefalsedoresu-kubernetes-womeng-jian-tahua

Kubernetes外のAPIにPodの作成を依頼可能、ということはkubuletが属しているクラスタの外部(たとえば、今回だとEKSのMasterに対してリクエストを投げている)にリクエストを投げられる、ということだったのですが、今回初めてMasterも含めて1つのクラスタの中に構成していく、ということの方を知ったので*5精進せねば...と焦る感じでした。

ECSばかりさわっていて、この辺の話を聞いたことがなかったので大丈夫かな、と思ったのですが、わかりやすい発表が多くてとても助かりました。

*1:kubectlコマンドの結果を受け付けて、そのコマンドによるリクエスト結果をクラスタに反映させるために常駐しているプロセスのことをさすみたい

*2:マルチテナント化、といった用語で説明される状況が恐らくこれ

*3:ECSではBinpackで設定すればよいみたいです

*4:同じ仮想ENIを利用するコンテナの集合。 Webサーバとログサーバを1つのPodに入れて扱うなど。ECSでいうとタスクに近い

*5:https://ja.wikipedia.org/wiki/Kubernetes など

AWS Web Servicesを使ったサーバーレスアプリケーション開発ガイドを読みました

Amazon Web Servicesを使ったサーバーレスアプリケーション開発ガイド

Amazon Web Servicesを使ったサーバーレスアプリケーション開発ガイド

上の本を読みました。全体的に

  • Lambdaになれました
  • Vueになれました*1

という感じなのですが、個人的に特に気になったトピック2つだけ並べておきます。

サーバレスで何をやるにしろまずIAMのことを考える件

  • サーバレスのサービスを利用する

と書いてあったときにやる手順が、

  1. AWSサービスへの信頼ポリシーがインラインポリシーとして埋め込まれたロールを作成する
  2. サーバレスで実行したい関数で使う予定のリソースへのアクセス権限を記述したポリシーを書く
  3. 1.のロールに2.のポリシーを付与する
  4. 関数実行時のロールとして3.のロールを指定する
  5. (あるいは0.)Lambdaで実行したい関数などを書く
  6. 5の関数をデプロイして実行

となっていて、サーバレスをやるぞ、といったときに、想定しているのが5とか6だけだったので、自分がAWSに慣れてないだけかもな、と思いつつも、サーバレスよりまずIAMだなぁ、と思いました。

いや、自分で実行するタイミングとか制御しにくくなるので権限管理は余計に大事なんですがが。

めんどくさがっていても仕方ないので、せめて

  • Lambda
  • CloudFormation

の2つについては、それぞれ何をするサービスで、どういう権限が必要かをざっくりまとめておきます。

Lambda *2

使い方

あらかじめPython3.6など特定のバージョンの特定の言語のプログラムが動かせる実行環境がいくらか用意されているので、利用する実行環境を指定して、zipなどでコードをアップロードする。

実行条件はSDKからの呼び出し、タイマーでの設定(CloudWatchを利用)、他サービスでのイベント駆動(S3にファイルがアップロードされたらメタデータが入ったJSONを引数に実行開始、的な)などがある *3

与える必要のある権限
  • 連携先のサービス(たとえばS3に)に与える権限で、Lambda関数へのアクセスを許可するための権限( lambda:InvokeFunction )
  • 実行するコードでアクセスするリソースへのアクセス権限
  • ログを吐くためCloudWatchへのアクセス権限

CloudFormation *4

使い方

AWS CloudFormationを使うと、設定ファイルを書くことで、AWSのリソースの集合をスタックという単位で壊したり更新したり作成したりできるようになります。

ひらたくいうと、AWSが用意したYAMLで書けるTerraform*5で、大きな特徴として、必ずデプロイ用の設定を保管するS3のバケットを用意する必要があります

Lambda + DynamoDB, API Gateway など、サーバーレス向きサービスだけ利用する場合は、SAMというサーバーレス用のフレームワークを用いて簡潔に記述できます。

与える必要のある権限

CLIで実行している間は開発者がなんでも権限を持っていたりするので気にならないんですが、CIの工程にCloudFormationによる環境構築を含めるときには

  • package コマンドで作成した設定ファイルetc. をアップロードするS3のバケットへのアクセス権
  • CloudFormationで操作する予定のサービスのアクションの許可(たとえば、DynamoDBをいじる設定を書いたのにDynamoDBを作成、削除、取得する権限がないと実行できない)
  • IAMパスロール
  • CloudFormationの変更セット*6を作成するための権限*7
  • Lambda関数などサービスのリソースのロールにポリシーをアタッチ/デタッチするための権限(iam:AttachRolePolicy / iam:DettachRolePolicy)

などが必要です。細かくは、本の p.246~ に例があって勉強になりました。

DynamoDBについて

本書を読んで

という具合に「RDSはLambdaに向かない」の意味を理解して、スキャンなどのAPIもさわって理解した気になっていたのですが、

などのスライドを見ていると、トランザクションの整合性に対する理解が特に弱いし全体的にまだよくわかっていないな...という感じなので精進します。

そういえば、indexを貼ってないキーでの検索はフルスキャンが強調されていますが、indexを貼ってないキーで検索するとフルスキャン、というのはRDBMSでもそうですね。

Kinesisについても触ってはみたもののよくわかってないな、という気持ちがあるのでがんばっていくことにします。

現場からは以上です。

*1:フロントエンドから直接データストア触ることが増えるんだったらバックエンドの人も何が起きてるのか調べるために最低限フロントエンド読めた方がよさそう、と思いましたがほどほどに頑張ります

*2:https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/with-s3-example.html

*3:テスト用のイベントデータ作成に困った時はコンソールでテストイベントの設定をクリックして出てくるイベントテンプレートのサンプルが便利です http://www.atmarkit.co.jp/ait/articles/1706/13/news008_2.html

*4:https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/with-s3-example.html

*5:作成、更新するときは、deployコマンドの前にpackageコマンドを実行するのですが、そうすると更新しようとしている内容を出力ファイルに吐き出してくれます。Terraformがapplyだろうがplanだろうがこれから更新する内容に差分があればコンソールに出力するようになっているので、え、何も出さずに更新するの? と最初少しビビりました

*6:https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-changesets-create.html

*7:CLIで同一のスタックに対し変更 => 再package & deploy を実行していたものの、変更セットについては特に意識せずに終わったので、CLIでスタックを更新する時は裏で作成されているっぽい

Rackの概要、RackミドルウェアとRackアプリケーションの違いについて

この記事はRubyアドベントカレンダーの14日目の記事です。

最近Rackにさわることがあったのでこの記事では

についてまとめます。

Rack概要

RackはWebサーバとRuby及びRubyフレームワークとの間でやりとりをするためのインタフェースを提供するためのライブラリです。

具体的にはRackは

  1. Webサーバに来たリクエストを決まったキーを持つハッシュに加工し *1 、そのハッシュを引数にしてRubyで動いているアプリケーションのプログラムを呼び出す
  2. Rubyのアプリケーションから受け取る['200', {'Content-Type' => 'text/html'}, ['A barebones rack app.']] のような形式の配列を加工して、Webサーバが扱う変数に代入する

のはたらきをする Rack ハンドラー 、設定ファイルを読み込み、実行環境に合わせたWebサーバを起動する rackup コマンド、設定ファイルを書く際に利用する Rack::Builder DSL を提供し、

Rackハンドラーから呼び出されるRuby及びRubyフレームワークのアプリケーションプログラム側には

  • ハッシュを引数に call メソッドを呼ぶと ['200', {'Content-Type' => 'text/html'}, ['A barebones rack app.']] のような形式のレスポンスを返す

という規約( Rackプロトコル *2 )を守ることを要求します。

この規約に則ったRubyのオブジェクトを Rackアプリケーション と呼びます。

f:id:woshidan:20181214222452p:plain

RackアプリケーションとRackミドルウェア

Rackアプリケーションの中には、レスポンスを返すこと自体を目的とせず、Rackアプリケーションの call メソッドの前後に処理を挟むことを目的として実装される Rackミドルウェア と呼ばれるものがあります。

Rackアプリケーション/Rackミドルウェアと呼ばれるものは両方ともハッシュを引数に call メソッドを呼ぶと所定の形式の配列を返します。そういう意味ではまとめてRackアプリケーションといえますが、両者には以下のような違いがあります。

  • rackupコマンドに与える設定ファイル( config.ru )上で利用を指定するときのDSLが異なる
    • Rackアプリケーションは run Rack::SampleApp, Rackミドルウェアuse Rack::SampleMiddleware で指定
  • Rackミドルウェアには追加の規約があり、1番目の引数に app を受け取る initialize メソッドを定義する 必要がある

Rackミドルウェア(Rackアプリケーション)は自分のコンストラクタに引数として受け取ったアプリケーションに、さらにenvを渡す形で入れ子のように実行していくのですが、最後に実行されるRackアプリケーションを特別に エンドポイント と呼びます。

f:id:woshidan:20181214222517p:plain

参考

負荷試験のアプローチとWeb系でクラウドサービスを利用している場合のそれぞれのアプローチに対する感想について

この記事はソフトウェアテストアドベントカレンダーの8日目の記事です。

以前、担当しているサービスの負荷試験を行ったのですが、負荷試験やパフォーマンステストとふわっといった時、いくつかアプローチの種類があるようなので今回の記事ではそれらの簡単にまとめて実際Web系でAWSなどクラウドサービス使っててどう思ってるかについて書きます。

それぞれのアプローチの呼び方については、Oracleの定義*1であったり、ソフトウェアテスト標準用語集*2であったりに違いがあるようなので、この記事ではOracleが使っている名称に基づくことにします。

TL;DR

継続的な負荷の監視は実質的に負荷試験の一種だなぁ、と思いました。

負荷テストのアプローチについて

負荷テストを考える際のアプローチには大きく分けて

  • 性能テスト: 想定している負荷に対し、どの程度のスループットやピーク時の処理量が出せるか
  • 限界テスト: システムが想定している以上の負荷、あるいは限界以上の負荷を与えた場合何が起こるか
  • 耐久テスト: 高負荷で長時間運用時に何が起こるか

の3つがあると思います。実際は要件に合わせて、これらのアプローチを組み合わせて行っていることも多いですが、まずそれぞれのアプローチについてどういったことを計測するのかなどを確認します。

性能テスト

サービスや新機能のリリース前に、システムの結合試験や統合試験ではテスト用のユーザでのみ動作させていると思うのですが、ユーザの数が少ないと遅い処理でも問題なく動いてるように見えます。

性能テストでは、システムが想定している負荷に対し、どの程度のスループット応答時間を返すかを確認します。

限界テスト

システムが想定している以上の負荷を与えて、システムに何が起こるのかを確認します。

たとえば、リクエスト量が想定している負荷の2倍を与えた時のシステムの振る舞い、たとえば、

  • トップページが500エラーになってしまうか
  • 特定のデータストアにアクセスが集中してスループットが下がってしまわないか
  • クラッシュしないか

といったようなことを確認します。

耐久テスト

高負荷で長時間動かした場合にどう動かすかを確認します。

たとえば、

  • 微量のメモリリークがあるプロセスのせいで、メモリ使用量が時間とともに増加する
  • キャッシュの保持期限設定を間違えてキャッシュのメモリが不足する
  • ログの出力が多すぎて、ログファイルがディスクを圧迫
  • 処理にメモリを使いすぎて、GCなどが起こりすぎて処理速度に影響

といったようなことが起きないか確認します。

それぞれのアプローチに対する個人的な感想

個人的な感想ですが、本題です。

性能テストについて

自分がWeb系の自社サービスでAWSなどのクラウドサービスを使っているという前提で話すと、かなり大規模なサービスを持っている会社がそれに付随するようなサービスを新しく展開する、といったような場合以外は、あまり性能テストという形でやらないのかな、という感じです。

  • 小さいリリースを小刻みに行っていく場合リリースごとに想定される負荷があまり変化しない
  • クラウドサービスを利用している場合、サービスを継続的に監視してCPU使用率などが高くなったタイミングで一つ上のクラスのサーバにすぐ変更することが容易

なので、リリース後の継続的な監視で済ますことが多そうです。

弊社の場合、機能のリリース前にステージング環境などある程度用意しているデータの規模が大きい環境でしばらくサーバの負荷に変化がないかなどを監視するといった方法を取ることが多いです。

限界テストについて

テレビCMなどでのアクセス増予測や、新機能リリースのために試験している印象が実はそこまでないです。

  • 新機能がそこまでヒットする予測が毎回ない*3

というのと、

  • 高負荷で障害が発生してからの負荷に対応できるインフラの調達が30分以内とかで可能
    • オートスケールの設定台数を変える(数分くらい)、で済むことが多い
  • 小さいサービスが急にバズって高負荷で止まっても仕方ないよね、頑張ってねという業界の空気がある(気がする)

ので、決済とかクリティカルなサービス以外では、リリース後にその場で対応することが多い印象があります*4

来年ユーザー数を2倍に増やす事業計画とかがあって、その前提にあわせて試験したりすることもあるかな、という感じです。ぶっちゃけると新規リリース以外の場合、リリース直前の試験の段階でパフォーマンスの問題が見つかっても、負荷に対応するためのアーキテクチャの構築、ならびに新しいアーキテクチャへのデータ移行が間に合わないので、先手を打って試験をすることもあります。

テレビで急に取り上げられた場合は違いますが、テレビCMを打つ場合、Web系のベンチャーあたりだと「ここが社運をかけた投資・拡大のタイミング」と踏まえた事業計画に基づいて行なっているので、上記のような事業計画がありそれに沿って動くのかな、という気がしています。気だけですが。

耐久テスト

これもサーバ側だと監視ですます印象です。

  • 監視していると数日に一回メモリ使用量が80%などを超えるサーバがある
    • だいたい再起動など比較的簡単な対応をすればその場はしのげるので暫時対応で時間を稼ぎながら根本対応をしていく

みたいな。リリースしたらお客さんが運用する受託であったり、リリースしたらユーザの手元で動くアプリの場合はまた違うのかな、と思います。

締め切りギリギリのポエムですが、監視は試験である、とはこのことなんだなぁ、ということで現場からは以上です。

参考

*1:https://www.oracle.com/technetwork/jp/ats-tech/tech/useful-class-8-520782-ja.html

*2:http://jstqb.jp/dl/JSTQB-glossary.V2.3.J01.pdf

*3:このお話は一般論です

*4:それでなんとかならないソシャゲとかあった気がしますがどういう修羅なんでしょうか

いまさらRSpecに少し慣れてテストをすっきり書く方法について少し覚えた話

いまさらRSpecをさわってきて慣れて覚えた話もメモしておきます。

TL;DR

  • letで宣言した変数の初期化用パラメータで置き換えたくなったパラメータもletで宣言しておくとよさそう
  • shared_context や shared_examplesでまとめるのがちょうどいいくらいの事例としてちょうどいいのは、外部公開するAPIの異常系のレスポンスのテストくらいでは

慣れてきて覚えた話

letで宣言した変数の初期化用パラメータで置き換えたくなったパラメータもletで宣言しておく

これは同僚の方から教えていただいていいな、と思ったんですが、

describe "post comment" do
  let(:params) { { user_name: "HOGE", comment: "...", "book_id": 3 } }

  context "valid request" do
    it "can be posted" do
      ...
    end
  end

  context "invalid user" do
    let(:params) { { user_name: "", comment: "...", "book_id": 3 } }

    it "cannot be posted" do
      ...
    end
  end

  context "invalid comment" do
    let(:params) { { user_name: "HOGE", comment: "INVALID_FORMAT", "book_id": 3 } }

    it "cannot be posted" do
      ...
    end
  end

  context "invalid book" do
    let(:params) { { user_name: "HOGE", comment: "...", "book_id": nil } }

    it "cannot be posted" do
      ...
    end
  end
end

のような場合、 params の値の一部がそれぞれの context で異なるので、それぞれの context ブロックでパラメータを宣言しなおしていますが、

describe "post comment" do
  let(:user_name) { "HOGE" }
  let(:comment) { "..." }
  let(:book_id) { 3 }
  let(:params) { { user_name: "HOGE", comment: "...", "book_id": 3 } }

  context "valid request" do
    it "can be posted" do
      ...
    end
  end

  context "invalid user" do
    let(:user_name) { "" }

    it "cannot be posted" do
      ...
    end
  end

  context "invalid comment" do
    let(:comment) { "INVALID_FORMAT" }

    it "cannot be posted" do
      ...
    end
  end

  context "invalid book" do
    let(:book_id) { nil }

    it "cannot be posted" do
      ...
    end
  end
end

のように、example内で触る変数はparamsだけかもしれないんですが、それぞれのcontextで注目しているパラメータだけあとで置き換えられるように、宣言の時に工夫しておくとすっきりするなと思いました。

shared_context や shared_examplesでまとめるのがちょうどいいくらいの事例としての外部公開するAPIの異常系のレスポンスのテスト

この話がしたくてこの記事を書きはじめたのに随分長くなってしまったのですが、RSpecでは、shared_contextを使ってspecの中で繰り返し登場するテストケースを

describe "post comment" do
  let(:user_name) { "HOGE" }
  let(:comment) { "..." }
  let(:book_id) { 3 }
  let(:params) { { user_name: "HOGE", comment: "...", "book_id": 3 } }

  context "valid request" do
    it "can be posted" do
      ...
    end
  end

  context "invalid user" do
    let(:user_name) { "" }

    it "cannot be posted" do
      ...
      expect(:status).to eq 400
      expect(:body).to eq "Invalid Request" # このケース自体がいいかは微妙ですが...
    end
  end

  context "invalid comment" do
    let(:comment) { "INVALID_FORMAT" }

    it "cannot be posted" do
      ...
      expect(:status).to eq 400
      expect(:body).to eq "Invalid Request"
    end
  end

  context "invalid book" do
    let(:book_id) { nil }

    it "cannot be posted" do
      ...
      expect(:status).to eq 400
      expect(:body).to eq "Invalid Request"
    end
  end
end

のような場合、

describe "post comment" do
  let(:user_name) { "HOGE" }
  let(:comment) { "..." }
  let(:book_id) { 3 }
  let(:params) { { user_name: "HOGE", comment: "...", "book_id": 3 } }

  context "valid request" do
    it "can be posted" do
      ...
    end
  end

  shared_examples "invalid request" do
    it "returns 400 response" do 
      expect(:status).to eq 400
      expect(:body).to eq "Invalid Request"
    end
  end

  context "invalid user" do
    let(:user_name) { "" }

    it "cannot be posted" do
      ...
      it_behaves_like "invalid request"
    end
  end

  context "invalid comment" do
    let(:comment) { "INVALID_FORMAT" }

    it "cannot be posted" do
      ...
      it_behaves_like "invalid request"
    end
  end

  context "invalid book" do
    let(:book_id) { nil }

    it "cannot be posted" do
      ...
      it_behaves_like "invalid request"
    end
  end
end

のように、同じテストコードを実行している部分を shared_examples に定義して it_behaves_like "SHARED_EXAMPLE_NAME" で呼び出すことができます。

こうするとテストケースはDRYになりますが、

  • 一方でテストが読みにくくなったり
  • バリデーションエラーなどは案外全く同じコードにならなかったり
    • たとえば、上記のサンプルコードは実際はパラメータごとに invalid user などのレスポンスを返したほうが親切でしょう

して、使いどころが難しいんですが、外部公開しているAPIの場合、

  • エラーの内容からAPI内部の処理を推測できないようにわざと詳細なレスポンスを返さないケースがある
  • 外部公開しているAPIが複数あるとしても、機能ごとにエラー時のレスポンスを変えない場合が多い
    • たとえば、一覧取得用API投稿用APIで認証エラー時のレスポンス内容は変えない方が親切でしょう

という感じで使ってみるとよいかもしれないな、と思ったのでメモです。

参考

https://qiita.com/jnchito/items/42193d066bd61c740612 https://qiita.com/etet-etet/items/7babe4856a1cd62b9ecb

ペアワイズ法について簡単にまとめて手を動かしてテストケースが削れるのを眺めてみた

この記事はソフトウェアテストの小ネタアドベントカレンダーの2日目の記事です。

この記事では、大量にあるテストケースをある程度不具合検出率を担保しつつ効率よく削るための手法であるペアワイズ法についてその概要をまとめ、組み合わせが小さい場合について手で実際に行ってみて、なぜテストケースが減らせるのか確認します。

TL;DR

  • テスト対象のパラメータの値を単純に組み合わせていくと、テストケースがあっという間に増えすぎてしまう
  • 実際に影響があるのは2つくらいのパラメータの値の組み合わせであることが多いので、パラメータでペアを作って、そのペアごとにすべての値をテストできるようにテストケースを作成するようにすると効率がいいのでは、というのが ペアワイズ法 の基本の考え方
  • ペアごとにすべての値をテストすればよい、としたことで1つのペアの条件をテストしながら他のペアの条件もテストしていくことでテストケースが削減される様子を眺めてみた

単純に条件の組み合わせを増やしていくとテストケースがあっという間に増えてしまう

テストを作成する際、テスト対象に入力されるパラメータの組み合わせからテストケースを作成することが多いと思います。

たとえば、以下のような入力欄があるログインフォームがあるとして

  • 会員IDの欄
  • パスワードの欄

がありそうですが、これらに

  • 未入力か
  • 正常に入力されているか
  • 異常な値が入力されているか

などの状態があると思うんですが、律儀にテストしていると

  • 3 * 3 = 9

パターンになります。これだけのログインフォームならまだいいですが、例えば会員登録時のフォームで、さらに

  • 性別
  • 都道府県
  • メールアドレス

などの項目がある場合、律儀に同様の組み合わせを試験に追加しようとすると

  • 9 * 27 = 243

とちょっと真面目に組み合わせの試験をする気がなくなってきますね。

ペアワイズ法とは

ところで、上の例では、すべてのパラメータの組み合わせが同等に結果に影響をあたえる、つまり、

  • 会員IDに関する結果を見たい時、他の4つのパラメータ(パスワード/性別/都道府県/メールアドレス)のすべての組み合わせを考える必要がある

としているので、

  • 会員IDが未入力
  • 会員IDが正常に入力されている
  • 会員IDに異常な値が入力されている

の3つのケースそれぞれに対して、他の4つのパラメータの組み合わせ(81通り)をテストするということで243通りになっているのですが、実際、会員IDに利用したい値が正常かどうかについて都道府県のフォームの入力値はあまり関係なさそうですよね。

つまり、

  • 複数のパラメータの組み合わせがテスト結果に影響をあたえるとしても、すべてのパラメータの組み合わせを使う必要はない
  • 実際テスト結果に影響をあたえるパラメータの組み合わせは2つくらいのパラメータからなることが多い

ので、すべてのパラメータから2個ずつ*1のペアを作って、そのペアについてはすべての結果を試験するようにテストケースを作成していこう、というのがペアワイズ法の考え方みたいです。

組み合わせの数が小さい場合について、実際に手作業でやってみる

ペアワイズ法を使ってみるとどういう風になるか、具体例を使って考えてみます。今回は、

  • 会員ID
  • パスワード
  • 性別

のパラメータについて、パラメータごとの値を、正常に入力されている(o)/未入力()/異常な値が入力されている(-)として、3つのパラメータのすべての値の組み合わせで表を作ると

会員ID パスワード 性別
o o o
o o -
o o
o - o
o - -
o -
o o
o -
o
- o o
- o -
- o
- - o
- - -
- -
- o
- -
-
o o
o -
o
- o
- -
-
o
-

と27パターンありますが、特に禁則などを指定せず、組み合わせの強さ2でペアワイズ法を利用した場合、

会員ID パスワード 性別
o o o
o - o
o o
- o -
- - -
- -
o
-
o - x
o -
- o o
- - x
o o
- x

の15パターンに減ります。

なぜ減るかというと、ペアワイズ法でテストケースを作成していく時、個々のテストケースにおいてペアにしていないパラメータの値の影響は気にしないことにしているので、複数のペアについて1つのテストケースである程度同時にテストしていくことが可能になるからです*2

たとえば、上の例だと、最初の9行で

会員ID パスワード
o o
o -
o
- o
- -
-
o
-

のように、会員IDとパスワードのペアについてすべての組み合わせ(9通り)をテストしていますが、この時、

会員ID パスワード 性別
o o o
o - o
o o
- o -
- - -
- -
o
-

のように、性別の値も入れ替えておくことで、

  • 会員IDとパスワードの組み合わせのテストをしながら
  • パスワードと性別の値のペア(9通り)のテストが同時に実行でき
  • 会員IDと性別のペアのテストの一部(3通り)も進められる

ので、ペアワイズ法を用いた場合のテストケースは、もとの3つのパラメータのすべての値の組み合わせをテストしていた場合と比べて 27 パターンから 27 - 9 - 3 = 15 パターンに減ります。

また、これまでパラメータと書いてきた入力条件にあたるものを 因子、因子ごとに取りうる値のことを 水準 と呼びます。

参考

ペアワイズ法を利用する際に使うPICTのツールや、ペアワイズ法そのものについてもっと詳しい解説記事はたくさんありますが、この記事を書く際に参考にしたものをリンクして、この記事はおしまいにします。

http://www.atmarkit.co.jp/ait/articles/1503/16/news149.html https://qiita.com/mitch0807/items/2d8af40fc58d359232ba

*1:これは実際のテストケースの状態によって調整する。ペアを作る時のパラメータの個数を「組み合わせの強さ」といって、2個ずつのパラメータでペアを作る時の組み合わせの強さは2.

*2:実際にその組み合わせはありえない、など、結果に影響があるパラメータについては指定することも可能です http://www.atmarkit.co.jp/ait/articles/1503/16/news149.html

いまさらRSpecを触ってみて最初のうち戸惑っていた部分の話

この記事はRuby on Rails Advent Calendarの1日目の記事です。

実は、この夏からはじめて仕事で本格的に RSpec を触ることになり、少し慣れてきたところで最初に感じたことをメモしておきます。

RSpecの構文は慣れない人にはこんな風に映ることもあるんだーと笑っていただけますと幸いです。

TL;DR

  • subjectlet でテストで利用する変数の宣言と初期化をする記法に慣れるまで時間がかかった
  • it .. do ... end のブロックを example ということ、 example の中にテストコードを書いていくことがわからなかった
  • describecontext に文法上の違いがあるかと思って身構えていた

慣れないうちに戸惑った話

テストコードやテスト対象をどこにどうやって記述したらいいかわからない

なかなか斬新な話なんですが、どういうことかというと、たとえば、自分がこれまで一番テストを書いてきたのはAndroidなので、Java用のテストフレームワーク JUnit5を例に挙げると

// https://junit.org/junit5/docs/current/user-guide/#writing-tests-nested より
@DisplayName("A stack")
class TestingAStackDemo {

    Stack<Object> stack;

    @BeforeEach
      void createNewStack() {
      stack = new Stack<>();
    }

    @Test
    @DisplayName("is instantiated with new Stack()")
    void isInstantiatedWithNew() {
        new Stack<>();
    }

    @Nested
    @DisplayName("when new")
    class WhenNew {

        @BeforeEach
        void createNewStack() {
            stack = new Stack<>();
        }

        @Test
        @DisplayName("is empty")
        void isEmpty() {
            assertTrue(stack.isEmpty());
        }

        // ...

        @Nested
        @DisplayName("after pushing an element")
        class AfterPushing {

            String anElement = "an element";

            @BeforeEach
            void pushAnElement() {
                stack.push(anElement);
            }

            @Test
            @DisplayName("it is no longer empty")
            void isNotEmpty() {
                assertFalse(stack.isEmpty());
            }

            // ...
        }
    }
}

のような感じなんですが、

  • 基本的にテストはクラスの単位でまとめられていて
  • インスタンス変数などの定義は通常のクラスと同様に行ってよく
  • テストの各ケースも通常のメソッドと補足情報をアノテーションで指定したりするが、同様に書いていけばよい
  • 各テストケース実行前の変数の初期化は BeforeEach などのアノテーションがついた箇所で宣言とは別の箇所で行う

んですね。一方同じ内容をRSpecで書くと以下のようになります。

describe "A stack" do
  let(:stack) { Stack.new }

  describe "when new" do
    it "is empty" do
      expect(stack.empty?).to eq true
    end

    # ...

    describe "after pushing an element" do
      let(:an_element) { "an element" }

      it "is no longer empty" do
        expect(stack.empty?).to eq false
      end

      # ...
    end
  end
end
  • まず、テストのためにクラスを定義しない
    • 内部実装をよくわかってないので違うのかもしれませんが、少なくともそういう書き方をしない
  • let で変数の宣言と初期化の方法を一緒に記載してしまう。変数の初期化だけのために前処理のブロックを用意していない
  • テストの内容を書いていく箇所もメソッドではなくてブロックの中

という具合で、かなり戸惑いが大きかったです。

特に3番目について、RSpecでは it ... do ... end のブロックで囲まれた部分にテストのコードを書いていき、その単位を example といって、その中にexampleなどを書いてくのですが、最初は

describe ブロックや context ブロックの中にどうして処理を書いていけないのか( example のそれと同じブロックやんけ)」

みたいな感じであわあわしてました。

また、テスト対象が1つに決まっている場合は subject という機能を使うと

  • テスト対象が1つに決まっていることを明確化され
  • expect(Hogehoge).to みたいにテスト対象の指定をしない is_expected.to という書き方が可能となる

のですが、これも最初「テスト対象どこに行った??」という感じでした。

# subject を使わない場合
describe "A stack" do
  let(:stack) { Stack.new }

  describe "when new" do
    it "is empty" do
      expect(stack.empty?).to eq true
    end
# subject を使う場合
describe "A stack" do
  let(:stack) { Stack.new }
  subject { stack.empty? }
  # 特にテスト対象がインスタンスそのものでなく、
  # インスタンスのメソッドの結果だったりする場合は
  # 直感的に理解できるようになるまで時間がかかった

  describe "when new" do
    it "is empty" do
      is_expected.to eq true
    end

エイリアスがたくさんある

また、テストコードを読んでいると

describe "..." do
  describe "..." do
    it "..." do
      ...
    end
  end

  context "..." do
    it "..." do
    end
  end
end

みたいな同じ文法の位置に違う単語があって、最初は文法的に何か意味があるんだろうか、と思ったんですが、

  • it / specify/ example は同じ意味で1つのテストケースのコードをグループ化するために用いられる
  • describe / context は同じ意味で example をグループ化するために用いられる
  • これらのエイリアスは自然な英語でテストが記述できるように用意されている

そうです。

まとめ

黙って一回チュートリアル。謙虚な気持ち、大事。

現場からは以上です。

慣れてきて覚えたことの話も書こうかと思ったのですが、長くなったので別記事にします。

参考

JUnit 5 User Guide

qiita.com