20211130のRubyに関する記事は12件です。

gem を作って公開してみた 〜準備編〜

0. はじめに Ruby や Railsの開発を行なっているとアプリケーションの機能拡充をしたりする上でgemを使う方はいるがご自身でgemの開発、公開をしたことがある方もいらっしゃらないかと思います。 その方向けにgemの開発〜公開までのフローについてまとめました。 記事としては以下の3つに分けて説明していこうと思います。 今回は準備編です。 準備編 開発編 公開編 今回は私が公開した以下のgemをベースにお話しします。 開発環境 私が上記の開発、公開を行った環境は以下の通りとなります。 OS: Mac ruby: 2.5.8 bundler: 2.2.27 1. 準備編 今回の記事では、gemの開発に必要なものについての紹介とgemのビルド(雛形の作成)までを解説していきます。 2. gemの開発、公開に必要なもの 開発〜公開に必要なものは以下の通りとなります。 メールアドレス 後述するRubyGems.org(gemの公開しているサイト)のアカウント作成、開発する際のgemにemailを記載する箇所等で使います。 RubyGems.org gemの公開時にはこちらのサイト(https://rubygems.org/) のアカウントが必要となります。新規登録からメールアドレス、ユーザー名、パスワードの3項目を入力すれば問題ありません。 詳しい説明は開発編の際に行います bundler 今回では主にgemのビルド時に必要になります 3. 〜gemのビルド 3-1. bundlerのインストール、アップデート すでに最新版のbundlerのインストールをされている方はこちらスキップで構いません。 $ gem install bundler # インストール $ gem update bundler # アップデート 3-2. gemのビルド gemのビルドは以下のコマンドでを実行できます。 $ bundle gem [gem名] -t # ex) bundle gem ruby_calendar -t ※【注意】  [gem名] ですが既に同名の名前のものが公開されている場合開発を終えても公開することができません。 ですので既に同名のものがないかをあらかじめ確認することをオススメします。 確認方法ですが、以下の方法があります(個人的にはCLIでの確認の方が分かりやすいかなと思います) 3-2-1. CLIコマンドでgemの検索を行う $ gem search ^rails *** REMOTE GEMS *** rails (6.1.4.1) # <-- 例えば`rails`という同名のgemでは公開できないということです rails-3-settings (0.1.1) rails-acm (0.1.0) rails-action-args (0.1.1) ... 3-2-2. GUIからgemの検索を行う RubyGemsからでも検索が行えます https://rubygems.org/search?query=rails 3-3. gemのビルド実行時の質問 初めてビルドを実行した際にはいくつか質問されます。対話式なので該当するもので進んでいけばOKです。 Do you want to generate tests with your gem? Future `bundle gem` calls will use your choice. This setting can be changed anytime with `bundle config gem.test`. Enter a test framework. rspec/minitest/test-unit/(none): gemのテストは何を使用するか(rspec/minitest/test-unit/(なし)) gemで使用するテストツールをここで導入できます そのままEnterを押すと何も導入されないで進みます Do you want to set up continuous integration for your gem? Supported services: * CircleCI: https://circleci.com/ * GitHub Actions: https://github.com/features/actions * GitLab CI: https://docs.gitlab.com/ee/ci/ * Travis CI: https://travis-ci.org/ Future `bundle gem` calls will use your choice. This setting can be changed anytime with `bundle config gem.ci`. Enter a CI service. github/travis/gitlab/circle/(none):  CIの導入はどうするか尋ねられます(github/travis/gitlab/circle/(なし)) gemで使用するCIの設定を尋ねられます こちらもそのままエンターを押すと何も導入されないで進みます Do you want to license your code permissively under the MIT license? This means that any other developer or company will be legally allowed to use your code for free as long as they admit you created it. You can read more about the MIT license at https://choosealicense.com/licenses/mit. y/(n): MIT_Licenseを使うかどうかを尋ねられます(yes/(no)) 特段問題なければyesでもよいかと思います Do you want to include a code of conduct in gems you generate? Codes of conduct can increase contributions to your project by contributors who prefer collaborative, safe spaces. You can read more about the code of conduct at contributor-covenant.org. Having a code of conduct means agreeing to the responsibility of enforcing it, so be sure that you are prepared to do that. Be sure that your email address is specified as a contact in the generated code of conduct so that people know who to contact in case of a violation. For suggestions about how to enforce codes of conduct, see https://bit.ly/coc-enforcement. y/(n): Code of Conductを含めるか尋ねられます(yes/(no)) 特段問題なければyesでもよいかと思います Do you want to include a changelog? A changelog is a file which contains a curated, chronologically ordered list of notable changes for each version of a project. To make it easier for users and contributors to see precisely what notable changes have been made between each release (or version) of the project. Whether consumers or developers, the end users of software are human beings who care about what's in the software. When the software changes, people want to know why and how. see https://keepachangelog.com y/(n): changelogを含めてもよいか尋ねられています(yes/(no)) 詳しくは こちら 特段問題なければyesでもよいかと思います(変更履歴を書いておくとgemを利用する人も複数人で開発をする人にも分かりやすくなるはずです) Do you want to add rubocop as a dependency for gems you generate? RuboCop is a static code analyzer that has out-of-the-box rules for many of the guidelines in the community style guide. For more information, see the RuboCop docs (https://docs.rubocop.org/en/stable/) and the Ruby Style Guides (https://github.com/rubocop-hq/ruby-style-guide). y/(n): Rubocopを適用しますかと尋ねられています(yes/(no)) 静的解析ツールで一般的なコーディング規約で書かれているかみてくれるツールです 他のせて機械的ツールやlintツールを利用する予定がなければ入れてもよいかと思います こちらのやりとりを終えれば完成です! ディレクトリにビルドしたgem名のディレクトリ名ができているかと思います。 4. 最後に  こちらで準備についてとビルドまでの流れを説明しました。ひとまずこれでgemの枠組みまでは作成できたかと思います。  次回は開発編です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Apple silicon (M1 Max)でMailCatcherをインストールする

M1 MacでMailCatcher( https://mailcatcher.me/ )をインストールしようとした際にちょっとつまづいたので、解決策を備忘録的に。 環境 MacBook Pro (2021) M1 Max macOS Monterey v12.0.1 ruby 2.6.8p205 (2021-07-07 revision 67951) [universal.arm64e-darwin21] うまくいかなかった時 普通にMailCatcherのサイトにあるように % gem install mailcatcher を叩いたら ERROR: While executing gem ... (Gem::FilePermissionError) You don't have write permissions for the /Library/Ruby/Gems/2.6.0 directory. とパーミッションエラーが出たので、「あちゃ、ヤベヤベ……」とsudoつけて再度gem install mailcatcherをしたんですが…… ERROR: Error installing mailcatcher: ERROR: Failed to build gem native extension. current directory: /Library/Ruby/Gems/2.6.0/gems/thin-1.5.1/ext/thin_parser /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/bin/ruby -I /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0 -r ./siteconf20211130-18489-1g2wtrg.rb extconf.rb checking for main() in -lc... yes creating Makefile current directory: /Library/Ruby/Gems/2.6.0/gems/thin-1.5.1/ext/thin_parser make "DESTDIR=" clean current directory: /Library/Ruby/Gems/2.6.0/gems/thin-1.5.1/ext/thin_parser make "DESTDIR=" compiling parser.c parser.rl:142:7: error: implicit declaration of function 'thin_http_parser_has_error' is invalid in C99 [-Werror,-Wimplicit-function-declaration] if (thin_http_parser_has_error(parser) ) { ^ parser.rl:142:7: note: did you mean 'http_parser_has_error'? ./parser.h:44:5: note: 'http_parser_has_error' declared here int http_parser_has_error(http_parser *parser); ^ parser.rl:144:14: error: implicit declaration of function 'thin_http_parser_is_finished' is invalid in C99 [-Werror,-Wimplicit-function-declaration] } else if (thin_http_parser_is_finished(parser) ) { ^ 2 errors generated. make: *** [parser.o] Error 1 make failed, exit code 2 と再度エラー。 ありゃ、以前のIntel Macの時は問題なく出来てたんだけどな…… thinを個別にインストールしてみるも…… エラーメッセージを見るとthin(Webサーバ)のインストールで躓いているようなので % sudo gem install thin -v '1.5.1' でインストールを試みるも、やはりエラーが出る。え〜ん( ; ; ) どうもこのinvalid in C99というのが怪しい。 エラーメッセージにimplicit declaration of function 'thin_http_parser_has_error' is invalid in C99とあるのでこれでググるとこんなページを発見。 ふむふむ、C言語の規格違反エラーによってビルドが止まっているのね。エラーを無視する指定をしてあげれば良いらしい。 % sudo gem install thin -v '1.5.1' -- --with-cflags="-Wno-error=implicit-function-declaration" これでどうだ! Successfully installed thin-1.5.1 やっほ〜い♪ 最後にもう一度MailCatcherをインストール これで依存ファイルが入ったはずなので、 % sudo gem install mailcatcher でインストールすると、無事インストール出来ました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

教員からエンジニアになってみて

前職について 前職は小学校教員をしていました。 1日の流れとしては、朝7時半には学校に着き、朝からせわしなく過ごすことも多かったです。 朝の7時50分には教室で、子どもたちがやってきた宿題を丸付けしたり子どもの対応など。 そこから子供たちが帰る3時、4時まで働き、そこから会議や保護者対応や明日の授業準備など。 普段帰る時間は19時ごろでした。縛られている時間は多かったかなと思います。 エンジニアになろうと思ったきっかけ このまま教員を続けるか迷っていたことや昔からパソコンが好きなのもあり、働きながら初めは独学でHTMLやCSSのプログラミングの勉強を始めました。 やっているうちにとても楽しくなり、毎日勉強をしていました。転職を考えはじめたのですがどのレベルまでいけば転職ができる、実務ができるのかわからなくて不安だったので、オンラインスクールにも通いました。そのスクールでrailsを学ぶことができ、課題もこなし、実際エンジニアとして働いている人からのコードレビューももらい、自信もついたのでエンジニアに転職しようと決めました。 エンジニアとして働いてみた感想 エンジニアになってみて、会社によって違いますが、私の会社は、フレキシブルタイム制なので大体毎日10時ごろ出勤してます。本当に自分なりのペースで仕事をできるのが、教員時代と違ったなと思います。 働き方もとても自由で、リモートでされている方も多くいてまだ会ったことない人もいます。 場所にしばられすぎないこともいいところですが、私の場合、会社の方が集中しやすいと思いできるだけ出勤するようにしています。しかし、体調が少しすぐれず、家で仕事をしたいというときには、それでも大丈夫なので、そのあたりはフレキシブルでありがたい限りです。 そして、働き方はというと、会社の中のチームで開発をしているので想像していたよりコミュニケーションもあり、休憩をとる時間も自由ですので毎日のお昼ご飯は同じ会社の人といろんなところに食べに出かけています。 仕事は初めはついていけるのかという不安もありましたが、先輩エンジニアの方に教えてもらえたり、会社からも自分にあったものから任されていくので、ついていけないということはありませんでした。毎日成長を感じれているのでモチベーションにもつながっています。 今後はどんなエンジニアを目指したいか 教員を今までしてきていたので、もっと技術力を高めて今度は自分がチームの中でリーダーになり、自分のように経験の少ないエンジニアをマネジメントしていけるようなポジションにつきたいと思っています。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS IoT1-Clickで家族へ「帰ります」通知を送信できるようにしてみました

はじめに 先日、AWSを使ってインフラの構築作業を行ったのですが、AWSにとても興味を持てたので、実際に生活で使えそうな何かを作成しようと思い、色々と考えてみました。 私の友人に「Lambda王」というあだ名の人がおり、私はlambdaを触ったことがなかったので、どんな感じで使えるのかを確かめようと思い、今回はLambdaを取り入れることにしました。 私はいつも仕事から帰る時に奥さんに「今から帰ります」と通知をしているので、これを簡単に実行してくれるものを作ってみました。Lambda関数を実行するトリガーとして「AWS iot-1-click」で関数が実行される仕組みにしています。 すでに、色々な情報が溢れており、今さら感が否めませんが、自分の勉強も兼ねてやってみました。 システム構成図 システム構成は以下の通りです lambdaランタイム設定 Ruby 2.7.0 使用したIotデバイス SORACOM LTE-M Button 開発の流れ 1 SORACOM LTE-M BUttonのデバイス登録 2 LINENotifyでアクセストークンの発行 3 lambda関数の作成 4 コードのアップロード 5 実験・感想 SORACOM LTE-M Buttonのデバイス登録 今回仕様したデバイスは、AWS IoT1-ClickというAWSサービスにデバイスの登録を行わないと使用できません。 デバイス登録に関する流れはこちらを参考にしました。 デバイス登録をする際にDNSという番号を入力しないといけないのですが、その番号がどこにあるのか分からず、苦戦したのでそちらについてご紹介します。 写真を見てもらうと一目瞭然ですが、SORACOM LTE-M Buttonの裏側の電池パックを入れるところにDNS番号が記載されています。 そちらをAWS IoT1-Clickのコンソール画面上に登録します。 LINENotifyでアクセストークンの発行 こちらからLINENotifyのトップページにアクセスし、ログインした後に、「マイページ」をクリックします。 遷移した画面で「トークンを発行」をクリックすると、LINENotifyのアクセストークンが表示されるので、後で使うので忘れないようにコピーして保管しておきます。 このアクセストークンを使ってLINEの特定の通知先に通知を送ることができます。 AWS Lambda AWS Lambdaは、サーバを構築することなく、Lamda関数と呼ばれるコードを実行することができるサービスで、Faas(Function as a Service)と呼ばれるものです。 サーバを構築、管理しなくていい上に、料金も、関数が実行される短い時間だけであり、EC2を構築して運用するよりもメリットが大きいサービスです。 Lambda関数のトリガー Lambda関数を実行するためには、何かしらのきっかけ(トリガー)が必要です。 AWS iot-1-clickのイベントリソースの種類はプッシュ型の中の非同期呼び出し(処理の実行命令だけを送る)になり、今回はこちらがトリガーになります。 Lambda関数のコーディング方法(3つ) コンソール上で直接コードを書く ローカル環境でコードを書いて、zipファイルにしてアップする(容量は10MBまで) S3にアップしたzipファイルを参照する ruby 2.7.0での開発環境構築 lambda関数のRuby対応バージョンがruby2.7.0だったので、開発環境も統一させます。 普段使っているバージョンが異なっているので、今回の開発についてはruby 2.7.0で実現させたいので、バージョンを一時的に変更させます。 d@dMacBook-Pro ~ % rbenv versions system * 2.6.5 (set by /Users/xxxxxxx/.rbenv/version) # rubyバージョンが2.6.5しかなかったので、2.7.0をインストールしていきます d@dMacBook-Pro ~ % brew update d@dMacBook-Pro ~ % rbenv install 2.7.0 d@dMacBook-Pro ~ % rbenv versions system * 2.6.5 (set by /Users/xxxxxxxx/.rbenv/version) 2.7.0 # 現在のディレクトリのプロジェクトのみに適用 $ rbenv local 2.7.0 # 開発環境のみruby 2.7.0になってるのを確認します d@dMacBook-Pro lineNotify_ruby % rbenv local 2.7.0 d@dMacBook-Pro lineNotify_ruby % ruby -v ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-darwin20] # ルートディレクトリではバージョンは元の状態です d@dMacBook-Pro ~ % ruby -v ruby 2.6.5p114 (2019-10-01 revision 67812) [x86_64-darwin20] lambda関数の作成 コードの作成は、こちらの公式を参考にさせていただきました。 https://notify-bot.line.me/doc/ja/ こちらも参考にしています。 https://docs.ruby-lang.org/ja/latest/library/net=2fhttp.html require 'net/http' require 'uri' class LineNotify NOTIFY_TOKEN = ENV["NOTIFY_TOKEN"] APIURI = URI.parse("https://notify-api.line.me/api/notify") def make_request(notify_message) # postリクエスト作成 request = Net::HTTP::Post.new(APIURI) request["Authorization"] = "Bearer #{NOTIFY_TOKEN}" request.set_form_data({message: notify_message}) return request end def send(notify_message) request = make_request(notify_message) response = Net::HTTP.start(APIURI.host, APIURI.port, use_ssl: APIURI.scheme == "https") do |https| https.request(request) end end end line = LineNotify.new notify_message = "今から帰らせていただきます" response = line.send(notify_message) puts response.code puts response.body AWSLambdaにコードをアップする まずは関数を作成します。 この時にランタイム設定のバージョンが開発環境と同じであることを確認します。 関数を作成したら、秘匿情報を環境変数に定義して、関数実行時に読み込めるようにします。 AWSLambdaコンソール画面上の「設定」項目に「環境変数」の項目があるので、ここから設定できます。 AWSLambdaにコードをアップする zipコマンドでファイルを圧縮する d@dMacBook-Pro lineNotify_ruby % zip -r lineNotify_ruby.zip . 詰まりポイント 今回は、ローカルで作成したディレクトリをzipファイルで圧縮してlambdaにアップしました。 コードをアップする際は、作成したプロジェクトディレクトリ内でzipファイルを作成しないと、パスの読み込みがローカル上とlambda上で異なってしまい、エラーが発生するので、注意してください。 また、作成したコードが記載されているファイルは、zipファイルのルートに配置して圧縮するようにします。 AWSLambdaコンソール画面に戻り、圧縮したファイルをアップします。 圧縮ファイルをアップすると、コンソール画面上にアップしたコード類が表示されるようになります。 詰まりポイント このままのコードでは、Lambda関数は実行できません。 Lambda関数が実行される際、コードが記載されているファイル名とランタイム設定に記載されているメソッド名が呼び出されて、関数が実行されます。 コード内には、Lambda関数を実行する際に、呼び出されるメソッドを定義しておく必要があります。 よって、作成したコードを以下のように修正します。 send_iamgoinghome_notify.rb require 'net/http' require 'uri' class LineNotify NOTIFY_TOKEN = ENV["NOTIFY_TOKEN"] APIURI = URI.parse("https://notify-api.line.me/api/notify") def make_request(notify_message) request = Net::HTTP::Post.new(APIURI) request["Authorization"] = "Bearer #{NOTIFY_TOKEN}" request.set_form_data({message: notify_message}) return request end def send(notify_message) request = make_request(notify_message) response = Net::HTTP.start(APIURI.host, APIURI.port, use_ssl: APIURI.scheme == "https") do |https| https.request(request) end end end # ここを修正 def lambda_handler(event:, context:) line = LineNotify.new notify_message = "今から帰らせていただきます" response = line.send(notify_message) puts response.body end lambda_handlerメソッドを定義して、引数にevent:, context:を渡せるようにします。 この引数を記載しないとエラーになります。 LineNotifyクラスを作成するメソッドを定義し、ラインタイムに記載されている呼び出しメソッドがファイル名.定義したメソッド名になっていることを確認してください。 今回の場合は、send_iamgoinghome_notify.lambda_handlerが実行されます。 設定が異なっている場合は、編集して、呼び出されるメソッド名を統一させます。 Lambda関数のテスト 以上の手順でLambda関数を設定したら、テスト実行して、きちんとコードが実行されるか確認します。 デバイスにLambda関数を登録 AWS Iot1-Clickに登録したデバイスに作成したLambda関数を登録します。 まずは、プロジェクトを作成します。 「プロジェクト名」と「説明」を適宜入力します。 プロジェクトを作成した後は、デバイステンプレートを定義し、作成したLambda関数を登録します デバイステンプレートを定義したら、プレイスメントを作成して、完成です。 実験 完成はこのような感じです。 感想 とても簡単に実生活で使えそうなものが完成しました。 いやー、とても面白かったです。 実際に何かを作って完成したら、おー!ってなります。色々と詰まりポイントもあり、自分の勉強にもなりました。 長文記事をご覧くださり、ありがとうございました。また、何か勉強がてら作ってみたいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【オブジェクト指向】カプセル化

カプセル化 オブジェクト同士の紐付き(関係性)を薄くし、独立性を高め、再利用や交換といった保守性を高める効率の考え方。 class Foo def initialize(foo='foo', bar='bar') @foo = foo @bar = bar end end f = Foo.new f.foo #=> NoMethodError: undefined method `foo' for #<Foo:0x0000562351fbab60 @foo="foo", @bar="bar"> attr_accessor :foo, :barと書くだけでインスタンス変数@fooと@barにアクセスできるように class Foo attr_accessor :foo, :bar def initialize(foo='foo', bar='bar') @foo = foo @bar = bar end end f = Foo.new f.foo #=> "foo" f.bar #=> "bar" attr_accessorメソッドを使って書くと メソッドの呼び出し時に毎回変数を書かなくてよい。 class Square attr_accessor :height,:width def initialize(height,width) @height = height @width = width end # 面積計算 def calc_area @height * @width end # 正方形の時trueになる def square? if @height == @width true else false end end end sq = Square.new(20,30) sq.square? #=> false sq.height = 30 #=> 30 sq.calc_area #=> 900 sq.square? #=> true attr_accessorメソッドを使わずに書くと メソッドを呼び出すときに毎回変数が必要になる。 メソッドに新たな変数を加える変更をしたいときに全メソッドに変数を付け加えなければならない。 →カプセル化でオブジェクト同士の独立性を高めてコードを管理しやすくする。 class Square # 面積計算 def calc_area(height,width) height * width end # 正方形の時trueになる def square?(height,width) if height == width true else false end end end sq = Square.new sq.square?(20,30) #=> false sq.calc_area(30,30) #=> 900 sq.square?(30,30) #=> true 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby】メソッドのオーバーライド

メソッドのオーバーライド 親クラスを継承した子クラスで同じメソッドを定義するとメソッドがオーバーライドされる。 class Parent def talk puts '早く寝なさい' end end class Child < Parent def talk puts '眠い' end end p = Parent.new p.talk #=> 早く寝なさい p = Child.new p.talk #=> 眠い super オーバーライドされる前のメソッドを呼び出すことができるメソッド。 superで親クラスのtalkメソッドを呼び出している。 class Parent def talk puts '早く寝なさい' end end class Child < Parent def talk super puts '眠い' end end p.talk #=> 早く寝なさい #=> 眠い 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【RSpec】APIのリクエストテストでgemはあまり使われていないらしい

APIのリクエストテストをやる際、 jsonが期待通りに取れているかをもっとわかりやすく書けないかと思ったときにいくつかgemを発見した。 jsonapi-rspec rspec-json_matcher 最後のコミットが何年も前だったこと、starの数もあまり多くなかったこと、実際に使ってみて使いづらかったことを踏まえてgemは使わずやることにした。 gemを使ったAPIのリクエストテストの方法をあまり記事で見かけなかったので、みんなそもそもgemを使わないのかなーとも思った。 spec.rb ... it '自分の本の情報が昇順に返ること' do json = JSON.parse(response.body) expect(response.status).to eq 200 expect(json['books']).to eq([ {"id"=>book.id, "type"=>"mystery", "date"=>"2021-01-01", "punched_at"=>1609459200}, {"id"=>book2.id, "type"=>"love", "date"=>"2021-01-05", "punched_at"=>1609804800}, {"id"=>book1.id, "type"=>"mystery", "date"=>"2021-01-11", "punched_at"=>1610323200} ]) end ...
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby用テスティングフレームワーク「Oktest.rb」の紹介

(Ruby Advent Calendar 2021 その2 第7日目) 自作の「Oktest.rb」というテスティングフレームワークについて紹介します。Ruby >= 2.0 用です。 URL: https://github.com/kwatch/oktest/tree/ruby/ruby Oktest.rbの特長 「Oktest.rb」は、RSpecをより書きやすくしたようなテスティングフレームワークです。次のような特徴があります。 ok {1+1} == 2 というスタイルでアサーションが書ける。 DI (Dependency Injection) に似た「Fixture Injection」機能。 JSONデータの仕様が記述しやすい。 便利なヘルパー関数が充実。 テストケースのフィルタリングがコマンドラインからできる。 他と比べてコードサイズが小さく、動作も高速(RSpecの約5倍)。 Oktest.rbでのテストコードと、Test::Unitでのテストコードを比較してみます。Oktest.rbのアサーションが直感的であることがわかります。 ### Oktest ### Test::Unit require 'oktest' # require 'test/unit' # Oktest.scope do # # topic "Example" do # class ExampleTest < Test::Unit::TestCase # spec "...description..." do # def test_1 # ...description... ok {1+1} == 2 # assert_equal 2, 1+1 not_ok {1+1} == 3 # assert_not_equal 3, 1+1 ok {3*3} < 10 # assert 3*3 < 10 not_ok {3*4} < 10 # assert 3*4 >= 10 ok {@var}.nil? # assert_nil @var not_ok {123}.nil? # assert_not_nil 123 ok {3.14}.in_delta?(3.1, 0.1) # assert_in_delta 3.1, 3.14, 0.1 ok {'aaa'}.is_a?(String) # assert_kind_of String, 'aaa' ok {'123'} =~ (/\d+/) # assert_match /\d+/, '123' ok {:sym}.same?(:sym) # assert_same? :sym, :sym ok {'README.md'}.file_exist? # assert File.file?('README.md') ok {'/tmp'}.dir_exist? # assert File.directory?('/tmp') ok {'/blabla'}.not_exist? # assert !File.exist?('/blabla') pr = proc { .... } # exc = assert_raise(Error) { .... } ok {pr}.raise?(Error, "mesg") # assert_equal "mesg", exc.message end # end # end # end # end # インストールとスケルトン作成 インストールは gem install oktest で行います。 $ gem install oktest $ oktest --help Oktest.rbにはスケルトン(サンプルコード)を作成する機能があるので、それを使ってみましょう。 $ mkdir test $ oktest --skeleton > test/example_test.rb $ less test/example_test.rb 実行は次のどれかで行います。このうち oktest コマンドだと様々なオプションが指定できます(後述)。 $ ruby test/example_test.rb # ruby <ファイル名> $ oktest test/example_test.rb # oktest <ファイル名> $ oktest test # oktest <ディレクトリ名> 実行すると、次のような表示結果になります。 $ oktest test # または oktest -s verbose test * Class * #method_name() - [pass] 1+1 should be 2. - [pass] fixture injection examle. ## total:2 (pass:2, fail:0, error:0, skip:0, todo:0) in 0.001s Oktest.rbでの表示結果には色がつきます。他のテスティングライブラリでは、正常なら「緑」、失敗やエラーなら「赤」を使いますが、一部の人には緑と赤の区別がつきにくいことが知られています。そのため、Oktest.rbでは正常時の色として「シアン」を使っています(以前は「青」を使っていましたが、ターミナルが黒背景だと青は見にくいので、背景が白でも黒でも見やすいシアンに変更しました)。 チュートリアル 基本的な書き方 Oktest.rbでのテストスクリプトの書き方は、次のようになります。 test/example01_test.rb # coding: utf-8 require 'oktest' class Hello def hello(name="world") return "Hello, #{name}!" end end Oktest.scope do topic Hello do topic '#hello()' do spec "returns greeting message." do actual = Hello.new.hello() ok {actual} == "Hello, world!" end spec "accepts user name." do actual = Hello.new.hello("RWBY") ok {actual} == "Hello, RWBY!" end end end end これを見ると、RSpecのような構造化された記述をしつつ、RSpecより直感的にアサーションが書けることが分かります。 テストケースの構造を topic() と spec() で記述し、全体を Oktest.scope() で囲む。 アサーションは ok {actual} == expected のように記述する。 全体を `Oktest.scope()` で囲むのは、トップレベルに `topic()` や `spec()` を定義するのを避けるためです。RSpecではトップレベルに `describe()` などのメソッドを定義するので、RSpecを使った場合とそうでない場合でプログラムの挙動が変わる可能性があります。テスティングフレームワークはこのような「汚染」をすべきではない(あるいは最小限に抑えるべき)という思想のもとに、Oktest.rbは設計されています。 アサーションが `ok(1+1) == 2` ではなく `ok {1+1} == 2` という書き方になっているのは、`ok` の直後に空白があったほうがアサーションが読みやすいからです。Ruby では `ok (1+1) == 2` と書くと `ok((1+1) == 2)` とみなされてしまうので、この書き方は採用しませんでした(`(ok (1+1)) == 2` とみなされないと実装上困るのです)。また `ok_(1+1) == 2` という書き方も検討しましたが、やはり `ok` の直後は半角空白が望ましいと思い、最終的に `ok {1+1} == 2` という書き方を採用しました。これだと `ok` の直後に半角空白があっても `(ok {1+1}) == 2` と解釈されるので、見た目も実装上も都合がよいです。 このテストスクリプトを実行してみましょう。 $ oktest test/example01_test.rb # または ruby test/example01_test.rb ## test/example01_test.rb * Hello * #hello() - [pass] returns greeting message. - [pass] accepts user name. ## total:2 (pass:2, fail:0, error:0, skip:0, todo:0) in 0.000s なお topic() の引数には任意の値を指定でき、たとえば topic Hello do のようにクラスオブジェクトを指定できます。これに対して、spec() の引数には文字列を指定します。 また RSpec で仕様を記述するには it で始まる文章にする必要がありますが、Oktest.rb では it() ではなく spec() を使うのでそのような制約がなく、自然な文章で仕様を記述できます。 アサーションの失敗、およびエラー アサーションが失敗したり、何らかのエラー(例外)が発生したときの表示を見てみましょう。 test/example02_test.rb require 'oktest' Oktest.scope do topic 'other examples' do spec "example of assertion failure" do ok {1+1} == 2 # pass ok {1+1} == 0 # FAIL end spec "example of something error" do x = foobar # NameError end end end 実行結果: $ oktest test/example02_test.rb # or: ruby test/example02_test.rb ## test/example02_test.rb * other examples - [Fail] example of assertion failure - [ERROR] example of something error ---------------------------------------------------------------------- [Fail] other examples > example of assertion failure test/example02_test.rb:9:in `block (3 levels) in <main>' ok {1+1} == 0 # FAIL $<actual> == $<expected>: failed. $<actual>: 2 $<expected>: 0 ---------------------------------------------------------------------- [ERROR] other examples > example of something error test/example02_test.rb:13:in `block (3 levels) in <main>' x = foobar # NameError NameError: undefined local variable or method `foobar' for #<#<Class:...>:...> ---------------------------------------------------------------------- ## total:2 (pass:0, fail:1, error:1, skip:0, todo:0) in 0.000s ここでは色がついていませんが、実際には失敗とエラーは赤色で表示されます。 アサーションが未実行のときは警告 失敗やエラーとは違いますが、ok{} が呼び出されたけどアサーションが何も実行されなかった場合は、警告メッセージが出ます。 たとえば ok {1+1} == 2 と書くつもりが間違って ok {1+1} としか書かれなかった場合、アサーションが何も実行されないので、「warning: ok() is called but not tested yet」という警告メッセージが出ます。 あるいは ok {1+1} == var と書くつもりが変数名を間違えて ok {1+1} == vaa と書いてしまったとしましょう。この場合、vaa の時点で NameError が発生します。すると ok {1+1} は呼ばれたもののアサーションである == が実行されないため、やはり「warning: ok() is called but not tested yet」という警告メッセージが(NameError とは別に)出ます。 この警告メッセージが出たら、アサーションが意図せず未実行になっているので、たとえ失敗やエラーになっていなくてもテストコードを修正してください。 テストケースのスキップとToDo 何らかの条件によってテストケースをスキップするには、skip_when <条件>, <理由> を使います。 またプログラムは未実装だけどテストコードを先に用意した場合は、TODO() を使います。テストコードをまだ書いてないけどテストすべき内容を書き留めておきたいときは、spec() のブロック引数を未指定にします。 test/example03_test.rb require 'oktest' Oktest.scope do topic 'other examples' do spec "example of skip" do # Rubyのバージョンが3.0未満ならスキップする skip_when RUBY_VERSION < "3.0", "requires Ruby3" ok {1+1} == 2 end spec "example of todo" # ブロック引数のない spec() は TODO 相当 spec "example of todo (when passed unexpectedly)" do TODO() # プログラムが未実装なので、 # このテストケースは失敗になるべき。 ok {1+1} == 2 # もし失敗しなければ、それは # 「意図しない成功」という失敗になる。 end end end 実行結果: $ oktest test/example03_test.rb # or: ruby test/example03_test.rb ## oktest test/example03_test.rb * other examples - [Skip] example of skip (reason: requires Ruby3) - [TODO] example of todo - [Fail] example of todo (when passed unexpectedly) ---------------------------------------------------------------------- [Fail] other examples > example of todo (when passed unexpectedly) test/example03_test.rb:14:in `block (2 levels) in <top (required)>' spec "example of todo (when passed unexpectedly)" do spec should be failed (because not implemented yet), but passed unexpectedly. ---------------------------------------------------------------------- ## total:2 (pass:0, fail:1, error:0, skip:1, todo:1) in 0.000s 実行結果の表示スタイル Oktest.rb では、-s <STYLE> オプションで表示スタイルを変更できます。オプションの指定は、ファイル名の前と後のどちらでも構いません(つまり oktest -s <STYLE> <file> でも oktest <file> -s <STYLE> のどちらでもよい)。 Verboseモード(デフォルト): $ oktest test/example01_test.rb -s verbose # or -sv ## test/example01_test.rb * Hello * #hello() - [pass] returns greeting message. - [pass] accepts user name. ## total:2 (pass:2, fail:0, error:0, skip:0, todo:0) in 0.000s Simpleモード: $ oktest test/example01_test.rb -s simple # or -ss ## test/example01_test.rb * Hello: * #hello(): .. ## total:2 (pass:2, fail:0, error:0, skip:0, todo:0) in 0.000s Compactモード: $ oktest test/example01_test.rb -s compact # or -sc test/example01_test.rb: .. ## total:2 (pass:2, fail:0, error:0, skip:0, todo:0) in 0.000s Plainモード: $ oktest test/example01_test.rb -s plain # or -sp .. ## total:2 (pass:2, fail:0, error:0, skip:0, todo:0) in 0.000s Quietモード: $ oktest test/example01_test.rb -s quiet # or -sq ## total:2 (pass:2, fail:0, error:0, skip:0, todo:0) in 0.000s -s オプションは、先頭の1文字だけでも指定できます。たとえば -s plain のかわりに -sp と指定できます。 デフォルトでは -s verbose スタイルで表示されます。デフォルトを変更したいなら、環境変数 $OKTEST_RB にコマンドオプションを設定してください。 $ export OKTEST_RB="-s plain" ディレクトリを指定して実行する oktest コマンドの引数に、ファイル名ではなくディレクトリ名を指定すると、そのディレクトリ以下のテストスクリプトがすべて実行されます。 たとえば、test ディレクトリに3つのテストスクリプトがあるとします。 $ ls test/ example01_test.rb example02_test.rb example03_test.rb このようなとき、oktest test を実行すると3つのテストスクリプトがすべて読み込まれて実行されます。 $ oktest -s compact test # or: ruby -r oktest -e 'Oktest.main' -- test -s compact test/example01_test.rb: .. test/example02_test.rb: fE ---------------------------------------------------------------------- [Fail] other examples > example of assertion failure test/example02_test.rb:9:in `block (3 levels) in <top (required)>' ok {1+1} == 0 # FAIL -e:1:in `<main>' $<actual> == $<expected>: failed. $<actual>: 2 $<expected>: 0 ---------------------------------------------------------------------- [ERROR] other examples > example of something error test/example02_test.rb:13:in `block (3 levels) in <top (required)>' x = foobar # NameError -e:1:in `<main>' NameError: undefined local variable or method `foobar' for #<#<Class:...>:...> ---------------------------------------------------------------------- test/example03_test.rb: st ## total:6 (pass:2, fail:1, error:1, skip:1, todo:1) in 0.000s テストスクリプトのファイル名は、`test_xxx.rb` または `xxx_test.rb` である必要があります。`test-xxx.rb` や `xxx-test.rb` ではないので注意してください。 `oktest` コマンドを使わず、`ruby -r oktest -e 'Oktest.main' -- test -s compact` のように実行することもできます。これはRubyコマンドのパスを指定して実行したい(たとえば `/usr/bin/ruby` ではなく `/opt/local/bin/ruby` を使いたい)場合に便利です。 タグとフィルタリング scope() と topic() と spec() には、キーワード引数を使ってタグを指定できます。 test/example04_test.rb require 'oktest' Oktest.scope do topic 'Example topic' do topic Integer do spec "example #1" do ok {1+1} == 2 end spec "example #2", tag: 'old' do # タグ名: 'old' ok {1-1} == 0 end end topic Float, tag: 'exp' do # タグ名: 'exp' spec "example #3" do ok {1.0+1.0} == 2.0 end spec "example #4" do ok {1.0-1.0} == 0.0 end end topic String, tag: ['exp', 'old'] do # タグ名: 'old' と 'exp' spec "example #5" do ok {'a'*3} == 'aaa' end end end end タグ名を使って、テストケースをフィルタリングできます。つまり、特定のテストケースだけを実行できます。フィルタリングには、* や ? や [] や {} のようなメタキャラクタが使えます。 $ oktest -F tag=exp test/ # タグ名でフィルタ $ oktest -F tag='*exp*' test/ # タグ名のパターンでフィルタ $ oktest -F tag='{exp,old}' test/ # 複数のタグ名でフィルタ タグ名ではなく、トピック名やスペック名でもフィルタできます。 $ oktest -F topic='*Integer*' test/ # トピック名のパターンでフィルタ $ oktest -F spec='*#[1-3]' test/ # スペック名のパターンでフィルタ 特定のテストケースだけを除外して実行したい場合は、= のかわりに != を使って指定します。 $ oktest -F spec!='*#5' test/ # スペック 'example #5' を除外 $ oktest -F tag!='{exp,old}' test/ # タグ名 'exp' と 'old' を除外 case_when と case_else case_when() と case_else() は、仕様の条件を表現します。skip_when() とは違い、条件に合った場合だけ実行する/スキップするというものではないので、間違えないようにしてください(実際のところ、case_when() と case_else() は内部的には topic() の一種なのです)。 test/example05_test.rb require 'oktest' Oktest.scope do topic Integer do topic '#abs()' do case_when "value is negative..." do spec "converts value into positive." do ok {-123.abs()} == 123 end end case_when "value is zero..." do spec "returns zero." do ok {0.abs()} == 0 end end case_else do spec "returns itself." do ok {123.abs()} == 123 end end end end end 実行結果: $ ruby test/example05_test.rb ## test/example05_test.rb * Integer * #abs() - When value is negative... - [pass] converts value into positive. - When value is zero... - [pass] returns zero. - Else - [pass] returns itself. ## total:3 (pass:3, fail:0, error:0, skip:0, todo:0) in 0.001s 単項演算子 topic() には単項演算子の + がつけられます。また spec() には単項演算子の - がつけられます。これらをつけると、テストケースの構造が視覚的にわかりやすくなります。 require 'oktest' Oktest.scope do + topic('example') do # 単項演算子の `+` + topic('example') do # 単項演算子の `+` - spec("1+1 is 2.") do # 単項演算子の `-` ok {1+1} == 2 end - spec("1*1 is 1.") do # 単項演算子の `-` ok {1*1} == 1 end end end end 単項演算子をつけるときは、+ topic '...' do や - spec "..." do のように書くとRubyの文法エラーになります。かわりに、+ topic('...') do や - spec("...") do のように書いてください。 単項演算子をつけると、エディタやIDEでの自動インデント機能が意図したようには機能しなくなります。「テストケースの構造が視覚的にわかりやすくなる」というメリットはあるものの、デメリットも大きいので、無理して採用する必要はありません。 テストコードの自動生成 Oktest.rb では、プログラムを読み込んでテストコードのひな形を自動生成する機能があります。クラスやメソッドが topic() の引数になり、プログラム中の #; から始まる行コメントが spec() の引数になります。 たとえば次のようなプログラムがあるとします。 hello.rb class Hello def hello(name=nil) #; default name is 'world'. if name.nil? name = "world" end #; returns greeting message. return "Hello, #{name}!" end end このプログラムからテストコードのひな形を生成するには、次のようにします。 $ oktest --generate hello.rb > test/hello_test.rb 生成されたテストコードは次のようになります。 test/hello_test.rb # coding: utf-8 require 'oktest' Oktest.scope do topic Hello do topic '#hello()' do spec "default name is 'world'." spec "returns greeting message." end # #hello() end # Hello end またコマンドラインオプションを --generate=unaryop にすると、topic() と spec() に単項演算子がつきます。 $ oktest --generate=unaryop hello.rb > test/hello2_test.rb test/hello2_test.rb # coding: utf-8 require 'oktest' Oktest.scope do + topic(Hello) do + topic('#hello()') do - spec("default name is 'world'.") - spec("returns greeting message.") end # #hello() end # Hello end トピック内でのメソッド定義 topic() のブロック内でメソッドを定義すると、それを spec() の中から呼び出せます。 require 'oktest' Oktest.scope do topic "Method example" do def hello() # topic() のブロック内でメソッドを定義 return "Hello!" end spec "example" do s = hello() # spec() のブロック内でそれを呼び出す ok {s} == "Hello!" end end end 親トピック内で定義されたメソッドも呼び出せます。子トピック内で定義されたメソッドは呼び出せません。 require 'oktest' Oktest.scope do + topic('Outer') do + topic('Middle') do def hello() # topic() ブロック内でメソッドを定義 return "Hello!" end + topic('Inner') do - spec("inner spec") do s = hello() # 親トピックで定義されたメソッドを呼び出すのはOK ok {s} == "Hello!" end end end - spec("outer spec") do s = hello() # 子トピックで定義されたメソッドを呼び出すのはエラー ok {x} == "Hello!" end end end アサーション RSpec ではアサーションの書き方が複雑なので、実のところ「この書き方でいいのかな?」と疑念を抱きながら書いてる人も多いのではないでしょうか。Oktset.rb ではアサーションがとても直感的なので、自信を持ってアサーションが書けます。 アサーションの書き方 Oktest.rb では、アサーションを以下のように書きます。ここで a は actual を表し、e は expected を表すものとします。 ok {a} == e # a == e なら成功 ok {a} != e # a != e なら成功 ok {a} === e # a === e なら成功 ok {a} !== e # a !== e なら成功 ok {a} > e # a > e なら成功 ok {a} >= e # a >= e なら成功 ok {a} < e # a < e なら成功 ok {a} <= e # a <= e なら成功 ok {a} =~ e # a =~ e なら成功 ok {a} !~ e # a !~ e なら成功 ok {a}.same?(e) # a.equal?(e) なら成功 ok {a}.in?(e) # e.include?(a) なら成功 ok {a}.in_delta?(e, x) # e-x < a < e+x なら成功 ok {a}.truthy? # !!a == true なら成功 ok {a}.falsy? # !!a == false なら成功 ok {a}.file_exist? # File.file?(a) なら成功 ok {a}.dir_exist? # File.directory?(a) なら成功 ok {a}.symlink_exist? # File.symlink?(a) なら成功 ok {a}.not_exist? # ! File.exist?(a) なら成功 ok {a}.attr(name, e) # a.__send__(name) == e なら成功 ok {a}.keyval(key, e) # a[key] == e なら成功 ok {a}.item(key, e) # ok {a}.keyval(key, e) と同じ ok {a}.length(e) # a.length == e なら成功 .attr() と .keyval() は、メソッドを連続して呼び出せます。 ok {a}.attr(:name1, 'val1').attr(:name2, 'val2').attr(:name3, 'val3') ok {a}.keyval(:key1, 'val1').keyval(:key2, 'val2').keyval(:key3, 'val3') 述語アサーション 述語メソッド(メソッド名が '?' で終わり、かつtrueまたはfalseを返すようなメソッド)は、自動的にアサーションとして利用できます。 ok {a}.nil? # ok {a.nil?} == true に相当 ok {a}.empty? # ok {a.empty?} == true に相当 ok {a}.key?(e) # ok {a.key?(e)} == true に相当 ok {a}.is_a?(e) # ok {a.is_a?(e)} == true に相当 ok {a}.include?(e) # ok {a.include?(e)} == true に相当 ok {a}.between?(x, y) # ok {a.between?(x, y)} == true に相当 たとえば、ok {a}.empty? は ok {a.empty?} == true に相当します。しかし a.empty? が false だったとき、前者では a の値がエラーメッセージの中に表示されるのに対し、後者では a の値が表示されません。アサーション失敗時のために、後者ではなく前者の書き方をしてください。 Ruby の `Object#equal?()` はどのクラスでも上書きすべきではないため、`ok {actual}.equal?(expected)` と書いても述語アサーションとして機能しません。かわりに `ok {actual}.same?(expected)` を使ってください。 述語アサーションの活用例として、Pathname() を挙げておきます。Pathname() オブジェクトには述語メソッドがたくさん定義されているので、それらをアサーションとして利用できます。Pathname() についてはpathname.rbのマニュアルを参照してください。 require 'pathname' # !!!!! ok {Pathname(a)}.owned? # ok {Pathname(a).owned?} == true に相当 ok {Pathname(a)}.readable? # ok {Pathname(a).readable?} == true に相当 ok {Pathname(a)}.writable? # ok {Pathname(a).writable?} == true に相当 ok {Pathname(a)}.absolute? # ok {Pathname(a).absolute?} == true に相当 ok {Pathname(a)}.relative? # ok {Pathname(a).relative?} == true に相当 後述するように、Oktest.rbではカスタムアサーションを定義できます。しかしそれだとOktest.rbに強く依存してしまいます。それよりも Pathname() のような述語メソッドを備えたラッパーオブジェクトを使うことを検討してください。 否定アサーション not_ok{} または ok{}.NOT を使うと、アサーションの意味を反転できます。 not_ok {a} == e # a == e なら失敗 ok {a}.NOT == e # a == e なら失敗 not_ok {a}.file_exist? # File.file?(a) なら失敗 ok {a}.NOT.file_exist? # File.file?(a) なら失敗 実際には、ok {a}.NOT == e ではなく ok {a} != e と書くべきだし、ok {a}.NOT.file_exist? は ok {a}.not_exist? とすべきです。後述する ok {}.NOT.raise? を除いて、否定アサーションはなるべく使わないほうがいいでしょう。 例外のアサーション 例外が発生するかどうかのアサーションは、次のようにします。 ## 基本的な書き方 pr = proc do "abc".len() # NoMethodError が発生 end ok {pr}.raise?(NoMethodError) ok {pr}.raise?(NoMethodError, "undefined method `len' for \"abc\":String") ok {pr}.raise?(NoMethodError, /^undefined method `len'/) ok {pr}.raise? # NoMethodError例外が発生すれば成功、何も発生しなければ失敗 ## 例外オブジェクトを使いたいとき ok {pr}.raise?(NoMethodError) {|exc| ok {exc.class} == NoMethodError ok {exc.message} == "undefined method `len' for \"abc\":String" } raise ではなく throw されたかを調べるには、ok{}.throw? を使います。 ## Symbolをthrowする場合の書き方 pr2 = proc do throw :quit # :quit を throw する(raise ではないことに注意) end ok {pr2}.throw?(:quit) # :quit が throw されたら成功、それ以外は失敗 例外が何も発生しないことを確かめたいときは、否定アサーションを使います。このとき、例外クラスとエラーメッセージは指定しません。 ok {pr}.NOT.raise? # 例外クラスとエラーメッセージは何も指定しないこと not_ok {pr}.raise? # 上と同じ もしテスト対象のプロラムが例外クラスを指定せずに raise している場合は、アサーションでも例外クラスを省略してエラーメッセージだけを指定できます。 pr = proc do raise "something wrong" # 例外クラスを省略してraiseしてるなら、 end ok {pr}.raise?("something wrong") # アサーションでも例外クラスを省略できる 実は、ok{}.raise?() では例外クラスの比較を == 演算子で行っており、.is_a?() メソッドは使っていません。これは次のようなサンプルコードで確かめられます。 ## 前提:ZeroDivisionError は StandardError を継承しており、 ## StandardError は Exception を継承している pr = proc { 1/0 } # ZeroDivisionError が発生 ok {pr}.raise?(ZeroDivisoinError) # 成功(exc.class == ZeroDivisionError だから) ok {pr}.raise?(StandardError) # エラー(exc.class != StandardError だから) ok {pr}.raise?(Exception) # エラー(exc.class != Exception だから) 奇妙に感じるかもしれませんが、これは「アサーションの意図しない成功」を防ぐための仕様です。たとえば MiniTest では assert_raises(NameError) { .... } というアサーションが、NameError だけでなく NoMethodError が発生したときでも成功となってしまいます。なぜなら、Ruby では NoMethodError クラスが NameError クラスを継承しているからです。 require 'minitest/spec' require 'minitest/autorun' describe "assert_raise()" do it "results in success unexpectedly" do ## NameError と指定しているのに NoMethodError もキャッチしてしまう。 ## なぜなら NoMethodError は NameError のサブクラスだから。 assert_raises(NameError) do "str".foobar() # この行はNoMethodErrorを発生する。 end end end Oktest.rb ではこのような「アサーションの意図しない成功」を防げます。なぜなら、ok{}.raise?() では例外クラスの比較を == で行い、.is_a?() を使わないからです。 require 'oktest' Oktest.scope do topic 'ok().raise?' do spec "doesn't catch subclasses." do pr = proc do "str".foobar() # NoMethodError が発生 end ok {pr}.raise?(NoMethodError) # 成功 ok {pr}.raise?(NameError) # エラー:NoMethodError が発生 end end end もし子クラスの例外も対象としたい場合は、ok{}.raise? のかわりに ok{}.raise! を使ってください。上のサンプルコードを、.raise? から .raise! に変更すると次のようになります。 require 'oktest' Oktest.scope do topic 'ok().raise!' do spec "catches subclasses." do pr = proc do "str".foobar() # NoMethodError が発生 end ok {pr}.raise!(NoMethodError) # 成功 ok {pr}.raise!(NameError) # これも成功!!!!! end end end カスタムアサーション 独自のアサーションメソッドを追加するには、次のようにします。 require 'oktest' ## 独自のアサーションメソッドを追加する Oktest::AssertionObject.class_eval do def readable? # custom assertion: file readable? _done() result = File.readable?(@actual) __assert(result == @bool) { "File.readable?($<actual>) == #{@bool}: failed.\n" + " $<actual>: #{@actual.inspect}" } self end end ## 使い方 Oktest.scope do topic "Custom assertion" do spec "example spec" do ok {__FILE__}.readable? # 独自のアサーションメソッド end end end カスタムアサーションを使うよりも、述語メソッドを備えたラッパーオブジェクト(例:`Pathname()`)を使うことを検討してください。作り方は次を参考にしてください。 class AssertDate def initialize(actual) @actual = actual end def inspect() # エラーメッセージの表示に必要 return @actual.inspect() end def year?(expected) ; @actual.year == expected; end def month?(expected); @actual.month == expected; end def day?(expected) ; @actual.day == expected; end def end_of_month? nextday = @actual + 1 return nextday.day == 1 end end def AssertDate(actual) # 好みでどうぞ return AssertDate.new(actual) end ## 使い方 actual = Date.new(2021, 12, 31) ok {AssertDate.new(actual)}.year?(2021) ok {AssertDate.new(actual)}.end_of_month? フィクスチャ テストケースの前処理と後処理 テストケースの前処理と後処理には、次のメソッドを使います。 before do ... end ‥‥ spec() ごとの前処理 after do ... end ‥‥ spec() ごとの後処理 before_all do ... end ‥‥ topic() ごとの後処理 after_all do ... end ‥‥ topic() ごとの後処理 test/example21a_test.rb require 'oktest' Oktest.scope do topic "Fixture example" do before do # spec() ごとの前処理 puts "=== before() ===" end after do # spec() ごとの後処理 puts "=== after() ===" end before_all do # topic() ごとの前処理 puts "*** before_all() ***" end after_all do # topic() ごとの後処理 puts "*** after_all() ***" end spec "example spec #1" do puts "---- example spec #1 ----" end spec "example spec #2" do puts "---- example spec #2 ----" end end end 実行結果: $ oktest -s quiet test/example21a_test.rb *** before_all() *** ← before_all()を実行 === before() === ← before()を実行 ---- example spec #1 ---- === after() === ← after()を実行 === before() === ← before()を実行 ---- example spec #2 ---- === after() === ← after()を実行 *** after_all() *** ← after_all()を実行 ## total:2 (pass:2, fail:0, error:0, skip:0, todo:0) in 0.000s before()/after()/before_all()/after_all() は、topic() または scope() ごとに定義できます。 before() ブロックは、内側の topic/scope で定義されたものよりも、外側の topic/scope で定義されたもののほうが先に呼び出される。 after() ブロックは、外側の topic/scope で定義されたものよりも、内側の topic/scope で定義されたもののほうが先に呼び出される。 test/example21b_test.rb require 'oktest' Oktest.scope do topic 'Outer' do before { puts "=== Outer: before ===" } # !!!!! after { puts "=== Outer: after ===" } # !!!!! topic 'Middle' do before { puts "==== Middle: before ====" } # !!!!! after { puts "==== Middle: after ====" } # !!!!! topic 'Inner' do before { puts "===== Inner: before =====" } # !!!!! after { puts "===== Inner: after =====" } # !!!!! spec "example" do ok {1+1} == 2 end end end end end 実行結果: $ oktest -s quiet test/example21b_test.rb === Outer: before === ==== Middle: before ==== ===== Inner: before ===== ===== Inner: after ===== ==== Middle: after ==== === Outer: after === ## total:1 (pass:1, fail:0, error:0, skip:0, todo:0) in 0.000s もし before/after/before_all/after_all の各ブロックの中でエラーが発生したら、テストスクリプトの実行は即時に中止されます。 クリーンアップハンドラ at_end() at_end() を使うと、テストケース実行後に行う後始末処理を登録できます。after() と違い、テストケースごとに異なる後始末処理を at_end() では登録できます。 test/example22_test.rb require 'oktest' Oktest.scope do topic "Auto clean-up" do spec "example spec" do tmpfile = "tmp123.txt" File.write(tmpfile, "foobar\n") at_end do # 後始末処理を登録してから File.unlink(tmpfile) end # ok {tmpfile}.file_exist? # アサーションを実行する end end end 1つのテストケースの中で at_end() を複数回呼び出せます。 at_end() で登録された処理がテストケースの終わりに実行されます。このとき、登録されたのとは逆の順番で処理が実行されます。 at_end() で登録された処理が実行されてから、after() のブロックが実行されます。 at_end() で登録された処理でエラーが発生すると、テストスクリプトの実行が中断します。 名前つきフィクスチャ topic() または scope() のブロックの中において、fixture() { ... } はフィクスチャを定義します(つまりフィクスチャを生成するブロックを登録します)。 また spec() のブロックの中では、fixture() はフィクスチャを生成します。 test/example23_test.rb require 'oktest' Oktest.scope do fixture :alice do # フィクスチャを定義 {name: "Alice"} end fixture :bob do # フィクスチャを定義 {name: "Bob"} end topic "Named fixture" do spec "example spec" do alice = fixture(:alice) # フィクスチャを生成 bob = fixture(:bob) # フィクスチャを生成 ok {alice[:name]} == "Alice" ok {bob[:name]} == "Bob" end end end フィクスチャ定義では、ブロック引数を指定できます。 test/example24_test.rb require 'oktest' Oktest.scope do fixture :team do |mem1, mem2| # 引数つきフィクスチャ定義 {members: [mem1, mem2]} end topic "Named fixture with args" do spec "example spec" do alice = {name: "Alice"} bob = {name: "Bob"} team = fixture(:team, alice, bob) # 引数を指定してフィクスチャを生成 ok {team[:members][0][:name]} == "Alice" ok {team[:members][1][:name]} == "Bob" end end end フィクスチャ定義は、topic() または Oktest.scope() の中でのみ行なえます。 フィクスチャの後始末が必要なら、フィクスチャ定義のブロックの中で at_end() を使ってください。 ## フィクスチャを定義する fixture :tmpfile do tmpfile = "tmp#{rand().to_s[2..5]}.txt" File.write(tmpfile, "foobar\n", encoding: 'utf-8') ## 後始末が必要なので at_end を使う at_end { File.unlink(tmpfile) if File.exist?(tmpfile) } # !!!!! tmpfile end フィクスチャ・インジェクション (Fixture Injection) spec() と fixture() のブロック引数は、フィクスチャ名を表します。Oktest.rb は、フィクスチャ名をもとにフィクスチャを生成し、自動的に spec() と fixture() のブロック引数に指定します。これはいわゆる DI (Dependency Injection) と同じ仕組みです。 test/example25_test.rb require 'oktest' Oktest.scope do fixture :alice do # フィクスチャを定義 {name: "Alice"} end fixture :bob do # フィクスチャを定義 {name: "Bob"} end ## fixture() のブロック引数に、フィクスチャが自動的に生成されて渡される fixture :team do |alice, bob| # !!! フィクスチャ・インジェクション !!! {members: [alice, bob]} end topic "Fixture injection" do ## spec() のブロック引数に、フィクスチャが自動的に生成されて渡される spec "example spec" do |alice, bob, team| # !!! フィクスチャ・インジェクション !!! ok {alice[:name]} == "Alice" ok {bob[:name]} == "Bob" # ok {team[:members]}.length(2) ok {team[:members][0]} == {name: "Alice"} ok {team[:members][1]} == {name: "Bob"} end end end 実験的な機能として、フィクスチャ名に `this_topic` を指定すると `topic()` の第1引数の値がインジェクトされ、また `this_spec` を指定すると `spec()` の第1引数の値がインジェクトされます。これは実験的機能なので、将来変更・廃止される可能性があります。 フィクスチャ定義のブロックに引数があると、その引数名に対応した他のフィクスチャがインジェクトされます。これによりフィクスチャの依存関係を指定できます。 test/example26_test.rb require 'oktest' Oktest.scope do ## 引数をとるフィクスチャ fixture :team do |alice, bob| # alice と bob に依存する {members: [alice, bob]} end fixture :alice do {name: "Alice"} end fixture :bob do {name: "Bob"} end topic 'Example' do ## フィクスチャ「team」を指定すると、自動的に「alice」と「bob」も生成される spec "team fixture" do |team| ok {team} == {members: [{name: "Alice"}, {name: "Bob"}] } end end end フィクスチャの依存関係がループしていると、実行時エラーになります。たとえば `fixture :a do |b| 1 end` と `fixture :b do |a| 2 end` という定義だと、フィクスチャ a が フィクスチャ b に依存し、かつ b が a に依存しています(相互依存)。この場合、a または b を生成すると `Oktest::LoopedDependencyError: fixture dependency is looped: a=>b=>a` というエラーが発生します。 依存関係のチェックはフィクスチャ定義の段階では行われず、フィクスチャをインジェクションするときに行われます。言い換えると、依存関係にループがあってもテストスクリプトを読み込んだだけではエラーは発生せず、実行してみて初めて発生します。 scope() に fixture: というキーワード引数を指定すると、フィクスチャの値を上書きできます。「このテストケースではフィクスチャの値を少し変えたい」という場合に利用できます。 test/example27_test.rb require 'oktest' Oktest.scope do fixture :user do |uname, uid: 101| # `uid` はキーワード引数 {name: uname, id: uid} end fixture :uname do "Alice" end topic 'Example' do ## spec() のキーワード引数 `fixture:` で、フィクスチャの値を上書き spec "example", fixture: {uname: "Bob", uid: 201} do # !!!!! |user| ok {user[:name]} == "Bob" # != "Alice" ok {user[:id]} == 201 # != 101 end end end fixture: キーワード引数は使いこなすのに慣れが必要なので、たいていの場合は spec() の中で fixture() を素直に呼び出すほうが簡単でしょう。 test/example28_test.rb require 'oktest' Oktest.scope do fixture :user do |uname, uid: 101| # `uid` はキーワード引数 {name: uname, id: uid} end fixture :uname do "Alice" end topic 'Example' do spec "example" do # `fixture:` キーワード引数を止めて、 user = fixture(:user, "Bob", uid: 201) # かわりに `fixture()` を呼び出す ok {user[:name]} == "Bob" # != "Alice" ok {user[:id]} == 201 # != 101 end end end グローバルスコープ よく使うフィクスチャは、テストスクリプトごとに定義するよりも、共通で使うファイルに分離するほうがいいでしょう。このような場合は、Oktest.scope() のかわりに Oktest.global_scope() を使ってください。 test/fixtures.rb require 'oktest' ## 複数のテストスクリプトで使われるフィクスチャは、 ## グローバルスコープの中で定義する Oktest.global_scope do # これがグローバルスコープ fixture :alice do {name: "Alice"} end fixture :bob do {name: "Bob"} end fixture :team do |alice, bob| {members: [alice, bob]} end end 他のテストスクリプトでこれらのフィクスチャを使うには、このファイルを読み込んでください(例:require_relative 'fixtures')。 ヘルパー関数 Oktest.rbには、テストを書くのに便利な関数があらかじめ用意されています。ここではそれらを紹介します。 capture_sio() capture_sio() は、標準入力と標準出力と標準エラー出力を横取り(キャプチャ)します。 test/example31_test.rb require 'oktest' Oktest.scope do topic "Capturing" do spec "example spec" do data = nil sout, serr = capture_sio("blabla") do # !!!!! data = $stdin.read() # 標準入力から読み込む puts "fooo" # 標準出力へ書き出す $stderr.puts "baaa" # 標準エラー出力へ書き出す end ok {data} == "blabla" ok {sout} == "fooo\n" ok {serr} == "baaa\n" end end end capture_sio() の第1引数は、標準入力 ($stdin) からの入力データを表します。これは省略可能なので、その場合は caputre_sio() do ... end のように書けます。 もし $stdin.tty? == true や $stdout.tty? == true が必要なら、capture_sio(tty: true) do ... end のように呼び出してください。 dummy_file() dummy_file() は、ダミーファイルを一時的に作成します。作成されたダミーファイルは、テストケース終了後に自動的に削除されます。 test/example32_test.rb require 'oktest' Oktest.scope do topic "dummy_file()" do spec "usage #1: without block" do ## ダミーファイルを作成する tmpfile = dummy_file("_tmp_file.txt", "blablabla") # !!!!! ok {tmpfile} == "_tmp_file.txt" ok {tmpfile}.file_exist? # ファイルが存在する ## ダミーファイルは、spec() ブロックの終わりに自動的に削除される end spec "usage #2: with block" do ## ブロック引数つきだと、ダミーファイルはブロックの終わりで削除される。 ## またブロックの最後に評価した値が戻り値となる。 result = dummy_file("_tmp_file.txt", "blabla") do |tmpfile| # !!!!! ok {tmpfile} == "_tmp_file.txt" ok {tmpfile}.file_exist? # ファイルが存在する 1234 end ok {result} == 1234 ok {"_tmp_file.txt"}.not_exist? # ファイルが存在しない end end end dummy_file() の第1引数にはダミーファイルのファイル名を指定します。もし第1引数が nil なら、重複しないファイル名が自動生成されます。 dummy_dir() dummy_dir() は、ダミーのディレクトリを一時的に作成します。作成されたダミーディレクトリは、テストケースの終わりに自動的に中身ごと削除されます。 test/example33_test.rb require 'oktest' Oktest.scope do topic "dummy_dir()" do spec "usage #1: without block" do ## ダミーディレクトリを作成する tmpdir = dummy_dir("_tmp_dir") # !!!!! ok {tmpdir} == "_tmp_dir" ok {tmpdir}.dir_exist? # ディレクトリが存在する ## ダミーディレクトリは、spec() ブロックの終わりに中身ごと自動削除される end spec "usage #2: with block" do ## ブロック引数があると、ブロックの終わりで自動的に削除される。 ## またブロックの最後に評価された値が戻り値となる。 result = dummy_dir("_tmp_dir") do |tmpdir| # !!!!! ok {tmpdir} == "_tmp_dir" ok {tmpdir}.dir_exist? # ディレクトリが存在する 2345 end ok {result} == 2345 ok {"_tmp_dir"}.not_exist? # ディレクトリが存在しない end end end dummy_dir() の第1引数にはディレクトリ名を指定します。もし第1引数が nil なら、重複しないディレクトリ名が自動的に生成されます。 dummy_values() dummy_values() は、Hashオブジェクトの値を一時的に変更・追加し、テストケースの終わりで自動的に元に戻します。 test/example34_test.rb require 'oktest' Oktest.scope do topic "dummy_values()" do spec "usage #1: without block" do hashobj = {:a=>1, 'b'=>2, :c=>3} # `:x` is not a key ## Hashオブジェクトの値を一時的に変更・追加し、 ## spec() ブロックの終わりで自動的に元に戻す。 ret = dummy_values(hashobj, :a=>100, 'b'=>200, :x=>900) # !!!!! ok {hashobj[:a]} == 100 # 変更されている ok {hashobj['b']} == 200 # 変更されている ok {hashobj[:c]} == 3 # 変更されている ok {hashobj[:x]} == 900 # 追加されている ok {ret} == {:a=>100, 'b'=>200, :x=>900} end spec "usage #2: with block" do hashobj = {:a=>1, 'b'=>2, :c=>3} # `:x` is not a key ## ブロック引数があれば、ブロックの終わりで自動的に元に戻す。 ## またブロックの最後に評価した値が戻り値となる。 ret = dummy_values(hashobj, :a=>100, 'b'=>200, :x=>900) do |keyvals| # !!!!! ok {hashobj[:a]} == 100 # 変更されている ok {hashobj['b']} == 200 # 変更されている ok {hashobj[:c]} == 3 # 変更されている ok {hashobj[:x]} == 900 # 追加されている ok {keyvals} == {:a=>100, 'b'=>200, :x=>900} 3456 end ok {hashobj[:a]} == 1 # 元に戻っている ok {hashobj['b']} == 2 # 元に戻っている ok {hashobj[:c]} == 3 # 元に戻っている not_ok {hashobj}.key?(:x) # 元に戻っている ok {ret} == 3456 end end end dummy_values() は、環境変数を格納している ENV オブジェクトを一時的に変更するときにとても便利です。たとえば dummy_values(ENV, 'LANG'=>'en_US.UTF-8') とすれば、環境変数 $LANG の値を一時的に変更できます。 dummy_attrs() dummy_attrs() は、オブジェクトの属性値を一時的に変更します。 test/example35_test.rb require 'oktest' class User def initialize(id, name) @id = id @name = name end attr_accessor :id, :name end Oktest.scope do topic "dummy_attrs()" do spec "usage #1: without block" do ## オブジェクトを作成する user = User.new(123, "alice") ok {user.id} == 123 ok {user.name} == "alice" ## オブジェクトの属性を一時的に変更し、spec()ブロックの終わりで元に戻す ret = dummy_attrs(user, :id=>999, :name=>"bob") # !!!!! ok {user.id} == 999 # 変更されている ok {user.name} == "bob" # 変更されている ok {ret} == {:id=>999, :name=>"bob"} # 変更内容が戻り値となっている end spec "usage #2: with block" do ## オブジェクトを作成する user = User.new(123, "alice") ok {user.id} == 123 ok {user.name} == "alice" ## ブロック引数があると、ブロックの終わりで元に戻す。 ## またブロックの最後に評価した値が戻り値となる。 ret = dummy_attrs(user, :id=>999, :name=>"bob") do |keyvals| # !!!!! ok {user.id} == 999 # 変更されている ok {user.name} == "bob" # 変更されている ok {keyvals} == {:id=>999, :name=>"bob"} 4567 end ok {user.id} == 123 # 元に戻っている ok {user.name} == "alice" # 元に戻っている ok {ret} == 4567 end end end dummy_ivars() dummy_ivars() は、オブジェクトのインスタンス変数を一時的に変更・追加し、テストケースの終わりで自動的に元に戻します。 test/example36_test.rb require 'oktest' class User def initialize(id, name) @id = id @name = name end attr_reader :id, :name # setter, not accessor end Oktest.scope do topic "dummy_attrs()" do spec "usage #1: without block" do ## オブジェクトを作成する user = User.new(123, "alice") ok {user.id} == 123 ok {user.name} == "alice" ## インスタンス変数を一時的に変更し、spec()ブロックの終わりで自動的に元に戻す ret = dummy_ivars(user, :id=>999, :name=>"bob") # !!!!! ok {user.id} == 999 # 変更された ok {user.name} == "bob" # 変更された ok {ret} == {:id=>999, :name=>"bob"} # 戻り値は変更内容 end spec "usage #2: with block" do user = User.new(123, "alice") ok {user.id} == 123 ok {user.name} == "alice" ## ブロックつきで呼び出すと、ブロックの終わりで自動的に元に戻る。 ## またブロックの最後に評価した値が戻り値となる。 ret = dummy_ivars(user, :id=>999, :name=>"bob") do |keyvals| # !!!!! ok {user.id} == 999 # 変更された ok {user.name} == "bob" # 変更された ok {keyvals} == {:id=>999, :name=>"bob"} 6789 end ok {user.id} == 123 # 元に戻った ok {user.name} == "alice" # 元に戻った ok {ret} == 6789 end end end recorder() recorder() は、Benry::Recorder オブジェクトを生成します。これを使うと、どのメソッドがどんな引数で呼ばれたかを記録できます。詳しくは Benry::Recorder README を参照してください。 test/example37_test.rb require 'oktest' class Calc def total(*nums) t = 0; nums.each {|n| t += n } # or: nums.sum() return t end def average(*nums) return total(*nums).to_f / nums.length end end Oktest.scope do topic 'recorder()' do spec "records method calls." do ## 記録対象となるオブジェクト calc = Calc.new ## メソッド呼び出しを記録するオブジェクトとメソッド名を指定する rec = recorder() # !!!!! rec.record_method(calc, :total) ## メソッドを呼び出す v = calc.average(1, 2, 3, 4) # calc.average() は内部で calc.total() を呼び出す p v #=> 2.5 ## メソッド呼び出しの記録を調べる p rec.length #=> 1 p rec[0].obj == calc #=> true p rec[0].name #=> :total p rec[0].args #=> [1, 2, 3, 4] p rec[0].ret #=> 2.5 end spec "defines fake methods." do ## 記録対象となるオブジェクト calc = Calc.new ## オブジェクトにフェイクのメソッドとその戻り値を定義する rec = recorder() # !!!!! rec.fake_method(calc, :total=>20, :average=>5.5) ## フェイクのメソッドを呼び出す v1 = calc.total(1, 2, 3) # フェイクのメソッドの戻り値 p v1 #=> 20 v2 = calc.average(1, 2, 'a'=>3) # フェイクのメソッドはどんな引数も受けつける p v2 #=> 5.5 ## メソッド呼び出しの記録を表示する puts rec.inspect #=> 0: #<Calc:0x00007fdb5482c968>.total(1, 2, 3) #=> 20 # 1: #<Calc:0x00007fdb5482c968>.average(1, 2, {"a"=>3}) #=> 5.5 end end end partial_regexp() partial_regexp() は、文字列を正規表現オブジェクトに変換します。このとき、文字列の中の一部分にだけ正規表現のメタキャラクタを使い、残りの部分ではメタキャラクタをエスケープします。partial_regexp() を使うと、あたかも「一部にだけ正規表現が使える文字列」のような感覚でアサーションを書けます。 例として、次のような関数 f1() を考えます。この関数は複数行の文字列を生成し、その中には今日の日付と16文字のランダム文字列を含んでいます。つまり、f1() の戻り値は呼び出すごとに変化します。 def f1() today = Date.today.to_s # ex: '2021-12-31' secret = Random.bytes(8).unpack('H*')[0] # ex: "cd0b260ac728eda5" return <<END * [config.date] #{today} * [config.secret] #{secret} END end ff1() が返す文字列は呼び出しごとに変化するので、アサーションを書くには正規表現が必要です。Rubyの正規表現リテラルを使うと、次のようになるでしょう。 topic 'f1()' do spec "generates multiline string." do expected = /\A\* \[config\.date\] \d\d\d\d-\d\d-\d\d\n\* \[config\.secret\] [0-9a-f]+\n/ ok {f1()} =~ expected end end 見ての通り、とても複雑な正規表現になってしまいます。 正規表現の x オプション (例:/.../x) を使うと、正規表現リテラルを複数行に分けて書けます。ただし、メタキャラクタ (*, ., []) と半角空白をエスケープする必要があります。 topic 'f1()' do spec "generates multiline string." do expected = /\A \*\ \[config\.date\]\ \ \ \d\d\d\d-\d\d-\d\d\n \*\ \[config\.secret\]\ [0-9a-f]+\n \z/x # !!!!! ok {f1()} =~ expected end end 少しましになりましたが、十分複雑ですね。 このような場合には、partial_regexp() がとても便利です。partial_regexp() は文字列を正規表現オブジェクトに変換ます。このとき {== ==} で囲まれた部分だけ正規表現のメタキャラクタが使え、それ以外の部分ではメタキャラクタは自動的にエスケープされます。 topic 'f1()' do spec "generates multiline string." do ## 複数行文字列を正規表現オブジェクトに変換する。 ## このとき、`{== ==}` で囲まれた部分だけメタキャラクタが使え、 ## 残りの部分では Regexp.escape() によりエスケープされる。 expected = partial_regexp <<'END' # !!!!! * [config.date] {== \d\d\d\d-\d\d-\d\d ==} * [config.secret] {== [0-9a-f]+ ==} END ok {f1()} =~ expected ## これは次と同じ #expected = /\A #\*\ \[config\.date\]\ \ \ \d\d\d\d-\d\d-\d\d\n #\*\ \[config\.secret\]\ [0-9a-f]+\n #\z/x # !!!!! #ok {f1()} =~ expected end end 埋め込みの {== [0-9a-f]+ ==} では読みやすくするために半角空白を入れられるようにしていますが、半角空白を入れずに {==[0-9a-f]+==} と書いてもいいです。でも読みにくくなるので、特にこだわりがなければ半角空白を入れましょう。 partial_regexp() には引数が4つあります。 def partial_regexp(pattern, begin_='\A', end_='\z', mark='{== ==}') partial_regexp() は、先頭に \A を、また末尾に \z を自動的につけます。これをつけたくない場合は、第2引数と第3引数に空文字列を指定します。 partial_regexp <<-'END', '', '' # !!!!! ... END デフォルトの埋め込みの目印は {== ==} ですが、第4引数で変更できます。 partial_regexp <<-'END', '\A', '\z', '%% %%' # !!!!! * [config.date] %% \d\d\d\d-\d\d-\d\d %% * [config.secret] %% [0-9a-f]+ %% END partial_regexp!() というヘルパー関数もあります。partial_regexp() と partial_regexp!() の違いは、戻り値である正規表現オブジェクトの .inspect() メソッドにあります。前者の .inspect() は Ruby の正規表現リテラルの形式になり、後者の .inspect() は partial_regexp() の呼び出し形式になります。アサーション失敗時にどちらの形式で表示してほしいかを考えて、partial_regexp() と partial_regexp!() の好きなほうを使ってください。 r1 = partial_regexp <<-'END' * [config.date] {== \d\d\d\d-\d\d-\d\d ==} * [config.secret] {== [0-9a-f]+ ==} END p r1 #=> /\A # \*\ \[config\.date\]\ \ \ \d\d\d\d-\d\d-\d\d\n # \*\ \[config\.secret\]\ [0-9a-f]+\n # \z/x r2 = partial_regexp! <<-'END' # !!!!! * [config.date] {== \d\d\d\d-\d\d-\d\d ==} * [config.secret] {== [0-9a-f]+ ==} END p r2 #=> partial_regexp(<<PREXP, '\A', '\z') # * [config.date] {== \d\d\d\d-\d\d-\d\d ==} # * [config.secret] {== [0-9a-f]+ ==} # PREXP JSON Matcher Oktest.rb では、JSONデータのアサーションが簡単にかける方法を用意しています。ただし乱用は禁物です。 JSON Matcherの簡単なサンプル たとえばこのようなJSON(の基となる)データがあったとします。 actual = { "name": "Alice", "id": 1001, "age": 18, "email": "alice@example.com", "gender": "F", "deleted": false, "tags": ["aaa", "bbb", "ccc"], #"twitter": "@alice", } 従来のテストスクリプトでは、JSONデータのアサーションは要素ごとに個別に書く必要がありました。 ## 要素ごとにアサーションを個別に書く ok {actual[:name]} == "Alice" ok {actual[:id]}.between?(1000, 9999) ok {actual[:age]}.is_a?(Integer) ok {actual[:email]} =~ /^\w+@example\.com$/ ok {actual[:gender]}.in?(["M", "F"]) ok {actual[:deleted]}.in?([true, false]) ok {actual[:tags]}.all? {|tag| tag =~ /^\w+$/ } if actual[:twitter] ok {actual[:twitter]} == "@alice" end Oktest.rb の JSON Matcher を使うと、これをすっきりとした形で書けます。 require 'set' ok {JSON(actual)} === { # `JSON()` と `===` 演算子を使う "name": "Alice", # スカラー値 "id": 1000..9999, # Rangeオブジェクト "age": Integer, # クラスオブジェクト "email": /^\w+@example\.com$/, # 正規表現オブジェクト "gender": Set.new(["M", "F"]), # 集合オブジェクト ("M" または "F") "deleted": Set.new([true, false]), # 真偽値 (true または false) "tags": [/^\w+$/].each, # Enumeratorオブジェクト (!= Array obj) "twitter?": /^@\w+$/, # キー 'xxx?'は非必須を表す } 内部では、これを次のように変換して実行しています。=== 演算子を使っていること、左右を入れ替えて比較していることがポイントです。また Enumerator オブジェクトは特別扱いしています。 "Alice" === actual["name"] # スカラー値 (1000..9999) === actual["id"] # Rangeオブジェクト Integer === actual["age"] # クラスオブジェクト /^\w+@example\.com$/ === actual["email"] # 正規表現オブジェクト Set.new(["M", "F"]) === actual["gender"] # 集合オブジェクト ("M" または "F") Set.new([true, false]) === actual["deleted"] # 真偽値 (true または false) actual["tags"].each {|x| /^\w+$/ === x } # Enumeratorオブジェクト (!= Array obj) if actual.key?("twitter") # キー 'xxx?'は非必須を表す /^@\w+$/ === actual["twitter"] end テストスクリプト全体は次のようになります。 test/example41_test.rb require 'oktest' require 'set' # !!!!! Oktest.scope do topic 'JSON Example' do spec "simple example" do actual = { "name": "Alice", "id": 1001, "age": 18, "email": "alice@example.com", "gender": "F", "deleted": false, "tags": ["aaa", "bbb", "ccc"], #"twitter": "@alice", } ## JSON Matcherを使ったアサーション ok {JSON(actual)} === { # `JSON()` と `===` 演算子を使う "name": "Alice", # スカラー値 "id": 1000..9999, # Rangeオブジェクト "age": Integer, # クラスオブジェクト "email": /^\w+@example\.com$/, # 正規表現オブジェクト "gender": Set.new(["M", "F"]), # 集合オブジェクト ("M" または "F") "deleted": Set.new([true, false]), # 真偽値 (true または false) "tags": [/^\w+$/].each, # Enumeratorオブジェクト (!= Array obj) "twitter?": /^@\w+$/, # キー 'xxx?'は非必須を表す } end end end Ruby 2.4 とそれ以前では `Set#===()` が定義されていないため、上のコードは実行エラーになります。その場合は次のコードをテストスクリプトに追加してください。 (追加コード)1 require 'set' unless Set.instance_methods(false).include?(:===) # for Ruby 2.4 or older class Set; alias === include?; end end なお JSON Matcher では、Enumerator オブジェクトは Array オブジェクトとは違う役割を持っているので注意してください。 actual = {"tags": ["foo", "bar", "baz"]} ## Array:配列全体が一致するか調べる ok {JSON(actual)} == {"tags": ["foo", "bar", "baz"]} ## Enumerator:配列の要素が正規表現やSetやRangeにマッチするか調べる ok {JSON(actual)} == {"tags": [/^\w+$/].each} 入れ子データのサンプル JSONデータが入れ子になっている場合のサンプルコードは次のようになります。 test/example42_test.rb require 'oktest' require 'set' # !!!!! Oktest.scope do topic 'JSON Example' do spec "nested example" do actual = { "teams": [ { "team": "Section 9", "members": [ {"id": 2500, "name": "Aramaki", "gender": "M"}, {"id": 2501, "name": "Motoko" , "gender": "F"}, {"id": 2502, "name": "Batou" , "gender": "M"}, ], "leader": "Aramaki", }, { "team": "SOS Brigade", "members": [ {"id": 1001, "name": "Haruhi", "gender": "F"}, {"id": 1002, "name": "Mikuru", "gender": "F"}, {"id": 1003, "name": "Yuki" , "gender": "F"}, {"id": 1004, "name": "Itsuki", "gender": "M"}, {"id": 1005, "name": "Kyon" , "gender": "M"}, ], }, ], } ## JSON Matcherを使ったアサーション ok {JSON(actual)} === { # `JSON()` と `===` 演算子が必要 "teams": [ { "team": String, "members": [ {"id": 1000..9999, "name": String, "gender": Set.new(["M", "F"])} ].each, # Enumerator object (!= Array obj) "leader?": String, # key 'xxx?' means optional value } ].each, # Enumerator object (!= Array obj) } end end end より複雑なサンプル OR(x, y, z) は、x または y または z にマッチします。 例:OR(String, Integer) は、文字列または整数にマッチします。 AND(x, y, z) は、x かつ y かつ z にマッチします。 例:AND(Integer, 1..1000) は、1以上1000以下の整数にマッチします。 キー "*" は、Hashオブジェクトのどのキーでもマッチします。 Any() は何にでもマッチします。 test/example43_test.rb require 'oktest' require 'set' Oktest.scope do topic 'JSON Example' do ## OR()を使う例 spec "OR() example" do ok {JSON({"val": "123"})} === {"val": OR(String, Integer)} # OR() ok {JSON({"val": 123 })} === {"val": OR(String, Integer)} # OR() end ## AND()を使う例 spec "AND() example" do ok {JSON({"val": "123"})} === {"val": AND(String, /^\d+$/)} # AND() ok {JSON({"val": 123 })} === {"val": AND(Integer, 1..1000)} # AND() end ## キー '*' とANY()を使う例 spec "`*` and `ANY` example" do ok {JSON({"name": "Bob", "age": 20})} === {"*": Any()} # '*' と Any() end ## それらを組み合わせて使う例 spec "complex exapmle" do actual = { "item": "awesome item", "colors": ["red", "#cceeff", "green", "#fff"], "memo": "this is awesome.", "url": "https://example.com/awesome", } ## assertion color_names = ["red", "blue", "green", "white", "black"] color_pat = /^\#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/ ok {JSON(actual)} === { "colors": [ AND(String, OR(Set.new(color_names), color_pat)), # AND() と OR() ].each, "*": Any(), # 任意のキー (`"*"`) と値 (`ANY`) にマッチ } end end end (注意:/^\d+$/ はそれだけで値が文字列オブジェクトであることを前提としているので、わざわざ AND(String, /^\d+$/) とする必要はありません。また 1..1000 はそれだけで値が整数であることを前提としているので、わざわざ AND(Integer, 1..1000) と書く必要はありません。) ## こう書くだけでよい ok {JSON({"val": "A"})} === {"val": /^\d+$/} # implies String value ok {JSON({"val": 99 })} === {"val": 1..100} # implies Integer value ## こう書く必要はない ok {JSON{...}} === {"val": AND(String, /^\d+$/)} ok {JSON{...}} === {"val": AND(Integer, 1..100)} JSON Matcher のためのヘルパー関数 JSON Matcher のためのヘルパー関数がいくつか用意されています。 Enum(x, y, z) は Set.new([x, y, z]) の短縮記法です。 Bool() は Enum(true, false) の短縮記法です。 Length(3) は長さ3にマッチし、Length(1..3) は長さ1以上3以下にマッチします。 test/example44_test.rb actual = {"gender": "M", "deleted": false, "code": "ABCD1234"} ok {JSON(actual)} == { "gender": Enum("M", "F"), # Set.new(["M", "F"]) と同じ "deleted": Bool(), # Enum(true, false) と同じ "code": Length(6..10), # 長さは6以上10以下 } Tips MiniTest で ok {} を使いたい MiniTest でも ok {actual} == expected のスタイルでアサーションを書きたいなら、minitest-ok gem をインストールしてください。Oktest.rb は必要ありません。 $ gem install minitest-ok test/example51_test.rb require 'minitest/spec' require 'minitest/autorun' require 'minitest/ok' # !!!!! describe 'MiniTest::Ok' do it "helps to write assertions" do ok {1+1} == 2 # !!!!! end end 詳細は minitest-ok README を見てください。 Rack アプリケーションをテストする rack-test_app gem を使うと、Rack アプリケーションのテストがしやすくなります。 $ gem install rack-app_test test/example52_test.rb require 'rack' require 'rack/lint' require 'rack/test_app' # !!!!! require 'oktest' ## Rackアプリケーションのサンプル app = proc {|env| text = '{"status":"OK"}' headers = {"Content-Type" => "application/json", "Content-Length" => text.bytesize.to_s} [200, headers, [text]] } ## Rackアプリケーションのラッパーオブジェクトを作る $http = Rack::TestApp.wrap(Rack::Lint.new(app)) ## テストコード Oktest.scope do + topic("GET /api/hello") do - spec("returns JSON data.") do response = $http.GET("/api/hello") # Rackアプリを呼び出す ok {response.status} == 200 ok {response.content_type} == "application/json" ok {response.body_json} == {"status"=>"OK"} end end end エンドポイントごとにトピックを分けて、その中でヘルパー関数を定義するといいでしょう。トピックが分かれていれば、ヘルパー関数名は同じで構いません。 Oktest.scope do + topic("GET /api/hello") do ## トピックごとにヘルパー関数を定義 def api_call(**kwargs) $http.GET("/api/hello", **kwargs) end - spec("returns JSON data.") do resp = api_call() # Rackアプリを呼び出す ok {resp.status} == 200 ok {resp.content_type} == "application/json" ok {resp.body_json} == {"status"=>"OK"} end end + topic("POST /api/hello") do ## トピックごとにヘルパー関数を定義 def api_call(**kwargs) $http.POST("/api/hello", **kwargs) end .... end end 環境変数 $OKTEST_RB 環境変数 $OKTEST_RB に、デフォルトのコマンドラインオプションを設定できます。たとえば oktest コマンドのデフォルトの出力スタイルを変更するには、次のようにします。 ### デフォルトの出力スタイルを plain スタイルに変更する $ export OKTEST_RB="-s plain" ### すると '-s' オプションを指定しなくても、出力が plain スタイルになる $ ruby test/foo_test.rb Traverserクラス Oktest.rb では Traverser というクラスが用意されています。これはいわゆるVisitorパターンの実装クラスです。 test/example54_test.rb require 'oktest' Oktest.scope do + topic('Example Topic') do - spec("sample #1") do ok {1+1} == 2 end - spec("sample #2") do ok {1-1} == 0 end + case_when('some condition...') do - spec("sample #3") do ok {1*1} == 1 end end + case_else() do - spec("sample #4") do ok {1/1} == 1 end end end end ## Traverserクラスを継承して、on_topic() や on_scope() を上書きする。 ## ただし on_topic() の中では yield が必要(on_scope() では必要ない)。 class MyTraverser < Oktest::Traverser # !!!!! def on_scope(filename, tag, depth) # !!!!! print " " * depth print "# scope: #{filename}" print " (tag: #{tag})" if tag print "\n" yield # should yield !!! end def on_topic(target, tag, depth) # !!!!! print " " * depth print "+ topic: #{target}" print " (tag: #{tag})" if tag print "\n" yield # should yield !!! end def on_case(cond, tag, depth) # !!!!! print " " * depth print "+ case: #{cond}" print " (tag: #{tag})" if tag print "\n" yield # should yield !!! end def on_spec(desc, tag, depth) # !!!!! print " " * depth print "- spec: #{desc}" print " (tag: #{tag})" if tag print "\n" end end ## 継承したTraverserクラスを実行する Oktest::Config.auto_run = false # stop running test cases MyTraverser.new.start() 実行結果: $ ruby test/example54_test.rb # scope: test/example54_test.rb + topic: Example Topic - spec: sample #1 - spec: sample #2 + case: When some condition... - spec: sample #3 + case: Else - spec: sample #4 ベンチマーク Oktest.rb の gem ファイルにはベンチマークスクリプトが付属しています。実行方法は次の通りです。 $ gem install oktest # ver 1.2.0 $ gem install rspec # ver 3.10.0 $ gem install minitest # ver 5.14.4 $ gem install test-unit # ver 3.4.4 $ cp -pr $GEM_HOME/gems/oktest-1.2.0/benchmark . $ cd benchmark/ $ rake -T $ ruby --version ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-darwin18] $ rake benchmark:all 詳しい実行結果は Oktest.rbのREADME を見てもらうとして、サマリーだけを紹介すると次の通りです。Oktest.rbの実行速度がRSpecの約5倍であることが分かります。 Oktest: 6.815 real 6.511 user 0.257 sys Oktest (--faster): 6.401 real 6.123 user 0.240 sys RSpec: 32.062 real 27.778 user 4.383 sys MiniTest: 9.140 real 8.657 user 0.705 sys Test::Unit: 19.580 real 19.020 user 0.885 sys RSpecが遅いのは、「仕様をテストコードとして記述する(仕様とテストコードを一致させる)」という野心的なゴールを掲げているからです。たとえRSpecが遅くとも、高いゴールを目指すその姿勢は尊重されるべきでしょう。 Oktest.rbはそこまでの野心は持っておらず、「仕様は文字列で記述するだけにとどめ、テストコードが直感的に書けることに注力する」と割り切っています。そのおかげでRSpecと比べてOktest.rbはシンプルな実装で済んでおり、結果として大きく高速化できています。 IMHO、RSpecが遅いのはゴールの高さを考えれば理解できるのですが、Test::Unitが遅い理由がよく分かりません(未調査)。Test::Unitは本来ならMiniTestと同程度の速度がでるはずです。PowerAssertを使わず assert_equal を使っているのにこれだけ遅いのなら、改善の余地が大いにありそうです。 まとめ Ruby向けテスティングフレームワーク「Oktest.rb」を紹介しました。Oktest.rbはアサーションが直感的で、いろんな便利機能が備わっており、コンパクトな実装で動作も高速です。特に Fixture Injection と JSON Matcher と partial_regexp() と at_end() は便利であり、個人的にとても気に入っています。ぜひ使ってみてください。 なおPython用もあります。 ドキュメント:https://pythonhosted.org/Oktest/ 紹介スライド:https://bit.ly/3E3XL6C Oktest.rb では Set#===() を自動定義しません。なぜなら、テスティング用ライブラリが既存のクラスやモジュールを変更すべきではない(しても最小限に留めるべき)と考えているからです。 ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsである程度実装ができるようになったら

はじめに 記念すべき DMM WEBCAMP Advent Calendar 2021 1日目を担当します @SawaShuyaです! Railsで開発を進めていると、3~4カ月ほどで設計から実装まである程度自力で開発できるようになってくると思います! 今回は、railsをある程度自力で書くことができるようになった方向けに、次にどのようなことを学んでいけばいいのかを書いていきたいと思います 構成は 1. rubyを学ぶ 2. railsの開発スキルを磨く 3. 周辺知識を学ぶ となっておりますので、興味のある部分だけでも見ていただけたらと思います! 1. Rubyを学ぶ railsはすべてrubyで書かれてます。rubyを深めていくことで、これまでは何気なく扱っていたclassやmodule、オブジェクトなどを理解することができ、より効率の良い開発ができるようになります! では突然ですが、以下のコードを実行した時にどういった結果になるでしょうか? class Fuga def display "fuga display" end def self.display "fuga class display" end end class Hoge < Fuga def display "hoge display" end def self.display super "hoge class display" end def self.message p "message : " + display end end Hoge.message こちらのコードはRubyの基本的な文法やオブジェクトへの理解があると解くことができます! こちらが難しいなと感じた方は、是非一緒にrubyを世界を覗きましょう rubyを知り開発能力を上げていきましょう! 解答・解説 【解答】 "message : hoge class display" 【解説】 Hoge.message でクラスメソッドのmessageが呼び出されます。 def self.message p "message : " + display end そのなかでdisplayメソッドを呼び出していますが、rubyではselfが省略されているため、self.displayとなります。今クラスメソッド内であるためselfにはHogeが代入されています。そのため、クラスメソッドのdisplayが呼び出されます。クラスメソッドのdisplayは、classの継承があるのでFugaクラスのものとHogeクラスの2種類が存在します。同名であれば自分のクラスのメソッドが優先されるため、Hogeクラス内のクラスメソッドのdisplayが呼び出されることになります。 class Hoge < Fuga def self.display super "hoge class display" end end superでFugaクラスのクラスメソッドのdisplayが呼び出されますが、rubyではreturnを記述していない場合、一番下のコードが戻り値になるため、"hoge class display"が返されるという流れです。 〇 基本文法 rubyの基本文法を知ることで、コード量を減らすことができます! ここでは条件分岐の構文を中心に、いくつか基本文法を紹介したいと思います! 基本文法 三項演算子(条件演算子) ifを使った条件文はよくお使いなるかと思いますが、三項演算子を用いた条件文の記述もあります! 今、a, bの変数に整数が与えられたとき、if文を用いて小さい値を出力するプログラムが以下のようにあったとします # 値の小さいほうを出力 if a < b result = a else result = b end p result こちらでも問題なく動作するのですが、条件演算子を使うと楽にコードを記述できます! # 条件演算子 # (条件式) ? (trueの時の値) : (falseの時の値) result = a < b ? a : b p result 5行の記述を1行にまとめることができました!使いどころによっては、非常に簡単にコードを書けるのでお勧めです 論理演算子 if文を書いていると、ついついif文をネストしすぎてしまうことありますよね... そんなときは論理演算子を使って、if文のネストを解消しましょう! # 論理演算子 (条件式1) && (条件式2) => 条件式1も条件式2もtrueのときtrueを返す   ※条件式1がfalseのときは条件式2は評価されない (条件式1) || (条件式2) => 条件式1もしくは条件式2のどちらかtrueのときtrueを返す   ※条件式1がtrueのときは条件式2は評価されない (正確には最後に評価したオペランドの値を返すのですが、条件式では上のように認識で問題ないです) 今、a, b, cの変数に整数が与えられたとき、if文を用いてその最小値を出力するプログラムが以下のようにあったとします if a < b if a < c result = a else result = c end else if b < c result = b else result = c end end これを論理演算子を用いると if a < b && a < c result = a elsif b < a && b < c result = b else result = c end ネストがないシンプルな形になりました! case式 条件分岐ではifやunlessをよく使うかと思いますが、caseを用いた方法もあります! # case式 case (式) when (条件式) .... when (条件式) .... else .... end caseによって指定した式の評価値を、それぞれのwhenにて(条件式) === (式)で評価し、最初にマッチしたwhen節内の処理を行います!(===は包含判定です。式の評価値が条件式の範囲内か評価します) 式を複数回書く必要がないため、スッキリとした表記をすることができます! 今、a, bに自然数が与えられ、それらの和が0~9、10~29、20以上かを判断するプログラムが以下にあったとします if (0..9) === (a + b) p "0~9" elsif (10..19) === (a + b) p "10~19" else p "20~" end (a + b)という式を複数回記述していますが、これをcase文に書き換えると case (a + b) when (0..9) p "0~9" when (10..19) p "10~19" else p "20~" end となります!スッキリしましたね! 〇 オブジェクト指向 オブジェクト指向とはrubyなど多くのプログラミング言語が採用している、システム構成のことを指します! classとは何か、継承とは何かなどなど、少しのイメージを持つだけで、プログラミング自体の構造を把握することができ、railsへの理解がグッと高まります!pythonやC++, JAVAなどなど、多くの言語で採用されてますので、ruby以外の言語にチャレンジするときにも、非常に役に立つ知識になるでしょう ここでは図を交えながら、ざっくりとしたイメージを伝えたいと思います! オブジェクト指向 まずはクラスとインスタンスについてお話します! クラスとは同じ性質を持たせる集まりのことを指します! 例えば、動物も魚もいる動物園のアプリケーションがあった際には以下のように、まとまりごとにクラスを作成します!その中で一つのオブジェクト(データ)をインスタンスと呼びます。 それぞれのクラスには、インスタンスメソッド、クラスメソッドが設定されており、それらを実行することで処理を行います インスタンスメソッドは同クラスのインスタンス(個別データ)に対して、クラスメソッドはそのクラス全体に対して処理をかけます。基本的に他のクラスのメソッドは使うことができません。 しかしながら、図にある通り、「情報の保存・更新」機能はすべてのクラスに共通した機能です。同じ機能であれば、1か所で管理したほうが、コードの修正などが行いやすくなるため、これらをまとめていく必要があります。 その際に登場するのがクラスの継承です クラス継承は、他のクラスのメソッドを自由に使うことができるようにできる機能です! そのため、図のように情報の保存・更新といった機能を一つのクラスを管理し、それを継承することで、機能の共有をすることができます! 実はこの構造はrailsアプリケーションでも確認することができます。 railsの場合保存する機能を扱うクラスはActiveRecordです!それをApplicationRecordが継承して、他のモデルはApplicationRecordを継承して、.saveや.update()を使うことができるようになります! railsのmodel内のファイルを確認すると、class名や継承先が以下になっているようなことがわかります! application_record.rb class ApplicationRecord < ActiveRecord::Base self.abstract_class = true end employee.rb class Employee < ApplicationRecord def work_time ...... end def self.sort_for_number ...... end end ここではclassと継承周りでざっくりと解説を行いました! オブジェクト指向はシステムを理解する上でも、効率の良い開発を目指すうえでも、かなり強力な知識になります! 今回は一部省いている部分もあるので、ぜひ一度学習してみるといいと思います その際には昨年のDMM WEBCAMP Advent Calendar 2020の記事が参考になるので、こちらもまた是非ご覧ください! 〇 組み込みクラス rubyにはもともと備わっている組み込みクラスがあり、他の言語と比較しても便利なメソッドが多く備わっているといわれています! ここでは、開発に便利そうなメソッドを紹介したいと思います! 組み込みクラス Arrayクラス 配列に対するメソッドになります!配列はrailsにおいてwhere構文などで検索をかけたときなどにも登場するため、Arrayクラスのメソッドは覚えておくと、開発効率が上がります! Arrayクラスのメソッド each_with_index each文のように一つずつ値を取り出して処理を行う際に、取り出した順番も値として保持することができるメソッドです。 a = ["hoge", "fuga", "piyo"] a.each_with_index do |value, i| p i, value end # => 0 "hoge" 1 "fuga" 2 "piyo" map each文のように値を取り出して、ブロック内の処理を行い、結果を配列で返すメソッド。要素すべてに何か処理を行いたいときに使用すると便利です。 a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] # すべての要素を2倍にして配列で返す p a.map {|k| k * 2} sort 配列の順番を変えることのできるメソッドです。ブロックを持たせるとその評価に従って任意の順番に並べ替えることができます。 <=>はUFO演算子などと呼ばれ、sortメソッドではこれの評価値をもとに順番を決定しています。 # UFO演算子 1 <=> 2 # => -1 2 <=> 2 # => 0 3 <=> 2 # => 1 a = [3, 10, 1, 9, 8, 5, 6, 7, 4, 2] p a.sort # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] # 隣り合う2つの数を左から取り出して、2つ目の値が1つ目の値より大きければ入れ替え p a.sort{|a, b| b <=> a} # => [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] first, last 配列の最初と最後の要素を取得します。引数を持たせることで取り出す要素数を指定できます。 a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] p a.first # => 1 p a.first(3) # => [1, 2, 3] p a.last # => 10 p a.last(3) # => [8, 9, 10] sum 合計を出すことができます。each文でなどで一つ一つ取り出すより、処理スピードが格段に速いです。 a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] p a.sum # => 55 Hashクラス {"orange" => "fruit", "warter" => "drink"}のように{(key) => (value), ....}といった形式をHashといいます! こちらもrailsを開発をしていると、パラメータやenumなど様々な場面で見ることがあります!配列ほど登場回数は多くないので、基本的な部分を抑えていきましょう! Hashクラスのメソッド [] keyを入力することでvalueを取り出すことができます。 a = {"orange" => "fruit", "warter" => "drink"} p a["orange"] # => "fruit" keys Hashのkeyの部分を取り出し、配列として出力します。 a = {"orange" => "fruit", "warter" => "drink"} p a.keys # => ["orange", "water"] values Hashのvalueの部分を取り出し、配列として出力します。 a = {"orange" => "fruit", "warter" => "drink"} p a.values # => ["fruit", "drink"] 数値型クラス プログラミングに数値計算は必須です!数値クラスはNumeric, Float, Integer, Fixnum, Rational, Complexなど多くのクラスがあります!一つ一つ重要ではありますが、今回はその中でもrails開発に使えそうなものだけピックアップしてみました! 数値型クラスのメソッド times Integerクラスのtimes メソッドでも繰り返し文を作成することが可能です!レシーバー(整数)の回数分ブロックの処理を行います! 繰り返しの回数が決まっていて、シンプルに書きたいときにおすすめです! sum = 0 # 10回分(iは0から9まで)の処理 10.times {|i| sum += 1} p sum # => 45 modulo Fixnumクラスのmoduloは、剰余(数を割ったときのあまり)を出力します!idが3の倍数の時に処理したい!!などというときに使ったりします! %も同様の処理を行います。 p 9.modulo 3 # => 0 p 10.modulo 3 # => 1 p 11 % 3 # => 2 2. Railsの開発スキルを磨く railsの技術の向上にはrails自身をより使いこなしていく必要もあります! 初級レベルから中級レベルになるために意識したいことや、便利なツールをまとめましたので、ご参考ください! 〇可読性の高いコードを目指す railsに限らず、プログラミングは複数人での作業を行ったり、また自分のコードをデバッグするなどの作業を行います。そのため、出来る限り見やすいコードを書く癖をつけていくことが望ましいです。どういった観点に着目をするべきかまとめてみました! また、自分のコードが正しく書ているのかをざっくりチェックしてくれるRubocupというGemもあります 可読性を上げるために意識すること インデント インデントを正しく使用することで、コードが一段と見やすくなります! ruby自体はインデントがなくとも動作はするのですが、どこから開始してどこで終了するかが一目でわかると、エラーの発見が早くなることがあります! インテンドを意識する 命名(変数名、クラス名、カラム名 など) rubyは動的型付け言語であるため、変数に自動で型が割り振られます。そのため、より一層変数の中にどんな情報が入っているのかを分かりやすくする必要があります!また、railsはデータベースも扱うため、それぞれ適切な命名が必要になります!以下の点に気を付けてみましょう! 単数形、複数形の変数を区別して使えているか 不要な部分でインスタンス変数を使っていないか 不要な引数を取っていないか クラス名から何の処理を行うか判別ができるか カラム名は誰から見ても同じ認識となる名前となっているのか 複数個所で共通コード プログラミングにはDry(Don't Repeat Yourself)原則(もしくはOAOO(Once and Only Once)原則)というものがあります!同じコードを2か所以上で使用しないことを提唱しています。 複数個所で同じコードを管理していると、1つに修正を加えた際に他の修正も必要になるため、無駄な労力やエラーの原因に繋がります。そのため、メソッドとして処理をまとめることや、部分テンプレートを使ってViewをまとめるなど、共通する部分は1か所で管理することが望ましいです! 共通する処理はメソッドを作成してまとめる viewで共通する箇所は部分テンプレートを用いてまとめる 長いコード ついつい開発に夢中になっていると、コントローラに非常に長い処理を書いてしまう場合があります! 基本的にはコントローラでは簡潔に変数の代入や、リダイレクトの設定を行うため、このようなプログラミング処理については、メソッドとしてまとめていく必要があります! 長くなってしまったコードは処理ごとにメソッドにまとめる if文などのネスト構造は最小限にとどめる 〇 開発効率が上がるおすすめGem rubyには開発効率のあがる便利なGemがたくさんあります!また、こんな機能を実装したいなぁと思ったときには、まずGem探しからやってみると、意外とあったりします! Gemを有効的に活用し、開発効率を上げていきましょう! ここでは、そのなかでも私がよく使っている便利Gemを紹介したいと思います おすすめGem better_errors better_errorsはデバッグ用のGemになります! railsのデフォルトのエラー画面でも十分わかりやすいですが、デバッグをする際にはこちらが便利です! better_errorsを導入すると、エラーが出た際には以下のような画面になります ①エラー内容 ②エラー箇所 ③エラーコードとコンソール ④パラメーターや呼び出しているアクション、変数に格納されている値など デフォルトのエラー画面では表示されない、変数を一覧で見ることができます!またエラー箇所で処理が止まり、byebugやbinding.pryのようにコンソールでコードを入力できるので、デバッグ効率が格段にあがります! slim slimはテンプレートエンジンのGemになります! railsはデフォルトでerbとなっておりますが、erbでの記述は<% %>などの記述が多く、長いviewを書くときには面やや面倒に感じてしまいます... そこでコードの記述が少なくて済むテンプレートエンジンとしてslimをお勧めします! 以下のようにerbの記述があったとします sample.html.erb <h1 id = "title">Title</h1> <p id = "sub-title">Subtitle</p> <div class = "links"> <%= link_to "top", root_path %> </div> <table class = "items"> <% @items.each do |item| %> <tr> <td><%= item.name %></td> <td><%= item.introduction %></td> </tr> <% end %> </table> これをslimに直すと以下のようになります! sample.html.slim #title | Title #sub-title | Subtitle .links = link_to "top", root_path table.items - @items.each do |item| tr td = item.name td = item.introduction "<", "%", ">", "end"は不要で、htmlタグも括弧や閉じタグを省略します! 基本的にインデントで階層を判断されます! コード量はerbと比較すると非常に少なくなるので、開発効率は良くなります! 途中までerbで書いてしまっていても、html2slimのようなGemもあり、自動で変換してくれるので、是非活用してみてください 他にもhamlといった読み込み速度がはやいテンプレートエンジンのGemもあるので、興味があればこちらも使ってみてください! ridgepole ridgepoleはCookPadが開発した、migrationファイルなしでデータベースを扱うことができるGemになります! railsはデフォルトでmigrationファイルを使ってデータベースの操作を行いますが、開発が大規模になるとたくさんのマイグレーションファイルに溢れてしまいます... またカラムの追加や削除、型の変更などなど、変更にもすべて新規のmigrationファイルが必要で非常にややこしくなります... そこで便利なのがridgepoleです!こちらはdbディレクトリ直下に○○.schemaファイルを作成し、普段migrationファイルに記述する内容を記載します! db/items.schema create_table "items", force: :cascade do |t| t.string "name", null: false t.text "introduction", null: false t.datetime "created_at" t.datetime "updated_at" end カラムの変更がしたいときは、ここに追加や変更をし、ridgepoleを実行するだけです! ファイルが増えることも、ややこしくなることもありません 3. 周辺知識を学ぶ railsはwebアプリを簡単に作成できるフレームワークです! railsは非常に強力なフレームワークであるため、データベースやOSの知識がなくとも楽々と開発をすることができます! しかしながら裏を返せば、何も意識しなければrails以外の知識を身に着ける場面がないということになります... 本来webアプリに必要な知識はたくさんあります!それらを少しずつ学習していくことで、バックエンド領域全体としての理解を深めることができrails以外の言語をやることになっても、必ず役に立つでしょう 〇 データベース railsではデフォルトでSQLiteと呼ばれるデータベースを扱ってます! 他にもMySQLやPostgreSQLなどといった有名なデータベースを使うこともできます! ここではざっくりとrailsとデータベースの関連をみていきたいと思います!! データベース それぞれのデータベースは"SQL"と呼ばれるデータベース言語を使うことによって、テーブルの作成やカラムの作成を行うことができます! ではrailsではデータベースを扱っていますが、どこでSQLが書かれているのでしょうか? それはrailsにもともと備わるActiveRecordが担ってくれています! 例えば sample.rb item = Item.find(1) # => #<Item id: 1, name: "hoge"> item.update(name: "fuga") このようなコードが実行できたときターミナルでは以下のような文章が表示されているかと思います! terminal : # Item Update (0.4ms) UPDATE `items` SET `updated_at` = '20xx-xx-xx xx:xx:xx', `title` = 'fuga' WHERE `id` = 1 SQL (0.8ms) COMMIT : これがActiveRecordによって生成されたSQLです!.updateメソッドの実行のみでSQLに変換して更新作業をしてくれています! このActiveRecordはSQLを知らなくてもデータベースを触れてしまうという、非常に強力な機能です!まさにrailsの強みですね railsを使っているだけではSQLを学ぶ必要に迫られることはありませんが、データベースを扱っている以上は知っておいて損はない知識かと思います!SQLも様々な構文があるのでprogateやSQL攻略でお手軽に練習できますので、ぜひ学んでみてください! 〇 システムインフラ ミドルウェアから深い部分をシステムインフラと呼ぶことがあります!(ミドルウェアは主にサーバー関連を指しています) railsで開発を進めても、いざ公開しようとした際には、このシステムインフラの知識が必要になります これらは非常に深い領域ですので、ここではざっくりとした内容と、その学習のおすすめをお伝えしたいと思います! システムインフラ railsやphp、JAVAなどで作るアプリケーションはすべてシステムインフラの上で稼働しています。 すなわち、アプリを公開するにはこれらの知識が少なからず必要になってきます! ただ近年はIaas(Infrastructure as a Service)のAWS, Azul, GCPなどを使って公開することが多いため、ミドルウェア ~ OSの領域の理解があるといいでしょう!それぞれの概要は以下の通りです ミドルウェア webサーバー: Apacheやnginxが主流で、ユーザーからのリクエストを処理します。 Apサーバー:  アプリケーションとしての処理を行います。rails sで起動しているのがApサーバーで、railsではPumaを使います。 DBサーバー: データベース関連の処理を行います。 OS Linux各種(Ubuntu, CentOs, Amazon Linux etc.), mac, windows Herokuなどこれらをひっくるめていい感じに公開してくれるツールもありますが、無料枠ではURL指定などができないことや、連続駆動時間が決まっていることなど制限が多くあります... 自分の思い通りに公開をしたいのであれば、これらインフラ周りの知識は少なからず必要になるかと思います! すべてを学ぶににはAWSなどを使って、アプリを公開してみるのが一番手っ取り早いと思いますが、料金がかかってしまうことや学習コストが大きいと感じます... そのため学習の際には、まずDockerなどを使って、自分で一からrailsの環境構築を行ってみるといいと思います! OSを設定して必要なパッケージなどをインストールしてということを、手を動かしながら無料でできるのでおすすめです! 終わりに いかがでしたでしょうか? 今回は紹介させていたことがすべてではありません!自分のスキルアップは自分で行っていくものですので、自分流の学習を実践していただけるとよろしいかと思います! ただその中でも、みなさまのrailsの開発スキルアップの参考になれば嬉しく思います 明日は@nabekikikiさんです!おたのしみに!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

slimを使うとき

slimの書き方 サンプル / SLIM doctype html css: h3{color:blue} h3 何かしら入力 p 表示できる ターミナルでの実行の仕方 rake slimrb -p 1115.slim > 1115.html もしくは、エクスプローラーの窓に cmd /c rake と入力すると一発で出来る。 これはrakeをインストールしてたり、簡単に作成できるrakefileを作っている前提での話。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

バイナリーサーチを利用して要素を検索する

調べたことを整理して落とし込めるためにまとめます。 Rubyを使用しています。 バイナリーサーチとは データの検索をする際に用いられる手法のことを言います。 ソート済みのデータ 同一の値が存在しない配列に入ったデータ などが対象です。 検索したい値 = A リストの中央に位置する値 = B  を用意し、AがBより大きければリストの左側にある値を検索対象から除外します。 これを繰り返すことで検索にかかるコストを減らすことができます。 実際に書いたコード def binary_search(array, num) first = 0 last = array.length - 1 while first <= last center = (first + last) / 2 if num == array[center] return puts "#{num}は配列の#{center}番目に存在します" elsif num > array[center] first = center + 1 else last = center - 1 end end puts "#{num}は配列内に存在しません" end array=[1,3,5,6,9,10,13,20,26,31] puts "検索したい数字を入力してください" num = gets.to_i binary_search(array, num) 検索したい値を入力すると、 「○は配列の△番目に存在します」 存在しない値なら 「○は配列内に存在しません」 という結果が返ってきます。 array=[1,3,5,6,9,10,13,20,26,31] puts "検索したい数字を入力してください" num = gets.to_i binary_search(array, num) この部分で検索したい数字を入力して変数numに代入したのち、変数numと配列arrayをbinary_searchへと渡しています。 def binary_search(array, num) first = 0 last = array.length - 1 ~省略 end firstに0を、lastに配列arrayの最後に位置する値の添字を代入しています。 このfirstとlastが検索対象になります。 while first <= last center = (first + last) / 2 while文を使用し、firstはlastよりも小さいという条件式がfalseとなるまで処理を繰り返します。 firstとlastを足して2で割った数を変数centerに代入しています。 これは、配列arrayの中央に位置する値の添字となります。 if num == array[center] return puts "#{num}は配列の#{center}番目に存在します" elsif num > array[center] first = center + 1 else last = center - 1 end if文を使用し、検索対象が配列の中央に位置する値を同じであるか、そうでないか、 また、それよりも大きい値であるかを比較しています。 同じ値であったなら配列の何番目に存在するかを表示します。 違う値であったなら、検索対象を限定して再度検索をかけるように処理を記述します。 検索対象が配列の中央に位置する値より大きい値であったなら、firstにcenterに1足した数字を 小さい値であったなら、lastにcenterから1引いた数字を再代入しています。 def binary_search(array, num) while first <= last ~省略 end puts "#{num}は配列内に存在しません" end そうして値が見つかることなくfirstとlastの値が同じになる(配列arrayの最後の値まで検証し終わる)と、while文に指定した条件式の結果がfalseとなり処理が終了します。 そして、○は配列内に存在しませんと表示されます。 コードを書いていて感じたこと バイナリーサーチを使用して検索をするだけなら、配列arrayの値を再代入しながら限定していくこともできます。 最初はこの方法でコードを書いていました。 ただ、結果として配列の何番目に存在するのかを表示する必要があるため、この方法ではダメだとわかりコードを書き直しました。 色々試行錯誤しましたがなかなか思いつかず、検索をすることにして見つけたのがこの方法です。 コードを読んで理解したことを思い出しながら書きました。 決して丸々コピーなんてしてません。 まとめている中で検索をする処理と、結果を表示する処理は分けたほうが良かったのかな?とも思いました。 コードに関して指摘していただけると勉強の励みになります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

個人ブログのスパム対策に関して(Rails)

概要 先日、Railsで開発した個人ブログを公開しました。 - Niken log - サービス概要 その公開に伴いGoogleのサーチコンソールに登録したは良いものの、問い合わせフォームに、RichardcenceさんやGeorgewetさんなど知らない外国人から「登録したら〇〇ドルもらえる!」というスパムが大量に届いたため、その対策として行ったことを簡単にまとめます。 1. URLのバリデーション 早急にbot等でのスパムメールを送れないようにするため、まず初めに問い合わせフォームの本文にURLのバリデーションを設定しました。 model.rb ## URLの正規表現を生成して定数へ VARID_URL_REGEX = /\A#{URI::DEFAULT_PARSER.make_regexp(%w(http https))}\z/ ## 上記の正規表現が含まれていたらNG validates :message, presence: true, format: { without: VARID_URL_REGEX } 参考サイト1 参考サイト2 このバリデーションだけで恐らく大半のスパムbotは送れなくなると思います。(自身のサイトで企業案件などを募集している方はURLが入力できなくなるのでご注意ください。) 2. googleのreCAPTCHA 加えて自分の場合は、botからの送信対策として、googleのreCAPTCHAを導入しました。 ※reCAPTCHAとは、ユーザー登録等で、信号とかバスとか選択させられたり、自分がロボットか否か確認させられたりするやつです。参考:WHAT IS RECAPTCHA? 実装はpikawakaさんのサイトを参考に行いました。 今回の実装の注意点として、 シークレットキーとサイトキーの発行 自分のサイトの場合、ユーザーからの問い合わせを円滑にするため、v2非表示(サイトの右下に利用規約が表示されるタイプです)を利用しました。その際、サイトキーとシークレットキーが発行されますが、非表示とチェックボックスを切り替える際にキーを発行し直す必要があります。 環境変数の設定 上記で発行した各キーを読み込むために、.envファイルもしくはcredentialへの記載が必要です。 自分はcredentialを用いて、config/initializers下に以下のコードを記載しました。 ※設定の詳細は割愛しますが、開発/本番環境で用いるcredentials.ymlも編集しています。 recaptcha.rb Recaptcha.configure do |config| config.site_key = Rails.application.credentials.recaptcha[:site_key] config.secret_key = Rails.application.credentials.recaptcha[:secret_key] end submitボタンをreCAPTCHAの非表示用のタグに変更 非表示のreCAPTCHAを用いる場合は、通常のform_withなどで用いるsubmitボタンのタグをinvisible_recaptcha_tagsに変更する必要があります。 参考:recapthca gemのREADME sample.html.erb <%= invisible_recaptcha_tags({ text: "送信", class: "btn btn-success w-25" }) %> 以上を行うと、フォームの送信時にbotかどうか、確認してくれるようになります。 まとめ 上記を行ったことで、1日最大で300件近く届いていたスパムメールが0になりました! (サイト自体の訪問者も少ないので、問い合わせ自体が0になりました…) スパムで困っている方の少しでも力になれれば幸いです。 また、編集リクエストなどあれば、気軽にコメント/リクエストください。 ここまでお読みいただきありがとうございました。 完全な宣伝です 以下の個人ブログを運営しています。 - Niken Log - サイト/運営者に関して サイトに関しての意見や共通の趣味がある方からの連絡、その他諸々大歓迎です。(スパムはご遠慮ください笑) ぜひ気軽にサイトまでお越しください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む