- 投稿日:2020-06-21T23:50:46+09:00
Rubyの配列操作
map
人数が5人以上のgroupのidを配列で返すイメージ
ids = groups.map{ |group| group.id if group.count >= 5} #=> [1, 2, nil, 4, ・・・]条件に当てはまらない要素はnilになる
.reject(&:blank?)
配列からnilと空文字を無くす
ids = groups.map{ |group| group.id if group.count >= 5}.reject(&:blank?) #=> [1, 2, 4, ・・・]filter_map
上記をまとめて。Ruby2.7から利用可能
ids = groups.filter_map{ |group| group.id if group.count >= 5} #=> [1, 2, 4, ・・・]join
連結した文字列を返す。引数に入れた文字列を挟み込みことができる
['A','B','C'].join #=> "ABC" ['A','B','C'].join('|') #=> "A|B|C"
- 投稿日:2020-06-21T23:18:59+09:00
Rails アセットプレコンパイル時のエラー'ActionView::Template::Error (The asset "application.css" is not present in the asset pipeline.'の解消方法
背景
- 作成したRailsアプリケーションをデプロイしてページにアクセスしたところ、以下のエラー画面が出てアクセスできなかった。
- そこで、log/production.rbを確認したところ、以下のエラーが出ていた
log/production.rbActionView::Template::Error (The asset "application.css" is not present in the asset pipeline.):このエラーの解決に非常に手間取ったので、正しい方法かはわからないですが、自分の環境での解決策をシェアします。
環境
Rails 5.2.4
Ruby 2.6.4解決策
config/environments/production.rbを以下のように編集したら直った
# Do not fallback to assets pipeline if a precompiled asset is missed. - config.assets.compile = false + config.assets.compile = true参考文献
- 投稿日:2020-06-21T23:07:43+09:00
Ruby で gRPC のエラーハンドリング をする際のTips
Ruby で gRPC クライアントを実装する際の、エラーハンドリングのTipsをまとめる
前提
- Ruby 2.3.8
- Rails 5.0.7.2
- grpc gem 1.2.8
tl;dr
- gRPC コールに対するエラーレスポンスは
GRPC::BadStatus
型の例外なので、gRPC コールする際は必ず resuce する- 捕捉した Exception の詳細情報を取得したい場合、
to_rpc_status
メソッドでGoogle::Rpc::Status
型にキャストする- gRPC サーバーから error details が返ってきた場合、Any 型で返ってくるので、適切な型に unpack する必要がある
- gem のエラークラス https://github.com/grpc/grpc/blob/master/src/ruby/lib/grpc/errors.rb
まずはエラーを捕捉する
gRPC コールのレスポンスは基本的には以下の Code のどれかが返ってくる。
enum Code { OK = 0; CANCELLED = 1; UNKNOWN = 2; INVALID_ARGUMENT = 3; DEADLINE_EXCEEDED = 4; NOT_FOUND = 5; ALREADY_EXISTS = 6; PERMISSION_DENIED = 7; UNAUTHENTICATED = 16; RESOURCE_EXHAUSTED = 8; FAILED_PRECONDITION = 9; ABORTED = 10; OUT_OF_RANGE = 11; UNIMPLEMENTED = 12; INTERNAL = 13; UNAVAILABLE = 14; DATA_LOSS = 15; }https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto
gRPC サーバーの実装としては正常系は Code OK(0) を返し、エラーであれば OK 以外の Code(1~15) を返すよう作るかと思う。エラーが返ってきた場合、grpc gem 側で勝手に exception を発生させるらしく、rescue しないとそこでプログラムがストップしてしまう。
なので、以下のように rescue で例外を拾ってあげる必要がある。begin stub = HogeProto::HogeService::Stub.new("localhost:50051", :this_channel_is_insecure) res = stub.get_hoge rescue GRPC::BadStatus => ex res = "error" rescue ex res = "unexpected error" endgem で定義されている エラー class を見ると、全ての class が
BadStatus
class を継承していることがわかる。実際に、発生した Exception の継承ツリーを見ると
p ex.class.ancestors # =>[GRPC::InvalidArgument, GRPC::BadStatus, GRPC::Core::StatusCodes, StandardError, Exception...となっており、基本的に
GRPC::BadStatus
で全ての Exception を捕捉できる。もっと詳細に条件分けして処理したい場合は
GRPC::InvalidArgument
のような単位で拾うこともできる。捕捉した Exception から情報を取り出す
捕捉した Exception から情報を取り出してみよう。
BadStatus
インスタンスには4つのフィールドがある。
https://github.com/grpc/grpc/blob/d48d39c4324f06a6da24bb4f67e8ef21166ba65b/src/ruby/lib/grpc/errors.rb#L49-L52例えば、Go で実装した gRPC サーバーから以下のようにエラーを作成して ruby に返してみる。
server.gofunc (s *Server) set_name(name string) { st := status.New(codes.InvalidArgument, "invalid username") return nil, st.Err() }client.rbbegin stub = HogeProto::HogeService::Stub.new("localhost:50051", :this_channel_is_insecure) res = stub.set_name("hoge1") rescue GRPC::BadStatus => ex p ex.code # => 3 p ex.message # => "3:invalid username" p ex.details # => "invalid username" res = "error" rescue ex res = "unexpected error" endStatus Code
3
と"invalid username"
というメッセージが取得できたerror details を取得したい場合は、 to_rpc_status メソッドを使う
上記までで、Status Code とエラーメッセージが取得できた。
しかし、実際に作り込む場合、エラーの詳細を渡すために error details を使うことも多いと思う。
https://christina04.hatenablog.com/entry/grpc-error-details
https://grpc.io/docs/guides/error/#richer-error-modelもし、 error details 含めた詳細な情報が欲しければ、
to_rpc_status
メソッドを使うことでより詳細な情報を取得することができる。
to_rpc_status
の実装は以下になるが、これを使うことで、Google::Rpc::Status
型にキャストでき、trailer メタデータを含めた詳細な情報を取り出すことができる。
https://github.com/grpc/grpc/blob/d48d39c4324f06a6da24bb4f67e8ef21166ba65b/src/ruby/lib/grpc/errors.rb#L63-L75先ほどの実装例に、error details を追加してみた。
server.gofunc (s *Server) set_name(name string) { st := status.New(codes.InvalidArgument, "invalid username") // error details の情報を作ってセットする desc := "The username must only contain alphanumeric characters" v := &errdetails.BadRequest_FieldViolation{ Field: "username", Description: desc, } br := &errdetails.BadRequest{} br.FieldViolations = append(br.FieldViolations, v) st, _ = st.WithDetails(br) return nil, st.Err() }client.rbrequire 'google/rpc/error_details_pb' begin stub = HogeProto::HogeService::Stub.new("localhost:50051", :this_channel_is_insecure) res = stub.set_name("hoge1") rescue GRPC::BadStatus => ex p ex.class # => GRPC::InvalidArgument p ex.to_rpc_status.class # => Google::Rpc::Status p ex.to_rpc_status # => <Google::Rpc::Status: code: 3, message: "invalid username", details: [<Google::Protobuf::Any: type_url: "type.googleapis.com/google.rpc.BadRequest", value: "\nC\n\tusername\x126The username must only contain alphanumeric characters">]> ex.to_rpc_status.details.each do |detail| p detail.type_url # => "type.googleapis.com/google.rpc.BadRequest" p detail.unpack(Google::Rpc::BadRequest) # => <Google::Rpc::BadRequest: field_violations: [<Google::Rpc::BadRequest::FieldViolation: field: "username", description: "The username must only contain alphanumeric characters">]> end res = "error" rescue ex res = "unexpected error" end注目すべきは details
details: [<Google::Protobuf::Any: type_url: "type.googleapis.com/google.rpc.BadRequest", value: "\nC\n\tusername2\x126The username must only contain alphanumeric characters">]
の部分。
これが、trailer として送られてきた error details のデータであり、なにやらGoogle::Protobuf::Any
型のデータと value というデータがある。
error details は複数設定でき、配列で返ってくるので、取り出すには each で回してあげる必要がある。
type_url
というのは error details の型の定義場所が格納されていて、Google がデフォルトで用意した型を使用していれば、"type.googleapis.com/google.rpc.BadRequest"
となる。もちろん独自で定義した型を error details としてセットすることもできるので、その場合は自分がprotoファイルで定義した場所が格納される。また、返ってきたインスタンスは
Google::Protobuf::Any
型となっていて、このままだとデータがシリアライズされた状態でうまく取り出せない。そこで、使用するのがunpack
メソッド。
type_url
で型を判定し、unpack
で戻したい型にキャストしてあげることで、ようやく、 error details を取り出すことができた。
ちなみに、Google::Rpc::BadRequest
の型定義を参照するには、google/rpc/error_details_pb
をインポートしておく必要がある点に注意。所感
error details を取り出すのに結構手間取った。
https://grpc.io/docs/languages/ruby/quickstart/ のサンプルコードにはここまで詳しく載ってないので、結局はいちいち gem の中身まで見にいったりしなければならない状況なのがちょっと辛い。
- 投稿日:2020-06-21T20:44:22+09:00
dockerイメージ内にyarnをインストールする
背景
Dockerを利用して作成したRailsアプリ(v5.2.4)を本番環境にデプロイする際、アセットをプリコンパイルしようとしたところ、以下のエラーがでて実行できなかった。
$docker-compose run web bundle exec rake assets:precompile RAILS_ENV=productionStarting excite-map_db_1 ... done Yarn executable was not detected in the system. Download Yarn at https://yarnpkg.com/en/docs/instal解消方法
Dockerfileに以下を追加
RUN curl https://deb.nodesource.com/setup_12.x | bash RUN curl https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list RUN apt-get update && apt-get install -y nodejs yarn postgresql-client参考
- 投稿日:2020-06-21T20:07:47+09:00
【hidden_field】rails hidden_field を使って情報を送ろう!!!!
【ゴール】
hidden_fieldを使って情報を送る
【メリット】
■ paramaterの理解度向上
■ hidden_fieldの使い方理解
■ MVCの理解度向上【開発環境】
■ Mac OS catalina
■ Ruby on Rails (5.2.4.2)
■ Virtual Box:6.1
■ Vagrant: 2.2.7【実装】
アプリケーション作成
※「scaffold」で一気に作成
mac.terminal$ rails new hidden_lesson $ rails g scaffold Item name:string text:text price:integer amount:integer $ rails db:migrateアプリケーション調整
※ルートを追記
※homeにitemsのindexページが来るように
※postメソッドで確認画面へのルートを作成config/routes.rbroot 'items#index' post 'items/confirm' => 'items#confirm'※views/itemsにconfirm.html.erbを作成
※items/_form.html.erbを編集
※hidden_fieldでUserには見えないようにページで情報を保持します!!!items/confirm.html.erb<h3>Confirm page</h3> <%= form_with model:@item do |form| %> <div class="field"> <%= form.label :name %> <%= @item.name %> <%= form.hidden_field :name %> </div> <div class="field"> <%= form.label :text %> <%= @item.text %> <%= form.hidden_field :text %> </div> <div class="field"> <%= form.label :price %> <%= @item.price %> <%= form.hidden_field :price %> </div> <div class="field"> <%= form.label :amount %> <%= @item.amount %> <%= form.hidden_field :amount %> </div> <div class="actions"> <%= form.submit %> </div> <% end %>items/_form.html.erb<%= form_with(model: item, local: true) do |form| %> #これを↓に変更 <%= form_with(model: item, local: true, url: items_confirm_path) do |form| %>以上です。商品詳細画面遷移前に確認画面が挟まれているはずです。
【合わせて読みたい】
■form_withに関して
https://qiita.com/tanaka-yu3/items/50f54f5d4f4b8dfe19f3■local: trueに関して
https://qiita.com/hayulu/items/5bf26656d7433d406ede■確認画面作成に関して
https://qiita.com/tomoharutt/items/7959d28764912c64562f■formの引数が2つの場合
https://qiita.com/tanaka-yu3/items/94d2b9fccc9577756127
- 投稿日:2020-06-21T18:18:48+09:00
minitestでモック、スタブする(RR、WebMock、MiniTest::Mockを使う)
この記事について
仕事で、railsのテストフレームワークにminitestを使っています。そして、railsプロジェクトの中では、モックやスタブをするときに、RR、WebMockといったgemや、minitest標準のモックであるMiniTest::Mockが使われています。
テストを書くときに、モックやスタブの書き方に戸惑うことが多くありました。
この記事では、テストダブルとはどんなものか、モックとスタブの違いはなにか、RR、WebMock、MiniTest::Mockそれぞれの使い方について記述します。テストダブル(モック、スタブ)とは
まず、テストダブルとはどんなものでしょう。また、モック、スタブの違いはなんでしょう。
- テストダブルとは、ソフトウェアテストにおいて、テスト対象が依存しているコンポーネントを置き換える代用品(ダブルは代役、影武者を意味する)
- モックもスタブも、テストダブルの一種
テストダブルの5つのバリエーション
テストダブルは、xUnit Test Patternの書籍によると、5つのバリエーションがあります。
1. テストスタブ
- テスト対象コードが依存する実際のコンポーネントを置き換えるために使用する
- テスト時の呼び出しときに、あらかじめ決められた値を返すように設定する
2. テストスパイ
- テスト対象コードが実行された時の間接的な出力をキャプチャし、後のテストによる検証のために保存する
- 呼び出しに基づく情報を記録するスタブ
3. モックオブジェクト
- テスト対象コードが実行される際に、テスト対象コードからの間接的な出力を検証するために使用するオブジェクト
- 間接出力の検証に重きが置かれる
- 期待した呼び出しが行われたかを検証できる(どんな引数で呼ばれたか、など)
4. フェイクオブジェクト
- テスト対象コードの依存コンポーネントを置き換えるオブジェクト
- 依存コンポーネントと同じ機能を実装しているが、よりシンプルな方法で実装されている
- フェイクを利用する一般的な理由は、実際の依存コンポーネントがまだ利用できない、遅すぎる、または副作用があるためにテスト環境で使用できない、などがある
5. ダミーオブジェクト
- テスト対象コードのメソッドシグネチャの中に、パラメータとしてオブジェクトを必要とする場合に、ダミーオブジェクトを使う(テストもテスト対象コードでもこのオブジェクトを気に掛けていない場合)
私は、スタブとモックをよく混同していましたが、スタブは依存するコンポーネントを置き換えるものであり、モックはテスト対象コードからの出力が期待通りであるか検証するものである、ということを理解しました。
参考
Test Double / xUnit Patterns.com
wiki テストダブル
自動テストのスタブ・スパイ・モックの違いRR
RRは、Rubyのテストダブルのフレームワークのgemです。
読み方は、'Double Ruby'と読むそうです。
RRにはアダプタが用意されているので、RSpec、Test::Unit、MiniTest/MiniSpecなどのテストフレームワークと統合することができるようです。GitHub:https://github.com/rr/rr
公式:http://rr.github.io/rr/RR is a test double framework that features a rich selection of double techniques and a terse syntax.
(訳:RRは、豊富なダブルテクニックと簡潔な構文を特徴とするテストダブルフレームワークです。)RRの使い方
RRには、モック、スタブ、プロキシ、スパイが実装されています。
RRのGitHubページのサンプル通りですが、こんな感じで書けます。スタブ
stub
で、スタブする(実際の呼び出しを置き換える)ことができます。# 何も返さないメソッドをスタブする stub(object).foo stub(MyClass).foo # 常に値を返すスタブメソッド stub(object).foo { 'bar' } stub(MyClass).foo { 'bar' } # 特定の引数で呼び出されたときに値を返すスタブメソッド stub(object).foo(1, 2) { 'bar' } stub(MyClass).foo(1, 2) { 'bar' }詳細はstubのページ参照。
モック
mock
で、期待した呼び出しが行われるか検証するモックを作成できます。# メソッドが呼ばれることを期待する # objectのfooメソッドが呼ばれることを期待する mock(object).foo mock(MyClass).foo # メソッドに期待値を作成し、常に指定した値を返すようにスタブする # objectのfooメソッドが'bar'を返すことを期待する mock(object).foo { 'bar' } mock(MyClass).foo { 'bar' } # 特定の引数を持つメソッドに期待値を作成し、それを返すためにスタブを作成する # objectのfooメソッドが引数1, 2で呼ばれ、'bar'を返すことを期待する mock(object).foo(1, 2) { 'bar' } mock(MyClass).foo(1, 2) { 'bar' }詳細はmockページ参照。
スパイ
stub
と、assert_received
やexpect(xxx).to have_received
の記述を組み合わせて、スパイ(呼び出された情報を記録するスタブ)が書けるようです。
(公式GitHubには、Test::UnitとRspecでの書き方はありましたが、minitestでの書き方は載っていませんでした。)# RSpec stub(object).foo expect(object).to have_received.foo # Test::Unit stub(object).foo assert_received(object) {|o| o.foo }プロキシ
proxy
を使うと、メソッドを完全にオーバーライドせずにインターセプトして新しい戻り値を設定したスタブやモックが作れるようです。# 既存のメソッドを完全にオーバーライドせずにインターセプトして # 既存の値から新しい戻り値を取得する stub.proxy(object).foo {|str| str.upcase } stub.proxy(MyClass).foo {|str| str.upcase } # 上記の例でやってることに加えて、さらに期待値のモックを作成する mock.proxy(object).foo {|str| str.upcase } mock.proxy(MyClass).foo {|str| str.upcase } # クラスの新しいメソッドをインターセプトし、戻り値にダブルを定義する stub.proxy(MyClass).new {|obj| stub(obj).foo; obj } # 上記の例でやってることに加えて、.newに期待値のモックを作成する mock.proxy(MyClass).new {|obj| stub(obj).foo; obj }詳細はmock.proxy、stub.proxyページ参照。
クラスのインスタンス
any_instance_of
で、インスタンス作成時にメソッドをスタブしたりモックできます。また、stub.proxy
を使うと、インスタンスそのものにアクセスできるようになります。# MyClass のインスタンスの作成時にメソッドをスタブする any_instance_of(MyClass) do |klass| stub(klass).foo { 'bar' } end # インスタンス自体にアクセスできるようにする別の方法 # MyClass.newされたインスタンスobjをスタブしている stub.proxy(MyClass).new do |obj| stub(obj).foo { 'bar' } end詳細は#any_instance_ofページ参照。
Pureなモックオブジェクト
モックのためだけにオブジェクトを使用したい場合は、空のオブジェクトを作成することで可能です。
mock(my_mock_object = Object.new).helloショートカットとして
mock!
を使うこともできます。# 空の #hello メソッドを持つ新しいモックオブジェクトを作成し、そのモックを取得する # モックオブジェクトを #subject メソッドで取得できる my_mock_object = mock!.hello.subject#dont_allow
#dont_allow
は#mock
の逆で、ダブルには絶対にコールされないという期待を設定します。ダブルが実際に呼び出された場合、TimesCalledError
が発生します。dont_allow(User).find('42') User.find('42') # raises a TimesCalledErrorその他
RRは
#method_missing
を使ってメソッドの期待値を設定しているそうです。これにより、#should_receive
や#expects
メソッドを使う必要がありません。
また、引数の期待値を設定するために#with
メソッドを使う必要がないそうです。(使いたければ使えます)mock(my_object).hello('bob', 'jane') mock(my_object).hello.with('bob', 'jane') # withがついているが上と同じRRは、ブロックを使って戻り値を設定することをサポートしています。(お好みで、
#returns
を使うことができます)mock(my_object).hello('bob', 'jane') { 'Hello Bob and Jane' } mock(my_object).hello('bob', 'jane').returns('Hello Bob and Jane') # returnsがついているが上と同じ
#times
、#at_least
、#at_most
、#any_times
メソッドでモックの期待する呼び出し回数を調整できます。#with_any_args
でどんな引数での呼び出しも許容したり、#with_no_args
で引数なしの呼び出しを期待したり、#never
でメソッドが呼ばれないことを期待したりできます。
もっと詳しい情報は、API overviewを参照ください。WebMock
WebMockは、RubyでHTTPリクエストのスタブやモックを設定するためのgemです。
RRとの違いは、HTTPリクエストに特化している部分でしょうか。GitHub:https://github.com/bblimke/webmock
Library for stubbing and setting expectations on HTTP requests in Ruby.
機能として、以下を提供しています。
- HTTP リクエストを低レベルの http クライアントの lib レベルでスタブ化 (HTTP ライブラリを変更する際にテストを変更する必要はない)
- HTTP リクエストに対する期待値の設定と検証
- メソッド、URI、ヘッダ、ボディに基づいたリクエストのマッチング
- 異なる表現における同じ URI のスマートマッチング (エンコードされた形式と非エンコードされた形式)
- 異なる表現での同じヘッダのスマートなマッチング
- Test::Unit、RSpec、minitest のサポート
WebMockの使い方
WebMockのGitHubページのサンプルから、使い方を抜粋します。
スタブ
stub_request
でリクエストをスタブすることができます。uri のみに基づくスタブ付きリクエストとデフォルトのレスポンス
stub_request(:any, "www.example.com") # スタブ(anyを使う) Net::HTTP.get("www.example.com", "/") # ===> Successメソッド、URI、ボディ、ヘッダに基づいたスタブリクエスト
# スタブ stub_request(:post, "www.example.com"). with(body: "abc", headers: { 'Content-Length' => 3 }) uri = URI.parse("http://www.example.com/") req = Net::HTTP::Post.new(uri.path) req['Content-Length'] = 3 res = Net::HTTP.start(uri.host, uri.port) do |http| http.request(req, "abc") end # ===> Successリクエストボディをハッシュと照合
ボディが、URL-Encode、JSON、XML のいずれかのとき、リクエストボディをハッシュと照合できます。
# スタブ stub_request(:post, "www.example.com"). with(body: {data: {a: '1', b: 'five'}}) RestClient.post('www.example.com', "data[a]=1&data[b]=five", content_type: 'application/x-www-form-urlencoded') # ===> Success RestClient.post('www.example.com', '{"data":{"a":"1","b":"five"}}', content_type: 'application/json') # ===> Success RestClient.post('www.example.com', '<data a="1" b="five" />', content_type: 'application/xml') # ===> Success
hash_including
を使うと、部分的なハッシュとリクエストボディを照合できます。# bodyをhash_includingで部分的なハッシュで照合 # bodyが全て一致していなくても照合できる stub_request(:post, "www.example.com"). with(body: hash_including({data: {a: '1', b: 'five'}})) RestClient.post('www.example.com', "data[a]=1&data[b]=five&x=1", :content_type => 'application/x-www-form-urlencoded') # ===> Successクエリパラメータの照合
ハッシュでクエリパラメータを照合できます。
# スタブ stub_request(:get, "www.example.com").with(query: {"a" => ["b", "c"]}) RestClient.get("http://www.example.com/?a[]=b&a[]=c") # ===> Successボディと同様、
hash_including
で部分ハッシュとクエリパラメータと照合できます。stub_request(:get, "www.example.com"). with(query: hash_including({"a" => ["b", "c"]})) RestClient.get("http://www.example.com/?a[]=b&a[]=c&x=1") # ===> Success
hash_excluding
を使うと、クエリパラメータ に含まれていない状態に照合できます。stub_request(:get, "www.example.com"). with(query: hash_excluding({"a" => "b"})) RestClient.get("http://www.example.com/?a=b") # ===> Failure RestClient.get("http://www.example.com/?a=c") # ===> Successカスタムレスポンスを返すスタブ
to_return
でカスタムレスポンスを返すスタブを設定できます。# スタブ stub_request(:any, "www.example.com"). to_return(body: "abc", status: 200, headers: { 'Content-Length' => 3 }) Net::HTTP.get("www.example.com", '/') # ===> "abc"エラーをraiseする
# クラスで宣言された例外のraise stub_request(:any, 'www.example.net').to_raise(StandardError) RestClient.post('www.example.net', 'abc') # ===> StandardError # 例外インスタンスのraise stub_request(:any, 'www.example.net').to_raise(StandardError.new("some error")) # 例外メッセージで例外をraise stub_request(:any, 'www.example.net').to_raise("some error")
to_timeout
で、タイムアウト例外のraiseもできます。stub_request(:any, 'www.example.net').to_timeout RestClient.post('www.example.net', 'abc') # ===> RestClient::RequestTimeout繰り返すリクエストに複数の異なるレスポンス
リクエストが繰り返された時、複数の異なるレスポンスを返すことができます。
また、to_return
、to_raise
、to_timeout
をthen
でつないで複数のレスポンスを返したり、times
を使ってレスポンスを返す回数と指定することもできます。stub_request(:get, "www.example.com"). to_return({body: "abc"}, {body: "def"}) Net::HTTP.get('www.example.com', '/') # ===> "abc\n" Net::HTTP.get('www.example.com', '/') # ===> "def\n" # すべてのレスポンスが使用された後、最後のレスポンスが無限に返される Net::HTTP.get('www.example.com', '/') # ===> "def\n"ネットワークへのリアルリクエストを許可または無効化
WebMock.allow_net_connect!
で、実際のネットワークへのリクエストを許可できます。WebMock.disable_net_connect!
で無効化することもできます。
特定のリクエストを許可しながら、外部リクエストを無効にすることもできます。# 実際のネットワークへのリクエストを許可 WebMock.allow_net_connect! stub_request(:any, "www.example.com").to_return(body: "abc") Net::HTTP.get('www.example.com', '/') # ===> "abc" Net::HTTP.get('www.something.com', '/') # ===> /.+Something.+/ # 実際のネットワークへのリクエストを無効化 WebMock.disable_net_connect! Net::HTTP.get('www.something.com', '/') # ===> Failure他にも、様々な方法で、スタブすることができます。他の使い方のサンプルコードはStubbingページを参照ください。
期待値の設定(モック)
WebMockのGitHubページには、Test::Unit、RSpecでの期待値の設定方法の記述はありましたが、minitestについて記述がありませんでした。
minitestは、Test::Unitと同様の書き方ができそうです(参考)。Test::Unit/minitest
assert_requested
やassert_not_requested
を使います。require 'webmock/test_unit' stub_request(:any, "www.example.com") uri = URI.parse('http://www.example.com/') req = Net::HTTP::Post.new(uri.path) req['Content-Length'] = 3 res = Net::HTTP.start(uri.host, uri.port) do |http| http.request(req, 'abc') end assert_requested :post, "http://www.example.com", headers: {'Content-Length' => 3}, body: "abc", times: 1 # ===> Success assert_not_requested :get, "http://www.something.com" # ===> Success assert_requested(:post, "http://www.example.com", times: 1) { |req| req.body == "abc" }スタブを使って期待値を設定するためには、以下のように書きます。
stub_get = stub_request(:get, "www.example.com") stub_post = stub_request(:post, "www.example.com") Net::HTTP.get('www.example.com', '/') assert_requested(stub_get) assert_not_requested(stub_post)Rspec
expect
とhave_requested
を組み合わせて書きます。require 'webmock/rspec' expect(WebMock).to have_requested(:get, "www.example.com"). with(body: "abc", headers: {'Content-Length' => 3}).twice expect(WebMock).not_to have_requested(:get, "www.something.com") expect(WebMock).to have_requested(:post, "www.example.com"). with { |req| req.body == "abc" } # Note that the block with `do ... end` instead of curly brackets won't work! # Why? See this comment https://github.com/bblimke/webmock/issues/174#issuecomment-34908908 expect(WebMock).to have_requested(:get, "www.example.com"). with(query: {"a" => ["b", "c"]}) expect(WebMock).to have_requested(:get, "www.example.com"). with(query: hash_including({"a" => ["b", "c"]})) expect(WebMock).to have_requested(:get, "www.example.com"). with(body: {"a" => ["b", "c"]}, headers: {'Content-Type' => 'application/json'})
a_request
とhave_been_made
を組み合わせて以下のようにも書けます。expect(a_request(:post, "www.example.com"). with(body: "abc", headers: {'Content-Length' => 3})). to have_been_made.once expect(a_request(:post, "www.something.com")).to have_been_made.times(3) expect(a_request(:post, "www.something.com")).to have_been_made.at_least_once expect(a_request(:post, "www.something.com")). to have_been_made.at_least_times(3) expect(a_request(:post, "www.something.com")).to have_been_made.at_most_twice expect(a_request(:post, "www.something.com")).to have_been_made.at_most_times(3) expect(a_request(:any, "www.example.com")).not_to have_been_made expect(a_request(:post, "www.example.com").with { |req| req.body == "abc" }). to have_been_made expect(a_request(:get, "www.example.com").with(query: {"a" => ["b", "c"]})). to have_been_made expect(a_request(:get, "www.example.com"). with(query: hash_including({"a" => ["b", "c"]}))).to have_been_made expect(a_request(:post, "www.example.com"). with(body: {"a" => ["b", "c"]}, headers: {'Content-Type' => 'application/json'})).to have_been_madeスタブを使って期待値を設定するためには、以下のように書きます。
stub = stub_request(:get, "www.example.com") # ... make requests ... expect(stub).to have_been_requested詳細は期待値の設定ページを参照ください。
その他
WebMock.reset!
で現在のスタブとリクエストの履歴をすべてリセットしたり、WebMock.reset_executed_requests!
で実行されたリクエストのカウンタのみをリセットできます。
WebMock.disable!
やWebMock.enable!
で、WebMock を無効にしたり有効にしたり、一部の http クライアントアダプタのみを有効にすることができます。
他の機能については、WebMockのGitHubページのサンプルコードを参照ください。MiniTest::Mock
最後に、MiniTest::Mockは、minitestに含まれているモックオブジェクトのフレームワークです。
公式ドキュメント:http://docs.seattlerb.org/minitest/Minitest/Mock.html
A simple and clean mock object framework.
All mock objects are an instance of Mock.
(シンプルでクリーンなモックオブジェクトフレームワークです。すべてのモックオブジェクトは MiniTest::Mockのインスタンスです。)MiniTest::Mockの使い方
スタブ
オブジェクトをスタブする
stub
は、Minitest::Mock のオブジェクト拡張です。
スタブが有効なのはブロック内のみで、ブロックの最後にスタブはクリーンアップされます。また、スタブする前にメソッド名が存在している必要があります。
stub_any_instance
メソッドは、クラスのインスタンス上にメソッドスタブを作成できます。minitest-stub_any_instance_ofのgemを導入すると使うことができます。
- stub:オブジェクトのメソッドをスタブする
- stub_any_instance_of:クラスのインスタンスメソッドをスタブする
stub
のサンプルコードです。require 'minitest/autorun' # スタブする対象のクラス class Hello def say 'Hello!' end end hello = Hello.new # helloオブジェクトのsayメソッドが'Hello, this is from stub!'を返すようにスタブする hello.stub(:say, 'Hello, this is from stub!') do hello.say #==> "Hello, this is from stub!" end # ブロックを抜けるとスタブは無効になる hello.say #==> "Hello!"
stub_any_instance
を使うと、インスタンスメソッドのスタブを以下のように書けます。インスタンスメソッドのスタブを書くときはこちらの方が使える場面が多そうです。require 'minitest/autorun' require 'minitest/stub_any_instance' # minitest-stub_any_instance_ofのgemも必要 # スタブする対象のクラス class Hello def say 'Hello!' end end # Helloクラスの任意のインスタンスのsayメソッドが'Hello, this is from stub!'を返すようにスタブする Hello.stub_any_instance(:say, 'Hello, this is from stub!') do Hello.new.say #==> "Hello, this is from stub!" end # ブロックを抜けるとスタブは無効になる Hello.new.say #==> "Hello!"モック
expectメソッド
`expect(name, retval, args = [], &blk)
メソッド名(name)が呼ばれ、オプションで引数(args)またはブロック(blk)を指定し、戻り値(retval)を返すことを期待します。require 'minitest/autorun' @mock.expect(:meaning_of_life, 42) @mock.meaning_of_life # => 42 @mock.expect(:do_something_with, true, [some_obj, true]) @mock.do_something_with(some_obj, true) # => true @mock.expect(:do_something_else, true) do |a1, a2| a1 == "buggs" && a2 == :bunny end引数は、'==='演算子を使って期待される引数と比較されるので、より具体的な期待値が少なくて済むようになっています。(含まれるか?で比較される)
require 'minitest/autorun' # users_any_stringメソッドがStringに含まれる場合、trueを返す @mock.expect(:uses_any_string, true, [String]) @mock.uses_any_string("foo") # => true @mock.verify # => true(期待通りにモックが呼ばれたのでtrueになる) @mock.expect(:uses_one_string, true, ["foo"]) @mock.uses_one_string("bar") # => raises MockExpectationError(期待通りにモックが呼ばれなかったため)メソッドが複数回呼ばれる場合は、それぞれに新しい期待値を指定します。これらは定義した順番で使用されます。
require 'minitest/autorun' @mock.expect(:ordinal_increment, 'first') @mock.expect(:ordinal_increment, 'second') @mock.ordinal_increment # => 'first' @mock.ordinal_increment # => 'second' @mock.ordinal_increment # => raises MockExpectationError "No more expects available for :ordinal_increment"verifyメソッド
すべてのメソッドが期待通りに呼び出されたことを確認します。期待通りに呼ばれたら
true
を返します。モックオブジェクトが期待通りに呼ばれなかった場合、MockExpectationError
を発生させます。詳しくは、MiniTest::Mockページを参照ください。
最後に
RRもWebMockも、公式ドキュメントに十分な使い方のサンプルが掲載されていたので、一読してみると良さそうです。MiniTest::Mockの情報量は少なめだったので、
irb
やrails c
でmock
やstub
の動きを確認してみると、想像がつきやすくなると思いました。(実行時に、require 'minitest/autorun'
が必要です。)参考情報
RR / GitHub
RRのページ
WebMock / GitHub
MiniTest::Mock
MiniTest stub
minitest-stub_any_instance
Mock、Stub勉強会(ruby)
自動テストのスタブ・スパイ・モックの違い
Test Double / xUnit Patterns.com
minitest で stub, mock を使う
wiki テストダブルxUnit Test Pattern
テストダブルのバリエーションを調べていると、こちらのxUnit Test Patterns: Refactoring Test Codeの書籍がよく出てきました。
英語版しか出版されていないようですが、Webで内容を確認できました(英語です)。
http://xunitpatterns.com
- 投稿日:2020-06-21T14:58:15+09:00
【Rails】ancestryを用いた多階層カテゴリー機能の実装『Bootstrap3でウィンドウ作ってみた編』
目標
開発環境
・Ruby: 2.5.7
・Rails: 5.2.4
・Vagrant: 2.2.7
・VirtualBox: 6.1
・OS: macOS Catalina前提
下記実装済み。
・Slim導入
・Bootstrap3導入
・Font Awesome導入
・ログイン機能実装
・投稿機能実装
・多対多のカテゴリー機能実装
・多階層カテゴリー機能実装(準備編)
・多階層カテゴリー機能実装(seed編)
・多階層カテゴリー機能実装(作成フォーム編)
・多階層カテゴリー機能実装(編集フォーム編)1.コントローラーを編集
homes_controller.rb# 追記 def category_window @children = Category.find(params[:parent_id]).children end【解説】
① Ajax通信で送られてきたパラメーターに対応するカテゴリーの、子カテゴリーを抽出し、インスタンス変数に代入する。
@children = Category.find(params[:parent_id]).children2.
json.jbuilderファイル
を作成・編集ターミナル$ touch app/views/homes/category_window.json.jbuildercategory_window.json.jbuilderjson.array! @children do |children| json.id children.id json.name children.name end【解説】
①
get_category_children
アクションで抽出したレコードを繰り返し処理し、配列を作成する。json.array! @children do |children|② 各IDと名前を
①
で作成した配列に格納する。json.id children.id json.name children.name◎ 親カテゴリー(ビジネス)にマウスが乗っている場合の返り値
[ { "id": 2, "name": "金融" }, { "id": 6, "name": "経済" }, { "id": 9, "name": "経営" }, { "id": 13, "name": "マーケティング" }, ]◎ 子カテゴリー(金融)にマウスが乗っている場合の返り値
[ { "id": 3, "name": "株" }, { "id": 4, "name": "為替" }, { "id": 5, "name": "税金" }, ]3.ルーティングを追加
routes.rb# 追記 get 'get_category/new', to: 'homes#category_window', defaults: { format: 'json' }4.ビューを編集
application.html.slimbody header nav.navbar.navbar-default.navbar-fixed-top .container-fluid ul.nav.navbar-nav.navbar-right li.dropdown role='presentation' a.dropdown-toggle data-toggle='dropdown' href='#' role='button' aria-expanded='false' i.fas.fa-list-ul span | カテゴリーから探す span.caret ul.dropdown-menu role='menu' li role='presentation' - Category.where(ancestry: nil).each do |parent| = link_to parent.name, root_path, id: "#{parent.id}", class: 'parent-category' br li role='presentation' class='children-list' br li role='presentation' class='grandchildren-list'【解説】
※Bootstrapの書き方については省略します。
① ancestryの値が
nil
、つまり親カテゴリーを全て抽出し、プルダウンメニューに表示する。- Category.where(ancestry: nil).each do |parent| = link_to parent.name, root_path, id: "#{parent.id}", class: 'parent-category'② 子カテゴリーを表示する場所を用意する。
li role='presentation' class='children-list'③ 孫カテゴリーを表示する場所を用意する。
li role='presentation' class='grandchildren-list'5.JavaScriptファイルを作成・編集
ターミナル$ touch app/assets/javascripts/category_window.jscategory_window.js$(function() { function buildChildHTML(children) { let html = ` <a class="children-category" id="${children.id}" href="/"> ${children.name} </a> `; return html; } $('.parent-category').on('mouseover', function() { let id = this.id; $('.children-category').remove(); $('.grandchildren-category').remove(); $.ajax({ type: 'GET', url: '/get_category/new', data: { parent_id: id, }, dataType: 'json', }).done(function(children) { children.forEach(function(child) { let html = buildChildHTML(child); $('.children-list').append(html); }); }); }); function buildGrandChildHTML(children) { let html = ` <a class="grandchildren-category" id="${children.id}" href="/"> ${children.name} </a> `; return html; } $(document).on('mouseover', '.children-category', function() { let id = this.id; $.ajax({ type: 'GET', url: '/get_category/new', data: { parent_id: id, }, dataType: 'json', }).done(function(children) { children.forEach(function(child) { let html = buildGrandChildHTML(child); $('.grandchildren-list').append(html); }); $(document).on('mouseover', '.children-category', function() { $('.grandchildren-category').remove(); }); }); }); });【解説】
① 子カテゴリーのHTMLを作成する。
function buildChildHTML(children) { let html = ` <a class="children-category" id="${children.id}" href="/"> ${children.name} </a> `; return html; }② どの親カテゴリーにマウスが乗っているかによって、子カテゴリーの表示内容を変更する。
$('.parent-category').on('mouseover', function() { let id = this.id; $('.children-category').remove(); $('.grandchildren-category').remove(); $.ajax({ type: 'GET', url: '/get_category/new', data: { parent_id: id, }, dataType: 'json', }).done(function(children) { children.forEach(function(child) { let html = buildChildHTML(child); $('.children-list').append(html); }); }); });◎ 親カテゴリーにマウスが乗った時に発火するイベントを作成する。
$('.parent-category').on('mouseover', function() {});◎
category_window.json.jbuilder
から送られてきたIDを、変数へ代入する。let id = this.id;◎ とりあえず子カテゴリー以下を削除しておく。
$('.children-category').remove(); $('.grandchildren-category').remove();◎ パラメーター(parent_id)に先ほど作成した変数を設定して、
category_window
アクションを非同期で実行する。$.ajax({ type: 'GET', url: '/get_category/new', data: { parent_id: id, }, dataType: 'json', })◎ Ajax通信が成功した場合は対応する子カテゴリーのHTMLを作成し、表示する。
.done(function(children) { children.forEach(function(child) { var html = buildChildHTML(child); $('.children-list').append(html); }); });③孫カテゴリーのHTMLを作成する。
function buildGrandChildHTML(children) { var html = ` <a class="grandchildren-category" id="${children.id}" href="/"> ${children.name} </a> `; return html; }④ どの子カテゴリーにマウスが乗っているかによって、孫カテゴリーの表示内容を変更する。(
③
とほぼ同じなので説明は省略)$(document).on('mouseover', '.children-category', function() { var id = this.id; $.ajax({ type: 'GET', url: '/get_category/new', data: { parent_id: id, }, dataType: 'json', }).done(function(children) { children.forEach(function(child) { var html = buildGrandChildHTML(child); $('.grandchildren-list').append(html); }); $(document).on('mouseover', '.children-category', function() { $('.grandchildren-category').remove(); }); }); });注意
turbolinks
を無効化しないとプルダウンメニューが非同期で動作しないので、必ず無効化しておきましょう。
- 投稿日:2020-06-21T13:21:50+09:00
【Rails】アプリ開発の手順からGit管理まで
はじめに
未経験からバックエンドエンジニアを目指す22卒予定大学生@million_momobanです。
Webアプリの開発を進めつつ、就職やインターンの情報収集も行っています。22卒予定で私と同じようにエンジニア志望の方、是非繋がりましょう!
状況
Railsアプリ作成中にミスを犯してしまい、ただ1から作り直すくらいなら手順を記録して復習しながら作り直した方がやり直す理由にもなるし、同じ境遇の方の手助けにもなると考え今に至ります。
初学者である私がよくつまずく「環境構築」と「Git管理」に重点を置いてアプリ開発の一連の流れを記録し共有します。
記事内容でも技術面でもそれ以外でもコメント大歓迎です!というかください!!
こんな方へ
- Railsでアプリ開発したいけど腰が重い
- Gitを使っているようで使いこなせていない
- 疑似プルリクってよく聞くけど何なんだそれ
環境
- Mac
- Xcode / Homebrew / rbenv をインストール済み
- VSCode / iTerm2
- GitHubの登録・SSH接続済み
作業スペースの確保から
デスクトップからFinderを開いてUsers -> (ユーザー名)へと進み、今回はPortfolioという名前のディレクトリを作成しプロジェクトを管理します。もちろんコマンドラインから作成してもいいです。
ターミナルを開いてPortfolioまで移動、Rubyのバージョンを指定してインストールします。
2.6.5を指定している意味は特にないです。/Users/hoge/Portfolio$ rbenv install 2.6.5 ・・・ Installed ruby-2.6.5 to /Users/hoge/.rbenv/versions/2.6.5数分待ってこうなればオッケー?
他にもバージョンが存在していて、思った通りのバージョンにならなかったら$ rbenv global 2.6.5
でバージョンをその環境全体に適用してあげる。/Portfolio$ bundle init ・・・ Writing new Gemfile to /Users/hoge/Portfolio/Gemfile新しく生成されたPortfolio配下のGemfileをFinderから直接開く。中身が次のようになっていると思うので一番下の
# gem "rails"
をgem "rails"
にする。⌘+Sキーで上書き保存してファイルを閉じる。Gemfile# frozen_string_literal: true source "https://rubygems.org" git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } # gem "rails" ←ここを変更するできたらターミナルに戻って次のコマンドでrailsをインストールする。
/Portfolio$ bundle install --path vendor/bundleオプションの
---path vendor/bundle
はあってもなくても良いそうです。
参考: bundle install時に--path vendor/bundleを付ける必要性は本当にあるのか、もう一度よく考えてみようRailsプロジェクトの開始
インストール出来るRailsを確認してみます。
/Portfolio$ gem list rails *** LOCAL GEMS ***当然何もありません。
バージョンを指定してgemをインストールします。
5.2.4.2を指定しているのは以前の開発環境と同じにするため。/Portfolio$ gem install -v 5.2.4.2 rails ・・・ $ gem list rails *** LOCAL GEMS *** rails (5.2.4.2) ...gemがインストールされたのでようやくプロジェクトを開始できそうです。
Portfolio$ bundle exec rails _5.2.4.2_ new (プロジェクト名) -d mysql --skip-turbolinks --skip-test --skip-bundle create create README.md create Rakefile ・・・コマンドの解説は次の通り。場合によって追加したり削除したりしてください。
rails _ ○.○.○_
- バージョンを指定してインストール
-d mysql
- データベースを軽量で小規模向きであるデフォルトのSQLiteから高速で安定性のあるMySQLに変更。好みの問題だが最近ではこっちのほうがよく見かける。
--skip-turbolinks
- ターボリンクをオフに。ページの遷移が早くなります。
--skip-test
- minitestではなくRSpecを使うので。よくわからない人は後からでも変えられるので書かなくてもいいです。
--skip-bundle
- この後手動でbundleするので追加
終わったらプロジェクト名のディレクトリの中にあるGemfileに必要なgemを記述していきます。
上の方の記述にある'= 5.2.4.2'ですが、このように=にしてあげることでこれ以上のバージョンにならないようにしてあげます。Gemfile・・・ ruby '2.6.5' gem 'rails', '= 5.2.4.2' gem 'mysql2', '>= 0.4.4', '< 0.6.0' ・・・ターミナルから
$ cd (プロジェクト名)
でプロジェクトに移動し、$ bundle install
/portfolio/hoge$ bundle install ・・・ An error occurred while installing mysql2 (0.5.3), and Bundler cannot continue. Make sure that `gem install mysql2 -v '0.5.3' --source 'https://rubygems.org/'` succeeds before bundling.なにやらmysql2でエラーが発生した模様。
色々試してみましたがウェブ帳さんのRails5 gemでmysql2が インストールできないに記述してある$ bundle config --local build.mysql2 "--with-cppflags=-I/usr/local/opt/openssl/include" $ bundle config --local build.mysql2 "--with-ldflags=-L/usr/local/opt/openssl/lib"で解決しました。再度
bundle install
。
動作確認でrails s
をしlocalhost:3000にアクセスしてみる。接続もできていてバージョンもバッチリですね。
$ bundle exec rails db:create
でデータベースも作成しておきます。Git管理とGitHub
次にGit管理をしていきます。
ローカルリポジトリの管理にはSourcetreeやGitHub desktopなどがありますが、今回はVSCode上のもの+拡張機能で管理していきます。機能も初学者の個人開発程度であれば充分すぎますし、開くソフトも少なく済みます。
GitHistoryという拡張機能が便利なのでインストールしておきます。やってみる
GitHubのRepositoriesのNewから新しくプロジェクトを開始します。
Repository nameには好きな名前を、DescriptionやREADMEについてはそのままでokです。
Public(公開)にしていたら情報が漏れたりと危険なのでPrivateにチェックを入れておきましょう。
Create repositoryを押したら一旦VSCodeに移ります。
VSCodeを起動したら「フォルダを開く」から作成したプロジェクトを開きます。
ターミナルを開き、Create repositoryをした後に表示される5行のコマンドを順に入力していきます。/Portfolio$ git init $ git add . $ git commit -m "first commit" $ git remote add origin https://github.com/******/sample.git $ git push -u origin masterコマンドの説明としては次の通り。
git init
- gitによる管理を開始
git add .
- 編集したファイルをコミットするためのステージング環境に追加します。コミットをすることでゲームで言うセーブデータを一つ作るような工程を行うためその前準備みたいなものです。.(ドット) はカレントディレクトリ配下のすべてを意味し、この際では全てのファイルをコミットするための準備としてステージング環境に追加しています。
git commit -m "first commit"
- ローカルリポジトリに"first commit"(最初のコミット)というメッセージを残してコミットをしています。-m "○○"で○○というメッセージを追加することを意味します。「最初の城を攻略したよ」みたいなメモと一緒にセーブするような感覚です。
git remote add origin https...
- リモートのサーバ(https...)にoriginという短縮名をつけてあげているんです。毎回毎回https...を打っていたら面倒ですからね。originなのは慣習的にそう決まっているそう。
git push -u origin master
- pushすることでコミットしたしたファイル等を複数人に共有できるリモートリポジトリにアップロードします。masterというブランチにローカルリポジトリの変更を反映させます。ちなみにただmasterだけだとローカルリポジトリのmasterブランチということになります。
これで準備は整いました。
いざ開発
VSCodeのGit Historyを見ていきましょう。画像の通り1、2とクリックすると履歴を見ることができ、最初に行ったfirst commitを確認することができます。
3から現在masterというブランチにいることがわかります。ブランチ(枝)とは開発の環境を互いに影響を与えずに「修正用」とか「機能追加用」とか枝分かれさせていき柔軟に開発を進めるためのものです。
master と topic
デフォルトのmasterブランチには慣習的に安定版や完成品を置いておき、masterブランチ上で開発を行わない事が多いです。そのため新しく開発用のブランチを作成しましょう。3から新しくブランチを作成します。今回は'topic'と名前をつけました。
現在のGitの状態を見れる
git status
をターミナルで打ってみます。/Portfolio/hoge$ git status On branch topic nothing to commit, working tree cleanトピック(topic)ブランチにいるよ、コミットするものはないよ、作業スペースもキレイといっています。
addしてみる
開発用のtopicブランチにいることを確認したら少しファイルを弄ってみます。
gitignore
を設定し直しREADME.md
も少し変えてみました。サイドバーのGitマークをクリックし中身を見てみると先程の2つのファイルが作業スペースにあるので、+ボタンをクリックしてステージング環境に移動させてやります。
これはターミナルでいう$ git add
と同じことをしています。
ステージング済みになりコミットする準備が整いましたね。そしたらその上のテキストボックスにコミット内容を記述し、更に上のチェックボタンを押して変更をコミットします。
これもまたターミナルでいう$ git commit -m "動作確認"
と同等です。
再び履歴を見てみます。次の写真に注目すると、最新の状態である上段に緑色のtopicブランチ、下段(変更前)に緑色のmasterブランチと赤色のorigin/masterブランチがありますね。topicブランチで開発を進めているわけですから時系列が一番新しいのも理解できると思います。
さて、このまま変更をローカルのmasterブランチに反映させても問題なさそうなのでマージしたいと思います。まず左下のtopicを押してmasterブランチを選び、移動したこと確認します。
緑色のtopicの真下にあるMoreをクリックしてMerge thisを選びます。
topicを選択し、最終確認を済ませると...
きちんとmasterにもtopicで変更された内容が反映されました!!
プッシュとプルリクエスト
実は先程のブランチを切り替える際に押したtopicやmasterが書いてあるボタンの右に、雲マークもしくは2本の矢印が丸を描いているボタンがあると思います。それをクリックすれば簡単にリモートリポジトリへ現在の変更を同期することができます。
でもこれ考えてみてください。チーム開発していたとして突然、完成品のみを扱うと決めたmasterブランチに他のメンバーから確認もなしに変更を同期されるようなものです。
プルリクエストを作れば、マージするに値するのかチームメンバーにコードをレビューしてもらい、許可が下りればマージされるみたいなことが出来事故も防げます。個人開発ではありますが慣れておいて損はないでしょう。
やり方
topicブランチで開発を進め、3つのファイルの変更があったとします。+ボタンでステージングにaddをしてコミットメッセージを適当につけます。
コミットもして、先程の通りmasterブランチに切り替えてtopicブランチをマージします。
うん、予想通り。
ここからリモートリポジトリにプッシュをしていくのですが流れとしては次の通りです。ローカルリポジトリのmasterブランチからリモートリポジトリのorigin/topicブランチにプッシュする(正直topic → origin/topicでいい気がする)
↓
GitHub上でリモートリポジトリのorigin/topicブランチからorigin/masterへのプルリクエストを作る
↓
リポジトリの管理者にマージしてもらう。先程の続きからやっていきましょう。orign/topicへプッシュします。
あれ、リモートリポジトリにはまだmasterブランチしかないよ?と思った方は安心してください。ブランチがない場合は状況に応じて新しくブランチを作ってくれます。/Portfolio/○○$ git push origin topic ・・・ remote: Create a pull request for 'topic' on GitHub by visiting: remote: https://github.com/******/sample/pull/new/topic remote: To https://github.com/******/sample.git * [new branch] topic -> topicGitHub上でtopicへのプルリクエスト作ります。
topic -> topic は topic -> origin/topic のことですね。疑似チーム開発
次にGitHubを覗いてみます。プロジェクトのトップページへ行くと'topic' Compare & pull request とありますね。ここを押してプルリクを作成します。
上の枠には、topicからmasterへのプルリクを比較していてそれがマージ可能であることを示しています。
確認できたらタイトルとメッセージを書きます。タイトルにはコミットした際のメッセージが最初から当てられていると思います。
メッセージは自分以外の人が見ても理解できる内容を書くようにしましょう。
プルリクの書き方についてはこちらの方の記事がとても参考になりました。
→GitHub「完璧なプルリクの書き方を教えるぜ」メッセージも書けたら'Create pull request' します。
今回は個人開発でリポジトリの管理者も自分だけな為すぐにマージできますが、せっかくなので色々触ってみます。タブからFile changedをクリックします。
ここでは変更のあったファイルをレビューすることが出来ます。疑問や提案、称賛のコメントを積極的に残しましょう。
一通り書いたら右上部の'Review changes'から'Submit review'でレビューをします。
'Conversion'タブへ戻るとレビュー等が反映されています。
最終確認を済ませたら'Merge pull request'から'Confirm merge'でマージを完了。
これでやっと一連の流れが完了です。
このあたかも複数人で開発しているかのように見せる行為が疑似プルリクと言われる所以だと思います。まとめ
この記事を書いてみて、理解しているようで出来ていなかったことを沢山知ることが出来ました。それでも書いていて怪しいなと思うことも多く(特にブランチの運用あたり)、まだまだ勉強が必要だなと思いました。
初めてこんなにも長い文を書いたのでおかしな部分もあると思います!
これからも現在進行系で開発を進めながら記録を書き残しています。
プロジェクトが一段落ついたり、完成したりすればまた書こうと思っています。次はRSpecかな??地方在住でも独学でも情報系でなくても周りにプログラミングをやっている友達がいなくてもそれなりの成果物を完成させてバックエンドエンジニアとして就職が出来ることを証明して見せます!!
学生として就活情報やインターンなど経験も記事にして発信していくつもりです。
記事のことでも記事以外のことでもどんな内容でもいいのでコメントお待ちしています!!参考記事
- 投稿日:2020-06-21T10:19:19+09:00
ancestryを用いた多階層カテゴリー機能の実装『編集フォーム編』
目標
開発環境
・Ruby: 2.5.7
・Rails: 5.2.4
・Vagrant: 2.2.7
・VirtualBox: 6.1
・OS: macOS Catalina前提
下記実装済み。
・Slim導入
・Bootstrap3導入
・Font Awesome導入
・ログイン機能実装
・投稿機能実装
・多対多のカテゴリー機能実装
・多階層カテゴリー機能実装(準備編)
・多階層カテゴリー機能実装(seed編)
・多階層カテゴリー機能実装(作成フォーム編)実装
1.コントローラーを編集
books_controller.rbdef edit unless @book.user == current_user redirect_to books_path end @category_parent_array = Category.category_parent_array_create end def update if @book.update(book_params) book_categories = BookCategory.where(book_id: @book.id) book_categories.destroy_all BookCategory.maltilevel_category_create( @book, params[:parent_id], params[:children_id], params[:grandchildren_id] ) redirect_to @book else @category_parent_array = Category.category_parent_array_create render 'edit' end end【解説】
① 中間テーブルから編集する本に対応するレコードを全て抽出し、削除する。
book_categories = BookCategory.where(book_id: @book.id) book_categories.destroy_all2.ビューを編集
books/edit.html.slim/ 追記 .category-form = label_tag 'ジャンル' = select_tag 'parent_id', options_for_select(@category_parent_array), class: 'form-control', id: 'parent-category' i.fas.fa-chevron-down br注意
turbolinks
を無効化しないとセレクトボックスが非同期で動作しないので、必ず無効化しておきましょう。
- 投稿日:2020-06-21T10:19:19+09:00
【Rails】ancestryを用いた多階層カテゴリー機能の実装『編集フォーム編』
目標
開発環境
・Ruby: 2.5.7
・Rails: 5.2.4
・Vagrant: 2.2.7
・VirtualBox: 6.1
・OS: macOS Catalina前提
下記実装済み。
・Slim導入
・Bootstrap3導入
・Font Awesome導入
・ログイン機能実装
・投稿機能実装
・多対多のカテゴリー機能実装
・多階層カテゴリー機能実装(準備編)
・多階層カテゴリー機能実装(seed編)
・多階層カテゴリー機能実装(作成フォーム編)実装
1.コントローラーを編集
books_controller.rbdef edit unless @book.user == current_user redirect_to books_path end @category_parent_array = Category.category_parent_array_create end def update if @book.update(book_params) book_categories = BookCategory.where(book_id: @book.id) book_categories.destroy_all BookCategory.maltilevel_category_create( @book, params[:parent_id], params[:children_id], params[:grandchildren_id] ) redirect_to @book else @category_parent_array = Category.category_parent_array_create render 'edit' end end【解説】
① 中間テーブルから編集する本に対応するレコードを全て抽出し、削除する。
book_categories = BookCategory.where(book_id: @book.id) book_categories.destroy_all2.ビューを編集
books/edit.html.slim/ 追記 .category-form = label_tag 'ジャンル' = select_tag 'parent_id', options_for_select(@category_parent_array), class: 'form-control', id: 'parent-category' i.fas.fa-chevron-down br注意
turbolinks
を無効化しないとセレクトボックスが非同期で動作しないので、必ず無効化しておきましょう。続編
- 投稿日:2020-06-21T09:38:55+09:00
Rails プレゼンターを導入する
プレゼンターを導入する
プレゼンターとはView部分にあるロジックのHTMLコードを精製する役割を担います。
要はview部分をスッキリさせようぜってもんです。
プレゼンターはデコレーターとも呼ばれます。GemではDraper,Cellsなどが使われているようですが、
Gemを今回は使わずに実装をしていきます。helperはあかんのか?
ビューで使用するメソッドなのでヘルパーメソッドとして定義するのは自然です。
でもhelperはグローバルに定義される、メリットとデメリットがあります。
プロジェクトが大きくなるにつれて名前が衝突するリスクが増してしまいます。今回は以下のコードに含まれるロジック部分を分離していきます。
犯罪者リストのロジックです。
arrested?(逮捕されているか?)でtrueの場合は☑️をつけ、
そうでない場合は空欄の□となります。<% @members.each do |m| %> <%= m.arrested? ? raw("☑") : raw("☐") %>Modelに関するプレゼンターを作る
まずすべてのプレゼンターの祖先となるModelPresenterクラスを作ります。
呼び出し専用のobject属性とview_context属性が定義されています。app/presenters/model_presenter.rbclass ModelPresenter attr_reader :object, :view_context delegate :raw, to: :view_context def initialize(object, view_context) @object = object @view_context = view_context end end次にmodel_presenterクラスを継承してMemberプレゼンタークラスを作ります。
member_presenter.rbclass StaffMemberPresenter < ModelPresenter endERBテンプレートを編集
このクラスを用いてERBテンプレートを編集します。
MemberPresenterクラスのインスタンスを生成します。newメソッドの1つ目の引数にはMemberオブジェクト。
2つ目の引数には疑似変数selfを指定しています。
selfではRailsで定義されているすべてのヘルパーメソッドを利用できます。<% @members.each do |m| %> <% p = MemberPresenter.new(m, self) %> <%= m.arrested? ? raw("☑") : raw("☐") %>Presenterをにメソッドを定義する
ここで先ほど作ったMemberPresenterクラスにインスタンスメソッドを定義していきます。
member_presenter.rbclass MemberPresenter < ModelPresenter def arrested_mark object.arrested? ? view_context.raw("☑") : view_context.raw("☐") end endERBテンプレートを書き換える
今までのプレゼンターを利用してview部分をスッキリとさせます
<% @members.each do |m| %> <% p = MemberPresenter.new(m, self) %> <%= p.arrested_mark %>変更箇所には以下のようなコードが埋め込まれています。
m.arrested? ? raw("☑") : raw("☐")こんな感じでviewをスッキリさせることができました。
delegateを用いるともう少し簡潔にかけるのでその記事はまた書きたいと思います。
本日は以上です。
- 投稿日:2020-06-21T09:30:25+09:00
irbとpryの違いについて
irbはRuby付属の対話的環境。標準搭載。
pryはgem。Railsのデバックにはpryがオススメらしい。
イメージとしては、irbよりpryの方が少しリッチ。(そんなに変わらないが)
- 投稿日:2020-06-21T00:45:11+09:00
Rails Tutorial 第6版 学習まとめ 第9章
概要
この記事は私の知識をより確実なものにするためにRailsチュートリアル解説記事を書くことで理解を深め
勉強の一環としています。稀にとんでもない内容や間違えた内容が書いてあるかもしれませんので
ご了承ください。
できればそれとなく教えてくれますと幸いです・・・この章でやること
・ユーザーの任意でログイン情報を記憶しておき、ブラウザを再起動してもログインできる機能を追加する。
Remember me機能
↑でも述べた通りブラウザを閉じてもログインを保持する機能を実装する(Remember me)
トピックブランチを作成して作業を始める。記憶トークンと暗号化
これからの作業や作成物がなかなか難しいので先回り知識を確認する。
・トークンとは
コンピュータが使うパスワードのようなもの。
パスワードは人間が作成して人間が管理するがトークンはコンピュータが作成してコンピュータが管理する。・永続的cookiesと一時セッションについて
前章で作成した一時セッションはsessionメソッドを使って、cookiesにブラウザ終了時が有効期限のセッションを作成した。
今回はcookiesメソッドを使って期限が無限(正確には20年ほど)のセッションを作成する。
cookiesメソッドではsessionメソッドと違い情報が保護されないかつ、セッションハイジャックと呼ばれる攻撃の的になるため
ユーザーIDと記憶トークンをセットでcookiesに保存し、ハッシュ化したトークンをDBに保存することで
セキュリティを確保する。・具体的にどういう処理で実装するのか
1. cookiesメソッドを使って暗号化したユーザーIDと記憶トークンをブラウザに保存
2. DBにはハッシュ化した記憶トークン(記憶ダイジェスト)を同時に保存しておく。
3. 次回アクセス時はブラウザに保存されている期限付きcookiesのトークンとDBに保存された記憶ダイジェストを比較して
ログイン処理を自動で行う。大まかに内容を確認したので
さっそくDBに記憶ダイジェスト(remember_digest)を追加する。
rails g migration add_remember_digest_to_users remember_digest:string
以前説明した通りファイル名末尾にto_usersとつけることでusersテーブルにカラムを追加すると勝手に認識してくれる。remember_digestはユーザーが読み出せる内容ではないのでインデックスを追加する必要もない。
そのため、このままマイグレートする。記憶トークンを作成するにあたり、何を使うかだが
長くてランダムな文字列が好ましい。
SecureRandomモジュールのurlsafe_base64
メソッドが用途的にマッチしているのでこれを使っていく。
このメソッドは64種の文字を用いて、長さ22のランダム文字列を返すメソッド。
記憶トークンはこのメソッドを使って自動生成することにする。>> SecureRandom.urlsafe_base64 => "Rr2i4cNWOwhtDeVA4bnT2g" >> SecureRandom.urlsafe_base64 => "pQ86_IsKILLv4AxAnx9iHA"パスワードと同じくトークンはほかのユーザ⁻と重複しても問題ないが、一意なものを使うことで
ユーザーIDとトークンの両方が奪われでもしない限りはセッションハイジャックなどにもつながらない。新規でトークンを作成する(生成する)メソッドをuserモデルに定義していく。
def User.new_token SecureRandom.urlsafe_base64 endこのメソッドもユーザオブジェクトは不要のためクラスメソッドとして定義する。
つぎにrememberメソッドを作成していく。
このメソッドではDBにトークンに対応した記憶ダイジェストを保存する。
DBにremember_digestは存在するがremember_tokenは存在しない。
DBに保存したいのはダイジェストのみだがユーザーオブジェクトに紐づいたトークンに対するダイジェストを保存したいので
トークン属性にもアクセスしたい。
つまりパスワードの時と同じく仮想の属性としてトークンが必要になる。
パスワード実装時はhas_secure_passwordが自動生成してくれたが、今回は
attr_accessor
を使ってremember_tokenを作成する。user.rbclass User < ApplicationRecord attr_accessor :remember_token # before_save { self.email.downcase! } # has_secure_password # VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i # validates :name, presence: true, length:{maximum: 50} # validates :email, presence: true, length:{maximum: 255}, # format: {with: VALID_EMAIL_REGEX},uniqueness: true # validates :password, presence: true, length:{minimum: 6} # def User.digest(string) # cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : # BCrypt::Engine::cost # BCrypt::Password.create(string, cost: cost) # end # def User.new_token # SecureRandom.urlsafe_base64 # end def remember self.remember_token = User.new_token update_attribute(:remember_digest,User.digest(remember_token)) end endrememberメソッドの1行目の
self.remember_token = User.new_token
は
selfを書かないとremember_tokenというローカル変数が作成されてしまうためここでは必須。ここではパスワードにアクセスできないためupdate_attributeはバリデーションを素通りさせるために使っている。
演習
1.しっかり動く。
remember_tokenは22文字のランダム生成文字列
remember_digestはそれらのハッシュ化文字列になっていることが見てわかる。>> user.remember (0.1ms) begin transaction User Update (2.4ms) UPDATE "users" SET "updated_at" = ?, "remember_digest" = ? WHERE "users"."id" = ? [["updated_at", "2020-06-17 14:30:27.202627"], ["remember_digest", "$2a$12$u8cnBDCQX85e9gBHbQWWNeJq.mxKq8hhpt/FSYMtm2rI2ljWIhRLi"], ["id", 1]] (6.1ms) commit transaction => true >> user.remember_token => "lZaXgeF42y5XeP-EEPzstw" >> user.remember_digest => "$2a$12$u8cnBDCQX85e9gBHbQWWNeJq.mxKq8hhpt/FSYMtm2rI2ljWIhRLi" >>2.どちらも動作は同じ
class << self
を使うとendの間まではすべてクラスメソッドとして定義される。
ここでのselfキーワードはインスタンスオブジェクトではなくUserクラスそのものを表しているため認識違いに注意。ログイン状態の保持
永続cookiesに保存するためには
cookies
メソッドを使う。
sessionとおなじくハッシュとして使える。cookiesはvalue(値)とexpires(有効期限)を持っていて
cookies[:remember_token] = { value: remember_token, expires: 20.years.from_now.utc }とすることでcookies[:remember_token]に有効期限が20年のremember_tokenの値を保存できる。
また有効期限が20年というのはよく使われるのでRailsには専用メソッドが追加されていてcookies.permanent[:remember_token] = remember_tokenとしても同じ効果になる。
またユーザーIDも永続cookiesに保存するがそのまま保存するとIDがそのまま保存されてしまい、
cookiesがどのような形式で保存されているのか、攻撃者にバレバレになってしまうため、
暗号化する。
暗号化には署名付きcookieを使う。
cookies.signed[:user_id] = user.id
これで安全に暗号化して保存できる。もちろんユーザーIDも永続cookiesとして保存する必要があるのでpermanentメソッドをつないで使う。
cookies.permanent.signed[:user_id] = user.id
このようにユーザーIDと記憶トークンをセットでcookiesに入れることで
ユーザーがログアウトするとログインできなくなる(DBのダイジェストが削除されるため)最後にブラウザに保存されたトークンとDBのダイジェストを比較する方法だが
secure_passwordのソースコードを一部パクり
BCrypt::Password.new(remember_digest) == remember_token
このようなコードを使う。
このコードだとremember_digestとremember_tokenを直接比較している。
実は、Bcryptで==演算子が再定義されており、このコードは
BCrypt::Password.new(remember_digest).is_password?(remember_token)
という動作をしている。
これを利用して記憶ダイジェストと記憶トークンを比較するauthenticated?
メソッドを定義する。def authenticated?(remember_token) BCrypt::Password.new(remember_digest).is_password?(remember_token) endここでのremember_digestはself.remember_digestと同じである。
DBの記憶ダイジェストと引数に渡した記憶トークンを比較して正しければtrueを返すさっそくsessions_controllerのログイン処理部にremember処理を追加する。
def create user = User.find_by(email: params[:session][:email].downcase) if user&.authenticate(params[:session][:password]) log_in(user) remember user redirect_to user else flash.now[:danger] = "Invalid email/password combination" render 'new' end endここではrememberヘルパーメソッドを使う。(まだ定義していない)
↓rememberヘルパーメソッド
rb:sessions_helper.rb
def remember(user)
user.remember
cookies.signed.permanent[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token
end
わかりづらいので補足。
Userモデルに定義したrememberメソッドで
user.rb
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest,User.digest(remember_token))
end
ユーザーオブジェクトに対して記憶トークンと記憶ダイジェストを生成する。sessions_helperに定義したrememberメソッドで
1.Userモデルのrememberメソッドを呼び出し、トークンとダイジェストを生成。
2.cookiesにユーザーIDを暗号化して保存
3.cookiesに1で生成したトークンを保存の流れ。
メソッド名が被っているので注意。これでユーザー情報をcookiesに安全に保存できるようになったがログイン状態を見て
動的にレイアウトを変更するために使っていたcurrent_user
メソッドが一時セッションにしか
対応していないため修正する。def current_user #現在ログイン中のユーザ⁻オブジェクトを返す if (user_id = session[:user_id]) @current_user ||= User.find_by(id: user_id) elsif (user_id = cookies.signed[:user_id]) user = User.find_by(id: user_id) if user &. authenticated?(cookies[remember_token]) log_in user @current_user = user end end end・user_idというローカル変数を使うことでコードの重複を減らしている。
・ブラウザを開いた初回実行時は永続cookiesの処理が行われ、同時にログイン処理も行われるため
ブラウザを閉じるまでは@current_userにユーザーが保存されている。現時点だとログアウト処理(永続cookies)を削除する方法がないため
ログアウトできない。
(すでにあるログアウトアクションだと一時セッションを削除するだけなので、永続cookiesから情報を取り出して
自動でログインしてしまうためログアウトができない。)演習
2.動く。
>> user = User.first (1.1ms) SELECT sqlite_version(*) User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] => #<User id: 1, name: "take", email: "take.webengineer@gmail.com", created_at: "2020-06-14 02:57:10", updated_at: "2020-06-18 15:18:53", password_digest: [FILTERED], remember_digest: "$2a$12$tAZFCVr39lkPONLS4/7zneYgOE5pcYDM2kX6F1yKew2..."> >> user.remember (0.1ms) begin transaction User Update (2.8ms) UPDATE "users" SET "updated_at" = ?, "remember_digest" = ? WHERE "users"."id" = ? [["updated_at", "2020-06-18 15:23:21.357804"], ["remember_digest", "$2a$12$h3K3aZSBmXB7wGkNdsBrS.2/UaawMQ199DGMvTDU8upvvOKCzbeba"], ["id", 1]] (10.3ms) commit transaction => true >> user.authenticated?(user.remember_token) => trueユーザーを忘れる
現在、永続cookiesを削除していないため、ログアウトできない。
この問題を解決するためにforget
メソッドを定義する。
このメソッドで記憶ダイジェストをnilにする。
さらにsessions_helperにもforget
メソッドを定義することで
こちらではcookiesに保存されたユーザーIDと記憶トークンも削除する。def forget(user) #永続セッションを削除・記憶ダイジェストもリセット user.forget cookies.delete[:user_id] cookies.delete[:remember_token] end def log_out forget(current_user) session.delete(:user_id) @current_user = nil end一応ログアウト処理の流れをサラッとおさらい。
1. ユーザオブジェクトに保存された記憶ダイジェストをnilにする(Userモデルのforgetメソッド)
2. cookiesのユーザーIDと記憶トークンを削除(sessions_helperのforgetメソッド)
3. 一時セッションのユーザIDを削除
4. カレントユーザ(現在ログイン中のユーザー)をnilにする。演習
1.削除されている。(実行画面は省略)なおChromeだと以前と同じく一時セッションが残ってしまっているがアプリの動作上は
問題ない。2つの目立たないバグ
現時点で二つのバグが残っている。かなり面倒なので詳細に説明していく。
1つ目のバグ
複数のタブでログインしていて、タブ1でログアウトした後、タブ2でもログアウトした時。
タブ1内でlog_outメソッドを使ってログアウトした後だとcurrent_userがnilになっている。
この状態でもう一度ログアウトしようとすると削除するcookieが見つからないため失敗する。2つ目のバグ
別ブラウザで(Chrome、Firefoxなど)ログインしている時。
1. Firefoxでログアウトするとremember_digestがnilになる。
2. Chromeを閉じると一時セッションは削除されるがcookiesは残るため、ユーザーIDからユーザーを見つけることができてしまう。
3.user.authenticated?
メソッドで比較するremember_digestがFirefox側で既に削除されているため
比較対象がなくなりエラーが発生する。このバグを修正するためにまずはバグをキャッチするテストを書き
それを修正するコードを書く。
delete logout_path
これをログインテストのログアウト処理後にもう一度挿入することで2回ログアウトを再現する。このテストをパスさせるためには
ログイン中だけログアウト処理を行うようにすればいい。def destroy log_out if logged_in? redirect_to root_url end2つ目のバグについて、テストで異なるブラウザ環境を再現するのは難しいため、
Userモデルのremember_digestに関してのテストにとどめる。
具体的にはremember_digestがnilの時にはfalseを返すことをテストする。test "authenticated? should return false for a user with nil digest" do assert_not @user.authenticated?('') endテストをパスさせるためにauthenticated?メソッドを改良する
rb
def authenticated?(remember_token)
return false if remember_digest.nil?
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
remember_digestがnilの場合には即座にreturnキーワードでfalseを返し処理を終了させる。これで2つのバグが修正される。
演習
1.エラーが発生する。(実行画面は省略。)
2.これもエラーが発生する(EdgeとChrome)
3.確認済み。[Remember me]チェックボックス
次はRememberme機能には欠かせない、チェックボックスを実装する(チェックした時だけ記憶する機能)
<%= f.label :remember_me, class: "checkbox inline" do %> <%= f.check_box :remember_me %> <span>Remember me on this computer</span> <% end %>ラベルの内側に配置する理由に関してはhttps://html-coding.co.jp/annex/dictionary/html/label/
このサイトがわかりやすい
つまりラベルに指定されている者のどこをクリックしてもチェックボックスを押したのと同じ動作にできる。CSSで形を整えたら準備完了。
チェックボックスでparams[:session][:remember_me]に1 or 0が入るようになったので
1の時に記憶するようにすればいい。三項演算子を使って実装すると
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
remember user
の行をこれに差し替えるだけ
ちなみに三項演算子は条件文 ? trueの時の処理 : falseの時の処理という形式で書ける。
ちなみにparamsの数値はすべて文字列で記録されているため条件文の1は''で囲わないと
必ずfalse分が実行されてrememberできなくなるので注意演習
1.↑でも注意書きを書いたがparamsの条件は'1'としないとうまくいかない。うまくいけばcookiesに値が保存されて
うまく動く。2.
>> hungry = true => true >> hungry ? puts("I'm hungry now") : puts("I'm not hungry now") I'm hungry now => nil[Remember me]のテスト
Remembermeが実装できたのでテストも作成していく。
[Remember me]ボックスをテストする
直前の三項演算子で実装した
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
という部分はプログラムを触っている人だと1(真)0(偽)ということで
params[:session][:remember_me] ? remember(user) : forget(user)
と書きたくなるが、チェックボックスはあくまで1と0を返す。
Rubyでは1と0は真偽値ではなくどちらもtrueとして扱われるためこのように書くのは間違いになる。
このようなミスをキャッチできるテストを書かなければならない。ユーザーを記憶するためにはログインが必要になる。今までは逐一postメソッドを使ってparamsハッシュを送っていたが
毎回やるのはさすがに手間なのでログイン用のメソッドを定義する。
log_inメソッドとの混乱を防ぐためにlog_in_asメソッドとして定義する。test_helperclass ActiveSupport::TestCase # Run tests in parallel with specified workers parallelize(workers: :number_of_processors) # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. fixtures :all include ApplicationHelper # Add more helper methods to be used by all tests here... def is_logged_in? !session[:user_id].nil? end def log_in_as(user) session[:user_id] = user.id end end class ActionDispatch::IntegrationTest def log_in_as(user, password: 'password', remember_me: '1') post login_path, params:{ session: { email: user.email, password: password, remember_me: remember_me}} end endlog_in_asメソッドをActionDispatch::IntegrationTestとActiveSupport::TestCaseで2回別々に定義しているのは
統合テストではsession
メソッドを使えないから。
そのため統合テストでは代わりにpostリクエストを使ってログインしている。どちらのテストも同じ名前にすることで統合テストでも単体テストでもログインしたい時には何も気にせずlog_in_asメソッド
を呼べばいい。log_in_asメソッドを定義したのでRemember_meのテストを実装する。
test "login with remembering" do log_in_as(@user, remember_me: '1') assert_not_empty cookies[:remember_token] end test "login without remembering" do log_in_as(@user, remember_me: '1') delete logout_path log_in_as(@user, remember_me: '0') assert_empty cookies[:remember_token] end
log_in_as(@user, remember_me:'1')
デフォルト値を設定しているため本来不要だが比較しやすいようremember_me属性も入力している。演習
1.↑の統合テストでは仮想属性remember_tokenにアクセスできないためcookiesが空でないことだけをテストしていたが
assigns
メソッドを使うことで直前にアクセスしたアクションのインスタンス変数を取得できる。
上のテストの例ではlog_in_as
メソッド内でsessions_controllerのcreateアクションにアクセスしているため
createアクションで定義されたインスタンス変数の値をシンボルを使って読みだすことができる。
具体的には
現在createアクションで使われているのはuserというローカル変数なのでこれに@をつけて@userという
インスタンス変数に変えてしまうことでassignsメソッドが読み出せるようになる。
あとはテストでassigns(:user)
とすることで@userを読み出せる。users_login_test.rbtest "login with remembering" do log_in_as(@user, remember_me: '1') assert_equal cookies[:remember_token] , assigns(:user).remember_token endsessions_controller.rbdef create @user = User.find_by(email: params[:session][:email].downcase) if @user&.authenticate(params[:session][:password]) log_in(@user) params[:session][:remember_me] == '1' ? remember(@user) : forget(@user) redirect_to @user else flash.now[:danger] = "Invalid email/password combination" render 'new' end end[Remember me]をテストする
sessions_helperにログイン処理やセッション関連のヘルパーメソッドを実装してきたが
current_user
メソッドの分岐処理に関してテストが行われていない。
その証拠に何も関連性のない適当な文字列を代入してもテストがパスしてしまう。GREENのテスト↓
sessions_helper.rbdef current_user #現在ログイン中のユーザ⁻オブジェクトを返す if (user_id = session[:user_id]) @current_user ||= User.find_by(id: user_id) elsif (user_id = cookies.signed[:user_id]) テストしてないから日本語も許される。 user = User.find_by(id: user_id) if user &.authenticated?(cookies[:remember_token]) log_in user @current_user = user end endこれはまずいので
sessions_helper
ようのテストファイルを作成する。sessions_helper_test.rbrequire 'test_helper' class SessionsHelperTest < ActionView::TestCase def setup @user = users(:michael) remember(@user) end test "current_user returns right user when session is nil" do assert_equal @user, current_user assert is_logged_in? end test "current_user returns nil when remember digest is wrong" do @user.update_attribute(:remember_digest, User.digest(User.new_token)) assert_nil current_user end end1つ目のテストでは記憶したユーザとcurrent_userが同じかどうか確かめ、ログインしているかも確かめている。
こうすることでテストがcookiesにユーザーIDが存在した際に中身の処理が動いているか確認できる。2つ目のテストではremember_digestを書き換えることで
remember
メソッドで記録したremember_tokenと対応させない
ようにした際にcurrent_userが期待通りnilを返す、つまりauthenticated?
メソッドが
正しく動作しているかテストしている。また、補足として
assert_equal
メソッドは第1引数と第2引数を入れ替えても動作するが
書き方は第1引数に期待値、第2引数に実際の値と書かなければならないことに注意
このように書かなければエラーが発生した際にログの表示がかみ合わなくなってしまう。そしてこの段階ではもちろんテストは通らない。
入れておいた全く関係のない文を削除することでテストがパスする。
これでcurrent_userのどの分岐もテストできるようになったため回帰バグもキャッチできる。演習
1.記憶トークンと記憶ダイジェストが正しく対応していなくともuserが存在するだけでif文を通過してしまうため
返り値がnilでなくなってしまう。つまりテストも失敗する。FAIL["test_current_user_returns_nil_when_remember_digest_is_wrong", #<Minitest::Reporters::Suite:0x000055b13fd67928 @name="SessionsHelperTest">, 1.4066297989993473] test_current_user_returns_nil_when_remember_digest_is_wrong#SessionsHelperTest (1.41s) Expected #<User id: 762146111, name: "Michael Example", email: "michael@example.com", created_at: "2020-06-20 15:38:57", updated_at: "2020-06-20 15:38:58", password_digest: [FILTERED], remember_digest: "$2a$04$uoeG1eJEySynSb.wI.vyOewe9s9TJsSoI9vtXNYJxrv..."> to be nil. test/helpers/sessions_helper_test.rb:15:in `block in <class:SessionsHelperTest>'
↑current_userの返り値がnilになることが期待されているのに対し、userオブジェクトが返ってしまっていることを
エラーとして出力している。
- 投稿日:2020-06-21T00:43:17+09:00
progateで習得したrailsの基礎知識まとめ
どんな人向けの記事か
- Railsを学び始めて間もない人
- Railsで初めてアプリを作ろうとしている人
- Rails慣れはしているけど少し復習したい人
どんな記事か
Rails初学者である筆者が、先日修了したprogateのRailsコースで学んだことを並べていく記事です。
ところどころ他記事の情報で補完しているため、progate運営が表現・説明した内容とは異なる箇所もそれなりにあります。各種概念
サーバー
クライアント(ブラウザ)の要求に応じてデータを提供するコンピュータ。
Railsで作ったアプリをブラウザで表示するためには、このサーバーを起動させる必要がある。ビュー
ページの見た目を作るためのHTMLファイル。ブラウザとRailsのやりとりの中で、Railsからビューが返されることで、ページが表示される。
拡張子は.html.erb
。erbはEmbedded Rubyの略で、HTMLファイルにRubyスクリプトを埋め込むための仕組み。
下記のコントローラ・ルーティングと同様に、ページを作成するのに必要な3つの要素のうちの1つ。コントローラ
ブラウザにビューを返すためのもの。コントローラは目的に合わせてそれぞれ作成される。
例えば投稿に関する機能とユーザーに関する機能はそれぞれ別のコントローラファイルで規定される。アクション
コントローラ内の、ブラウザに返すビューを見つけ出すための機能。コントローラファイル内でメソッドとして表現される。
ルーティング
送信されたURLに対して、「どのコントローラ」の「どのアクション」で処理するかを決める、対応表のようなもの。
データベース
データを保存しておくための場所。
テーブル
データベース内でデータを管理している表。行ごとのデータをレコード、列ごとのデータをカラムと呼ぶ。
モデル
データベースとのやりとりを行うクラス。モデルのインスタンスを作り、それを保存する工程を経ることでデータをテーブルに格納できる。
マイグレーションファイル
データベースに変更を指示するためのファイル。
コーディング
ビューファイルで使うコード
<% %>
ex)<%= @post %>HTMLファイルにRubyコードを埋め込む際に、コードの前後に表記する。コードをブラウザに表示させたい場合は、コードの前後に
<%= %>
と表記する。link_toメソッド
<%= link_to("表示する文字", "URL") %> ex) <%= link_to("投稿一覧", "/posts/index")%>リンクを作成する。HTMLファイルで使う。
第3引数に{method: "post"}を追加することで、postとして定義されているルーティングにマッチするようになる。form_tag
<%= form_tag("URL") do %> データ <% end %> ex) <%= form_tag("/posts/update") do %> <textarea name="content"><%= @post.content %><textarea> <input type="submit" value="投稿"> <% end %>フォームに入力されたデータを、指定したURLに送信することができる。
しかしform_tag単体では意味がなく、textareaタグ(またはinputタグ)にname属性を指定することにより、name属性をキーとしたハッシュをアクション側に伝えることができる。
余談だが、上記のようにtextareaタグの間に値を置いておくことで、フォームを再入力する際にもともと入力していた内容の続きから入力できたりする。errors.full_messages
<% インスタンス名.errors.full_messages.each do |message| %> <%= message %> <% end %> ex) <% @posts.errors.full_messages.each do |message| %> <%= message %> <% end %>エラーメッセージを出力する。saveメソッドを呼び出した際にバリデーションに失敗すると、Railsでは自動的にエラーメッセージが生成されるようになっているため、each文を用いることですべて表示することができる。
yield
views/layout/application.html.erb<%= yield %>各ビューファイルは、application.html.erb内に表記されたyieldに代入される。
application.html.erbはサイト全体に適用するレイアウトを記載するビューファイル。コントローラファイルで使うコード
@変数
@post = "hogehoge"コントローラファイルで定義をすると、ビューファイルでRubyコードの埋め込みをする際に利用できるようになる。
newメソッド
モデル名.new(カラム名: 値) ex) post = Post.new(content: "hogehoge")モデルからインスタンスを作成する。
saveメソッド
インスタンス名.save ex) post.save作成したインスタンスをテーブルに保存する。
allメソッド
モデル名.all ex) posts = Post.allテーブル内の全てのレコードを取得する。
find_byメソッド
モデル名.find_by(カラム名: 値) ex) post = Post.find_by(id: 1)ある条件に合致するデータを1つ取得する。
whereメソッド
モデル名.where(カラム名: 値) ex) posts = Post.where(id: 1)ある条件に合致する複数のデータを取得する。
redirect_toメソッド
redirect_to("URL") ex) redirect_to("/posts/index")指定したページに転送することができる。
renderメソッド
render("フォルダ名/ファイル名") ex) render("posts/edit")別のアクションを経由せずに、直接ビューを表示することができる。
データの保存に失敗したときなどによく使われる。orderメソッド
モデル名.order(カラム名: :並び替えの順序) ex) @posts = Post.all.order(created_at: :desc)取得したデータの並び替えを行う。:desc は降順、:asc は昇順を表す。
destroyメソッド
インスタンス名.destroy ex) post.destroy指定したデータ(インスタンス)をデータベースから削除する。
変数session
session[:キー名] = 値 ex) session[:user_id] = @user.idページを移動してもログインユーザーの情報を保持し続けるためのもの。
nilを代入するとログイン状態ではないようにすることができる。params
ex1) @id = params[:id] ex2) @post = Post.new(content: params[:content])用例1. ルーティングで設定したURLの :○○ の値を取得する。
用例2. name="○○"がついたフォームの入力内容を受け取る。before_action
before_action 全アクションで共通する処理 ex) before_action :set_current_user, {only: [:edit, :update]}どのアクションを呼び出す前でも、必ず記述した処理が実行される。ファイルの一番上に記述する。
{only: [:アクション名]} を用いることで、処理が実行されるアクションを限定することができる。ルーティングファイルで使うコード
getメソッド
アプリ名/config/routes.rbget "URL" => "コントローラ名#アクション名" ex) get "/posts/index" => "posts#index"指定した情報を取得する。データベースを変更しない場合は大体getを使う。getと下記のpostは、HTTPメソッドと呼ばれている。
postメソッド
アプリ名/config/routes.rbpost "URL" => "コントローラ名#アクション名" ex) post "/posts/create" => "posts#create"データベースを変更する際、sessionの値を変更する際に使うメソッド。
HTTPメソッドは他にもいろいろあるらしいが、まだ学んでいないのでここでは省略。名前付きパラメータ
アプリ名/config/routes.rbget "posts/:id" => "posts#show"ルーティングのURL部分に : で始まる文字列を置くと、その文字列はパラメータとして認識される。
そのため、この例でいえば、/posts/○○ のようなすべてのURLをshowアクションに誘導することができる。
その関係で /posts/index のようなルーティングは、/posts/:id のルーティングに引っかからないようその前に記述する必要がある。モデルファイルで使うコード
validates
validates :検証するカラム名, {検証する内容} ex) validates :content, {presence: true}不正なデータがデータベースに保存されないように、データをチェックする(バリデーション)。
主な検証内容は以下の通り。
検証内容 意味 presecse: true そのカラムの値が存在するかどうかをチェックする length: {maximum: 文字数} 規定した文字数以上のデータは保存できないようにする uniqueness: true 重複したデータがデータベースに存在するかどうかチェックする マイグレーションファイルで使うコード
add_column :テーブル名, :カラム名, :データ型 ex) add_column :users, :image_name, :string既存のテーブルにカラムを追加する。changeメソッドの中に書く。
コマンドライン
rails new アプリ名
入力したアプリの名前と同名のフォルダが作成され、その中に開発に必要なフォルダやファイルが用意される。
rails server
サーバーの起動。
rails g controller コントローラ名 アクション名
コントローラの作成。gはgenerateでも可。
コントローラ名とアクション名は、HTTPメソッド(URL)と同じ名前を付けることが多い。
rails g model モデル名 カラム名:データ型
マイグレーションファイルとモデルファイルの作成。
モデル名は単数形かつ一文字目を大文字にする。
rails g migration ファイル名
マイグレーションファイルのみを作成。
ファイル名は add_image_name_to_users などの分かりやすい名前にする。
rails db:migrate
データベースへのマイグレーションファイルの反映。
マイグレーションファイルを作ったのにこれを実行しないとエラーが発生する。
まとめ
progateはよいです。
参考にした記事
参考にした書籍
- 投稿日:2020-06-21T00:43:17+09:00
progateで習得したRailsの基礎知識まとめ
どんな人向けの記事か
- Railsを学び始めて間もない人
- Railsで初めてアプリを作ろうとしている人
- Rails慣れはしているけど少し復習したい人
どんな記事か
Rails初学者である筆者が、先日修了したprogateのRailsコースで学んだことを並べていく記事です。
ところどころ他記事の情報で補完しているため、progate運営が表現・説明した内容とは異なる箇所もそれなりにあります。各種概念
サーバー
クライアント(ブラウザ)の要求に応じてデータを提供するコンピュータ。
Railsで作ったアプリをブラウザで表示するためには、このサーバーを起動させる必要がある。ビュー
ページの見た目を作るためのHTMLファイル。ブラウザとRailsのやりとりの中で、Railsからビューが返されることで、ページが表示される。
拡張子は.html.erb
。erbはEmbedded Rubyの略で、HTMLファイルにRubyスクリプトを埋め込むための仕組み。
下記のコントローラ・ルーティングと同様に、ページを作成するのに必要な3つの要素のうちの1つ。コントローラ
ブラウザにビューを返すためのもの。コントローラは目的に合わせてそれぞれ作成される。
例えば投稿に関する機能とユーザーに関する機能はそれぞれ別のコントローラファイルで規定される。アクション
コントローラ内の、ブラウザに返すビューを見つけ出すための機能。コントローラファイル内でメソッドとして表現される。
ルーティング
送信されたURLに対して、「どのコントローラ」の「どのアクション」で処理するかを決める、対応表のようなもの。
データベース
データを保存しておくための場所。
テーブル
データベース内でデータを管理している表。行ごとのデータをレコード、列ごとのデータをカラムと呼ぶ。
モデル
データベースとのやりとりを行うクラス。モデルのインスタンスを作り、それを保存する工程を経ることでデータをテーブルに格納できる。
マイグレーションファイル
データベースに変更を指示するためのファイル。
コーディング
ビューファイルで使うコード
<% %>
ex)<%= @post %>HTMLファイルにRubyコードを埋め込む際に、コードの前後に表記する。コードをブラウザに表示させたい場合は、コードの前後に
<%= %>
と表記する。link_toメソッド
<%= link_to("表示する文字", "URL") %> ex) <%= link_to("投稿一覧", "/posts/index")%>リンクを作成する。HTMLファイルで使う。
第3引数に{method: "post"}を追加することで、postとして定義されているルーティングにマッチするようになる。form_tag
<%= form_tag("URL") do %> データ <% end %> ex) <%= form_tag("/posts/update") do %> <textarea name="content"><%= @post.content %><textarea> <input type="submit" value="投稿"> <% end %>フォームに入力されたデータを、指定したURLに送信することができる。
しかしform_tag単体では意味がなく、textareaタグ(またはinputタグ)にname属性を指定することにより、name属性をキーとしたハッシュをアクション側に伝えることができる。
余談だが、上記のようにtextareaタグの間に値を置いておくことで、フォームを再入力する際にもともと入力していた内容の続きから入力できたりする。errors.full_messages
<% インスタンス名.errors.full_messages.each do |message| %> <%= message %> <% end %> ex) <% @posts.errors.full_messages.each do |message| %> <%= message %> <% end %>エラーメッセージを出力する。saveメソッドを呼び出した際にバリデーションに失敗すると、Railsでは自動的にエラーメッセージが生成されるようになっているため、each文を用いることですべて表示することができる。
yield
views/layout/application.html.erb<%= yield %>各ビューファイルは、application.html.erb内に表記されたyieldに代入される。
application.html.erbはサイト全体に適用するレイアウトを記載するビューファイル。コントローラファイルで使うコード
@変数
@post = "hogehoge"コントローラファイルで定義をすると、ビューファイルでRubyコードの埋め込みをする際に利用できるようになる。
newメソッド
モデル名.new(カラム名: 値) ex) post = Post.new(content: "hogehoge")モデルからインスタンスを作成する。
saveメソッド
インスタンス名.save ex) post.save作成したインスタンスをテーブルに保存する。
allメソッド
モデル名.all ex) posts = Post.allテーブル内の全てのレコードを取得する。
find_byメソッド
モデル名.find_by(カラム名: 値) ex) post = Post.find_by(id: 1)ある条件に合致するデータを1つ取得する。
whereメソッド
モデル名.where(カラム名: 値) ex) posts = Post.where(id: 1)ある条件に合致する複数のデータを取得する。
redirect_toメソッド
redirect_to("URL") ex) redirect_to("/posts/index")指定したページに転送することができる。
renderメソッド
render("フォルダ名/ファイル名") ex) render("posts/edit")別のアクションを経由せずに、直接ビューを表示することができる。
データの保存に失敗したときなどによく使われる。orderメソッド
モデル名.order(カラム名: :並び替えの順序) ex) @posts = Post.all.order(created_at: :desc)取得したデータの並び替えを行う。:desc は降順、:asc は昇順を表す。
destroyメソッド
インスタンス名.destroy ex) post.destroy指定したデータ(インスタンス)をデータベースから削除する。
変数session
session[:キー名] = 値 ex) session[:user_id] = @user.idページを移動してもログインユーザーの情報を保持し続けるためのもの。
nilを代入するとログイン状態ではないようにすることができる。params
ex1) @id = params[:id] ex2) @post = Post.new(content: params[:content])用例1. ルーティングで設定したURLの :○○ の値を取得する。
用例2. name="○○"がついたフォームの入力内容を受け取る。before_action
before_action 全アクションで共通する処理 ex) before_action :set_current_user, {only: [:edit, :update]}どのアクションを呼び出す前でも、必ず記述した処理が実行される。ファイルの一番上に記述する。
{only: [:アクション名]} を用いることで、処理が実行されるアクションを限定することができる。ルーティングファイルで使うコード
getメソッド
アプリ名/config/routes.rbget "URL" => "コントローラ名#アクション名" ex) get "/posts/index" => "posts#index"指定した情報を取得する。データベースを変更しない場合は大体getを使う。getと下記のpostは、HTTPメソッドと呼ばれている。
postメソッド
アプリ名/config/routes.rbpost "URL" => "コントローラ名#アクション名" ex) post "/posts/create" => "posts#create"データベースを変更する際、sessionの値を変更する際に使うメソッド。
HTTPメソッドは他にもいろいろあるらしいが、まだ学んでいないのでここでは省略。名前付きパラメータ
アプリ名/config/routes.rbget "posts/:id" => "posts#show"ルーティングのURL部分に : で始まる文字列を置くと、その文字列はパラメータとして認識される。
そのため、この例でいえば、/posts/○○ のようなすべてのURLをshowアクションに誘導することができる。
その関係で /posts/index のようなルーティングは、/posts/:id のルーティングに引っかからないようその前に記述する必要がある。モデルファイルで使うコード
validates
validates :検証するカラム名, {検証する内容} ex) validates :content, {presence: true}不正なデータがデータベースに保存されないように、データをチェックする(バリデーション)。
主な検証内容は以下の通り。
検証内容 意味 presecse: true そのカラムの値が存在するかどうかをチェックする length: {maximum: 文字数} 規定した文字数以上のデータは保存できないようにする uniqueness: true 重複したデータがデータベースに存在するかどうかチェックする マイグレーションファイルで使うコード
add_column :テーブル名, :カラム名, :データ型 ex) add_column :users, :image_name, :string既存のテーブルにカラムを追加する。changeメソッドの中に書く。
コマンドライン
rails new アプリ名
入力したアプリの名前と同名のフォルダが作成され、その中に開発に必要なフォルダやファイルが用意される。
rails server
サーバーの起動。
rails g controller コントローラ名 アクション名
コントローラの作成。gはgenerateでも可。
コントローラ名とアクション名は、HTTPメソッド(URL)と同じ名前を付けることが多い。
rails g model モデル名 カラム名:データ型
マイグレーションファイルとモデルファイルの作成。
モデル名は単数形かつ一文字目を大文字にする。
rails g migration ファイル名
マイグレーションファイルのみを作成。
ファイル名は add_image_name_to_users などの分かりやすい名前にする。
rails db:migrate
データベースへのマイグレーションファイルの反映。
マイグレーションファイルを作ったのにこれを実行しないとエラーが発生する。
まとめ
progateはよいです。
参考にした記事
参考にした書籍
- 投稿日:2020-06-21T00:12:15+09:00
Rubyでアルゴリズムを実装する:Day 4 -線形探索-
3日坊主を脱却して4日も続きました。
3日目はこちら<Rubyでアルゴリズムを実装する:Day 3 -二分探索->線形探索とは
ランダムなデータを最初から一つずつ比べて特定の値を見つけていく方法。
見つかったらそこで処理を終了する。
とーっても簡単なアルゴリズムだ
仕組みがわかったら即コーディングに移ろうlinerSearch.rb
コード
# 線形探索 def linerSearch(data, target) bool = -1 count = 0 while count <= data.length if data[count] == target bool = count break end count += 1 end bool end # 実行 print "格納する値:" data = gets.split().map(&:to_i) print "探す値:" target = gets.to_i search = linerSearch(data, target) if search >= 0 puts "#{target}は#{search+1}番目に見つかりました。" else puts "#{target}は見つかりませんでした。" endlinerSearchは数字が格納された配列と探す値を引数に取る。
配列の位置を示すcountが配列の大きさを超えるまでループ
データが見つかればboolにその時の配列の場所を代入してループ終了。
見つからなければboolは-1を返す。出力は見つかれば見つかった場所。見つからなければ見つからなかった旨を出力する。
最後に
やっぱり昨日にに比べたらとても簡単だった
ただ、イメージできているものをイメージ通りに実装するのって難しいなって思ったりもするさて、次回は再帰でもっとも有名なハノイの塔をやってみるか。。。