- 投稿日:2020-01-03T23:06:18+09:00
Rack入門 Rack Middleware編 (3/3)
前回 は、Rackのプロトコルを理解するために簡単なアプリを作りました。
今回はRackの重要な概念であるRack Middlewareについて学びます。目次
Rack Middlewareとは
はじめにややこしいことを言いますが、Rackはミドルウェア(Middleware)です。
アプリサーバーとフレームワーク間のやりとりを仲介しているため、ミドルウェアと呼ばれます。今回学ぶのはミドルウェアとは何か、ではなくてRack Middlewareについてです。
Rackには以下2つの概念があります。
Rack Application
- 前回学んだ、callメソッドを持つオブジェクトのことです
- StatusCode・Headers・Bodyの3つをレスポンスとして返します
- Rack Endpointとも呼ばれます
Rack Middleware
- 今回学ぶものです
- Rack Middlewareはcallメソッドを持つclassである必要があります
- Rack Applicationと違い、Responseを直接返すのではなく別の処理を呼び出しデータを加工するために使います
Rack Middlewareは、渡ってきたenv情報を加工し、次のmiddlewareまたはendpointに処理を引き渡すものです。
Hello Rack Middleware
Rack Middlewareはenv情報を加工し、次に引き渡すために利用します。
Bodyに「Hello Rack Middleware」と追加するだけのRack Middlewareを作成してみます。class App def call(env) [200, { "Content-Type" => "text/plain" }, ["HELLO Rack Endpoint!\n\n"]] end end # Rack Middlewareは以下条件を満たす必要がある # - classであること # - initializeでappを受け取ること # - callメソッドを実装し、Status/Headers/Bodyを返すこと (Rack Endpointと同じ条件) class HelloRackMiddleware def initialize(app) @app = app end def call(env) status, headers, body = @app.call(env) fixed_body = ["Hello Rack Middleware!\n"] + body [status, headers, fixed_body] end end use HelloRackMiddleware run App.newRack Middlewareをclassとして作り、useメソッドを呼び出すことでmiddlewareを追加できます。
アプリを起動し、レスポンスを見てみます。$ curl http://localhost:9292/ * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 9292 (#0) > GET / HTTP/1.1 > Host: localhost:9292 > User-Agent: curl/7.64.1 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: text/plain < Transfer-Encoding: chunked < Hello Rack Middleware! HELLO Rack Endpoint! * Connection #0 to host localhost left intact * Closing connection 0
Hello Rack Middleware!という文字列がBodyに追加されていますね。Middlewareはいくつでも追加できます。ただし、useの順序によってMiddlewareの動作順が異なることには注意が必要です。
class App def call(env) [200, { "Content-Type" => "text/plain" }, ["HELLO Rack Endpoint!\n\n"]] end end class HelloRackMiddleware def initialize(app) @app = app end def call(env) status, headers, body = @app.call(env) fixed_body = ["Hello Rack Middleware!\n"] + body [status, headers, fixed_body] end end class AnotherMiddleware def initialize(app) @app = app end def call(env) status, headers, body = @app.call(env) fixed_body = ["Another Middleware!\n"] + body [status, headers, fixed_body] end end use HelloRackMiddleware use AnotherMiddleware run App.newレスポンスは以下のとおりです。
Hello Rack Middleware! Another Middleware! HELLO Rack Endpoint!Content-Lengthを追加するMiddleware
もう少し実用的なmiddlewareを作ってみましょう。
Response HeaderにContent-Lengthを挿入するmiddlewareを作ります。class App def call(env) [200, { "Content-Type" => "text/plain" }, ["HELLO WORLD!", "Hello"]] end end class ContentLengthMiddleware def initialize(app) @app = app end def call(env) status, headers, body = @app.call(env) body_size = 0 body.each do |b| body_size += b.bytesize end headers["Content-Length"] = body_size.to_s [status, headers, body] end end use ContentLengthMiddleware run App.newアクセスしてみましょう。
$ curl -v http://localhost:9292/ * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 9292 (#0) > GET / HTTP/1.1 > Host: localhost:9292 > User-Agent: curl/7.64.1 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: text/plain < Content-Length: 17 < * Connection #0 to host localhost left intact HELLO WORLD!Hello* Closing connection 0ちゃんとContent-Lengthが設定されていますね。
Rackにあらかじめ用意されているRack Middleware
Content-Lengthを設定するmiddlewareなど、よく使うと思われるMiddlewareはrack本家で実装されています。
いくつか主要なものを紹介します。
Rack::ContentLength
- https://github.com/rack/rack/blob/master/lib/rack/content_length.rb
- Bodyをみて、Content-Lengthヘッダーを設定します
Rack::Deflater
Rack::ETag
Rack::Session::Cookie
- https://github.com/rack/rack/blob/master/lib/rack/session/cookie.rb
- cookieベースのSession管理を行います
Rack::Reloader
- https://github.com/rack/rack/blob/master/lib/rack/reloader.rb
- リクエストが来た際に、実行中のRubyファイルが更新されていたら自動でリロードします(開発時に利用するもの)
その他のmiddlewareは https://github.com/rack/rack/tree/master/lib/rack を参照してください。
Railsで使われているRack Middleware
Railsもrackのプロトコルに従い作成されています。
Railsで実際に使われているmiddlewareを覗いてみます。$ rails new racktest ...省略... $ cd racktest $ bin/rake middleware Running via Spring preloader in process 10795 use Webpacker::DevServerProxy use ActionDispatch::HostAuthorization use Rack::Sendfile use ActionDispatch::Static use ActionDispatch::Executor use ActiveSupport::Cache::Strategy::LocalCache::Middleware use Rack::Runtime use Rack::MethodOverride use ActionDispatch::RequestId use ActionDispatch::RemoteIp use Sprockets::Rails::QuietAssets use Rails::Rack::Logger use ActionDispatch::ShowExceptions use WebConsole::Middleware use ActionDispatch::DebugExceptions use ActionDispatch::ActionableExceptions use ActionDispatch::Reloader use ActionDispatch::Callbacks use ActiveRecord::Migration::CheckPending use ActionDispatch::Cookies use ActionDispatch::Session::CookieStore use ActionDispatch::Flash use ActionDispatch::ContentSecurityPolicy::Middleware use Rack::Head use Rack::ConditionalGet use Rack::ETag use Rack::TempfileReaper run Racktest::Application.routesいくつかrackで実装されているMiddlewareも利用しているのがわかります。
Railsで利用しているMiddlewareについては、https://guides.rubyonrails.org/rails_on_rack.html#internal-middleware-stack にて詳しく説明されています。まとめ
- Rack Middlewareは、渡ってきたリクエスト/レスポンスを加工するために利用する
- Middlewareはcallメソッドを実装したclassである必要がある
Rackをより理解するためには
全3回にわたって、Rackの基本的な概念を簡単なアプリを作りながら学びました。
思ったよりシンプルな構造でしたね。よりRackを理解するための参考資料を、以下に記載しておきます。
Rack本家のExample
- https://github.com/rack/rack/blob/master/lib/rack/lobster.rb
- アクセスするとロブスターを返します。
bundle exec ruby lib/rack/lobsterで起動できますRack::Request,Rack::Responseを使って処理をラップしているので少し複雑ですが、基本的な考え方は今回学んだとおりですResponse#finishは https://github.com/rack/rack/blob/master/lib/rack/response.rb#L66 のとおりで、StatusCode/Headers/Body のペアを返します- (Paste has a Pony って、なんだろう...)
次世代のRackやWSGIを考えてみる
- https://qiita.com/kwatch/items/67657fef43666479bb99
- Rackの問題点について言及されています。
RackのHTTP/2サポートについて
- https://techracho.bpsinc.jp/hachi8833/2017_07_03/42195
- https://github.com/tenderlove/the_metal/issues/5
- Rackは今回学んだ通り、リクエストに対して1つのレスポンスを返します。しかしHTTP/2だとレスポンスは複数返さないといけないので、さてどうしたものか、という点について言及されています。
- 投稿日:2020-01-03T23:05:42+09:00
Rack入門 Rack Application編 (2/3)
前回 はRackが必要とされた背景と、基本的な概念について説明しました。
今回は実際にRackプロトコルを使いアプリサーバーと通信するプログラムを作りながら、Rackに関する理解を深めていきます。目次
Hello Rack Application
基本を理解するために、簡単なRackアプリケーションを作ってみます。ブラウザーからアクセスされたら、「Hello Rack!」と返すだけのシンプルなアプリです。
まずは以下Gemfileを用意し、bundle installをしてください。アプリサーバーはpumaを使います。
source "https://rubygems.org" gem "rack" gem "puma"次に
config.ruというファイルを作成します。拡張子がruですが、中身はrubyのコードです。class HelloRackApp # callメソッドはenvを受け取り、3つの値(StatusCode, Headers, Body)を配列として返す def call(env) [200, { "Content-Type" => "text/plain" }, ["Hello Rack!"]] end end # callメソッドを呼び出せるObjectをrunに渡し、rackアプリを起動する run HelloRackApp.newターミナル上で以下のコマンドを実行し、Rackアプリを起動します。
bundle exec rackup -s pumarackupコマンドはデフォルトではconfig.ruを読み込み、Rackアプリを起動します。
bundle exec rackup config.ruとしてファイルを指定することもできます。
-s pumaはアプリサーバーの指定です。指定しない場合はpumaかthinかwebrickのいずれかが使われます。$ bundle exec rackup -s puma Puma starting in single mode... * Version 4.3.1 (ruby 2.7.0-p0), codename: Mysterious Traveller * Min threads: 0, max threads: 16 * Environment: development * Listening on tcp://127.0.0.1:9292 * Listening on tcp://[::1]:9292 Use Ctrl-C to stopport9292でRackアプリが起動しました。
pumaはWebサーバーとしての機能も持っているため、ブラウザーやコマンドからもアクセスできます。
curlでアクセスしてみます。$ curl -v http://localhost:9292 * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 9292 (#0) > GET / HTTP/1.1 > Host: localhost:9292 > User-Agent: curl/7.64.1 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: text/plain < Transfer-Encoding: chunked < * Connection #0 to host localhost left intact Hello Rack!* Closing connection 0Hello Rack!とレスポンスが返ってきました。Statusは
200 OK、Headerには指定したContent-Type: text/plainも含まれています。ブラウザーからもアクセスしてみます。Rackアプリケーションの形式
Rackアプリはアプリサーバーと通信するために、以下の形式(プロトコル)にしたがって作成する必要があります。
- RackアプリはObjectであり、callメソッドを呼び出せること。callメソッドは引数を一つ受け取ること
- callメソッド呼び出し後、以下3つの値を配列として返すこと
- (HTTP) Status Code
- Headers
- Body
- Status Codeは3桁のHTTPのステータスコード(100以上の数値)であること
- Headersはeachメソッドを実装し、yieldの際にkey/valueのペアを渡すこと。key/valueは必ずStringであること
- Bodyはeachメソッドを実装し、yieldの際にStringを渡すこと
実際はさらに細かく仕様が決まっていますが、要はこれだけです。
Rackの詳細な仕様は https://github.com/rack/rack/blob/master/SPEC に記載されています。yieldの際にStringを渡すこと、というのがわかりにくいですね。
rubyのyieldについて復習しておきます。[1,2,3,4,5].each { |v| puts v }上記コードの実行結果は以下となります。
1 2 3 4 5この
vがyieldしたときに渡ってくる値です。eachは以下のように実装できます。class MyArray def initialize(values) @values = values end def each for v in @values yield v end end end MyArray.new([1,2,3,4,5]).each { |v| puts v }RackのHeadersはkey/valueの2つの値を渡しますが、この値がStringであれば良いです。
Bodyは、yieldの引数として渡す値がStringであれば良いですね。Rackアプリが満たすべき要求はこれだけです。
もう一度最初のRackアプリのコードを見てみます。class HelloRackApp def call(env) [ 200, # 1番目はStatusで、3桁の数値を返す { "Content-Type" => "text/plain" }, # 2番目はHeaders, Hashはeachメソッドを実装済 # key/valueはStringあること ["Hello Rack!"] # 3番目はBody, Arrayはeachメソッドを実装済み # 中身はStringであること ] end end run HelloRackApp.new上記の形式に従っていれば、立派なRackアプリケーションの完成です。
HelloRackAppはclassである必要はありません。callメソッドを持ったObjectであればよくて、Headers/Bodyもeachを持ってさえいればよいので、以下のコードでも動きます。
class MyHeader def initialize(bodies) @bodies = bodies end def each yield "Content-Type", "text/plain" yield "Content-Length", @bodies.join.bytesize.to_s # NOTE: ValueはStringにしないといけない end end class MyBody def initialize(bodies) @bodies = bodies end def each for v in @bodies yield v end end end # NOTE: procはcallで呼び出し可能 app = proc do |env| bodies = ["こんにちはRack!\n\n", "Hello Rack!\n"] [200, MyHeader.new(bodies), MyBody.new(bodies)] end run app$ curl -v http://localhost:9292 * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 9292 (#0) > GET / HTTP/1.1 > Host: localhost:9292 > User-Agent: curl/7.64.1 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: text/plain < Content-Length: 34 < こんにちはRack! Hello Rack! * Connection #0 to host localhost left intact * Closing connection 0envを使ってリクエストを取り出す
callメソッドの引数envには、ブラウザーなどからのリクエストが格納されています。
以下のコードで中身を覗いてみます。class HelloRackApp def call(env) require "pp" pp env [200, {}, []] end end run HelloRackApp.newrackup後、サーバーにアクセスして結果を見てみます。
{"rack.version"=>[1, 3], "rack.errors"=> #<Rack::Lint::ErrorWrapper:0x00007f90a41c0c00 @error=#<IO:<STDERR>>>, "rack.multithread"=>true, "rack.multiprocess"=>false, "rack.run_once"=>false, "SCRIPT_NAME"=>"", "QUERY_STRING"=>"", "SERVER_PROTOCOL"=>"HTTP/1.1", "SERVER_SOFTWARE"=>"puma 4.3.1 Mysterious Traveller", "GATEWAY_INTERFACE"=>"CGI/1.2", "REQUEST_METHOD"=>"GET", "REQUEST_PATH"=>"/", "REQUEST_URI"=>"/", "HTTP_VERSION"=>"HTTP/1.1", "HTTP_HOST"=>"localhost:9292", "HTTP_USER_AGENT"=>"curl/7.64.1", "HTTP_ACCEPT"=>"*/*", "puma.request_body_wait"=>0, "SERVER_NAME"=>"localhost", "SERVER_PORT"=>"9292", "PATH_INFO"=>"/", "REMOTE_ADDR"=>"::1", "puma.socket"=>#<TCPSocket:fd 18, AF_INET6, ::1, 9292>, "rack.hijack?"=>true, "rack.hijack"=> #<Proc:0x00007f90a41c0fc0 /Users/nishio/.rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/gems/rack-2.0.8/lib/rack/lint.rb:525>, "rack.input"=> #<Rack::Lint::InputWrapper:0x00007f90a41c0c28 @input=#<Puma::NullIO:0x00007f90a585a8f8>>, "rack.url_scheme"=>"http", "rack.after_reply"=>[], "puma.config"=> #<Puma::Configuration:0x00007f90a41d7798> "rack.tempfiles"=>[] }envには以下の全てが混ざったデータがHash形式で格納されています。
HTTPリクエストヘッダー
- HTTP_VERSIONやHTTP_USER_AGENTなど、HTTPリクエストヘッダーの値がkey/value形式で入っている
- Keyは他の情報と区別するために、
HTTP_というprefixを付与しているHTTPリクエスト情報
- QUERY_STRING や REQUEST_URIなど、リクエストヘッダー以外のリクエスト情報が入っている
pumaやrackなどのアプリからの情報
- rack.version や puma.config など、各種アプリが付与した情報が入っている
envの値を利用することで、リクエストに応じたレスポンスを返せます。
試しに、以下アプリを作ってみます。
http://localhost:9292/helloにアクセスしたら、Hello!を返す- 上記以外のURIにアクセスしたら、
404 Not Foundを返すclass App def call(env) # NOTE: # REQUEST_PATHも利用できるが、実装されていないケースが存在する # PATH_INFOは必ず存在するので、urlはPATH_INFOを使って取り出す方が安全である # https://github.com/envato/jwt_signed_request/issues/15 path = env["PATH_INFO"] if path == "/hello" [200, { "Content-Type" => "text/plain" }, ["HELLO!"]] else [404, { "Content-Type" => "text/plain" }, ["404 Not Found"]] end end end run App.new上記アプリにアクセスしてみましょう。
$ curl -v http://localhost:9292/hello * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 9292 (#0) > GET /hello HTTP/1.1 > Host: localhost:9292 > User-Agent: curl/7.64.1 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: text/plain < Transfer-Encoding: chunked < * Connection #0 to host localhost left intact HELLO!* Closing connection 0$ curl -v http://localhost:9292/testtest * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 9292 (#0) > GET /testtest HTTP/1.1 > Host: localhost:9292 > User-Agent: curl/7.64.1 > Accept: */* > < HTTP/1.1 404 Not Found < Content-Type: text/plain < Transfer-Encoding: chunked < * Connection #0 to host localhost left intact 404 Not Found* Closing connection 0ファイルを返すアプリを作成する
画像ファイルをレスポンスとして返すアプリを作ってみます。
RackのBodyは文字列でさえあればなんでもよいので、以下のように実装すれば画像を返せます。class App def call(env) path = "./neko.jpg" # 画像ファイルは好きなものを用意 file = File.open(path, "rb") image_data = [file.read] # Bodyは必ずeachメソッドを持つ必要があるのでArrayとする [200, { "Content-Type" => "image/jpeg" }, image_data] ensure file.close end end run App.new上記コードにブラウザーからアクセスしてみます。
確かにこれでも動きはしますが、ファイル全体を一度メモリ上に読み出してBodyに詰める必要があります。
読み込み効率が悪いので、レスポンスがしんどいですね。Rackはファイルをレスポンスすることも考え、以下のようなコードを記述できます。
class App def call(env) path = "./neko.jpg" # 画像ファイルは好きなものを用意 file = File.open(path, "rb") [200, { "Content-Type" => "image/jpeg" }, file] end end run App.newRubyのFileは以下メソッドを持っています。
eachメソッド
- eachを使えば、一定データ量ずつファイルから読み出せます
- 例えば
File.each(100)とすれば、ファイルを100バイトずつ読み出せますcloseメソッド
- ファイルはopenしたら必ずcloseする必要があります
Rackは、
closeメソッドを持ったオブジェクトをbodyとして渡すと、各種処理を実行後にcloseを呼び出してくれます。このように、Fileオブジェクトを渡すことを想定した仕様があらかじめ定義されています。
rackupコマンドを使わずrackアプリを起動する
最後に、rackupコマンドを使わずにアプリを起動するコードを紹介します。
$ bundle exec ruby config.ru実装は以下コードとなります。
class App def call(env) [200, { "Content-Type": "text/plain" }, ["Hello Rack!"]] end end require "rack" app = Rack::Builder.new do run App.new end require "rack/handler/puma" Rack::Handler::Puma.run app
rackやpuma関係のファイルをrequireし、Rack::Builderで初期化したものをPumaのHandlerに渡せばよいです。
rackupコマンドは、このあたりのファイル読み込みをあらかじめ自動で行ってくれる便利コマンドです。まとめ
- Rackアプリはあらかじめ決められたRackプロトコルにしたがって作成する
- Specの詳細は https://github.com/rack/rack/blob/master/SPEC に記載されている
- Rackアプリはcallメソッドを持ったオブジェクトであること
- callメソッドは、引数を1つ受け取り、StatusCode・Headers・Bodyのペアを返す
- callメソッドの引数(env)から、リクエスト情報を取り出せる
- 投稿日:2020-01-03T23:05:18+09:00
Rack入門 概念編 (1/3)
RailsやSinatraなどのRuby製Webフレームワークを利用されている方は、Rackというキーワードを一度は目にしたことがあるのではないでしょうか。
よく聞くけど詳しくは知らない、そんなやつがRackです。
今回は自分の知識の整理も兼ねて、Rackとは何ものなのかについて調べたメモを、ここに残します。長かったので、全3回に分割しています。
目次
Rackとは何か。ひとことで
- Webアプリケーションサーバーとアプリケーションを接続するための標準化されたインターフェースのこと
Rackとは何か。詳細
Rackの公式リポジトリでは、Rackは以下のように記載されています。
a modular Ruby webserver interface 訳) モジュール化されたRuby Webサーバーインターフェースそして、
Supported web serversとして PumaやUnicorn が上げられています。このWebサーバーという表現はいろいろとややこしいので、本稿ではPuma/Unicornのことは アプリケーションサーバー(アプリサーバー) と呼びます。
Rackはなぜ必要なのか。その前に基本的な概念についておさらいします。
アプリケーションサーバーとは
アプリケーションサーバーって、なんでしたでしょうか。
ここで一度振り返っておきます。Webサーバー、アプリサーバーの代表的なソフトウェアは以下のとおりです。
Webサーバー
- nginx, apache など
アプリケーションサーバー
- Rubyでは Puma, Unicorn など
- JavaではTomcat など
Webシステムの多くは、いわゆる3層アーキテクチャと呼ばれる構成で設計されており、Webサーバーとアプリサーバーは別に運用します。
以下のような概念図をよく目にするのではないでしょうか。DBサーバーについては、今回は関係ないので説明を割愛します。
別にこの構成に従わなくてもウェブシステムは運用できますが、システムの性能を引き出すために3層アーキテクチャを採用することが多いです。
Webサーバー
Webサーバーは、ユーザーからのリクエストを受け取り、処理結果をレスポンスとして返すソフトウェアのことです。
受け取ったリクエストはWebサーバー自身が処理することもありますし、そのまま処理をアプリサーバーに委譲することもあります。
静的なコンテンツ(画像やファイル)は、Webサーバーでデータを引っ張ってきて返すことが多いです。
ビジネスロジックはRubyなどで書いたアプリに処理を委譲して、処理結果をレスポンスとして返すことが多いです。nginxなどのWebサーバーは、大量のアクセスが来ても素早く効率的にさばくための機構を持っています。
またSSL通信やデータを圧縮して返すなど、Webの面倒事を引き受けてくれるソフトウェアです。アプリケーションサーバー
アプリサーバーは、アプリをあらかじめ起動しておき、リクエストが来たらすばやく処理をするためのサーバーです。
アプリサーバーがないと、どうなるでしょうか。
昔のWebシステムでは、アプリサーバーは立てませんでした。
CGI全盛期は、Webサーバーにリクエストが来た後、プログラムを都度起動していました。
リクエストのたびにプログラムの起動から始めるというのは、巨大なアプリケーションになればなるほど起動コストが無視できないですよね。なので、あらかじめプログラムをメモリ上に読み込んでおき、アクセスが来たら処理を高速に返せるようアプリサーバーを用意します。
PumaやUnicornなどのアプリサーバーは、Webサーバーとしての最低限の機能も持っています。
Webサーバーを立てずにPumaだけでもシステムを運用できます。
ただ、大量のアクセスを効率的にさばくことはPuma単体ではできない(そこはWebサーバーの役割としている)ので、一般的には前段にWebサーバーを立ててリクエストを処理をします。Rackはどこで使われ、なぜ必要か
Rackはアプリサーバーとアプリ(Webフレームワーク)間のデータのやり取りに利用します。
PumaやUnicornといったアプリサーバーは、特定のアプリ専用(たとえばRails専用)というわけではなく、RackのプロトコルにしたがっているWebフレームワークであれば何でも利用できます。
Rackに対応しているRuby製のフレームワークは、Railsの他にSinatra, Padrino, Hanamiなどがあります。
アプリサーバーとアプリ間の通信仕様を定めておく(=インターフェースの標準化をしておく)ことで、
アプリサーバーとアプリケーションフレームワークの組み合わせを自由に変えることができます。
Rails専用のアプリサーバーを作る、Sinatra専用のアプリサーバーを作るっていうのは大変ですよね。アプリ・フレームワーク間の標準インターフェースを作るという流れは、PythonのWSGI(Web Server Gateway Interface)がはじめました。
PythonはもともとWebフレームワークとフレームワーク専用のアプリサーバーが乱立していましたが、WSGIはアプリとサーバーのその組み合わせを自由に変えられるよう標準インターフェースを定めました。
これにより、移植性の高いアプリサーバー/フレームワークを作ることができるようになりました。Rackの細かいところはWSGIと異なりますが、このWSGIの影響を強く受けています。
まとめ
- アプリサーバーとは、アプリをあらかじめ起動しておきレスポンスを高速に返すためのサーバー
- Rackはアプリサーバーとフレームワーク(アプリ)間のインターフェースを定めている
- Rackのプロトコルに従うことで、アプリサーバーとWebフレームワークの組み合わせを自由に変えることができる
- 投稿日:2020-01-03T22:58:58+09:00
【Rails】Strong Parameters
- 投稿日:2020-01-03T20:29:22+09:00
【Rails】devise_token_authで新規ユーザー登録時にUnpermitted parameters: が出たときの対処法
はじめに
gem devise_token_authを使っていて、新規ユーザー登録時に以下エラーが出たときの対処法を残します。
Unpermitted parameters: 保存されて欲しいカラム名自分でUserテーブルにカラムを追加して、保存しようとしたら
発生する内容です。今回、自分の場合は
:ageと:genderを追加しようとしたところ発生しました。(以下参照)Unpermitted parameters: :age, :genderこれを解決していきます。
環境
OS: macOS Catalina 10.15.1 zsh: 5.7.1 Ruby: 2.6.5 Rails: 6.0.2.1結論:
registrations_controllerに追記registrations_controllerclass Api::V1::Auth::RegistrationsController < DeviseTokenAuth::RegistrationsController private def sign_up_params # ここに :age, :genderを追記 params.permit(:name, :email, :age, :gender, :password, :password_confirmation) end def account_update_params params.permit(:name, :email) end end※反映するには
rails server再起動が必要です。理由
Strong Parameterではじかれてしまっているのが問題でした。
sign_up_paramsの中に追記することで、新規ユーザー登録のときだけ許可されるようになります。こいつも許可してあげてよ!という指定をすればOKですね。
おわりに
最後まで読んで頂きありがとうございました
どなたかの参考になれば幸いです
- 投稿日:2020-01-03T18:28:29+09:00
3ヶ月で Rails 4.2 → 5.2 にした軌跡
概要
資格スクエアの PM をやっています岩瀬と言います。資格スクエアは、法律系難関資格に特化したオンライン学習サービスです。
サービスを開始してから数年が経ち、1つの大きな問題がありました。それは Rails のメジャーアップグレードです。
長年の負債が溜まっていたので、なかなかメジャーアップグレードできずに苦しんでいたのですが、フリーランスのカルパスさん(yhirano55)の加入で一気に進みました。
カルパスさんが方針を考えて実装し、それをレビューでサポートしたラスタムさん(rastamhadi)はじめ、チーム一丸となって取り組んだ結果、わずか3ヶ月で成し遂げられるミラクルを目撃したので、その内容をまとめました。
記事の目的
テストカバレッジが低くても、Rails のメジャーアップグレードができるという知見を共有するためです。
背景
Rails メジャーアップグレードは適宜対応していくのが最善ですが、以下の課題があったため、作業が難航していた状態でした。
- サービス成長を優先していたため、Rails のメジャーアップグレードが後回しになっていた
- サービス開始から5年以上が経過していたため、負債が溜まりメンテナンスコストが高くなっていた
- サービス立ち上げ時はテストコードを書いていなかったため、カバレッジが 32 %と低い状態であった
ですが、Rails 6.0 が出るにあたって、いよいよ Rails 4 系のサポートが終了するため、早急に Rails 5 系に上げる必要がありました。
どうしようか悩んでいた時、ラスタムさんの紹介によってカルパスさんが加入して本格的に着手することになりました。
バージョン
以下は Rails のメジャーアップグレード作業に着手した際に使用していたバージョンです。
名称 バージョン Rails 4.2.11.1 Ruby 2.3.6 規模
資格スクエア は、2013年11月にサービスを立ち上げてから5年以上が経過しており、中規模と言えるような状態になっていました。規模の目安として、あくまで参考ですが2020年1月現在での各モジュールの数は以下になります。
名称 数 Controllers 177個 Models 190個 Code LOC 108,612行 戦略
上記の背景と規模を踏まえて、次の戦略で進めることにしました。
- メジャーアップグレードを優先して最小のテストカバレッジで進める
- テストでカバーできないものは動作確認で洗い出す
- 動作確認でも洗い出せなかったものは本番環境に反映後、即対応する
タイムライン
今回の取り組んだアップグレードのざっくりしたタイムラインは以下になります。
年月日 内容 2019年8月27日 Rails 5.0 へメジャーアップグレード作業開始 2019年10月17日 Rails 5.0.7.2 にアップグレード完了 2019年10月23日 Ruby 2.6.5 にアップグレード完了 2019年11月19日 Rails 5.2.3 にアップグレード完了 手順
Rails 4.2 → 5.0
それではポイントを押さえて、実際に行ったメジャーアップグレード手順を紹介します。
1. 足場の確保
まずは開発環境を整え、テスト実行の足場を確保しました。
- 落ちているテストを直す
- 開発用途の gem を更新する(lock_diff を活用して changelog を追う)
2. モデルとテーブル定義の不一致を解消する
モデルによるレコード生成をテストコードで担保したいのですが、そもそもモデルとテーブル定義が不一致だったので、それを解消しました。
- annotate でモデルとテーブル定義の制約条件(validation)が一致していることを確認しやすいようにする
- 開発環境と本番環境のテーブル定義を一致させる
- テーブルの NOT NULL 制約とモデルの presence validation を一致させる
- テーブルの unique index とモデルの uniqueness validation を一致させる
3. 全モデルに最低限の単体テストを書く
ここで全モデルのテストコードを書きました。単体テストを網羅するのはあまりにも大変なため、最低限レコード生成を担保するテストを整備しました。
- 全モデルの factory を定義する
- レコードの生成が valid となる最低限のテストを用意する
ex.
spec/models/certificate_spec.rbrequire 'rails_helper' RSpec.describe Certificate do describe 'factory' do it 'has a valid factory' do expect(build(:certificate)).to be_valid expect(create(:certificate)).to be_persisted end end end4. 管理画面に最低限の結合テストを書く
管理画面は gem を使わず、独自実装であったため結合テストで最低限画面表示できることを担保しました。
ex.
spec/features/admin/certificates_spec.rbrequire 'rails_helper' RSpec.feature '管理ツール 資格管理', type: :feature do scenario '一覧が閲覧できる' do prepare_logged_in_admin_user within(:css, '#sidebar') do click_link '資格管理' end expect(page).to have_current_path admin_certificates_path expect(page).to have_selector 'h1.page-header', text: '資格管理' end end5. Rails 5.0 の変更点に応じて修正する
Rails 4.2からRails 5.0へのアップグレードに記載されている内容を1つ1つ確認しながら対応しました。
- Rails 5.0 から全 Model は ApplicationRecord という抽象クラスを継承する形式のため変更する
- ActionController::Parameters は今後 HashWithIndifferentAccess を継承しないので対応する
- migrationファイルを置き換える
ex.
db/migrate/20150302014237_create_admin_users.rb- class CreateAdminUsers < ActiveRecord::Migration + class CreateAdminUsers < ActiveRecord::Migration[4.2]6. Rails 5.0 に上げる Pull Request を作成する
ここで Rails 5.0 に上げる目処が立ったので、 Pull Request を作成しました。
- gem の依存バージョンを解決する
- Rails 5.0.7.2 にアップグレードする
- 設定ファイルの更新(app:update)
- テストが実行できる
- テストを実行し、落ちているものを修正する
- すべての警告を抑止する
7. テスト環境で隅々まで動作確認する
最低限のテストコードでカバーしているため、テスト環境で動作確認を行いました。その動作確認の結果、エラーになった箇所を1つ1つ解決していきました。資格スクエアで遭遇したエラーは以下になります。
- Rails 5.0 では存在しないコールバックをスキップすると ArgumentError になるので不要なコールバックを削除する
- params をそのまま使うと Rails 5.0 からセキュリティの都合で ArgumentError になるため、hash に変更する
- Rails 5.0 で collection_check_boxes の hidden 要素が最後から最初に変わったため、修正する
8. Rails 5.0 を本番環境にデプロイする
いよいよ Rails 5.0 に変更できる準備が整ったのでデプロイしました。
何か問題が発生しても対応できるようにエンジニアメンバーが稼働している勤務時間帯に反映しました。9. 本番環境で発生した問題に対処する
実際、デプロイ後にやはり問題が発生しました。具体的には以下のものになります。
- 本番反映前の Rails 4.2 で作られたジョブを Rails 5.0 で動作させるために一時的にパッチを当てる
ex.
config/initializers/temporary_active_record_patch.rbclass ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter class MysqlDateTime < ActiveRecord::Type::DateTime end end
- Rails アップグレードに伴い、セッションが切れる問題が発生したためにセッションをクリアするコマンドを実行する
$ RAILS_ENV=production bundle exec rails r 'Rails.cache.clear'これで Rails 4.2 → 5.0 へメジャーアップグレード完了です。
Rails 5.0 → 5.2
Rails 5.0 にメジャーアップグレードできましたが、ここで立ち止まらず引き続きマイナーアップグレードを行いました。それは Rails 5.2 まで上げると system spec が使えるようになり、javascript の動作をテストコードで担保できるようになるからです。
1. 整理整頓
メジャーアップグレードで把握した使用 gem の内、不要なものは削除するなど、メンテコストを下げました。
2. Rails 5.1 の変更点に応じて修正する
Rails 5.0からRails 5.1へのアップグレードに記載されている内容を1つ1つ確認しながら対応しました。
- ActionController::Parameters における HashWithIndifferentAccess のメソッド呼び出しのサポートが削除されたので対応する
- render :text を render :plain に変更する
- render :nothing を head :ok に変更する
3. ついでに API のテストカバレッジを上げて整理する
Rails で API を担っている領域のテストカバレッジが低かったので、requests spec で担保しました。
- 正常系と異常系の requests spec を整備する
- 不要なエンドポイントを削除する
4. Rails 5.1 に上げる Pull Request を作成する
ここで Rails 5.1 に上げる目処が立ったので、 Pull Request を作成しました。
- gem の依存バージョンを解決する
- Rails 5.1 にアップグレードする
- 設定ファイルの更新(app:update)
- テストが実行できる
- テストを実行し、落ちているものを修正する
- すべての警告を抑止する
5. Rails 5.2 の変更点に応じて修正する
引き続きRails 5.1からRails 5.2へのアップグレードに記載されている内容を1つ1つ確認しながら対応しました。
- uniq は Rails 5.1 で削除されるので distinct に置換する
6. Rails 5.2 に上げる Pull Request を作成する
ここで Rails 5.2 に上げる目処が立ったので、 Pull Request を作成しました。
- gem の依存バージョンを解決する
- Rails 5.2 にアップグレードする
- 設定ファイルの更新(app:update)
- テストが実行できる
- テストを実行し、落ちているものを修正する
- すべての警告を抑止する
7. テスト環境で隅々まで動作確認する
この段階でテストカバレッジは 47 % と以前よりはだいぶ上がっていますが、
まだまだテストコードで担保できていない機能があるので、テスト環境で動作確認を行いました。8. Rails 5.2 を本番環境にデプロイする
Rails 5.2 に変更できる準備が整ったのでデプロイしました。
9. 本番環境で発生した問題に対処する
Rails 5.2 のデプロイ後は特にエラーは発生しませんでした。Rails 5.0 の時と同様にセッションが切れる問題は再度発生することが予想されたので対応しました。
これで Rails 5.0 → 5.2 へマイナーアップグレード完了です。
その後
Rails 6.0 にはすぐに上げることができるのですが、特に使いたい機能があるわけではなかったので、ここで一旦立ち止まり強化することにしました。対応したことは以下になります。
- system spec による結合テストの整備(2020年1月時点でカバレッジ 53 %)
- Dependabot によるバージョン追従
- CI の実行速度の改善と整備
- CD の開発
まとめ
Rails 4.2 から 5.2 までアップグレードした3ヶ月の軌跡をまとめました。
最初、自分が資格スクエアをメジャーアップグレードしようと考えた時は、正直テストカバレッジが低いので相当難しいなという印象でした。
しかし、効果的な戦略と的確な変更によってわずか3ヶ月で成し遂げられた状況を見て、とても感動しました。そしてシステム面では新しい機能が使用できたり、パフォーマンスが向上したりと様々な恩恵を受けています。
この方法が自分たちと同じようにメジャーアップグレードに悩んでいる方々にとって参考になれば幸いです。
- 投稿日:2020-01-03T18:11:16+09:00
railsのDBで型をintegerからfloatへ変更しnumber_fieldで小数点を扱えるようにする
前回の記事の続きで初めてfloat型を扱ったのでメモ
やりたい事
- DBに間違ってinteger型で作ったカラムをfloat型に変更
- form_for内でnumber_fieldで小数点を扱えるようにする
- DBに保存した値を合計するメソッドを作る
参考
【Rails】form_for の number_field で小数を入力できるようにする
https://qiita.com/tegnike/items/07f789eb22c7a7bf6a19マイグレーションを使ったカラムの追加、削除、データ型の変更 [ 自分用メモ ]
https://qiita.com/dawn_628/items/13fa64dc6d600e921ce3【Rails】カラムの合計値を求める!
https://qiita.com/tomokichi_ruby/items/8758a91566957cfc5429number_fieldでは整数のみしか扱えない
new.html.erb<%= form_for @count_time do |f| %> <%= f.number_field :count_hour, class:'input_form' %> <%= f.submit '登録', class:'input_submit' %> <% end %>というフォームがあって,
new.html.erb<%= form_for @count_time do |f| %> <%= f.number_field :count_hour, step: '0.1', class:'input_form' %> <%# step: '0.1'を追加 %> <%= f.submit '登録', class:'input_submit' %> <% end %>
step: '0.1'を追加すると小数点以下第一位までの数値を扱えるようになりますこのままだとDBには整数でしか保存されない
DBの
count_hourはinteger型なので整数のみしか保存してくれないのでカラムの型を変更するターミナル$ rails g migration change_data_count_hour_to_count_time Running via Spring preloader in process 94544 invoke active_record create db/migrate/20200103074431_change_data_count_hour_to_count_time.rbカラム名がcount_hourでテーブル名がcount_timeになります
20200103074431_change_data_count_hour_to_count_time.rbclass ChangeDataCountHourToCountTime < ActiveRecord::Migration[5.2] def change change_column :count_times, :count_hour, :float #追記。テーブル名は複数形で end end
rails db:migrateしますこれでfloat型に変更できたのでnumber_fieldから送られる小数点の値がDBへ保存できるようになりました
DBの値をビューへ描画
index.html.erb<table> <tr> <th>日付</th> <th>時間</th> </tr> <% @count_time.each do |count_time| %> <tr> <td><%= count_time.created_at.to_date %></td> <td><%= count_time.count_hour %>時間</td> </tr> <% end %> </table>sumメソッドを使い学習時間の合計を表示させる
count_time_controller.rbclass CountTimesController < ApplicationController def index @count_time = CountTime.where(user_id: current_user.id).order('updated_at DESC').limit(5) @count_hour = @count_time.sum(:count_hour) end endsumメソッドの引数にカラムを指定するで合計値を出します
完成
まとめ
ひとまずこれで自分が最初に想像していた主要機能が完成しました。開発にそんな時間がかからないアプリでしたが知らない事も学べたので良かったかなと。あとは思い付いた追加の機能を開発していこうと思います。
終わり
- 投稿日:2020-01-03T17:36:45+09:00
HerokuでS3に画像をアップロードした話[Rails][S3][CarrierWave][fog]
転職活動用にポートフォリオを作っています。
DTPの作品も多く、画像が全部でおよそ18MBあったのですが、Herokuの無料版の容量は5MBのため、画像の外部ストレージとしてS3を利用するようにしました。調べたら、多くの人がつまづいているようでしたので、自分のためにも手順をメモしておこうと思います。
S3の利用設定
まずは、S3の利用設定を行います。手順については、こちらのサイトを大変参考にさせていただきました。
【Rails】S3へ『CarrierWave+fog』を使って画像アップロードする方法
↑こちらの記事以上に丁寧な説明はないと思いますので、S3の設定についてはこちらをご参照いただければ幸いです。
gemをインストール
画像アップロードに必要な、以下のgemをインストールします。
carrierwave…画像をアップロードするgemmini_magick…画像をリサイズするgemfog…S3などのクラウドストレージ内の画像を参照するgemそれぞれ、設定の書き方は以下の通り。
Gemfilegroup :production do gem 'pg', '0.18.0' gem 'rails_12factor', '0.0.2' gem 'fog-aws' end gem 'carrierwave' gem 'mini_magick'忘れずに
bundle installします。なお、私の場合ですが
fogというgemではaws関連のdependencyのgemがインストールされず、fog-awsを利用しないと先に進めませんでした。アップローダーの生成
CarrierWaveをインストールすると、画像のアップローダーの生成ができるようになりますので、以下のコマンドで生成します。
$ rails generate uploader カラム名最後の「カラム名」は、画像を必要とするモデルのデータベースのカラム名です。単数形、頭文字大文字で表記します。私は、すでにあったテーブルのカラム名から、
Imageにしました。なお、画像のファイル名が文字列のため、imageカラムのデータ型はstring型です。
$ rails generate uploader Image create app/uploaders/image_uploader.rb ←生成CarrierWaveに、imageカラムがuploaderと関連していることを認識させるためには、
mount_uploaderというメソッドを使います。(今回モデル名はwork)models/work.rbclass Work < ApplicationRecord mount_uploader :image, ImageUploader endviewファイルの書き換え
そして、今度はビューファイルをいくつか変更します。まずは、ファイルをアップロードする機能をviewに持たせます。画像をアップロードするフォームのタグを
file_fieldにしました。views/works/new.html.haml= f.label :image, "画像:" = f.file_field :imageそして、workの一覧ページの書き方も、CarrierWaveに合わせて書き換えます。
views/index.html-# 書き換え前 - @works.each do |work| = image_tag work.image -# 書き換え後 - @works.each do |work| = image_tag work.image.urlwork.imageの後にurlをつけるのがポイントです。
以上の動作により、無事画像のアップロードができました。本番環境で画像のアップロード先を変える
この時、画像ファイルはデフォルトでは、ローカルのフォルダにアップロードさせています。
views/uploaders/image_uploader.rbstorage :file
storage :fileという記述は、ローカルのファイルシステムに画像を保存するための記述です。これだと、Herokuにデプロイした時に容量を圧迫してしまうので、本番環境だけS3に画像をアップするようにしたいと思います。
views/uploaders/image_uploaderif Rails.env.production? storage :fog else storage :file end
if Rails.env.production?で本番環境ではクラウドストレージに保存するようにしています。
さらに、同じく、image_uploaderに下記のように記載します。views/uploaders/image_uploaderif Rails.env.production? CarrierWave.configure do |config| config.fog_credentials = { # Amazon S3用の設定 :provider => 'AWS', :region => ENV['S3_REGION'], # S3に設定したリージョン。 :aws_access_key_id => ENV['S3_ACCESS_KEY'], :aws_secret_access_key => ENV['S3_SECRET_KEY'] } config.fog_directory = ENV['S3_BUCKET'] endそれぞれ、以下のように設定します。ちなみに、東京のリージョンは
ap-northeast-1です。$ heroku config:set S3_ACCESS_KEY="S3のAccessキーを入力" $ heroku config:set S3_SECRET_KEY="同様に、Secretキーを入力" $ heroku config:set S3_BUCKET="Bucketの名前を入力" $ heroku config:set S3_REGION="Regionの名前を入力"ここまで書いたらようやく本番環境にデプロイです。デプロイして動作を試します。
・・・できた!!!無事アップロード完了です!!
追伸:エラーへの対処
なお、本番環境ではすぐに順調に動作をしたわけではなく、私の場合は
Access Deniedというエラーに遭いました。
- 投稿日:2020-01-03T17:18:28+09:00
コントローラー名とアクション名でSCSSを呼び出す
例えば WelcomeController の index アクションが呼ばれた場合、デフォルトの状態だとビューには welcome.css(.scss) がロードされるのだけど、これを呼ぶかどうかについて、ファイルの有無とか controller での変数で判定できたらいいのになという話。
色々方法はあるかと思うのですが、以下のようにしてみました。
assets/stylesheets/application.css
= require_self
*= require_directory .
デフォルトだと require_tree . となっている箇所を require_directory . にする。こうすることでルートディレクトリ( assets/stylsheets/ )の css しか自動でインポートしないようにする。ついでにスタイルシートのディレクトリも以下のように views っぽくする。
f
ltcmdr927:20121209105229p:plain
HEAD タグは application.html.erb で編集しているので、そこを修正する。
views/layouts/shared/application.html.erb
<%= stylesheet_link_tag "application", :media => "all" %>
<%= partial_stylesheet_link_tag controller.controller_name, controller.action_name, @partial_css_disabled %>
<%= javascript_include_tag "application" %>
stylesheet_link_tag と javascript_include_tag の間に新しい helper メソッドを追加する。引数でビューを読んだ時のコントローラ名とアクション名を指定。@partial_css_disabled は後述。app/helpers/application_helper.rb
module ApplicationHelper
def partial_stylesheet_link_tag(controller_name, action_name, disabled = true)
if disabled == false || disabled.nil?
if File.exist?("#{Rails.root.to_s}/app/assets/stylesheets/#{controller_name}/#{action_name}.css.scss")
return stylesheet_link_tag "#{controller_name}/#{action_name}"
end
end
endend
partial_stylesheet_link_tag メソッドを書く。disabled が false かあるいは nil かを判定し、正なら scss ファイルの有無を判定し、最終的にコントローラ/アクションの stylesheet_link_tag を戻す、という感じ。基本的にはファイルの有無で stylesheet タグが追加されるかどうかを判定するようになるので、例えばファイルは存在するけどタグは追加したくないという場合は、下記のように controller で @partial_css_disabled 変数に true を指定してやる。 true を指定するとファイルが存在していてもタグが追加されない。
app/controller/welcome_controller.rb
class WelcomeController < ApplicationController
def index
@partial_css_disabled = true
endend
若干イモっぽい感じがしなくもないですが、個人的にはこれで管理しやすくなりました。(コントローラの階層構造が深くなった場合については調査して後々追記予定。)
- 投稿日:2020-01-03T17:01:46+09:00
Railsチュートリアルメモ - 第4章・第5章
Railsチュートリアル第4章はこちら
Railsチュートリアル第5章はこちらRailsチュートリアルで気づいたことのメモを記載していきます
4.1 動機
Rubyに関する知識を深めていく章。
手始めに条件分岐で異なる文字列を返すhelperを自作する。4.2 文字列とメソッド
ポイント
- 文字列の式展開 => Rubyはシングルクォート文字列の中では式展開を行わない(シェルスクリプトと同じ)
- putsメソッド(printメソッド + 行末の改行)
- nil
- 暗黙の戻り値 => メソッド内にreturnを明示しなくても最後に評価した式をreturnする
- mixed in => moduleをincludeすること
- Railsでは自動的にヘルパーモジュールをmixed inしてくれるため、include行を書かなくてもOK
4.3 他のデータ構造
たぶんここらへんで眠くなって落脱する人が多い気がする、、、
初心者は流し読みにして、後でわからないところが出てきたら戻ってくるくらいの方が良さそうポイント
- Rubyの配列はゼロオリジン(ゼロ・インデックス)
- 比較演算子(==、!=)
- 破壊的メソッド(メソッドの最後が!)
- pushメソッド (または同等の<<演算子)
- 範囲 (range) e.g (0..9).to_a
Rubyは改行と空白を区別しない(式の途中で改行してもOK)
4.4 Rubyにおけるクラス
ポイント
- リテラルコンストラクタ、名前付きコンストラクタ
- superclassメソッド
- selfキーワード
- eachメソッド
- ブロック e.g. { |i| puts 2 * i } => pythonでいうlambda?
- ハッシュとシンボル
- 以下は等価
- { :name => "Michael Hartl" }
- { name: "Michael Hartl" }
- 組み込みクラスの変更が可能
- attr_accessor => attributeに対しるaccessorを作成するための記法
- initialize => 初期化メソッド(pythonでいうinitと同じ)
4.5 最後に
割愛
5.1 構造を追加する
html/cssについての解説の章。
ポイント
- パーシャル(DOMの一部を_始まりのファイルに切り出せる)
5.2 Sassとアセットパイプライン
SASS(CSSプリプロセッサー)の説明とRailsでの指定の仕方についての章。
ポイント
- アセットディレクトリ(*/assetsで表される静的ファイルを目的別に分類するディレクトリ)
- マニフェストファイル(アセットディレクトリにあるファイルをどのようにまとめるかを定義するファイル。実際の処理はSprocketsというgemが行う)
- プリプロセッサエンジン(.scssや.erbなどを変換して、html/css/jsに置き換えてくれるやつ。Sprocketsがアセットをまとめてくれた後にプリプロセッサエンジンが実行される)
- Sassの変数
- $始まりで変数を定義できる
- bootstrap lessで定義されている変数(@gray-light)なども$gray-lightと書けば使用可能
5.3 レイアウトのリンク
- 名前付きルート
- config/routes.rbの中で
get '/help', to: 'static_pages#help', as: 'helf'のように書くとhelp_pathやhelp_urlのような形で呼び出せるようになる- 統合テストの作成
rails generate integration_test site_layoutで統合テストを作成できるrails test:integrationで統合テストの実行5.4 ユーザー登録: 最初のステップ
6章以降でユーザー登録処理を作成していくための導入。ここではこれまでの章の振り返り的な内容。
5.5 最後に
割愛
- 投稿日:2020-01-03T14:36:20+09:00
【Rails】バリデーション
覚えたバリデーションのメモとして残します。
トリガされるメソッド
- create
- create!
- save
- save!
- update
- update!
書き方
app/models/person.rbclass Person < ApplicationRecord #ここに記載 #validates :属性, バリデーションヘルパー endバリデーションヘルパー
presence: true
属性が「空でない」
length: {maximum: 140}
属性の値の長さが140以下
uniqueness: true
属性の値が一意(unique)であり重複していない
format: { with: /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i }
有効なメールアドレスかどうか
- 投稿日:2020-01-03T13:46:24+09:00
Routing Error No route matches [PATCH] "/admin/users/1"が発生
最近の勉強で学んだ事を、ノート代わりにまとめていきます。
主に自分の学習の流れを振り返りで残す形なので色々、省いてます。
Webエンジニアの諸先輩方からアドバイスやご指摘を頂けたらありがたいです!Formtastic::UnknownInputError Unable to find input class Input
このエラーが起きた状況は、Active Adminで作成したユーザー管理画面において編集機能を追加するために
actionsに:editを追加した所、発生しました。app/admin/users.rbactions :index , :show , :editRouting Errorと書いていることから、ルーティングに何らかのエラーが出ていることが考えられる。
さらにその下に
No route matches [PATCH] "/admin/users/1"
と書いてあることから、"/admin/users"ってところと[PATCH]がマッチしていないことが考えられる。"rake routes"で、どの様に設定されているルーティングを見てみた所、エラー通りadmin_user_pathの[PATCH]が無かったです。
しかし、routes.rbの設定を変更してもエラーが直らず、というかactionsの方を追記しないといけないのではと思い。
以下の様に記述した所、Routing Errorが直りました。app/admin/users.rbactions :index , :show , :edit, :updateこれに直ぐに気づくことができなかったのが恥ずかしいです。。。
参考にした記事
・エラー解決Routing Error No route matches - Qiita
・rails APIモード+ActiveAdminで、管理者画面にてユーザーを削除しようとしたらNo route matchesのエラーが発生した際の対処法 - Qiita
- 投稿日:2020-01-03T13:03:11+09:00
rails: belongs_to :user, optional: trueとは?
簡単に説明すると、belongs_toで
optional: trueを設定することにより外部キーのnilを許可できるようになります。
optional: trueはどんなときに使われるのか?例えば、Q&Aのようなサービスを作る際にuserテーブルとquestionテーブルがあるとする。
その際に、ログインしていないユーザーからでも質問をポストできるシステムを作るときに使われたりする。
基本的にアソシエーションを書くときは外部キーが無いっていうことはあまりないがたまにある時もあるのでその際にoptional: trueをつけなかったりするとバグの温床になるので気をつける必要がある。ただDBの設計上、外部キーのnilを許可することが少ない気がするので、あまり使う設定ではないかもしれません。
- 投稿日:2020-01-03T12:57:29+09:00
pickadate.jsを使ってフォームにカレンダーを表示させる
こんにちは!スージーです!
久し振りにrailsでアプリ開発を開始したのでその際に使ったdatepickerについてまとめdatepickerとは
日付入力フォームを選択した時に表れるカレンダー
参考記事
結局、どのdatepickerが一番使い勝手がよいのかhttps://qiita.com/knt45/items/6d74f6785cd4547ae53b
Rails Application Build Guides
https://rails.densan-labs.net/form/datetime_register_form.htmlpickadate.js
https://amsul.ca/pickadate.js/pickadate-rails
https://github.com/veracross/pickadate-rails準備
Gemfilegem 'pickadate-rails'で
bundle installapplication.js//= require pickadate/picker 追記 //= require pickadate/picker.date 追記 //= require pickadate/picker.time 追記 //= require jquery_ujs //= require_tree .application.scss@import "pickadate/classic"; @import "pickadate/classic.date"; @import "pickadate/classic.time";cssレイアウトは
default又はclassicが選べるようになっているJaファイル編集
datepicker.js$(function() { $( "#datepicker" ).pickadate(); // カレンダー表示のイベント });Jsはこれだけで使えるようになる。リファレンスには色々なレイアウトのカレンダーが用意されている。
turbokinksでイベント発火しない
jQueryあるあるですがturbolinksをtrueのままにしているとページをリロードしないとイベント発火しないので、今回は不要なので削除。
application.js,application.html.erbのturbolinksに関するコードは削除。gem 'turbolinks'も削除してbundle installします。contorollerのアクション作成
datepickers_controller.rbclass datepickersController < ApplicationController def new @datepicker = Datepicker.new end def create @datepicker = Datepicker.create(datepicker_params) if @datepicker.save redirect_to :root else render :new end end private def datepicker_params params.require(:start_day).permit(:start_day).merge(user_id: current_user.id) end endこの辺はお決まりのnew/createアクションです
完成
カレンダーで日付を選択して登録できている事をパラメータを確認
ターミナルStarted POST "/start_days" for ::1 at 2020-01-03 12:50:13 +0900 Processing by StartDaysController#create as HTML Parameters: {"utf8"=>"✓", "authenticity_token"=>"4Ug39AI0MM0azF2XqNEcTOSQNjpNWQ+ovCSI7PFrPPatvEM8avbMZ6MII5JdxZnni+Hssbo7x6NTAjA0udiH7Q==", "datepicker"=>{"datepicker"=>"3 January, 2020"}, "commit"=>"登録"}パラメータの値がちゃんとDBに保存されています
まとめ
日付の選択は何かを登録する場面で使う事が多いかと思いますし、年月日をそれぞれ入力するのはUI/UX的に煩雑になってしまいますが、datepickerを使えば便利になりますし、簡単に実装もできました。
- 投稿日:2020-01-03T12:11:27+09:00
[Validation設定]ユーザー登録
Validation設定
ユーザー登録時につけたバリデーションの記録です。
セキュリティの関係で
^を\Aにしろと言われましたuser.rbvalidates :kana_firstname, :kana_lastname, presence: true, # カナのみ可 format: { with: /\A([ァ-ン]|ー)+\z/, message: "is must NOT contain any other characters than alphanumerics." }これでエラーは無くなりました!
参考
user.rbに書いた他のバリデーション
models/user.rbclass User < ApplicationRecord # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable, :omniauthable, omniauth_providers: [:facebook, :google_oauth2] validates :nickname, :firstname, :lastname, :birthday, presence: true validates :password, presence: true, length: { minimum: 7 }, # 英数字のみ可 format: { with: /\A[a-z0-9]+\z/i, message: "is must NOT contain any other characters than alphanumerics." } validates :email, presence: true, # 重複不可 uniqueness: { case_sensitive: false }, # 英数字のみ可,@を挟んだemailの形になっているか # /^\S+@\S+\.\S+$/ /\A[a-z0-9]+\z/i /^[a-zA-Z0-9]+$/ format: { with: /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i, message: "is must NOT contain any other characters than alphanumerics." } validates :kana_firstname, :kana_lastname, presence: true, # カナのみ可 format: { with: /\A([ァ-ン]|ー)+\z/, message: "is must NOT contain any other characters than alphanumerics." } has_many :sns_credentials
- 投稿日:2020-01-03T09:36:53+09:00
Railsチュートリアル 第13章 ユーザーのマイクロポスト - マイクロポストの作成・削除に関する機能を、テスト駆動開発で実装する
別のユーザーに所属するマイクロポストをfixtureに追加する
今後のテストで使うために、マイクロポストのfixtureに対して、最初のテストで使ったユーザーとは別のユーザーに属するマイクロポストを追加していきます。
test/fixtures/microposts.yml...略 + + ants: + content: "Oh, Is that what you want? Because that's haw you get ants!" + created_at: <%= 2.years.ago %> + user: :mkirisame + + zone: + content: "Danger zone!" + created_at: <%= 3.days.ago %> + user: :mkirisame + + tone: + content: "I'm sorry. Your words made sense, but your sarcastic tone did not." + created_at: <%= 10 minutes.ago %> + user: :rusami + + van: + content: "Dude, this van's, like, rolling probable cause." + created_at: <%= 4.hours.ago %> + user: :rusamiマイクロポストのUIに対する統合テストを追加する
マイクロポストのUIに対する統合テストの生成
例によって
rails generate integration_testコマンドによってテストを生成していきます。テストの名前はmicroposts_interface_testとします。# rails generate integration_test microposts_interface Running via Spring preloader in process 2380 invoke test_unit create test/integration/microposts_interface_test.rbマイクロポストのUIに対する統合テストの実装
マイクロポストのUIに対する統合テストの全体像は、以下のようになります。先程生成したばかりの
test/integration/microposts_interface_test.rbにコードを記述していきます。test/integration/microposts_interface_test.rbrequire 'test_helper' class MicropostsInterfaceTest < ActionDispatch::IntegrationTest def setup @user = users(:rhakurei) end test "micropost interface" do log_in_as(@user) get root_path assert_select 'div.pagination' # 無効な送信 assert_no_difference 'Micropost.count' do post microposts_path, params: { micropost: { content: "" } } end assert_select 'div#error_explanation' # 有効な送信 content = "This micropost really ties the room together" assert_difference 'Micropost.count', 1 do post microposts_path, params: { micropost: { content: content } } end assert_redirected_to root_url follow_redirect! assert_match content, response.body # 投稿を削除する assert_select 'a', text: 'delete' first_micropost = @user.microposts.paginate(page: 1).first assert_difference 'Micropost.count', -1 do delete micropost_path(first_micropost) end # 違うユーザーのプロフィールにアクセス(削除リンクがないことの確認) get user_path(users(:mkirisame)) assert_select 'a', text: 'delete', count: 0 end endテストの内容は以下になります。
- Homeページに、マイクロポストを表示する部分が実装されていること
- 内容が空のマイクロポストがMicropostsリソースに
POSTされた際に、RDBにマイクロポストが保存されないこと
- その際、エラーメッセージが
error_explanation属性を持つdiv要素としてレンダリングされていること- 有効な内容のマイクロポストがMicropostsリソースに
POSTされた際に、RDBにマイクロポストが保存されること
- その際、Homeページにリダイレクトされること
- リダイレクト後のHomeページに、当該マイクロポストの内容がレンダリングされていること
- 「delete」という文字を内容とする
a要素が存在すること
- マイクロポストの削除リンクを表す
- Micropostsリソースが正しい
DELETEリクエストを受け取った際に、RDBから対象のマイクロポストが削除されること
- 削除されるマイクロポストは、
setupメソッド中で指定したユーザーの最新のマイクロポスト- ログインユーザーとは違うユーザーのプロフィールにアクセスした際に、「delete」という文字を内容とする
a要素が存在しないことマイクロポストのUIに対する統合テスト実装時点でのテストの実行結果
上記コードを保存した時点で
test/integration/microposts_interface_test.rbを対象にテストを行うと、結果は以下のようになります。# rails test test/integration/microposts_interface_test.rb Running via Spring preloader in process 2401 Started with run options --seed 51205 FAIL["test_micropost_interface", MicropostsInterfaceTest, 3.5630840999947395] test_micropost_interface#MicropostsInterfaceTest (3.56s) Expected at least 1 element matching "div.pagination", found 0.. Expected 0 to be >= 1. test/integration/microposts_interface_test.rb:11:in `block in <class:MicropostsInterfaceTest>' 1/1: [===================================] 100% Time: 00:00:03, Time: 00:00:03 Finished in 3.57152s 1 tests, 1 assertions, 1 failures, 0 errors, 0 skips以下のようなメッセージが出て、テストが失敗しています。
Expected at least 1 element matching "div.pagination", found 0.. Expected 0 to be >= 1. test/integration/microposts_interface_test.rb:11私の環境では、
test/integration/microposts_interface_test.rbの11行目には以下のコードが記述されています。test/integration/microposts_interface_test.rb(11行目)assert_select 'div.pagination'まずはHomeページにマイクロポストのフィードを表示する部分を実装する必要がありそうですね。
Homeページにマイクロポストのフィードを表示する部分を実装する
Railsチュートリアル本文とは順番が前後しますが、テストの失敗内容を考えると、先に実装するのは「Homeページにマイクロポストのフィードを表示する部分」となります。
Railsチュートリアル本文には、「マイクロポスト投稿フォームと、マイクロポストのフィード表示部分が実装されたHomeページのモックアップ」が、図 13.13として示されています。
現在ログインしているユーザーのマイクロポストを全て取得する
ログイン済みユーザーのHomeページに表示するマイクロポストのフィードの内容は、「当該ユーザーが投稿したマイクロポスト」となります。このことは全てのユーザーに共通します。
「フィードの内容を取得する」という処理の内容は、全てのユーザーに共通する内容です。そのため、当該処理はUserモデルに実装するのが自然な流れとなります。編集対象のファイルは
app/models/user.rbですね。また、当該メソッドの名前は
feedとします。
feedメソッドの内容
feedメソッドの内容は、現在のところは、「whereメソッドにより、ログインユーザー自身の全てのマイクロポストを取得する」というものにしておきます。User#feed# 試作feedの定義 # 完全な実装は次章の「ユーザーをフォローする」を参照 def feed Micropost.where("User.id = ?", id) endなお、Railsチュートリアル本文では、今後「指定したユーザーの全てのマイクロポストを取得する」という内容にしていくことが示唆されています。
whereメソッドの第1引数中で使われている?の意味Micropost.where("User.id = ?", id)上記
whereメソッドの第1引数で使われている?は、「SQLクエリを組み立てる際、引数中の危険な文字をエスケープする処理を行わせる」という動作を意味します。これは、「SQLインジェクション」という深刻なセキュリティホールを回避するために非常に重要な実装となります。Railsに限らず、SQL文に変数を代入する処理を記述する際には、常にエスケープ処理とセットで記述することが強く望まれます。あえて
User.idを与えるような実装にした理由「ログインユーザー自身の全てのマイクロポストを取得する」のみであれば、以下の実装内容でも実現可能です。
def feed microposts endにもかかわらず、あえて
User.idを与えるような実装にしたのは、今後「指定したユーザーの全てのマイクロポストを取得する」という実装内容にしていくことを想定したためです。Userモデルに、実際に
feedメソッドを実装する上記の内容を踏まえて、
app/models/user.rbに対しては以下の変更を反映していきます。app/models/user.rbclass User < ApplicationRecord ...略 + + # 試作feedの定義 + # 完全な実装は次章の「ユーザーをフォローする」を参照 + def feed + Micropost.where("user_id = ?", id) + end private ...略 end
homeアクションにフィードのインスタンス変数を追加するHome画面にフィードを表示させるには、StaticPagesコントローラーの
homeメソッドに、フィード内容を与えるためのインスタンス変数を定義する必要があります。名前は@feed_itemsとしましょう。
@feed_itemsの定義は、さしあたって以下のようになります。@feed_items = current_user.feed.paginate(page: params[:page]) if logged_in?
app/controllers/static_pages_controller.rbの内容は、さしあたって以下のように変更します。app/controllers/static_pages_controller.rbclass StaticPagesController < ApplicationController def home + @feed_items = current_user.feed.paginate(page: params[:page]) if logged_in? end def help end def about end def contact end endステータスフィードのパーシャル
Homeビューにステータスフィードを表示させるためのパーシャルも必要になります。パーシャルそのものの名前は
feedとします。対応するファイル名はapp/views/shared/_feed.html.erbとなります。
app/views/shared/_feed.html.erbの内容は以下のようになります。app/views/shared/_feed.html.erb<% if @feed_items.any? %> <ol class="microposts"> <%= render @feed_items %> </ol> <%= will_paginate @feed_items %> <% end %>ステータスフィードのパーシャルからでも、単一のマイクロポストを表示するパーシャルを使うことができる
Railsチュートリアル本文においては、「ステータスフィードのパーシャルは、単一のマイクロポストを表示するパーシャルとは内容が異なる」という点が強調されています。
<%= render @feed_items %>
@feed_itemsを構成する個々の要素(より具体的には、前述feedメソッドの個々の戻り値)は、Micropostクラスのインスタンスです。このような場合においてRailsは、自身の機能により、暗黙的にMicropostのパーシャルを呼び出すことが可能なのです。以下に、Micropostのパーシャルの実体である
app/views/microposts/_micropost.html.erbを再掲しておきます。app/views/microposts/_micropost.html.erb(再掲)<li id="micropost-<%= micropost.id %>"> <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %> <span class="user"><%= link_to micropost.user.name, micropost.user %></span> <span class="content"><%= micropost.content %></span> <span class="timestamp"> Posted <%= time_ago_in_words(micropost.created_at) %> ago. </span> </li>Micropostに限らず、Railsのビューに対してリソースが渡された場合、Railsは対応する名前のパーシャルを、渡されたリソースのディレクトリ内から探し出すことができます。
ログインしている場合とログインしていない場合でHomeページの表示内容を変化させる
Homeページにステータスフィードを実装する前提条件として、「ログインしている場合とログインしていない場合でHomeページの表示内容を変化させる」という処理を実装する必要があります。
ここまでHomeページに実装してきたサインアップページヘのリンクは、ログインしていない場合にのみ必要となる内容です。ログインしている場合に対するHomeページの内容は、また別に定義する必要があります。
サインアップページへのリンクを、ログインしていない場合にのみ表示する
まずは「サインアップページへのリンクを、ログインしていない場合にのみ表示する」という実装内容を反映していきましょう。少々コードが汚いですが、ひとまず
if-elseで分岐を行うようにします。対象のファイルは
app/views/static_pages/home.html.erbですね。app/views/static_pages/home.html.erb<% provide(:title, "Home") %> <% if logged_in? %> <%#TODO: ログイン済みユーザーに対する処理の実装 %> <% else %> <div class="center jumbotron"> <h1>Welcome to the Sample App</h1> <h2> This is the home page for the <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a> sample application. </h2> <%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary" %> </div> <%= link_to image_tag("rails.png", alt: "Rails logo"), 'http://rubyonrails.org/' %> <% end %>Homeページにステータスフィードを追加する
- Userモデルにおいて、ログインユーザー自身の全マイクロポストをRDBから取得する機能
- StaticPagesコントローラーの
homeアクションにおける、フィード内容を中身とするインスタンス変数の定義- ステータスフィードのパーシャル
- ログインしている場合とログインしていない場合でHomeページの表示内容を変化させる機能
以上の実装が完成すれば、Homeページにステータスフィードを追加することができるようになります。
では、Homeパージにステータスフィードを追加していきます。
<% provide(:title, "Home") %> <% if logged_in? %> - <%#TODO: ログイン済みユーザーに対する処理の実装 %> + <div class="row"> + <aside class="col-md-4"> + <%#TODO: ユーザー情報表示部の実装 %> + <%#TODO: マイクロポスト投稿フォームの実装 %> + </aside> + <div class="col-md-8"> + <h3>Micropost Feed</h3> + <%= render 'shared/feed' %> + </div> + </div> <% else %> <div class="center jumbotron"> <h1>Welcome to the Sample App</h1> <h2> This is the home page for the <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a> sample application. </h2> <%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary" %> </div> <%= link_to image_tag("rails.png", alt: "Rails logo"), 'http://rubyonrails.org/' %> <% end %>これでHomeページにステータスフィードが追加できたはずです。
Homeページへのステータスフィードの追加が完了した時点でのテストの実行結果
Homeページへのステータスフィードの追加が完了した時点で、
test/integration/microposts_interface_test.rbを対象としたテストを実行すると、結果は以下のようになります。# rails test test/integration/microposts_interface_test.rb Running via Spring preloader in process 2446 Started with run options --seed 43986 ERROR["test_micropost_interface", MicropostsInterfaceTest, 3.7222297999833245] test_micropost_interface#MicropostsInterfaceTest (3.72s) NoMethodError: NoMethodError: undefined method `document' for nil:NilClass test/integration/microposts_interface_test.rb:16:in `block in <class:MicropostsInterfaceTest>' 1/1: [===================================] 100% Time: 00:00:03, Time: 00:00:03 Finished in 3.72699s 1 tests, 2 assertions, 0 failures, 1 errors, 0 skips私の環境では、
test/integration/microposts_interface_test.rbの13行目から16行目の内容は以下のようになっています。test/integration/microposts_interface_test.rb(13〜16行目)# 無効な送信 assert_no_difference 'Micropost.count' do post microposts_path, params: { micropost: { content: "" } } end assert_select 'div#error_explanation'エラーメッセージの内容は以下のようになっています。
NoMethodError: undefined method `document' for nil:NilClass「無効なマイクロポストの
POSTに対し、有効なオブジェクトを返してきていない」という趣旨のエラーと考えられます。まずは「無効なマイクロポストのPOSTに対する処理」を実装する必要がありそうですね。マイクロポスト作成機能の実装についての概要
「無効なマイクロポストの
POSTに対する処理」という言及を先にしてしまいましたが、まずはマイクロポスト作成機能の実装そのものについての説明が必要ですね。「マイクロポストを作成する」という処理は、「Micropostsコントローラーに対するHTTPの
POSTリクエストの発行」をトリガーとして行われます。「HTTPのPOSTリクエストを、コントローラーのcreateアクションに向けて発行する」という流れそのものは、過去に実装した「ユーザーのサインアップ」と類似しています。無効なマイクロポストの
POSTに対する処理Micropostsコントローラーの
createアクションマイクロポストの投稿に際し、Webブラウザで入力する必要十分なパラメーターをStrong Parametersに設定する
マイクロポストの投稿に際し、Webブラウザのフォームで入力する必要があるパラメーターは、マイクロポストの
content属性のみです。それ以外のパラメーターは、逆にPOSTリクエストから変更可能であってはなりません。Micropostコントローラーの
createアクションでは、「Webブラウザから送信されるPOSTリクエストのうち、content属性のみを受け取るようにし、それ以外の属性は受け取らないようにする」という実装が必要になります。ここで使うのはStrong Parameters機能ですね。
privateメソッド以降にmicropost_paramsというメソッドを定義し、Strong Parameters機能の使用に必要な実装を行っていきます。private def micropost_params params.require(:micropost).permit(:content) end
micropost_paramsメソッドでは、以下の条件について記述しています。
POSTリクエストのクエリパラメータにはmicropostパラメータを必要とするPOSTリクエストのmicropostパラメータのうち、contentパラメータのみを受理するRDBに保存されるマイクロポストを生成する
@micropost = current_user.microposts.build(micropost_params)上記コードにより、ログイン済みのユーザーに紐付けられた新たなマイクロポストを生成し、インスタンス変数
@micropostに格納します。micropost_paramsというのは、前の項で記述した通り、「POSTリクエストに必要なパラメータと、POSTリクエストから受理するパラメータ」について記述するメソッドとなります。無効なマイクロポストが
POSTされた場合の処理ここまで言及した内容により、ひとまず「無効なマイクロポストが
POSTされた場合の処理」を記述していくことができるようになります。対象としたapp/controllers/microposts_controller.rbです。app/controllers/microposts_controller.rbclass MicropostsController < ApplicationController before_action :logged_in_user, only: [:create, :destroy] def create + @micropost = current_user.microposts.build(micropost_params) + if @micropost.save + #TODO: ここに有効なマイクロポストの投稿に対する処理を書く + else + render 'static_pages/home' + end end ...略 + + private + + def micropost_params + params.require(:micropost).permit(:content) + end end上記実装が完了した時点でのテストの実行結果
# rails test test/integration/microposts_interface_test.rb Running via Spring preloader in process 2472 Started with run options --seed 5099 ERROR["test_micropost_interface", MicropostsInterfaceTest, 3.8275596000021324] test_micropost_interface#MicropostsInterfaceTest (3.83s) ActionView::Template::Error: ActionView::Template::Error: undefined method `any?' for nil:NilClass app/views/shared/_feed.html.erb:1:in `_app_views_shared__feed_html_erb__1031165391329753733_47227964624820' app/views/static_pages/home.html.erb:10:in `_app_views_static_pages_home_html_erb__2360347270319602419_47227964499260' app/controllers/microposts_controller.rb:9:in `create' test/integration/microposts_interface_test.rb:14:in `block (2 levels) in <class:MicropostsInterfaceTest>' test/integration/microposts_interface_test.rb:13:in `block in <class:MicropostsInterfaceTest>' 1/1: [===================================] 100% Time: 00:00:03, Time: 00:00:03 Finished in 3.84585s 1 tests, 1 assertions, 0 failures, 1 errors, 0 skips何か別のところでエラーを返すようになりましたね。
どのようなプロセスでエラーに至るのか見当がつかないので、
debuggerメソッドを挿入して確認してみましょう。...略 [5, 14] in /var/www/sample_app/app/controllers/microposts_controller.rb 5: @micropost = current_user.microposts.build(micropost_params) 6: if @micropost.save 7: #TODO: ここに有効なマイクロポストの投稿に対する処理を書く 8: else 9: debugger => 10: render 'static_pages/home' 11: end 12: end 13: 14: def destroy (byebug) [1, 7] in /var/www/sample_app/app/views/shared/_feed.html.erb 1: <% debugger %> => 2: <% if @feed_items.any? %> 3: <ol class="microposts"> 4: <%= render @feed_items %> 5: </ol> 6: <%= will_paginate @feed_items %> 7: <% end %> (byebug) @feed_items nil「マイクロポストの投稿が失敗し、
static_pages/homeがレンダリングされる時点で@feed_itemsがnilになってしまうこと」がエラーの原因のようですね。マイクロポストの投稿が失敗した場合に、
@feed_itemsに空の配列を渡すようにするRailsチュートリアル本文には、上記動作について、以下のように記述されています。
ただしささいなことではありますが、マイクロポストの投稿が失敗すると、 Homeページは
@feed_itemsインスタンス変数を期待しているため、現状では壊れてしまいます。最も簡単な解決方法は、リスト 13.50のように空の配列を渡しておくことです。実際にそのように実装してみましょう。
app/controllers/microposts_controller.rbclass MicropostsController < ApplicationController before_action :logged_in_user, only: [:create, :destroy] def create @micropost = current_user.microposts.build(micropost_params) if @micropost.save #TODO: ここに有効なマイクロポストの投稿に対する処理を書く else + @feed_items = [] render 'static_pages/home' end end ...略 endマイクロポストの投稿が失敗した場合に、
@feed_itemsに空の配列を渡すようにした時点でのテストの結果# rails test test/integration/microposts_interface_test.rb Running via Spring preloader in process 2573 Started with run options --seed 310 FAIL["test_micropost_interface", MicropostsInterfaceTest, 3.6217254000075627] test_micropost_interface#MicropostsInterfaceTest (3.62s) Expected at least 1 element matching "div#error_explanation", found 0.. Expected 0 to be >= 1. test/integration/microposts_interface_test.rb:16:in `block in <class:MicropostsInterfaceTest>' 1/1: [===================================] 100% Time: 00:00:03, Time: 00:00:03 Finished in 3.62327s 1 tests, 3 assertions, 1 failures, 0 errors, 0 skips
error_explanationクラスを持つdiv要素が見つからないというメッセージを出力してテストが失敗していますね。どうやらstatic_pages/homeのレンダリングまではうまくいっているようです。今度は、「static_pages/homeにerror_explanationクラスを持つdiv要素がレンダリングされるようにする」必要があります。マイクロポスト投稿フォームの仮パーシャルを追加する
Railsチュートリアル本文中の記載に従えば、ログイン済みユーザーのマイクロポスト表示画面において「
error_explanationクラスを持つdiv要素」がレンダリングされるのは、マイクロポスト投稿フォーム内となります。まずは、マイクロポスト投稿フォームとなる予定の仮パーシャルを作成し、そこに「error_explanationクラスを持つdiv要素」がレンダリングされるようにしましょう。app/views/shared/_micropost_form.html.erb<%= form_for(@micropost) do |f| %> <%= render 'shared/error_messages' object: f.object %> <% end %>
@micropostというインスタンス変数が出てきました。パーシャルapp/views/shared/_micropost_form.html.erbを使うのはHomeページなので、StaticPagesコントローラーのhomeアクションにも@micropostが必要になります。StaticPagesコントローラーの
homeアクションに、インスタンス変数@micropostの定義を追加するStaticPagesコントローラーの
homeアクションにおける@micropostの定義は以下になります。@micropost = current_user.microposts.build if logged_in?結果、StaticPagesコントローラーの
homeアクションの新たな定義は以下のようになります。class StaticPagesController < ApplicationController def home - @feed_items = current_user.feed.paginate(page: params[:page]) if logged_in? + if logged_in? + @micropost = current_user.microposts.build + @feed_items = current_user.feed.paginate(page: params[:page]) + end end def help end def about end def contact end end
logged_in?が真であることを条件とするメソッド呼び出しが2つになったので、if logged_in?を後置ifから前置ifに変更しています。マイクロポスト投稿フォームの仮パーシャルをHomeページで使うようにする
今作成したマイクロポスト投稿フォームの仮パーシャルは、Homeページで使うものです。
app/views/static_pages/home.html.erbの内容も、当該パーシャルを使用するように変更する必要があります。変更内容は以下の通りです。app/views/static_pages/home.html.erb<% provide(:title, "Home") %> <% if logged_in? %> <div class="row"> <aside class="col-md-4"> <%#TODO: ユーザー情報表示部の実装 %> - <%#TODO: マイクロポスト投稿フォームの実装 %> + <section class="micropost_form"> + <%= render 'shared/micropost_form' %> + </section> </aside> <div class="col-md-8"> <h3>Micropost Feed</h3> <%= render 'shared/feed' %> </div> <% else %> <div class="center jumbotron"> <h1>Welcome to the Sample App</h1> <h2> This is the home page for the <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a> sample application. </h2> <%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary" %> </div> <%= link_to image_tag("rails.png", alt: "Rails logo"), 'http://rubyonrails.org/' %> <% end %>エラーメッセージのパーシャルを再定義する
新たに必要となる実装
app/views/shared/_micropost_form.html.erb(抜粋)<%= form_for(@micropost) do |f| %> <%= render 'shared/error_messages' object: f.object %> <% end %>このコードは、エラーメッセージのパーシャルである
app/views/shared/_error_messages.html.erbの内容が現状のままでは正常に動作しません。理由は以下です。
- 現状の
app/views/shared/_error_messages.html.erbでは、内部で使うインスタンス変数が@user決め打ちである- 一方で、
app/views/shared/_micropost_form.html.erbから渡されるインスタンス変数は@micropostである
app/views/shared/_error_messages.html.erbが@userも@micropostも受け取ることができるような実装に変えねばなりません。
app/views/shared/_error_messages.html.erbを、@userも@micropostも受け取ることができるような実装に変更する
renderメソッドのオプションとして、以下のハッシュを与えているのがポイントです。object: f.object上記は「パーシャル中で使う変数名をキーとし、当該変数の内容となるオブジェクトを値とするハッシュ」です。例えば以上のハッシュを
renderメソッドのオプションとして与えた場合、レンダリングされるパーシャル(今回はerror_messagesパーシャル)にobjectという変数名でf.objectの内容を使うことができるようになります。
app/views/shared/_error_messages.html.erbの変更内容は以下の通りになります。app/views/shared/_error_messages.html.erb- <% if @user.errors.any? %> + <% if object.errors.any? %> <div id="error_explanation"> <div class="alert alert-danger"> - The form contains <%= pluralize(@user.errors.count, "error") %>. + The form contains <%= pluralize(object.errors.count, "error") %>. </div> <ul> - <% @user.errors.full_messages.each do |msg| %> + <% object.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> <% end %>エラーメッセージのパーシャルを書き換えた時点でのテストの実行結果
# rails test Running via Spring preloader in process 2599 Started with run options --seed 63753 ERROR["test_password_resets", PasswordResetsTest, 3.290402600017842] test_password_resets#PasswordResetsTest (3.29s) ActionView::Template::Error: ActionView::Template::Error: undefined local variable or method `object' for #<#<Class:0x000055e83a1e2530>:0x000055e83cbba6f0> Did you mean? object_id app/views/shared/_error_messages.html.erb:1:in `_app_views_shared__error_messages_html_erb__812901978115774136_47227970213020' app/views/password_resets/edit.html.erb:7:in `block in _app_views_password_resets_edit_html_erb___393354210430426510_47227969853980' app/views/password_resets/edit.html.erb:6:in `_app_views_password_resets_edit_html_erb___393354210430426510_47227969853980' test/integration/password_resets_test.rb:39:in `block in <class:PasswordResetsTest>' FAIL["test_micropost_interface", MicropostsInterfaceTest, 5.616371100011747] test_micropost_interface#MicropostsInterfaceTest (5.62s) Expected at least 1 element matching "div#error_explanation", found 0.. Expected 0 to be >= 1. test/integration/microposts_interface_test.rb:16:in `block in <class:MicropostsInterfaceTest>' 59/59: [=================================] 100% Time: 00:00:08, Time: 00:00:08 Finished in 8.64566s 59 tests, 299 assertions, 1 failures, 1 errors, 0 skips
PasswordResetsTestで以下のエラーが発生するようになりました。ActionView::Template::Error: undefined local variable or method `object'また、スタックトレースには以下の記載が残されています。
app/views/password_resets/edit.html.erb:7 app/views/password_resets/edit.html.erb:6エラーメッセージのパーシャルを書き換えたので、既存のソースコードも書き換える必要がある
エラーメッセージのパーシャル、とりわけ引数の渡し方を変更したため、エラーメッセージのパーシャルを使用している既存実装も書き換える必要が発生しました。具体的には、
app/views/password_resets/edit.html.erb内の実装ですね。app/views/password_resets/edit.html.erb<% provide(:title, 'Reset password') %> <h1>Reset password</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_for(@user, url: password_reset_path(params[:id])) do |f| %> - <%= render 'shared/error_messages' %> + <%= render 'shared/error_messages', object: f.object %> <%= hidden_field_tag :email, @user.email %> <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> <%= f.label :password_confirmation %> <%= f.password_field :password_confirmation, class: 'form-control' %> <%= f.submit "Update password", class: "btn btn-primary" %> <% end %> </div> </div>
app/views/password_resets/edit.html.erbの実装を変更した時点でのテストの結果# rails test test/integration/microposts_interface_test.rb Running via Spring preloader in process 2691 Started with run options --seed 19709 FAIL["test_micropost_interface", MicropostsInterfaceTest, 4.357053499988979] test_micropost_interface#MicropostsInterfaceTest (4.36s) Expected response to be a <3XX: redirect>, but was a <204: No Content> Response body: test/integration/microposts_interface_test.rb:22:in `block in <class:MicropostsInterfaceTest>' 1/1: [===================================] 100% Time: 00:00:04, Time: 00:00:04 Finished in 4.36191s 1 tests, 5 assertions, 1 failures, 0 errors, 0 skips
test/integration/microposts_interface_test.rbの22行目で、「リダイレクトされるべきところがリダイレクトされていない」というメッセージを出してテストが失敗しています。私の環境では、
test/integration/microposts_interface_test.rbの17〜22行目のコードは以下のようになっています。test/integration/microposts_interface_test.rb(22行目)# 有効な送信 content = "This micropost really ties the room together" assert_difference 'Micropost.count', 1 do post microposts_path, params: { micropost: { content: content } } end assert_redirected_to root_url無効なマイクロポストの送信に対する処理の実装は完成できたと判断できます。今度は、「有効な内容のマイクロポストを送信した場合に対する処理」を実装する必要があります。
有効なマイクロポストの
POSTに対する処理app/controllers/microposts_controller.rbclass MicropostsController < ApplicationController before_action :logged_in_user, only: [:create, :destroy] def create @micropost = current_user.microposts.build(micropost_params) if @micropost.save #TODO: ここに有効なマイクロポストの投稿に対する処理を書く <- この部分の実装 else @feed_items = [] render 'static_pages/home' end end # ...略 end有効なマイクロポストが
POSTされた場合、RDBのMicropostsテーブルへの保存が成功します。すなわち、@micropost.saveがtrueを返すということになります。有効なマイクロポストの
POSTに対する処理の内容Railsチュートリアル本文において、RDBのMicropostsテーブルへの保存が成功した場合の処理は以下のようになる旨が記載されています。
- マイクロポストの投稿が成功した旨をフラッシュメッセージで表示する
- Homeページ( / )にリダイレクトする
対応するコードは以下です。
flash[:success] = "Micropost created!" redirect_to root_url有効なマイクロポストの
POSTに対する処理の実装では、
app/controllers/microposts_controller.rbに対応するコードを追加していきましょう。app/controllers/microposts_controller.rbclass MicropostsController < ApplicationController before_action :logged_in_user, only: [:create, :destroy] def create @micropost = current_user.microposts.build(micropost_params) if @micropost.save - #TODO: ここに有効なマイクロポストの投稿に対する処理を書く + flash[:success] = "Micropost created!" + redirect_to root_url else @feed_items = [] render 'static_pages/home' end end def destroy end private def micropost_params params.require(:micropost).permit(:content) end end有効なマイクロポストの
POSTに対する処理を実装した時点でのテストの結果# rails test test/integration/microposts_interface_test.rb Running via Spring preloader in process 36 Started with run options --seed 50895 FAIL["test_micropost_interface", MicropostsInterfaceTest, 5.493641928999978] test_micropost_interface#MicropostsInterfaceTest (5.49s) <delete> expected but was <sample app>.. Expected 0 to be >= 1. test/integration/microposts_interface_test.rb:26:in `block in <class:MicropostsInterfaceTest>' 1/1: [===================================] 100% Time: 00:00:05, Time: 00:00:05 Finished in 5.49914s 1 tests, 8 assertions, 1 failures, 0 errors, 0 skips私の環境では、
test/integration/microposts_interface_test.rbの25〜26行目は以下のコードが記述されています。test/integration/microposts_interface_test.rb(25〜26行目)# 投稿を削除する assert_select 'a', text: 'delete'「マイクロポストの削除用リンクが描画されていない」という趣旨のメッセージですね。
有効なマイクロポストの
POSTに対する処理の実装までで成功するようになったテストの内容なお、以下のテストについては、ここまでの実装で問題なく成功しています。
test/integration/microposts_interface_test.rb(17〜24行目)# 有効な送信 content = "This micropost really ties the room together" assert_difference 'Micropost.count', 1 do post microposts_path, params: { micropost: { content: content } } end assert_redirected_to root_url follow_redirect! assert_match content, response.body「今投稿したばかりのマイクロポストの内容が、リダイレクト後のHomeページに描画されている」というテストも成功しています。
マイクロポストを削除する機能をMicropostsリソースに追加する
前提条件
「ユーザーは、自身が投稿したマイクロポストのみを削除することができる」という動作となることが前提です。
また、Railsチュートリアル本文には、「マイクロポストの削除リンクを含むHomeページのモックアップ」として、図 13.16が示されています。
違うユーザーのマイクロポストを削除しようとした際の動作に対するテストを追加する
違うユーザーのマイクロポストを削除しようとした場合、以下の動作となる必要があります。
- マイクロポストは削除されず、RDB上のマイクロポストの数も変化しない
- Homeページ( / )にリダイレクトされる
こちらは
test/controllers/microposts_controller_test.rbに追加していきます。テストの名前は「should redirect destroy for wrong micropost」とします。test/controllers/microposts_controller_test.rbrequire 'test_helper' class MicropostsControllerTest < ActionDispatch::IntegrationTest def setup @micropost = microposts(:orange) end test "should redirect create when not logged in" do assert_no_difference 'Micropost.count' do post microposts_path, params: { micropost: { content: "Lorem ipsum" } } end assert_redirected_to login_url end test "should redirect destroy when not logged in" do assert_no_difference 'Micropost.count' do delete micropost_path(@micropost) end assert_redirected_to login_url end + test "should redirect destroy for wrong micropost" do + log_in_as(users(:rhakurei)) + micropost = microposts(:ants) + assert_no_difference 'Micropost.count' do + delete micropost_path(micropost) + end + assert_redirected_to root_url + end endマイクロポストのパーシャルに削除リンクを追加する
動作のポイントは以下です。
- マイクロポストを投稿したユーザー自身のみに削除リンクが表示される
- 実際の削除の前に、「You sure?」という確認メッセージを出力する
上述動作を踏まえた上で、マイクロポストの削除リンクのコードは以下のようになります。
<% if current_user?(micropost.user) %> <%= link_to "delete", micropost, method: :delete, data: { confirm: "You sure?" } %> <% end %>マイクロポストの削除リンクのコードを、
app/views/microposts/_micropost.html.erbに追加していきます。app/views/microposts/_micropost.html.erb<li id="micropost-<%= micropost.id %>"> <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %> <span class="user"><%= link_to micropost.user.name, micropost.user %></span> <span class="content"><%= micropost.content %></span> <span class="timestamp"> Posted <%= time_ago_in_words(micropost.created_at) %> ago. + <% if current_user?(micropost.user) %> + <%= link_to "delete", micropost, method: :delete, data: { confierm: "You sure?" } %> + <% end %> </span> </li>マイクロポストのパーシャルに削除リンクを追加した時点でのテストの結果
マイクロポストのパーシャルに削除リンクを追加した時点での、
test/integration/microposts_interface_test.rbを対象としたテストの結果は以下のようになります。# rails test test/integration/microposts_interface_test.rb Running via Spring preloader in process 49 Started with run options --seed 16580 FAIL["test_micropost_interface", MicropostsInterfaceTest, 5.129021706999993] test_micropost_interface#MicropostsInterfaceTest (5.13s) "Micropost.count" didn't change by -1. Expected: 38 Actual: 39 test/integration/microposts_interface_test.rb:28:in `block in <class:MicropostsInterfaceTest>' 1/1: [===================================] 100% Time: 00:00:05, Time: 00:00:05 Finished in 5.13155s 1 tests, 9 assertions, 1 failures, 0 errors, 0 skips私の環境では、
test/integration/microposts_interface_test.rbの28〜30行目は以下のコードが記述されています。test/integration/microposts_interface_test.rb(28〜30行目)assert_difference 'Micropost.count', -1 do delete micropost_path(first_micropost) end
"Micropost.count" didn't change by -1.というのは、「正しいdeleteリクエストが送られたのに、RDB上から対象マイクロポストが削除されていない」という旨のメッセージですね。今度は、「正しくマイクロポストの削除処理が行われるdestroyアクションの実体」を実装する必要がありますね。マイクロポストの削除処理の実体を実装する
マイクロポストの削除処理は、対象となるマイクロポストを指定したHTTPの
DELETEリクエストをトリガーとして行われます。そのため、マイクロポストの削除処理の実体というのは、具体的には「Micropostsコントローラーのdestroyアクション」ということになります。マイクロポストの削除処理に必要となる実装
繰り返しになりますが、「ユーザーは、自身が投稿したマイクロポストのみを削除することができる」という動作となることが前提です。そのため、beforeフィルターの内容も、「削除対象のマイクロポストが、現在のユーザーと紐付いたものであるか」を検査するものである必要があります。
当該beforeフィルターの具体的な動作は以下のようになります。
DELETEリクエストのクエリパラメータとして与えられたマイクロポストをキーとして、現在のユーザーの全マイクロポストにfind_byメソッドで検索を行う- 検索結果に該当するマイクロポストがあった場合のみ、実際にマイクロポストの削除処理を行う
検索結果に該当するマイクロポストがなかった場合は、マイクロポストの削除処理を行わずにHomeページ( / )にリダイレクトします。
beforeフィルターの内容
上記実装のうち、beforeフィルターに関する部分のみを取り出すと以下のようになります。beforeフィルターに対応するメソッドの名前は、
correct_userとします。before_action :correct_user, only: [:destroy] private def correct_user @micropost = current_user.microposts.find_by(id: params[:id]) redirect_to root_url if @micropost.nil? endMicropostsコントローラーの
destroyアクションの内容一方、実際にRDBからマイクロポストを削除する処理を実装するのは、Micropostsコントローラーの
destroyアクション内となります。その動作は以下の通りです。
- RDBから当該マイクロポストを削除する
- マイクロポストが削除された旨をフラッシュメッセージで表示する
- リダイレクトする
対応するコードは以下のようになります。
Microposts#destroydef destroy @micropost.destroy flash[:success] = "Micropost deleted" redirect_to request.referrer || root_url end
request.referrerというメソッドの意味先程、単に「リダイレクトする」と書きました。実は、このときのリダイレクト先としては、以下の2つのURLを設定しています。
DELETEリクエストが発行されたページ- (
DELETEリクエストが発行されたページが特定できなかった場合)Homeページ
request.referrerというのは、上記のコードの場合、「DELETEリクエストが発行されたページ」を指します。マイクロポストを削除する機能をMicropostsリソースに追加した時点でのテストの結果
マイクロポストを削除する機能をMicropostsリソースに追加した時点での、
test/integration/microposts_interface_test.rbを対象としたテストの結果は以下のようになります。# rails test test/integration/microposts_interface_test.rb Running via Spring preloader in process 75 Started with run options --seed 25888 1/1: [===================================] 100% Time: 00:00:02, Time: 00:00:02 Finished in 2.92451s 1 tests, 10 assertions, 0 failures, 0 errors, 0 skips続いては
test/controllers/microposts_controller_test.rbを対象としたテストの結果です。# rails test test/controllers/microposts_controller_test.rb Running via Spring preloader in process 380 Started with run options --seed 1158 3/3: [===================================] 100% Time: 00:00:01, Time: 00:00:01 Finished in 1.18872s 3 tests, 6 assertions, 0 failures, 0 errors, 0 skips今度はテストスイート全体に対してテストを実施してみましょう。
# rails test Running via Spring preloader in process 107 Started with run options --seed 2017 59/59: [=================================] 100% Time: 00:00:10, Time: 00:00:10 Finished in 10.45135s 59 tests, 316 assertions, 0 failures, 0 errors, 0 skipsテストスイート全体も無事テストが成功しています。
実はこの時点でHomeページの実装が足りない
…あれ、ちょっと待ってください。Homeページの実装が足りないのにテストが全部成功してしまいました。具体的には、以下の実装が足りていません!
- サイドバーで表示されるべきユーザー情報のパーシャル
- マイクロポストの投稿フォーム
というわけで、上記2つの要素に対しての実装が必要となります。こちらも、Railsチュートリアル本文の記述を参考としつつ、テスト駆動で実装していきましょう。以下別記事となります。
- 投稿日:2020-01-03T09:34:30+09:00
Railsチュートリアル 第13章 ユーザーのマイクロポスト - 【発展】ユーザー情報のパーシャルとマイクロポスト投稿フォームを、テスト駆動開発で実装する
内容
Railsチュートリアル第13章の本文中において、テストが書かれることなしに実装されていた以下2つの要素を、テスト駆動で実装してみようというものです。
- ユーザー情報のパーシャル
- マイクロポスト投稿フォーム
Railsチュートリアル本文においては、「マイクロポスト作成フォームのあるホーム画面のモックアップ」として、図 13.10が挙げられています。
ユーザー情報のパーシャルの実装
ユーザー情報のパーシャルに含まれているべき要素に対するテスト
Railsチュートリアル本文によれば、サイドバーで表示されるべきユーザー情報のパーシャルには、以下の要素が含まれている必要があります。
gravatar_forクラスを持つimg要素- ユーザー名を内容とする
h1要素- マイクロポスト数を含む
span要素テストを追加するのは
test/integration/microposts_interface_test.rbです。追加するテストの内容は以下となります。test/integration/microposts_interface_test.rbrequire 'test_helper' class MicropostsInterfaceTest < ActionDispatch::IntegrationTest def setup @user = users(:rhakurei) end test "micropost interface" do log_in_as(@user) get root_path + assert_select 'img.gravatar' + assert_select 'h1', @user.name + assert_select 'span', /#{@user.microposts.count}/ assert_select 'div.pagination' ...略 end end「指定の文字列が含まれる指定の要素が存在するか」というテスト
例えば「
@user.microposts.count変数の内容が記述されているspan要素が存在するか」というテストであれば、以下のように記載します。assert_select 'span', /#{@user.microposts.count}/正規表現オブジェクトを意味する
//で、変数の内容を展開する#{}を囲っているのがポイントですね。サイドバーで表示されるべきユーザー情報のパーシャルに対しての、現時点のテスト結果
現時点での
test/integration/microposts_interface_test.rbに対するテストの結果は、以下のようになります。# rails test test/integration/microposts_interface_test.rb Running via Spring preloader in process 120 Started with run options --seed 17602 FAIL["test_micropost_interface", MicropostsInterfaceTest, 3.629997299999559] test_micropost_interface#MicropostsInterfaceTest (3.63s) Expected at least 1 element matching "h1", found 0.. Expected 0 to be >= 1. test/integration/microposts_interface_test.rb:12:in `block in <class:MicropostsInterfaceTest>' 1/1: [===================================] 100% Time: 00:00:03, Time: 00:00:03 Finished in 3.63972s 1 tests, 2 assertions, 1 failures, 0 errors, 0 skips「
h1要素が見当たらない」というメッセージを出してテストが失敗しています。サイドバーで表示するユーザー情報のパーシャルを実装する
まず、サイドバーで表示するユーザー情報のパーシャルの実体を実装していきます。ファイル名は
app/views/shared/_user_info.html.erbとします。app/views/shared/_user_info.html.erb<%= link_to gravatar_for(current_user, size: 50), current_user %> <h1><%= current_user %></h1> <span><%= link_to "view my profile", current_user %><span> <span><%= pluralize(current_user.microposts.count, "micropost") %></span>続いて、Homeページのビューでも当該パーシャルを使用するようにします。変更するファイルは
app/views/static_pages/home.html.erbですね。app/views/static_pages/home.html.erb<% provide(:title, "Home") %> <% if logged_in? %> <div class="row"> <aside class="col-md-4"> - <%#TODO: ユーザー情報表示部の実装 %> + <section class="user_info"> + <%= render 'shared/user_info' %> + </section> <section class="micropost_form"> <%= render 'shared/micropost_form' %> </section> </aside> <div class="col-md-8"> <h3>Micropost Feed</h3> <%= render 'shared/feed' %> </div> </div> <% else %> ...略 <% end %>サイドバーで表示するユーザー情報のパーシャルを実装した時点でのテストの結果
ここまでの実装が完了した時点で、
test/integration/microposts_interface_test.rbを対象としてテストを実行すると、結果は以下のようになります。# rails test test/integration/microposts_interface_test.rb Running via Spring preloader in process 224 Started with run options --seed 14923 1/1: [===================================] 100% Time: 00:00:03, Time: 00:00:03 Finished in 3.96801s 1 tests, 13 assertions, 0 failures, 0 errors, 0 skipsテストが成功していますね。ひとまず、サイドバーで表示するユーザー情報のパーシャルは正しく追加できたようです。
マイクロポスト投稿フォームの完全な実装
完全なマイクロポストの投稿フォームに含まれているべき要素に対するテスト
Railsチュートリアル本文によれば、サイドバーで表示されるべきマイクロポスト投稿フォームのパーシャルには、以下の要素が含まれている必要があります。
- /microposts に対して
POSTアクションを行うフォーム- マイクロポスト本文を入力するためのテキストフィールド
test/integration/microposts_interface_test.rbrequire 'test_helper' class MicropostsInterfaceTest < ActionDispatch::IntegrationTest def setup @user = users(:rhakurei) end test "micropost interface" do log_in_as(@user) get root_path assert_select 'img.gravatar' assert_select 'h1', @user.name assert_select 'span', /#{@user.microposts.count}/ + assert_select 'form[action="/microposts"]' + assert_select 'textarea' assert_select 'div.pagination' ...略 end endマイクロポスト投稿フォームに対するテストを実装した時点でのテストの結果
上記マイクロポスト投稿フォームに対するテストを実装した時点で、
microposts_interface_test.rbに対してテストを実行してみましょう。# rails test test/integration/microposts_interface_test.rb Running via Spring preloader in process 276 Started with run options --seed 9808 FAIL["test_micropost_interface", MicropostsInterfaceTest, 3.1817765000014333] test_micropost_interface#MicropostsInterfaceTest (3.18s) Expected at least 1 element matching "textarea", found 0.. Expected 0 to be >= 1. test/integration/microposts_interface_test.rb:15:in `block in <class:MicropostsInterfaceTest>' 1/1: [===================================] 100% Time: 00:00:03, Time: 00:00:03 Finished in 3.19284s 1 tests, 5 assertions, 1 failures, 0 errors, 0 skips「
textarea要素が存在しない」というメッセージを出力してテストが失敗しています。マイクロポスト投稿フォームの完全な実装
Railsチュートリアル本文によれば、マイクロポスト投稿フォームの完全な実装は以下の通りになります。
<%= form_for(@micropost) do |f| %> <%= render 'shared/error_messages', object: f.object %> <div class="field"> <%= f.text_area :content, placeholder: "Compose new micropost..." %> </div> <%= f.submit "Post", class: "btn btn-primary" %> <% end %>対応するソースファイルは
test/integration/microposts_interface_test.rbです。ここまでの実装で、エラーメッセージのパーシャルが出力できるようにするための実装は完了しています。新たに追加する必要があるのは以下の2つですね。
- マイクロポスト本文を入力するためのテキストフィールド
- 送信ボタン
早速実装を追加していきましょう。
app/views/shared/_micropost_form.html.erb<%= form_for(@micropost) do |f| %> <%= render 'shared/error_messages', object: f.object %> + <div class="field"> + <%= f.text_area :content, placeholder: "Compose new micropost..." %> + </div> + <%= f.submit "Post", class: "btn btn-primary" %> <% end %>マイクロポスト投稿フォームの完全な実装を終えた時点でのテストの結果
test/integration/microposts_interface_test.rbに対し、改めてテストを実行してみましょう。# rails test test/integration/microposts_interface_test.rb Running via Spring preloader in process 341 Started with run options --seed 28502 1/1: [===================================] 100% Time: 00:00:04, Time: 00:00:04 Finished in 4.07368s 1 tests, 15 assertions, 0 failures, 0 errors, 0 skips今度こそテストが成功しました。
テストスイート全体に対するテストの結果はどうでしょうか。
# rails test Running via Spring preloader in process 354 Started with run options --seed 14679 59/59: [=================================] 100% Time: 00:00:11, Time: 00:00:11 Finished in 11.81368s 59 tests, 321 assertions, 0 failures, 0 errors, 0 skipsテストスイート全体に対しても、無事テストが成功しましたね。
- 投稿日:2020-01-03T08:42:56+09:00
「Bundler could not find compatible versions for gem」なとき
概要
- rails tutorial(2020/1/3現在)にあるとおりにgemfileを編集してbundle installを実行したら、うまくいかなかったときの話。
Bundler could not find compatible versions for gem "actionpack": In Gemfile: rails (~> 6.0.2, >= 6.0.2.1) was resolved to 6.0.2.1, which depends on actionpack (= 6.0.2.1) rails-controller-testing (= 1.0.2) was resolved to 1.0.2, which depends on actionpack (~> 5.x, >= 5.0.1) Bundler could not find compatible versions for gem "listen": In Gemfile: listen (>= 3.0.5, < 3.2) guard (= 2.13.0) was resolved to 2.13.0, which depends on listen (>= 2.7, <= 4.0) spring-watcher-listen (~> 2.0.0) was resolved to 2.0.1, which depends on listen (>= 2.7, < 4.0)解決方法&参考にした情報
- 「インストールしたいバージョンの指定を、もっとゆるくしてあげればいいんだよ」
- 例)
- gem 'rspec-rails', '3.3.2' ではなく
- gem 'rspec-rails', '~> 3.3' のようにする
- https://github.com/bundler/bundler/issues/7034
参考までに私の修正した部分
修正前
・・・ gem 'rails-controller-testing', '1.0.2' gem 'minitest', '5.13.0' gem 'minitest-reporters', '1.1.14' ・・・修正後
・・・ gem 'rails-controller-testing', '~> 1.0.2' gem 'minitest', '~> 5.13.0' gem 'minitest-reporters', '~> 1.1.14' ・・・
- 投稿日:2020-01-03T00:28:25+09:00
WSLでRails6の環境構築
VSCode上で環境構築します。
WSLを有効にし、Ubuntuをインストール
こちらの記事通りに進めていけば簡単にできます。画像付きですごく分かりやすかったです。
UbuntuはLTS版を入れました。VSCodeの設定
VSCodeの拡張機能
Remote Developmentをインストール。これでWSLに接続して開発ができるようになる。
インストールしたら、VSCodeの一番左下に緑のアイコンがでるのでクリック。
Remote-WSL: New Windowを選択。これで設定終了。Ubuntuのパッケージを最新化
VScodeのターミナルを開いて、2つのコマンドを実行。かなり時間かかりました。
sudoコマンドを使うときに、Ubuntuで設定したパスワードを聞かれるかも。パッケージの最新化$ sudo apt update $ sudo apt upgrade -yrbenvのインストール
rbenvは、複数のRubyのバージョンを管理し、プロジェクトごとにRubyのバージョンを指定して使える。バージョンを変更したい場合も簡単。
参考: rbenvrbenvのインストールと設定#rbenvをインストール $ git clone https://github.com/rbenv/rbenv.git ~/.rbenv #パス指定と初期化処理を.bashrcに追加 $ echo 'export PATH="$HOME"/.rbenv/bin:$PATH' >> ~/.bashrc $ echo 'eval "$(rbenv init -)"' >> ~/.bashrc #パス指定と初期化処理の反映 $ source ~/.bashrc #バージョンが表示されれば成功 $ rbenv -vruby-buildのインストール
ruby-buildはRubyのインストールを簡単にしてくれる。
参考: ruby-build、ruby-buildのWikiruby-buildのインストール、その他必要パッケージ# ruby-buildのインストール $ mkdir -p "$(rbenv root)"/plugins ←これいるの?やらなくてもできた。 $ git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build #Rubyのインストールに必要なパッケージ。ruby-buildのWiki参照。かなり時間かかる。 $ sudo apt-get install autoconf bison build-essential libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libncurses5-dev libffi-dev libgdbm5 libgdbm-devRubyのインストール
ようやくrbenvを使ってRubyのインストールをします。安定版をインストールする。
恐ろしいくらい時間がかかりました。
最新の安定版の確認: こちらRubyのインストール#Rubyのインストール $ rbenv install 2.7.0 #PC全体で使うRubyのバージョンを指定する $ rbenv global 2.7.0 $ ruby -vRubyGemsのアップデート
RubyGemsを最新版にアップデートする。
$ gem update --systemRailsのインストール
Railsをインストールします。
最新のバージョンの確認: こちら$ gem install rails -v 6.0.2.1 $ rails -vNode.jsのインストール
Rails6から標準のWebpackerのインストールに必要。
# npmのインストール $ sudo apt install -y npm # Node.jsのインストール $ sudo npm install n -g $ sudo n stable $ node -vyarnのインストール
Webpackerのインストールに必要。
最初この方法でやったがうまくできなかった。
参考: yarn$ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - $ echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list $ sudo apt-get update && sudo apt-get install yarn $ yarn -v #エラー出た時のメモ $ sudo apt remove cmdtest $ sudo apt remove yarn $ sudo apt autoremoveSQLite3のインストール
Rails標準のデータベースなのでインストール。
$ sudo apt-get install libsqlite3-devアプリの作成
$ rails new sample_app $ cd sample_app $ rails sその他メモ
railsインストールするときノードキュメント
# gemでライブラリーをインストールした際にドキュメントを生成すると時間がかかる。 # そこで、ドキュメントを生成しない設定にする $ cd $ touch .gemrc $ echo "install: --no-document" >> .gemrc $ echo "update: --no-document" >> .gemrc # rbenvのアップデート # Rubyの最新バージョンを使いたいときは、rbenvをアップデートしてから導入 $ cd ~/.rbenv $ git pull $ cd plugins/ruby-build $ git pull # 新しいコマンドを要するgemをインストールしたとき使用 $ rbenv rehash # 新しく作ったディレクトリで作業するときは、そこで使うRubyのバージョンを指定する $ rbenv local バージョン感想
ほとんどエラー無くできたが、各種パッケージのインストールが長すぎて疲れました笑
それにすごい動作が遅い
- 投稿日:2020-01-03T00:01:44+09:00
Ruby on Rails を学ぶ _No.1
view(ビュー)
ページや「見た目」を作るためのHTMLファイルです。
ブラウザとrailsの通信でrailsからビューが返され、ページが表示されます。controller(コントローラー)
ページを表示するときにrailsの中ではコントローラを経由してビューをブラウザに返します。
具体的には、コントローラと同じ名前のビューフォルダから、あくしょんと同じ名前のHTMLファイルを探してブラウザに返します。■ コントローラファイルの中身
ファイルの中にtopメソッドが追加されます。コントローラの中のメソッドはアクションと呼ばれます。
→ アクションと同じ名前のHTMLファイルをブラウザに返します。html_controller.rbclass HomeController < ApplicationController def top #homeコントローラのtopアクションに対応するhtmlファイルを返す end endルーティング
ブラウザとコントローラを繋ぐ役割があります。
送信されたURLに対してどのコントローラのどのアクションで対応するかを決める対応表のような物です。■ ルーティングファイルの中身
ルーティングはconfig/routes.rb内に定義され以下のような文法で書かれます。routes.rbRails.application.routes.draw do get "home/top" => "home#top" #get "URL名" => コントローラ名#アクション名 end














