- 投稿日:2019-12-18T23:25:27+09:00
Railsで特定のURLからのリクエストを許可してiframeを表示させたい、、、
やりたいこと
Railsでiframeからのリクエストを許可して外部のサイトで表示させたい、、、
でもすべて許可するとなにかと怖いので特定のURLに絞りたい、、、ということで
Railsでは
response.headers['X-Frame-Options'] = 'SAMEORIGIN'が設定されていて自身のドメインしかiframeで表示出来ないそう
今回は特定のURLのみ許可したいので以下のように実装
class HogeController < ApplicationController after_action :allow_iframe, only: [:hoge] def hoge render "hoges/index", layout: false end private def allow_iframe url = "https://hoge.com" response.headers['X-Frame-Options'] = "ALLOW-FROM #{url}" response.headers['Content-Security-Policy'] = "frame-ancestors #{url}" end end注意点
なんと皆がよく使ってるであろうChrome、Safariなど多くのブラウザが
X-Frame-Options ALLOW-FROM
に対応していません1なので合わせて
Content-Security-Policy frame-ancestors
も記述してやりましょう2調べてみた感じIE以外対応してたので安心です3
効いてるか確認
Chrome DevToolsの
Network
パネルで確認できます
X-Frame-Options ALLOW-FROM
だけ設定してしまうと未対応ブラウザですべてのURLを許可する状態になってしまいます ↩
- 投稿日:2019-12-18T23:23:24+09:00
新規アプリケーションをGithubで管理する(Rails)
はじめに
Railsを使ってプログラミングの学習をしているのですが、新規アプリケーションを作成する際、Githubで管理する方法を忘れてしまうので、定着させる意味でも残したいと思います。
環境
OS: Mac
Rails 5.0.7.2
Ruby 2.5.1
MySQL
Github Desktop手順
1,新規アプリケーションを作成
ターミナル$ cd myapp #作成したいディレクトリへ移動 $ rails _5.0.7.2_ new my-blog -d mysql #Railsのバージョンを指定、DBはMySQLを指定する。 $ cd my-blog #作成したアプリケーションのディレクトリへ移動 $ rails db:create #データベース作成2,Github Desktopからローカルリポジトリにする
Github Desktop左上のCurrent Repository
をクリック→Add
→Add Existing Repository
を選択。Add Repositoryできない場合
ターミナルにて、作成したアプリのリポジトリ→git init
のコマンドを打てば解消されます。
左下から最初のコミットをする。
Publish repository
をクリック。
公開してもいいならKeep this code private
のチェックを外し、Publish Repository
をクリック。3,Githubのリモートリポジトリが作成される
- 投稿日:2019-12-18T22:37:32+09:00
オブジェクト指向設計を実践するためのまとめ
Zeals Advent Calendar 2019 の19日目の記事です。
サーバサイドエンジニアの小寺です。私がまだ初学者に近かった頃、「もっと早く知っておけばよかった!」と心の底から思ったオブジェクト指向設計についての知識を書き残しておこうと思います。
今回は、実務で求められる「保守性の高い読みやすいコード」を実践するために
オブジェクト指向設計実践ガイドを学習し、理解できたところを自分なりに要点をまとめ・メモし、備忘録としたものです。
(※4章までの内容が理解できれば十分実践していけると思いますので4章までしかまとめてないです)参考書籍
オブジェクト指向設計実践ガイド
本書のサンプルコードを引用しています。対象
- オブジェクト指向についてふんわり理解している人
- 責務がどうのこうのといったくだりをふんわり理解してる人
- オブジェクト指向設計を理解してステップアップしたい初学者
2章 単一責任を意識する
- なぜ単一責任が必要なのか
- 再利用したい
- 再利用できると未来の予期せぬ変更に対応できる
- 悪いコードを増殖させたくない
単一責任の見極め方
- 役割を聞く
# 「オブジェクト指向設計実践ガイド」 p41 class Gear attr_reader :chainring, :cog, :rim, :tire def initialize(chainring, cog, rim, tire) @chainring = chainring @cog = cog @rim = rim @tire = tire end : def gear_inches ratio * (rim + (tire * 2)) end endgear = Gear.new(52, 11, 26, 1.5) gear.ratio # ギアに対してギア比を聞くのは良い gear.gear_inches # ギアに対してgear_inchesを聞くのはしっくりこないような気がする gear.tire # ギアに対してタイヤのサイズを聞くのはもっとおかしいインスタンス変数の隠蔽
# 「オブジェクト指向設計実践ガイド」 p46 class Gear def initialize(chainring, cog) @chainring = chainring @cog = cog end def ratio @chainring / @cog.to_f # <-- 破滅への道 end endアクセサメソッド(attr_reader)で隠す
# 「オブジェクト指向設計実践ガイド」 p46 class Gear attr_reader :chainring, :cog # <-- def initialize : def ratio chainring / cog.to_f # <-- end end実装的には以下と同じ
def cog @cog end実際に得ている恩恵としては未来に複雑な実装が来た時に変更が簡単
def cog if 条件 @cog * 何かの計算 else (@cog * 何かの計算) + 複雑な計算 end endデータ構造の隠蔽
# 「オブジェクト指向設計実践ガイド」 p48 class ObscuringReferences attr_reader :data def initialize(data) @data = data end def diameters # 0はリム, 1はタイヤ <-- 何のデータがどこにあるか知っている状態 # 配列の構造が変わった時などに怖い data.collect {|cell| cell[0] + (cell[1] * 2) } end end # 以下のデータが必要になる @data = [[622, 20],[622, 23],...]Structクラスを使って構造を包み隠す
# 「オブジェクト指向設計実践ガイド」 p50 class ObscuringReferences attr_reader :wheels def initialize(data) @wheels = wheelify(data) end def diameters # wheelが rim と tire を持ってることだけ知っていればいい wheels.collect {|wheel| wheel.rim + (wheel.tire * 2) } end # これで誰でも wheel に rim/tire を送れる Wheel = Struct.new(:rim, :tire) def wheelify(data) data.collect {|cell| cell[0] + (cell[1] * 2) } end endメソッドから余計な責任を抽出する
メソッドもクラスと同じく単一の責任を持つべき(再利用が簡単になるので)
先ほどの diameters を見てみる# 「オブジェクト指向設計実践ガイド」 p52 class ObscuringReferences : def diameters # wheelsを繰り返し処理するのと、それぞれの直径を計算する # といった2つの責任を持っている wheels.collect {|wheel| wheel.rim + (wheel.tire * 2) } end : end上記のメソッドを簡単に変更できるようにしていく
# 「オブジェクト指向設計実践ガイド」 p52 class ObscuringReferences : def diameters # 直径を計算する処理を別にした wheels.collect {|wheel| diamete(wheel) } end # diameter を呼べるようになった(他で再利用可能になった) def diameter(wheel) wheel.rim + (wheel.tire * 2) end : end上記はよくある責任が複数あるわかりやすい例。
大抵はこれほど明確ではない。先ほどのGearクラスを思い出してみる。# 「オブジェクト指向設計実践ガイド」 p53 class Gear : # 何か不確定でのちにトラブルを起こしそうな気がする # このメソッドが2つ以上の責任を持ってしまっている def gear_inches ratio * rim + (tire * 2) end : endgear_inches に隠れている直径の計算を抽出してみる
# 「オブジェクト指向設計実践ガイド」 p53 class Gear : # Gearがgear_inchesの計算するのはよし def gear_inches ratio * diameter end # しかし、車輪の直径の計算までするのはおかしい def diameter rim + (tire * 2) end : endといったように、小さなリファクタリングではあるが Gear が持つべきでない責任が明らかになりました。
このように、あらゆるものを単一責任にしていくことで
- 隠蔽された性質を明らかにする
- 上記の例だと 「Gear は diameterを持つべきではない」
- コメントをする必要がない
- メソッドがコメントの役割を果たす
- 再利用を促進する
- 他のプログラマーは複製ではなく再利用をするようになる
- 他のクラスへの移動が簡単
- 小さなメソッドは簡単に動かすことができる
といった恩恵が受けれます
クラス内の余計な責任を隔離する
一旦全てを単一責任するとクラスのスコープが明白になってきます。
GearクラスにはいくつかのWheel(車輪)のような振る舞いが隠れていました。
ここで、新しくクラスを作ってもいいのですがそうなると変更のコストが大きいかもしれないのでコストを最小限にしつつGearの単一責任を保つようにします。# 「オブジェクト指向設計実践ガイド」 p55 class Gear attr_reader :chainring, :cog, :wheel def initialize(chainring, cog, rim, tire) @chainring = chainring @cog = cog @wheel = Wheel.new(rim, tire) end def ratio chainring / cog.to_f end def gear_inches ratio * wheel.diameter end # 隠れていた Wheel のような振る舞いを手に入れた # 書籍ではこの後追加要望があったタイミングで新しいクラスを定義する # 「あとで」決定できる力を取っておくことはアプリケーション開発において重要 Wheel = Struct.new(:rim, :tire) do def diameter rim + (tire * 2) end end end本書では、新しくアプリケーションに車輪に関する追加機能の要望が来たタイミングで Wheel クラスを新しく定義するよう書かれています。
アプリケーション開発において未来を予測することは困難なのでできる限り「あとで」決定できる力を残しておくのは非常に重要なことである3章 依存関係を管理する
依存関係を認識する
オブジェクトが次のものを知っている時、オブジェクト間には依存がある
- 他のクラスの名前を知っている
- GearはWheelというクラスが存在すると予想している
- self以外のメッセージの名前
- GearはWheelがdiameterに応答できると知っている
- メッセージが要求する引数
- Gearは Wheel.newするときに rim とtireが必要なことを知っている
- 引数の順番
- 最初が rim で 2番目が tire だと知っている
# 「オブジェクト指向設計実践ガイド」 p60 class Gear attr_reader :chainring, :cog, :rim, :tire def initialize(chainring, cog, rim, tire) @chainring = chainring @cog = cog # Wheel.newするときに rim とtireが必要なことを知っている @rim = rim @tire = tire end def ratio chainring / cog.to_f end def gear_inches # Wheelというクラスが存在すると予想している # Wheelがdiameterに応答できると知っている # 最初が rim で 2番目が tire だと知っている ratio * Wheel.new(rim, tire).diameter end : end class Wheel attr_reader :rim, :tire def initialize(rim, tire) @rim = rim @tire = tire end def diameter rim + (tire * 2) end : end依存が発生している時、依存が強固になりすぎると2つのオブジェクトは変更することが難しくなってくるので2つのオブジェクトがあたかも1つのオブジェクトとして振る舞うようになってしまう。
疎結合なコードを書く
依存を減らすための技法を紹介する
依存オブジェクトの注入
クラス内でインスタンスを生成するのではなく diameterを知っているオブジェクトを受け取るようにしておく
# 「オブジェクト指向設計実践ガイド」 p66 class Gear attr_reader :chainring, :cog, :wheel def initialize(chainring, cog, wheel) @chainring = chainring @cog = cog @wheel = wheel end def ratio chainring / cog.to_f end def gear_inches ratio * wheel.diameter end : end # Gearはdiameterを知っているオブジェクトを要求する Gear.new(52, 11, Wheel.new(26, 1.5)).gear_inches依存を隔離する
- インスタンス変数の作成の分離
- 制約が厳しく、オブジェクトを注入できないような場合
- クラス内で分離するしか方法がないとき
# 「オブジェクト指向設計実践ガイド」 p68 class Gear attr_reader :chainring, :cog, :rim, :tire def initialize(chainring, cog, rim, tire) @chainring = chainring @cog = cog # Gearはまだ知りすぎているが、gear_inchesの依存が減っている @rim = rim @tire = tire end : def gear_inches ratio * wheel.diameter end # wheel が呼ばれるまでインスタンスは作成されない def wheel @wheel || Wheel.new(rim, tire) end end
- 脆い外部メッセージを隔離する
- 外部への参照がクラスに埋め込まれていて、どれが変更されやすい時
# 「オブジェクト指向設計実践ガイド」 p70 def gear_inches # 恐ろしい計算 foo = some_intermedicate_result * wheel.diameter # 恐ろしい計算 end上記のように複雑な状況になった時にメソッドの中に依存が隠れてしまう。
そうした場合以下のように脆い部分を隔離する
# 「オブジェクト指向設計実践ガイド」 p71 def gear_inches # 恐ろしい計算 foo = some_intermedicate_result * diameter # 恐ろしい計算 end def diameter # もし、Wheelがdiameterに変更加えた場合副作用はここだけになる wheel.diameter end引数の順番への依存を取り除く
- 初期化の際にハッシュ使う
# 「オブジェクト指向設計実践ガイド」 p73 class Gear attr_reader :chainring, :cog, :wheel def initialize(args) @chainring = args[:chainring] @cog = args[:cog] @wheel = args[:wheel] end : end # 冗長にはなったが、引数の順番を知っていなくても大丈夫になった # ハッシュのkey名に依存はしているが、順番よりは健康的 Gear.new( chanring: 52, cog: 11, wheel: Wheel.new(26, 1.5) ).gear_inches安定性の高い引数をいくつか受け取って、安定性の低い引数はオプション引数で受け取るといった手法が多く用いられる
- 明示的にデフォルト値を設定する
# 「オブジェクト指向設計実践ガイド」 p74 def initialize(args) # 真偽値以外の場合はシンプルに || を使える @chainring = args[:chainring] || 40 @cog = args[:cog] || 18 @wheel = args[:wheel] end # 真偽値を使う or nilを設定する場合 def initialize(args) @chainring = args.fetch(:chainring, 40) @cog = args.fetch(:cog, 40) @wheel = args[:wheel] end
- 複数のパラメーターを用いた初期化を隔離する
- もし、引数を簡単に変更できないような状況下にあった場合ラッパークラスを定義して包み隠すようにする
# 「オブジェクト指向設計実践ガイド」 p77 # Gearがもし外部インターフェースの一部の場合(このクラスを変更できない場合) module SomeFramework class Gear attr_reader :chainring, :cog, :wheel def initialize(args) @chainring = args[:chainring] @cog = args[:cog] @wheel = args[:wheel] end : end end # 外部のインタフェースをラップし、自信を変更から守っている # 他のオブジェクトを生成することが目的のオブジェクトはファクトリーと呼ぶ module GearWrapper def self.gear(args) SomeFramework::Gear.new( args[:chainring], args[:cog], args[:wheel] ) end end GearWrapper.new( chanring: 52, cog: 11, wheel: Wheel.new(26, 1.5) ).gear_inchesこのテクニックは自分で変更がきかない外部のインターフェースに依存する場合に適している。
依存方向の管理
「自分より変更が少ないものに依存すべし」
4章 柔軟なインターフェースを作る
パブリックインターフェースとは
クラスのパブリックインターフェースを作り上げるメソッドは以下のような特性を備えている
- クラスの主要な責任を明らかにする
- 外部から実行されることが想定される
- 気まぐれに変更されない
- 他者がそこに依存しても安全
- テストで完全に文書化されている
パブリックインターフェースを見つける
「アプリケーション例: 自転車旅行会社」
ユースケースとして
「参加者は適切な難易度の、特定の日付の、自転車を借りられる旅行の一覧をみたい」見当をつけるためにシーケンス図を使う
オブジェクト間でやり取りされるメッセージを気軽に実験することができる
ここで起きる疑問
「Tripが利用可能な自転車まで調べなくてもいいんじゃないか?」シーケンス図を描くことによって
「このオブジェクトが〇〇という責任を負うべきなのだろうか?」と疑問が湧くようになるある旅行に対して自転車が利用可能かどうかTripクラスが見つけ出すべきでない場合、
Bycycleクラスがありそう。
- Tripはsuiable_tripsに責任があり
- Bycycleはsuitable_bycycle に責任を持つ
変更後は Tripから余計な責任は取り除けたものの Customer に移しただけ
変更後は Customer が何を望むのかと他のオブジェクトがどのようにそれを準備するのかまで知ってしまっている「どのように」を伝えるのではなく「何を」を頼む
新たなユースケース
「旅行が開始されるためには使われる自転車が全て整備されていることを確実にする」TripはMechanicがどのように整備するのか(
clean_bicycleして
pump_tiresして
lube_chainして
check_brakesするという手順
)を知ってしまっている。Mechanicが新たな整備手順を増やした時は、Tripも変更しなければならない
以下は対案
Tripにあった責任をほとんどMechanicに渡している
自転車の準備することに関して「どのように」はMechanicの責任になった
TripはMechanicにどんな改善があろうともprepare_bicycleから正しい振る舞いを得ることができる上記のようにMechanic と Trip の会話が 「どのように」から「何を」に変わった副作用として
Mechanicのパブリックインターフェースのサイズが小さくなったパブリックインターフェースが小さいということは他のところから依存されるメソッドがわずかしかないことを意味している
コンテキストの独立を模索
旅行の準備には「いつでも」自転車の準備が求められるためTripは「常に」prepare_bicycleメッセージを自身のMechanicへ送らなければならない
Tripが旅行が準備されることをMechanicに伝え、Mechanicは準備にbicyclesが必要なのでTripにコールバックをし、自転車の整備を行います。
こうして、整備士がどのように自転車を準備するかはMechanicクラスに隔離されました。
オブジェクトを見つけるためにメッセージを使う
Tripが知りすぎていたのを改善したが、今度はCustomerが知りすぎている。
上記のアプリケーションは要件を満たす新たなオブジェクトを必要としていることがなんとなく見えてきた。新たなTripFinderは安定したパブリックインターフェースを提供し、複雑な内部に関しては隠している
シーケンス図便利ー
まとめ
単一責任を意識しながらコードを書けるようになりましょう
- そのために常にメソッドの役割をメソッド自身に問い続ける
- 抽出できるものはできる限り抽出しておくと未来に変更が簡単になる
クラスの中で依存関係があることを認識する
- 状況に応じて疎結合にするための技法を使い分ける
オブジェクト間で交わされるメッセージを中心にアプリケーションを設計する
- 議論を進める時にはシーケンス図が有効
- どのようにではなくオブジェクトが「何を」要求するかに注目する
おわり
- 書籍の中に重要な説明がたくさんあるので本読みましょう(オブジェクト指向設計実践ガイド)
- 今回はあくまでコードベースで要点まとめただけなので
- 柔軟に進化し続けましょう
明日は20日目の @neuneu39 の番です。お楽しみに!!!
- 投稿日:2019-12-18T21:28:52+09:00
[Rails6]ActionTextの入力フォームが伸びて困る
はじめに
就活のポートフォリオサイトとしてAsobiというWebサイトを作成しました。(QiitaにAsobiに関しての記事を書いています。就活用ポートフォリオとしてWebサービス「Asobi」を作りました。)
このサイトの中でRails6から新しく導入されたActionText
を使っているのですが、入力フォームが行数に応じて伸びてしまいます。今回はそんなActionTextの入力フォームをいい感じにするためにやったことをご紹介します。
実行環境
- Ruby 2.6.5
- Rails 6.0.2
ActionTextとは
Rails6から実装されたリッチテキストコンテンツと編集機能を導入する機能です。
ActionTextのインストールと導入したいモデルとカラムの用意を行い、少しコードを書くだけでブログのようなリッチテキストエディタが導入できます。
Ajaxを利用した画像のアップロードも実装されており、アップロードされた画像は
ActiveStorage
を利用して保存されます。今回ActionTextの導入に関しては割愛させていただきます。
導入に関しては下記の記事が参考になるかと思います。Rails6新機能 ActionText使用方法
Rails 6 と Action text を使ってみる - もふもふ技術部伸びて困る
ActionTextで生成されるリッチテキストエディタの入力フォームは、行数に応じて高さが伸びるようになっています。
デフォルトの挙動
これをQiitaやはてなブログのような、はみ出た部分をスクロールバーで表示するように実装します。
こんな風な挙動にしたい
実装
Rails
側でこれを実装する方法を見つけられなかったため、CSS
で実装します。Trixについて
実装の前に、ActionTextに含まれる
Trix
について説明します。
Trix
とはリッチテキストエディタを実装するJavascriptのライブラリです。(ちなみに開発元はRuby on Railsと同じBasecampです)
Trixのドキュメントを見てみると、Place an empty <trix-editor></trix-editor> tag on the page. Trix will automatically insert a separate <trix-toolbar> before the editor.
Like an HTML <textarea>, <trix-editor> accepts autofocus and placeholder attributes. Unlike a <textarea>, <trix-editor> automatically expands vertically to fit its contents.とのことで、ページのTrixのエディタを入れたい場所に
trix-editor
タグを書き込むことで使えるようになります。ActionTextの場合、
erb
ファイルに<%= form.rich_text_area :body %>と記述することで
trix-editor
タグが生成されます。
そしてリッチテキストエリアへの入力はtrix-editor
タグの子要素にDOMとなって反映されていきます。なので、入力フォームのスタイルは
親要素であるtrix-editor
タグのCSSを記述することで調整できそうです。CSSの記述
入力フォームの高さを固定するために
min-height
とmax-height
を、そして入力フォームからはみ出した部分はoverflow-y: auto;
を設定することでスクロールバーで表示できるようにします。以下のCSSを記述します。(
actiontext.scss
が/app/assets
か/app/javascript
に生成されていると思うので、そこに追記することをオススメします)trix-editor { min-height: 20em; max-height: 20em; overflow-y: auto; }以上で目指す挙動の入力フォームになっていると思います。
まとめ
ActionTextは新しい機能のため中々情報が出てきません。もしかしたらActionTextの設定ファイルか何かでうまいことやれるかもしれないです。
何か直すべき点、間違った記述があればコメント等で指摘していただければと思います。
参考文献
- 投稿日:2019-12-18T20:51:59+09:00
未経験が受託企業に入って半年経ったので、学んだスキル全部書いてみる
はじめに
今年2019年6月にエンジニアとして晴れてキャリアをスタートさせました。
それから約半年間で本当にいろいろなことを学ばせて頂いたので、私ごとですが簡単に学んだことをまとめます。もちろんマスターなんてしておらず、全てが勉強中なうえ、なんならちょっとかじっただけのものまで書いてますので悪しからず。
一概に未経験といっても、自社開発/受託企業/SES企業、大手/メガベンチャー/スタートアップ、元IT業界/全くの素人、普段からパソコン触ってた/触ってない等いろいろなキャリアの始め方があると思うので、平均的な成長度合いなのかどうかは不明ですが、ほんの一例としてご参考ください。
■簡単な経歴
・良くも悪くもない普通の大卒
パソコンスキルはitunesで音楽入れる程度
・某アパレルチェーン企業で3年ほど勤務
内2年管理職
パソコンスキルはWordで毎週報告書を書く程度
・2018/12〜 Mac購入
某オンラインスクールで勉強開始
・2019/3〜 独学開始、都内もくもく会に週1参加、Menta契約、アプリ作成等
・2019/6〜 就職
・2019/12 現在0ヶ月〜半年で学んだこと
1)プログラミング技術
Ruby/Rails
ほぼ毎日触っていました。
基本的には既存のコードや他PJのコードをマネて書くことが多いですが、必ず意味を理解しながら次へ繋げています。
ただやはりまだまだ分からないことが多く、日々痛感しています。ここでいろいろ書蹴たらいいのですがキリがなさそうなので、また別の記事で書きます。
HTML/CSS
Railsに合わせてHTMLはslimで書いていました。
特にCSSの方は奥深すぎてほんとナメてました。めちゃくちゃ難しいです。
新システム開発時にいかに大勢のユーザーに"初めから"良い印象を持ってもらえるかと考えた時に、画面作成でかなり手こずりました。SQL
本番データをよく触らせて貰える案件だったので、抽出しなければならないことが多くありました。
入社前は全く勉強できてなかったのですが、基礎的なところからサブクエリの書き方、
inner join
left outer join
concat
あたりに触れ、
加えてlimit
をつけずクエリ重すぎて本番が落ちる等も経験できたので、とても良い経験()になりました。DB
上記と同じく、よく触らせて貰えました。
案件JOIN当初、railsdbから大事なレコードをそのまま物理削除してしまい、その直後5分後くらいに先方から怒りの電話がかかってきたのは良い思い出()です。mysqlコマンドやdump、トランザクションの仕組みなど知ることができました。
Git
入社前からもちろん触っていましたが、
git merge
やgit clone
等は正直あまりよく分かっていませんでした。現在でもまだそこまで幅広く扱えてないですが、
それでもgit add
前にgit status
やgit diff
で変更内容をちゃんと確認する習慣付けや、コンフリクト発生時の解消方法、git fetch
git stash
git reset --hard HEAD^
あたりを日常的に使えるようになりました。IDE操作
cloud9で開発していました(これ言うとよく社外の方に珍しいと言われます)。
https://qiita.com/shin1kt/items/03eed49c12104002a2c7
こちらにあるような様々なショートカットを教えて頂き、いち早く見たいファイルを引っ張り出したり、直前の操作を取り消す/戻すなど、格段にスピードが速くなりました(当たり前のことかも知れませんが)。
これを通してMacのショートカットキーを覚えたりすることも多かったです。
いずれはVScodeとか使って開発したいなという思いも密かにあります。テストの書き方
Rubyでminitestを書きました。
入社前はRSpecとともに中途半端な勉強で終わっていたので、実務を通して、調べながらであればなんとか書けるようになったかなと思います。
ただ担当PJのテスト管理が少し甘そうなので、しっかりテストを書いたら実際はどうなるのかな、と気になってはいます。セキュリティ対策
新システム開発の中でセキュリティ対策をしました。
Railsガイド等を参考に、
・総当たり攻撃・辞書攻撃対策
・CSRF対策
・セッションハイジャック対策
・個人情報保護(クライアントやログ)
・SSL化
・エラー文表記修正(「入力されたユーザー名は登録されていません」のような具体的なメッセージにしない等)
・パスワード強化
・リダイレクトとファイル対策
あたりを一通りやったかなと思います(まだまだあると思いますが)。今後はAWS側でのセキュリティ対策もしたいです。
Vim
いわゆるVimmerとは程遠いのですが、
本番サーバー内でdumpファイルを探したり、落ちた原因を探るためコードを直接いじってみたりして、黒い画面に抵抗がなくなる程度にはなりました。
cd
で(地道に)いろんなディレクトリに寄る旅をしたりしたので、普段全く触れないようなディレクトリやファイルがまだまだたくさんあるんだなーというのも実感できました。これで半ば強制的にLinuxの簡単な勉強にも繋がっていきました。
AWS
社内で勉強会を開催し、基礎システムの概要から掴んでみたり、PJごとにシステム構成を見ていったりしました。
ちなみに自分の案件はEC2を使わずLightsailというパッケージを使ってサーバーが構築されていたため少し特殊だったのですが、それも含め良い知見になりました。早いうちにこちらの資格取得まで結びつけたいです。。。
AWSソリューションズアーキテクトアソシエイトステージング/本番環境へのデプロイ
今でもそうですが毎週のようにリリース作業を行なっているので、抵抗がないのが怖いくらいの感覚になりました(おそらく異常なんですよね、これ)。
上記で述べたようにいろんな場面で何度もやらかして先輩方に迷惑をかけまくっているので、
今では ローカルでの確認/ステージング(テスト)環境での確認・打鍵/コマンドのテンプレ化・誤字脱字チェック 等をしっかり行なう習慣が(嫌でも)つきました。新システム作成〜システム構成〜リリース
AWSを触るということも含め、とても良い経験になりました。
DBは既存システムのものを共用したので当初「スキーマファイルはどこ??」「カラムはどうやって追加するの??」「ユーザーのパスワードが入ったかどうかってどこで確認できるの??」などパニックになったり、
まず初めにサーバーを立てようと既存サーバーのスナップショットを取ろうとしたらEC2すら使っておらず出鼻をくじかれたり、
ドメインとサブドメインの違いが分からずしばらくIPアドレスをそのまま打ち込んで画面を開いたり、
今となっては馬鹿らしいですが当時は本気で取り組んでいました。2)顧客対応
タスク/スケジュール管理
タスク量は多くマルチタスクになりがちなので、優先順位を決めたり、話が出たら漏れのないようにすぐメモしたり、不明点はすぐ聞くようにしました。
頻繁なチャットのやりとりも含め、1つのことのなかなか集中できない難しさを痛感しています。
うまくやるコツをぜひ教えて頂きたいです。工数見積もり
このタスクならだいたいこれくらいかかるだろうな、という予想を自分なりにするようにしていました。
それでもまだまだ多く工数を見積もってしまっているらしく、上司と答えあわせをするといつも大体半分くらいに修正されます泣とても難しいです。
見積書/請求書の書き方、渡し方
受託はお客様に納品して初めてお金を頂くので、書類もしっかりしたものを書く必要がありました。
渡す時期やお会いした時の渡すタイミング等にも気をつけたり、PDFでもデータ送信も行なったりしました(どこまでやるかはお客様にもよると思います)。肝心の月単位での工数計算に関しては、、、、まだまだなので、これから勉強です。
お客様を運用に乗せる気遣い/気配り
担当していた案件のお客様が、非IT/多くの部署や担当者がいる/レガシーな問題を抱えるお客様、ということも影響し、そもそも導入を嫌がっていたり、依頼したことを予定通りやってくれなかったりしました。
そのため、システムを入れるメリットや現状との具体的な比較、システムの使い方を実際に見せるなどしてお客様の抵抗を少しでも軽減できるように努めたり、食事をご一緒して少しでも親近感を持って頂けるようにしました。協力的なお客様であれば問題なかったと思うのですが、これはお会いするたびになかなか骨の折れる業務でした。
大人数に向けたシステム説明
結構大きなリリースを控えると、60名ほどのお客様の前で自分がシステムの説明をしました。
人前で説明するのはあまり緊張しない方なので助かりましたが、丁寧に説明できなかったりうまく伝えられなかった点があり、誰でも理解できる目線での話し方と内容にしないといけないと反省しました。
ただこれが、逆にベテランエンジニアだらけだった場合にどうなってたかと考えると、、、それも恐ろしいものです。
3)その他
基本的なPC操作
そもそも自分はPCに疎かったので、現役エンジニアに囲まれながら操作することでいろいろと技を盗めました。
ファイル形式の違いやデータ管理、ショートカットあたりでしょうか。タイピングスピードに関してはまだなかなかにひどいので、今後ブラインドタッチができるように頑張ります。
スプレッドシートの使い方
お客様と共有で使うものを中心に、スプレッドシートの使い方を覚えました。
ワードこそ前職で触っていましたが、エクセルは触ったことがなく今新たに使い方を覚えるのもちょっと億劫だったので、このタイミングでスプレッドシートだけでも使えるようになれて良かったです。
基本何にでも使えるので、やはり便利です。
チーム開発の流れ
Gitを中心に、ブランチをどのように管理するか、チーム全体でのコード管理はどうするか、プルリクをレビュー頂いたらどう修正して再レビュー頂くか、OKだったらどうマージするか、本番にはどのタイミングで反映させるか、ローカルの最新化はどうやるか、など一通り掴めました。
独学では単純に「コミット→プッシュ! コミット→プッシュ!」しかやってなかったので、現場で働かないとこれはなかなか掴めないですね。
分からない点の質問の仕方
初めは詰まったらすぐ質問してしまってましたが、まずは自分でひたすらググりまくって、それでもダメなら一体何がわからないのか質問内容をしっかり文章にまとめてから質問するようにしました。
質問内容をまとめているうちに、問題を俯瞰して見ることができたりググり方を変えたりすることに繋がったり、ググり方のコツを覚えるとだんだんとググり力も上がっていったりしたので、結構効果はあったと思います。
また周辺知識の定着度合いに比例して、「調べればわかりそう」と思うことも増えていきました。半年〜1年でやりたいこと
ここからはほんのメモ書き程度に、自分が今後の半年で学んでいきたいスキルをバーっと書いていきます。
先にこれ勉強すればいいのに、とかありましたら、ご指摘頂けると嬉しいです。フロント書きたい
ベタにvue.jsかreactあたり触れたいです。
、、、、の前にJSの基礎からしっかりやりたいですね。インフラを深く知る
AWSは基礎システムしかまだ触っていないので、よく聞くけどいまいち分かってないものから知見を深めていきたいです。
とりあえずセキュリティあたりが見れるようになると嬉しい。PLでチーム管理
責任を持って開発したり、スケジュール組んだりして、案件全体の流れを把握する経験がしたいです。
振られたタスクを必死でやってく、だけでは見えないところも多々あると思うので。デザイン
新システムをリリースした時に、まざまざとデザイン力のなさを感じました。
Web制作みたいなところの勉強をしたいです。
有名どころのデザインの本から数冊探して勉強してみます。IT基礎
いわゆるコンピューターリテラシーというものを全く知らないのはエンジニアとしてマズいと思うので、基礎の基礎であるITパスポートから基本情報の午前の出題範囲くらいは頭に入れておきたいです。
ググり力
マイナーな開発を調べている時に見る、英語の記事やドキュメントにまだまだ抵抗があります。
Qiita様や先人が書かれたブログ記事には日々お世話になっていますが、そろそろ幅広く読めるようになりたいです。
- 投稿日:2019-12-18T20:20:41+09:00
rails 部分テンプレート健忘禄
- 投稿日:2019-12-18T19:32:41+09:00
rails gするとDid you mean? deprecate_constantとエラーがでる
コントローラとビューを作成するためrails gすると下記のエラーが起きました。
備忘録としてまとめます。エラー文抜粋
r.rb:28:in `<class:ScaffoldControllerGenerator>' 10: from /Users/****/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/railties-5.0.7.2/lib/rails/generators/base.rb:168:in `hook_for' 9: from /Users/****/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/railties-5.0.7.2/lib/rails/generators/base.rb:168:in `each' 8: from /Users/****/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/railties-5.0.7.2/lib/rails/generators/base.rb:178:in `block in hook_for' 7: from /Users/****/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/railties-5.0.7.2/lib/rails/generators/base.rb:202:in `class_option' 6: from /Users/****/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/thor-1.0.0/lib/thor/base.rb:304:in `class_option' 5: from /Users/****/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/thor-1.0.0/lib/thor/base.rb:582:in `build_option' 4: from /Users/****/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/thor-1.0.0/lib/thor/base.rb:582:in `new' 3: from /Users/****/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/thor-1.0.0/lib/thor/parser/option.rb:11:in `initialize' 2: from /Users/****/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/thor-1.0.0/lib/thor/parser/argument.rb:24:in `initialize' 1: from /Users/****/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/thor-1.0.0/lib/thor/parser/option.rb:115:in `validate!' /Users/****/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/thor-1.0.0/lib/thor/parser/option.rb:140:in `validate_default_type!': undefined method `deprecation_warning' for Thor:Class (NoMethodError) Did you mean? deprecate_constant参考記事もなくて全く意味がわかりませんでしたが、
聞いたところGemfile.lockの下記の記載が原因らしいです。Gemfile.lockthor (1.0.0)thor (1.0.0)はrails newをした時にデフォルトで作られるため、thorのバージョンを変更します。
Gemfileの最終行に下記を記載します。
Gemfile.gem 'thor', '0.20.3'bundle installするために一度Gemfile.lockを削除します。
削除できたらbundle installを実行します。
bundle install後、Gemfile.lockのthorが「thor (0.20.3)」となっていることを確認します。
Gemfile.lockthor (0.20.3)確認できたらrails gができるようになっているはずです。
細かい原因が分かってなくて恐縮ですが、以上となります。
- 投稿日:2019-12-18T18:02:57+09:00
【Selenium】ここらへんラッパーしとくとスッキリするんじゃないか、ってクラス 【Ruby】
クラス宣言
require 'selenium-webdriver' require "rspec/expectations" class BaseTest TEST_STR = 'test' TEST_NUM = '100' TEST_YEAR = '2019' TEST_MONTH = '10' TEST_DAY = '10' include RSpec::Matchersデフォルトでテキスト入力したいときの定数を定義しておきます。
私はRSpec
のアサーションだけ使いたかったのでここでインクルードしてます。ウェブドライバー作成
def self.create_driver options = Selenium::WebDriver::Chrome::Options.new options.add_argument('--headless') #ヘッドレスモードで起動 options.add_argument('--window-size=3000,3000') #画面サイズを設定 driver = Selenium::WebDriver.for :chrome, options: options end driver = BaseTest::create_driver;ウェブドライバーをオプション付きで起動。
毎回オプション記述するのはめんどくさいので、ここにまとめておきます。
--headless
オプションは画面を表示しないで、動作します。たぶんこれ毎回オプションで指定するよね
--window-size
は自分の好きな画面サイズを入れといてください。
これを指定しないと、動作中にエラーが発生してスクショを取りたい時に、画面がスクロールされてると肝心の場面が写ってなかったりします。フォームにテキスト入力
def input_str(element, value = TEST_STR) element.clear element.send_keys value end def input_num(element, value = TEST_NUM) element.clear element.send_keys TEST_NUM end @name = driver.find_element(:id, 'name') @age = driver.find_element(:id, 'age') input_str(@name) input_num(@age, 20)取得したフォーム要素を引数に指定して、なにか文字を入力します。
入力するテキストにこだわりがないのならデフォルトの文字を入力します。数字系のバリデーションのために文字列入力と数字入力があります。
日付フォームに入力
def input_date(element, limit = true, y = TEST_YEAR, m = TEST_MONTH, d = TEST_DAY) if limit element.clear element.send_keys y element.send_keys m element.send_keys d else element.clear element.send_keys y element.send_keys(:tab) element.send_keys m element.send_keys d end end @date = driver.find_element(:id, data) input_date(@date, false)
limit
は、max属性がない日付フォームに対して入力する時に、falseを渡します。max属性がない日付フォームは、年が6桁まで入力できてしまうため、年を入力した後にtabキーを押すことで回避しています。
ドロップダウンメニューを選択
def select(element, value) select = Selenium::WebDriver::Support::Select.new(element) select.select_by(:value, value) end @fruit = driver.find_element(:id, fruit) select(@fruit, 1)ドロップダウンを選択するのは面倒なのでラップしておくと捗ります。
ラジオボタン、チェックボックスを選択
def input_radio(element) unless element.selected? element.send_keys(:space) end end def input_checkbox(element) unless element.selected? element.send_keys(:space) end end @sex = driver.find_element(:id, 'sex-man') @policy = driver.find_element(:id, 'policy') input_radio(sex) input_checkbox(policy)ラジオボタンとかチェックボックスを選択された状態にします。
すでに選択された状態で押すと非選択状態になってしまうので、selected?
で判定します。ラジオボタンやチェックボックスを押すときは
.click
しがちですが、.click
は要素が画面外にあると、動作してけないので、sene_keys(:space)
で選択します。配列のチェックボックスを受け取る
def input_checkboxs(elements, *values) elements.each do |checkbox| if values.include?(checkbox.attribute('value')) unless checkbox.selected? checkbox.send_keys(:space) end end end end @foods = diver.find_elements(:name, foods[]) input_checkboxs(@foods, 1, 3)関連する複数のチェックボックスをまとめて選択します。
find_element
じゃなくてfind_elements
で取得した要素を引数で渡すことに注意。要素はページごとにクラス変数でまとめて定義する
ラッパーではないですが、ページ内のすべての要素をクラス変数で最初に定義しておくと、変更が容易になります。
class UserEditTest < BaseInvestmentRegistTest @name = driver.find_element(:name, 'name') @id = driver.find_element(:id, 'id') @class = driver.find_element(:class, 'class') # xpathが変わったらここを変更するだけでok @xpath = driver.find_element(:xpass, '//*[@id="rso"]/div[2]/div/div[3]/div/div/div[1]/a/h3') def test_investment_regist #フォームに入力したり検証したりテスト失敗時にスクショを撮る
テストを
begin
~rescue
で囲んで例外が発生したらスクショを撮るようにしましょう。def test begin #フォームに入力したり検証したり rescue Exception => e puts e #テストに失敗したらスクショを撮る @driver.save_screenshot(SCREENSHOT_FILENAME) end endクラス全体
require 'selenium-webdriver' require "rspec/expectations" class BaseTest TEST_STR = 'test' TEST_NUM = '100' TEST_YEAR = '2019' TEST_MONTH = '10' TEST_DAY = '10' include RSpec::Matchers def self.create_driver options = Selenium::WebDriver::Chrome::Options.new options.add_argument('--headless') options.add_argument('--window-size=3000,3000') driver = Selenium::WebDriver.for :chrome, options: options end def input_text_str(element, value = TEST_STR) element.send_keys value end def input_text_num(element, value = TEST_NUM) element.send_keys value end def input_date(element, limit = true, y = TEST_YEAR, m = TEST_MONTH, d = TEST_DAY) if limit element.clear element.send_keys y element.send_keys m element.send_keys d else element.clear element.send_keys y element.send_keys(:tab) element.send_keys m element.send_keys d end end def select(element, value) select = Selenium::WebDriver::Support::Select.new(element) select.select_by(:value, value) end def input_checkboxs(elements, *values) elements.each do |checkbox| if values.include?(checkbox.attribute('value')) unless checkbox.selected? checkbox.send_keys(:space) end end end end def input_radio(element) unless element.selected? element.send_keys(:space) end end def input_checkbox(element) unless element.selected? element.send_keys(:space) end end end
- 投稿日:2019-12-18T17:58:51+09:00
Railsでセルフパスワード変更ページを作ってみた
はじめに
本記事は、社内の古くなっていたシステムをリニューアルした際の手順やノウハウをまとめたものになります。
また、当記事は以前の投稿記事「Ruby on Rails6.0の環境構築から新規プロジェクト作成まで(メモ)」で作成した環境を前提に作成していますので、参考にされる方はご注意ください。前提条件
- 開発環境はCloud9
- Linuxコマンドの使い方がわかる程度の力量
- Web開発は初学者クラスの力量(ruby on rails開発未経験/Progateのレッスンは修了済)
- HTML/CSSは難しい事はできないけど書いて読める程度の力量
- javascriptは難しい事はできないけど書いて読める程度の力量
参考記事
RubyのNet::SSHの使い方.リモートサーバー内でsuしたりrsyncするrubyスクリプトが作れるようになる【外部サイト】
Rails6プロジェクトの各種初期設定【外部サイト】
rails newするときによく使うオプションと、rails newした後によく行う設定
bootsnapについて調べてみた
Rails5.1ではAsset Pipeline捨てたほうがいいらしいので捨ててみた
application.html.erbのレイアウトの使い方と使わない方法【外部サイト】
yieldとcontent_forを使ってページ毎にタイトルを変更【外部サイト】
最新版で学ぶwebpack 4入門JavaScriptのモジュールバンドラ【外部サイト】
Rails 6+Webpacker開発環境をJS強者ががっつりセットアップしてみた(翻訳)【外部サイト】アプリケーションの概要
今回作成したアプリケーションは、Linux系サーバーOS上にあるユーザーのパスワードをSSH等でログインしなくても、Webページ上で変更できるにするためのものです。
「どんな時に使うの?」という話ですが、自社運用のメールサーバーをユーザー自身で定期的にパスワードを変更させたい時に使う事を想定しています。
仕様的には下記のような感じにしました。【ざっくり仕様】
- 入力された4つのパラメーター(ユーザーID、現行パスワード、新パスワード、新パスワード再確認)を元に対象となるサーバーへSSH接続してパスワード変更処理
- 正常終了された場合は、注意事項などが記載されたページを表示
- 異常終了された場合(パスワード間違いなど)は、再度元のページを表示して状況に応じて入力時の値を再設定
プロジェクト環境準備
今回はアプリケーション解説がメインのため、準備は簡単に流していきます。
まずは、プロジェクトに必要なディレクトリを作成しておきます。ディレクトリ作成$ mkdir -p PasswordChange/vendor/bundle/ $ tree PasswordChange PasswordChange └── vendor └── bundle 2 directories, 0 files次にRailsのインストールからbundleインストールまで実施します。
※手順の詳細は前回の記事を参照してください。
Ruby on Rails6.0の環境構築から新規プロジェクト作成まで(メモ)尚、今回のアプリケーションでは各種Action(Mail関連、データベース関連等)を利用しないため、Offにしてプロジェクトを作成します。
プロジェクト作成$ bundle exec rails new . --skip-action-mailer --skip-action-mailbox --skip-action-text --skip-active-record --skip-active-storage --skip-action-cable --skip-test --skip-bootsnap --skip-turbolinks --skip-sprockets --skip-coffee --skip-bundle
各オプション 説明 --skip-action-mailer action mailer のセットアップをスキップ --skip-action-mailbox action mailbox のセットアップをスキップ --skip-action-text action text のセットアップをスキップ --skip-active-record action record のセットアップをスキップ --skip-active-storage action storage のセットアップをスキップ --skip-action-cable action cable のセットアップをスキップ --skip-test Minitest のセットアップをスキップ --skip-bootsnap bootsnap のセットアップをスキップ --skip-turbolinks turbolinks のセットアップをスキップ --skip-sprockets sprockets のセットアップをスキップ --skip-coffee coffee のセットアップをスキップ --skip-bundle bundle のセットアップをスキップ 次に「webpacker」をインストールします
※オプションにそれらしいのがあったので最初はいけるかと思ったのですが、それだけでは不十分らしいので別途インストールwebpackerインストール$ bundle exec rails webpacker:install次に今回のアプリケーションで使用するgemを追加します。
今回追加するgemは「SSH接続用(net-ssh)」「通信確認用(net-ping)」「共通パラメーター設定用(settingslogic)」の3つになります。
※ちなみに、何故railsのインストール時に一緒に記述しないかというとGemfile上書き時に消えてしまったためです。Gemfile追記とインストール$ vim Genfile 【追記】 gem 'net-ping' gem 'net-ssh' gem 'settingslogic' $ bundle install次に今回作成予定の「controller」と「view」ファイルを作成します。
controllerとview作成$ bundle exec rails g controller users_password top_form complete create app/controllers/users_password_controller.rb route get 'users_password/top_form' get 'users_password/complete' invoke erb create app/views/users_password create app/views/users_password/top_form.html.erb create app/views/users_password/complete.html.erb invoke helper create app/helpers/users_password_helper.rb invoke assets invoke scss create app/assets/stylesheets/users_password.scss次に今回のアプリケーション用の「routes」設定します。
routes設定$ vim app/confing/routes.rb 【設定変更】 get 'complete' => 'users_password#complete' get 'top' => 'users_password#top_form' post 'top' => 'users_password#top' get '/' => 'users_password#top_form'「webpacker」関連の初期フォルダを作成します。
※フォルダ名はお好みでどうぞフォルダ作成$ mkdir -p app/javascript/src app/javascript/stylesheets「jquery」を「webpacker」にインストールします。
jqueryインストール$ yarn add jquery
environment.jsへ設定追記$ vim config/webpack/environment.js 【設定変更前】 const { environment } = require('@rails/webpacker') module.exports = environment 【設定変更後】 const { environment } = require('@rails/webpacker') const webpack = require('webpack') environment.plugins.prepend('Provide', new webpack.ProvidePlugin({ $: 'jquery/src/jquery', jQuery: 'jquery/src/jquery' }) ) module.exports = environmentapplication.jsへ設定追記$ vim app/javascript/packs/application.js 【追記】 require("jquery")「webpacker」でデフォルトエントリーポイント以外で、各ビュー単位のエントリーポイントを利用できるように設定を一部修正します。
※この設定をしないとコンパイルはされますが、エントリーポイント(CSS)の呼び出し(stylesheet_pack_tag)が上手くいきませんでした。
理由は正直よくわかっていません(;^ω^)。webpacker.ymlの修正$ vim config/webpacker.yml 【設定変更前】 extract_css: false 【設定変更後】 extract_css: true機能を実装
独自クラスの実装
まず、railsと直接関係ない「SSH接続パスワード変更」「共通パラメーター読込」の実装する準備をします。
フォルダと空ファイル追加$ mkdir -p app/lib $ touch app/lib/settings.rb app/lib/ssh_interactive.rb次に「SSH接続パスワード変更」の機能を追加します。
このクラスでは、デフォルト設定だと通常の「ID・パスワード」でSSH接続(※1)、オプションを付け足す事で「公開鍵認証」でSSH接続します。
パスワード変更部分は対話式で「passwd」コマンド実行となるため、環境によって出力メッセージが違う場合、変更(※2)が必要となります。
※1.実はcloud9環境で作成した関係で「公開鍵認証」はテストしましたが、「ID・パスワード」はテストできていなかったりします。
※2.「if data =~ /^(current) UNIX password/ then」等の部分が該当します。
SSH接続機能追加(ssh_interactive.rb)require 'bundler/setup' require 'rubygems' require 'net/ssh' require 'net/ping' class SshInteractive def initialize # SSH接続先アドレス @host = "127.0.0.1" # SSH接続用オプション設定 @option = { :port => 22 } end def set_host(host) @host = host end def set_port(port) @option[:port] = port end def set_publickey_auth(keyfile, passphrase) @option[:keys] = keyfile @option[:passphrase] = passphrase end def password_change(user_id, pass_old, pass_new, pass_verify) # パスワード追加 @option[:password] = pass_old unless @option.has_key?(:keys) && @option.has_key?(:passphrase) # コマンド設定 cmd = "passwd" # SSH接続 begin return -10 unless port_scan? #通信接続エラー Net::SSH.start(@host, user_id, @option) do |ssh| channel = ssh.open_channel do |ch| channel.request_pty do |ch, success| # ptyチェック return -11 unless success end channel.exec cmd do |ch, success| # コマンド送信と対話入力 return -12 unless success ch.on_data do |c, data| # メッセージ取り出し if data =~ /^\(current\) UNIX password/ then channel.send_data "#{pass_old}\n" #パスワードを送信する elsif data =~ /^.+Authentication token manipulation error/ then return -2 # パスワード間違い elsif data =~ /^BAD PASSWORD: The password fails the dictionary check/ then return -3 # 辞書攻撃チェック elsif data =~ /^New password/ then channel.send_data "#{pass_new}\n" #パスワードを送信する elsif data =~ /^Retype new/ then channel.send_data "#{pass_verify}\n" #パスワードを送信する elsif data =~ /^.+updated successfully/ then return 0 end end end end ssh.loop # SSHループ用 end return -99 # 予想しない終了 rescue return -1 # SSH接続エラー(ユーザーID間違い) end end private def port_scan? ping_tcp = Net::Ping::TCP.new(@host, @option[:port]) return ping_tcp.ping? end end次に「共通パラメーター読込」の機能を追加します。
このクラスでは、設定値やエラーメッセージ等を一元管理するために作成しています。
設定値は「config/application.yml」に記載します。共通パラメーター読込機能追加(settings.rb)class Settings < Settingslogic source "#{Rails.root}/config/application.yml" namespace Rails.env end共通パラメーター設定default: &default company: XXXXX system: XXXXXサーバー password: limit: 180日 alert: 30日 message: normal: パスワード変更が完了しました regular_access_err: パスワード変更処理を実施してください password_check_err: 古いパスワードと新しいパスワードが同じです password_verify_err: 新しいパスワードと新しいパスワード(確認)が一致しません password_terms_err: 複雑性を満たすパスワードになっていません password_lenght_err: パスワードの文字数が基準を満たしていません password_matchid_err: 新しいパスワードにユーザーIDと同じ文字列が含まれています authenticate_err: ユーザーID又はパスワードが間違っています password_nomatch_err: パスワードが間違っています dictionary_check_err: 辞書攻撃チェックに該当します connection_err: サーバーへの接続に失敗しています【管理者へ問い合わせてください】 unknown_err: 予期しないエラーが発生しました【管理者へ問い合わせてください】 production: <<: *default ssh_params: host: 000.000.000.000 port: 22 keys: /home/【ユーザー名】/.ssh/id_rsa passphrase: test development: <<: *default ssh_params: host: 000.000.000.000 port: 22 keys: /home/【ユーザー名】/.ssh/id_rsa passphrase: test test: <<: *default ssh_params: host: 000.000.000.000 port: 22 keys: /home/【ユーザー名】/.ssh/id_rsa passphrase: testWeb関連の機能作成(Controller)
さて、ここから本題のセルフパスワード変更ページを作成していきます。
まずは、パスワード変更処理前に完了画面に行けないようにするための設定を追加します。
before機能追加(application_controller.rb)class ApplicationController < ActionController::Base def authenticate_user if session[:user_id] == nil flash[:notice] = Settings.message.regular_access_err redirect_to("/top") end end end次にパスワード変更処理の機能追加します。
パスワード変更フォームでは、最初にパスワード入力チェックを実施します。
その後、問題なければ先程作成した「SSH接続機能」を呼び出してパスワード変更処理を実施します。
特に問題なく変更できれば完了画面へ飛ばして終了です。入力チェックエラーの場合は、フォーム画面に戻してエラーメッセージ「error_message」を表示します。
画面が切り替わったり、注視して欲しいメッセージを表示する場合は「flash」を使用しています。パスワード変更用(users_password_controller.rb)require 'ssh_interactive' require 'settings' class UsersPasswordController < ApplicationController before_action :authenticate_user, { only: [:complete]} def complete session[:user_id] = nil end def top_form end def top # パラメーター格納 data = { :user_id => params[:user_id], :pass_old => params[:password_old], :pass_new => params[:password_new], :pass_verify => params[:password_verify] } # 旧パスワードと新パスワード比較 if data[:pass_old] == data[:pass_new] data[:msg] = Settings.message.password_check_err error_msg(data) and return end # 新パスワード比較 if data[:pass_new] != data[:pass_verify] data[:msg] = Settings.message.password_verify_err error_msg(data) and return end # 複雑性を満たすパスワード確認 # 半角英小文1文字以上、半角大文字1文字以上、半角数字1文字以上、半角記号1文字以上 ! # $ % $ * + - / = @ ? if data[:pass_new] !~ /(?=.*?[a-z])(?=.*?[A-Z])(?=.*?\d)(?=.*?[!#$%&*+\-_\/=@\?])/ data[:msg] = Settings.message.password_terms_err error_msg(data) and return end # 文字数制限確認(8桁以上20桁以下) if data[:pass_new].length < 8 || data[:pass_new].length > 20 data[:msg] = Settings.message.password_lenght_err error_msg(data) and return end # パスワード内にユーザーIDが含まれているか if data[:pass_new] =~ /#{data[:user_id]}/ data[:msg] = Settings.message.password_matchid_err error_msg(data) and return end # SSHパスワード変更処理 ssh = SshInteractive.new ssh.set_host(Settings.ssh_params.host) ssh.set_port(Settings.ssh_params.port) ssh.set_publickey_auth(Settings.ssh_params.keys, Settings.ssh_params.passphrase) result = ssh.password_change(data[:user_id],data[:pass_old],data[:pass_new],data[:pass_verify]) # 結果判定 case result when 0 then # パスワード変更確認 flash[:notice] = Settings.message.normal session[:user_id] = data[:user_id] redirect_to("/complete") when -1 then data[:msg] = Settings.message.authenticate_err error_msg(data) when -2 then data[:msg] = Settings.message.password_nomatch_err error_msg(data) when -3 then data[:msg] = Settings.message.dictionary_check_err error_msg(data) when -10,-11,-12 then flash[:alert] = Settings.message.connection_err render("users_password/top_form") else flash[:alert] = Settings.message.unknown_err render("users_password/top_form") end end private def error_msg(**data) # 入力状態に戻してページ再表示 @error_message = data[:msg] @user_id = data[:user_id] @password_old = data[:pass_old] @password_new = data[:pass_new] @password_verify = data[:pass_verify] render("users_password/top_form") end endWeb関連の見た目部分作成
次に各ビューファイルを設定します。
共通画面(application.html.erb)<!DOCTYPE html> <html> <head> <title><%= content_for?(:html_title) ? yield(:html_title) : "パスワード変更システム" %></title> <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= javascript_pack_tag 'application' %> <%= stylesheet_pack_tag 'application' %> <%= yield(:html_head) %> </head> <body> <header> <div class="header-log"> <h1><%= Settings.company %></h1> </div> </header> <% if flash[:notice] %> <div class="flash"> <%= flash[:notice] %> </div> <% end %> <% if flash[:alert] %> <div class="flash"> <%= flash[:alert] %> </div> <% end %> <%= yield %> </body> </html>パスワード変更フォーム(top_form.html.erb)<% content_for :html_head do %> <%= stylesheet_pack_tag 'top_form' %> <% end %> <div class="main"> <div class="container"> <div class="heading">パスワード変更システム</div> <div class="form"> <div class="form-body"> <% if @error_message %> <div class="form-error"> <%= @error_message %> </div> <% end %> <%= form_tag("/top") do %> <p>ユーザーID</p> <input id="user_id" name="user_id" value="<%= @user_id %>" required max=10> <p>現行パスワード</p> <input type="password" id="password_old" name="password_old" value="<%= @password_old %>" required min=8 maxlength=20> <span class="field-icon"> <i toggle="password-field" class="fas fa-eye"></i> </span> <p>新しいパスワード</p> <input type="password" id="password_new" name="password_new" value="<%= @password_new %>" required min=8 maxlength=20> <p>新しいパスワード(確認)</p> <input type="password" id="password_verify" name="password_verify" value="<%= @password_verify %>" required min=8 maxlength=20> <input type="submit" value="変更"> <% end %> </div> </div> </div> <div class="container"> <div class="heading">システム説明</div> <div class="box"> <div class="desc-body"> <h2>概要</h2> <p>当システムでは、弊社で提供している<%= Settings.system %>のパスワード変更を実施できます。</p> <h2>パスワードポリシー</h2> <p>弊社提供の<%= Settings.system %>は下記ポリシーを適用しています。</p> <p>様々な脅威から情報資産を守るためにも、ポリシーを遵守いただくようお願い致します。</p> <ul> <li> 有効期限<%= Settings.password.limit %>(期限切れ利用不可) </li> <li> パスワード文字数は8~20文字以内 </li> <li> パスワード複雑性(下記を含む文字列) <ul> <li> 半角英小文字1文字以上(a-z) </li> <li> 半角英大文字1文字以上(A-Z) </li> <li> 半角数字1文字以上(0-9) </li> <li> 半角記号1文字以上<br/>利用可 ! # $ % & * + - _ / = @ ? </li> <li> ユーザーIDと同じ文字列禁止<br/>ユーザーID:taro<br/>新しいパスワード:Ka@1<span class="font_red">taro</span> </li> </ul> <li> その他 <ul> <li> 現行と類似したパスワードは使用しないでください。<br/>現行パスワード:Ka@isyai12<span class="font_red">3</span><br/>新しいパスワード:Ka@isya12<span class="font_red">4</span> </li> </ul> </li> </ul> </div> </div> </div> </div>完了画面<% content_for :html_head do %> <%= stylesheet_pack_tag 'complete' %> <% end %> <div class="main"> <div class="container"> <div class="heading">パスワード変更後の注意事項</div> <div class="box"> <div class="body"> <ul> <li> <p>お手数をおかけいたしますが、クライアント(パソコン等)側で設定されています<span class="font_red">パスワードの変更</span>をお願い致します。</p> </li> <li> <p>新しく設定したパスワードは忘れないように管理願います。</p> <p>万が一、パスワードがわからなくなってしまった場合は、申し訳ありませんが<span class="font_red">パスワードの再発行手続きのためにXXXの申請</span>をお願い致します。</span></p> </li> <li> <p>パスワードの有効期限は<%= Settings.password.limit %>です。</p> <p>有効期限切れ後は、<span class="font_red">パスワード忘れと同じ扱い</span>となります。</p> <p>尚、有効期限<%= Settings.password.alert %>前より毎日警告メールが発信されます。</p> </li> </ul> </div> </div> </div> </div>次にCSS関連の設定を追加していきます。
まずは、cssファイルとエントリーポイント登録を実施します。各種ファイルとエントリーポイント登録$ touch app/javascript/stylesheets/style.scss app/javascript/stylesheets/top_form.scss app/javascript/stylesheets/complete.scss $ touch app/javascript/packs/top_form.js app/javascript/packs/complete.js $ vim app/javascript/packs/application.js 【設定追加】 import '../stylesheets/style.scss' $ vim app/javascript/packs/top_form.js 【設定追加】 import '../stylesheets/top_form.scss' $ vim app/javascript/packs/complete.js 【設定追加】 import '../stylesheets/complete.scss'次にCSSファイルの設定を追加していきます。
全ビュー共通設定(style.scss)html { font: 100%/1.5 'Avenir Next', 'Hiragino Sans', sans-serif; line-height: 1.7; letter-spacing: 1px; } ul,li { list-style-type: none; } a { text-decoration: none; color: #2d3133; font-size: 14px; } h1, h2, h3, h4, h5, h6, p { margin: 0; } input { background-color: transparent; outline-width: 0; } form input[type="submit"] { border: none; cursor: pointer; } /* 共通レイアウト ================================ */ body { color: #2d3133; background-color: #3ecdc6; margin: 0; min-height: 1vh; } .main { position: absolute; top: 64px; width: 100%; height: auto; min-height: 100%; background-color: #f5f8fa; } .container { max-width: 600px; margin: 60px auto; padding-left: 15px; padding-right: 15px; clear: both; } /* ヘッダー ================================ */ header { height: 64px; position: absolute; z-index: 1; width: 100%; } .header-logo { float: left; padding-left: 20px; color: white; font-size: 22px; line-height: 64px; } /* フラッシュ ================================ */ .flash { padding: 10px 0; color: white; background: rgb(251, 170, 88); text-align: center; position: absolute; top: 64px; z-index: 10; width: 100%; border-radius: 0 0 2px 2px; font-size: 14px; } .font_red { color: red; }パスワード変更フォーム用(top_form.scss).heading { font-weight: 300; margin: 60px 0 20px; font-size: 48px; color: #bcc8d4; } .form { max-width: 600px; margin: 0 auto; background-color: white; box-shadow: 0 2px 6px #c1ced7; } .form-body { padding: 30px; } .form-error { color: #ff4d75; } .form input { width: 100%; border: 1px solid #d8dadf; padding: 10px; color: #57575f; font-size: 16px; letter-spacing: 2px; border-radius: 2px; box-sizing: border-box; } .form textarea { width: 100%; min-height: 110px; font-size: 16px; letter-spacing: 2px; } .form input[type="submit"] { background-color: #3ecdc6; color: white; cursor: pointer; font-weight: 300; width: 120px; border-radius: 2px; margin-top: 8px; margin-bottom: 0; float: right; } .form-body:after { content: ''; display: table; clear: both; } .box { max-width: 600px; margin: 0 auto; background-color: white; box-shadow: 0 2px 6px #c1ced7; } .desc-body { padding: 30px; } .box li { list-style-type: square; display: list-item; } h2 { position: relative; margin: 1.5em 0em; padding: 0.5em; background: #a6d3c8; color: white; } h2:before { position: absolute; margin-bottom: 1.0em; content: ''; top: 100%; left: 0; border: none; border-bottom: solid 15px transparent; border-right: solid 20px rgb(149, 158, 155); }完了画面用(complete.scss).heading { font-weight: 300; margin: 60px 0 20px; font-size: 36px; color: #bcc8d4; } .box { max-width: 600px; margin: 0 auto; background-color: white; box-shadow: 0 2px 6px #c1ced7; } .body { padding: 30px; } .box li { list-style-type: square; display: list-item; }後はどうでもいいおしゃれポイントとして「flash」が表示された後、
5秒後にフェードアウトするjsを追加します。javascriptファイル追加$ touch app/javascript/src/flash_message.js $ vim app/javascript/packs/application.js 【設定追加】 import '../src/flash_message.js'メッセージフェードアウト(flash_message.js)(function() { setTimeout("$('.flash').fadeOut('slow')", 5000) })これで一応作成完了です。
テスト機能とか本番環境用の設定とか色々ありますが、
それはまた別記事で紹介したいなと考えています。アプリケーション作成後の所感
題材選びに、失敗した失敗した失敗した失敗した失敗した
失敗した失敗した失敗した失敗した失敗した
失敗した失敗した失敗した失敗した失敗した
失敗した失敗した失敗した失敗した失敗したなんでオーソドックスなMVCモデルのアプリではなく、
DBを必要としない対話アプリを最初の題材に選んだのかというのが正直な感想。
※業務に関連する直近の課題をチョイスした結果なんですけどね。おまけに、無駄なこだわりを色々入れてみた結果、
工数1日で作成完了していたアプリケーションが工数10日(ほぼ調査)まで膨れ上がってしまった。これを見た初学者の皆様は題材選びと仕様設定には気を付けてください。
(おまけ)無駄にこだわって調べた点
- js、cssの管理をWebpackerで統一
- SSH接続部分は別クラスで実装したい
- 共通パラメーターの外部ファイル化
- css等のファイルをビュー単位で分割(applicationに書くのが嫌だった)
- 各ビューからhead内にCSSファイルを入れる
- プロジェクトで無駄に生成されるファイルを極力減らす
- 大してテストケースもないのに態々System Specを実装(別記事で紹介予定)
- 投稿日:2019-12-18T17:53:50+09:00
Ruby で DistIP,DistPort,SourceIP,SourcePort を指定して Tcp 通信を行うコード
きっかけ
通信の検証で5-tupleの影響を受けているのか確認する必要があった
5-tupleとは
IPヘッダに存在する5つの要素のことで
- 1.Source IP
- 送信元アドレス
- 2.Destination IP
- 宛先アドレス
- 3.Source Port
- 送信元ポート
- 4.Destination Port
- 宛先ポート
- 5.Protocol
- プロトコル
を指す
tcp.rbrequire 'socket' distIp = "111.111.111.111" distPort = 80 sourceIp = "222.222.222.222" sourcePort = 100 begin s = TCPSocket.open(distIp,distPort,sourceIp,sourcePort) s.print "GET / HTTP/1.0\r\n\r\n" print s.read ensure s.close if s endメモ
サーバに送信している部分で下記のヘッダー情報が必要かもしれないと思った
tcp.rbs.print "GET / HTTP/1.0\r\nUser-Agent: curl/7.19.7\r\nHost: localhost\r\nAccept: */*\r\nConnection: close\r\n\r\n"
- 投稿日:2019-12-18T17:44:48+09:00
Homebrewのruby-buildの更新を待たずにrbenvで最新版のRubyをインストールする方法
はじめに
Homebrewでrbenvとruby-buildの組み合わせでRubyをインストールしている場合は、以下のコマンドで最新版のインストールパッケージを取得することができる。
brew upgrade rbenv ruby-buildただし、最新のruby-buildがHomebrewに反映されるまでには数日かかることがあるため(※)、最新バージョンのRubyがリリースされてもすぐにインストールできないことがある。
※ 参考情報
ruby-buildの更新に時間がかかるのは、HomebrewへのPull request(以下PR)の作成とマージが必要になるから。
→ 例:ruby-build 20191205のPRHomebrewの更新を待たずにすぐにインストールしたい場合は、以下の手順で自力で最新版のインストールパッケージをセットアップすることができる。
手順1. ruby-buildのディレクトリへ移動する
ターミナルから以下のコマンドを入力してruby-buildのディレクトリへ移動する。
なお、下記コマンドの
20191205
の部分はローカルにインストールされているruby-buildのバージョンに応じて変わるので、適宜書き換えること。cd /usr/local/Cellar/ruby-build/20191205/share/ruby-build手順2. ruby-buildのPull requestを確認する
ruby-buildのcloseされたPRを確認し、目的のバージョンがすでに取り込まれていることを確認する。
https://github.com/rbenv/ruby-build/pulls?q=is%3Apr+is%3Aclosed
ここでは下記のPR(2.7.0-rc1を対象にしたもの)を例に取り上げる。
https://github.com/rbenv/ruby-build/pull/1389
手順3. PRで追加されたファイルをローカルに作成する
PRで追加されたファイルと同じ内容のファイルをローカルに作成する。
touch 2.7.0-rc1 vim 2.7.0-rc1追加したファイルの中身は以下のようになる。(2.7.0-rc1の場合)
install_package "openssl-1.1.1d" "https://www.openssl.org/source/openssl-1.1.1d.tar.gz#1e3a91bc1f9dfce01af26026f856e064eab4c8ee0a8f457b5ae30b40b8b711f2" mac_openssl --if has_broken_mac_openssl install_package "ruby-2.7.0-rc1" "https://cache.ruby-lang.org/pub/ruby/2.7/ruby-2.7.0-rc1.tar.bz2#1c5a02b63fa9fca37c41681bbbf20c55818a32315958c0a6c8f505943bfcb2d2" ldflags_dirs enable_shared standard verify_openssl手順4. インストールを実行する
ファイルを保存したら、インストール可能なバージョンが増えていることを確認する。
$ rbenv install -l | grep 2.7.0 2.7.0-dev 2.7.0-preview1 2.7.0-preview2 2.7.0-preview3 2.7.0-rc1 <= 新しく増えた jruby-9.2.7.0目的のバージョンが追加されていることを確認したら、インストールを実行する。
rbenv install 2.7.0-rc1まとめ
手順は以上です。もっといいやり方をご存じの方がいたらコメントをお願いします!
- 投稿日:2019-12-18T17:16:03+09:00
Windowsユーザーの私がrbenvでRubyのインストール
Windowsユーザーの私がrbenvでrubyのインストール
macと違い、windowsではPCにrbenvを直接インストールできない
準備として、windows内にubuntuという環境を作る必要がある。ubuntu環境は、windows内に作るもので、ここからの話は、windows内というよりは、windows内のubuntu内で起きることである。
(調べ方も当然、"ubuntu rbenv インストール"となる)
(注)rbenvだけだと、どうしてもmacのことばかり出てきてしまうので気をつける。しかし、PC自体の容量は空けとかないと、ここからのインストールの容量が足りないから、Windowsが無関係ってわけではない
(Windows内のubuntuが様々なものをインストールし、大きくなっていくから、ubuntuをかかえるWindowsの容量がないと、かかえきれない)https://mhaya18.hatenablog.com/entry/2018/09/01/090051
↑このサイトがおススメである。git clone https://github.com/rbenv/rbenv.git ~/.rbenv
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
~/.rbenv/bin/rbenv init
以上を入力して実行すると、
sudo apt install rbenv
ってやれば、rbenvをインストールできるよと教えてくれる。そこから言われた通り、
sudo apt install rbenv
でダウンロード途中でエラーすることがある
その際は前に述べたように、容量の確認
df -h
で確認できる。ちなみに自分はPC本体の容量がいっぱいいっぱいだった
:$ df -h
Filesystem Size Used Avail Use% Mounted on
rootfs 235G 235G 18M 100% /
none 235G 235G 18M 100% /dev
none 235G 235G 18M 100% /run
none 235G 235G 18M 100% /run/lock
none 235G 235G 18M 100% /run/shm
none 235G 235G 18M 100% /run/user
C: 235G 235G 18M 100% /mnt/c(このパターンは稀で、容量不足としては仮想環境に割り当てられるディスク容量が足りないことが多いんだとか)
データの削除をして、再びインストール実行
eval "$(rbenv init -)"
curl -fsSL https://github.com/rbenv/rbenv-installer/raw/master/bin/rbenv-doctor | bash
↑二つを実行し、確認できた。
その後、
メッセージででてきた手段でrubyインストール
↓
rbenv install 2.5.3rubyの構築が完了した
- 投稿日:2019-12-18T16:22:51+09:00
getsメソッドで入力された値を取得 ❏Ruby❏
Rubyで
gets
を読み込むとターミナルで入力待ちになります。
そこで入力された値を文字列として返します。実際にやっていきやす!
gets
だけだと勝手に改行されるので、それを防ぐために.chomp
を付けます。sample.rbword = gets.chomp puts wordターミナル$ ruby sample.rb ##入力待ち Hello World! ##適当に打ち込みenterを押すと… Hello World! ##結果が表示される※あくまで文字列を返す
gets
は数字を打ち込んでも文字列として返します。
数値として返したい場合は、.to_i
を付けます。sample.rbnumber = gets.to_i puts number + 3ターミナル$ ruby sample.rb ##入力待ち 5 ##適当に打ち込みenterを押すと… 8 ##結果が表示される ##numberには数値が入ったことがわかる。
【結論】
文字列を期待している場合は、gets.chomp
数値を期待している場合は、gets.to_i
を使う。
ではまた!
- 投稿日:2019-12-18T13:47:54+09:00
自分なりのテストの書き方 by RSpec
くふうカンパニーアドベントカレンダー20日目の記事です。
今年の新卒研修の中で自動テストの研修を担当してRSpecを使ったテストの書き方を説明しました。
しかしながら新卒エンジニア達からRSpecの使い方はわかったけれど、テストってどうやって書いていけばいいのと質問を受けたので私が普段どのようなステップを踏んでテストを書いているか振り返ってみることにしました。まだまだ不十分な研修で申し訳ない気持ちです?
使用するテストフレームワークは実務で利用しているRSpecです。新卒たちの質問
- そもそもテストって何をテストするの
- どこまでテストを書けばいいの
- どこから書き始めてばいいの
そもそもテストって何をテストするの
作っている機能が動作することを確認(テスト)します。
例えば名前とメールアドレスを登録できる機能があったとします。ユーザ目線ではフォームから名前とメールアドレス入力して確定すると、登録されたというメッセージと登録情報が画面に表示される機能となります。そして内部構造に注目するとデータの有効性を検証しデータベースに保存されて登録完了画面にリダイレクトする機能になります。
これらの機能が仕様通り振る舞いが正しく動作しているかテストで確認するようにします。
ユーザ目線で仕様通り動作するか確認するテストはE2EテストとしてSystem specで、開発者目線で動作しているメソッドの振る舞いを確認するテストはユニットテストとしてModel specやController specで書いています。
今回はとりあえずModel specを例に書きました。どこまでテスト書けばいいの
どの程度の品質基準を目指すのかはソフトウェアテストの基準を参考にしています。
例としてUserクラスに以下のようなメソッドがあったとします。
ユニットテストに関してはホワイトボックステストの条件網羅(C2)を目指して書いています。下のようにシンプルな処理の時は分岐網羅(C1)で留めているものもあったりしますが。。def able_to_send_mail? if name && email true else false end条件網羅の時は、nameとemailの両方がtrue、emailのみfalse、nameのみfalseの3つのテストケースのテストを書くようにします。nameがfalseであればemailの真偽は判定に影響しないのでnameとemailがfalseのケースは省略しています。条件を書くか省くか迷った場合は書いておけば漏れはないので問題はありません。
name name.present? email.present? if文の判定 ケース1 'bob' 'bob @mail.com' true true true ケース2 'bob' nil true false false ケース3 nil 'bob @mail.com' false true false 分岐網羅の時はこのようなコードがあった場合はif文がtureになるテストケースとfalseになるテストケースの2種類のテストを書くようにします。
name name.present? email.present? if文の判定 ケース1 'bob' 'bob @mail.com' true true true ケース2 nil 'bob @mail.com' false true false どこから書き始めればいいの
テストで網羅するテストケースはわかったので次は実際テストを順序立てて組み立てていきます。
テスト用のファイルが自動生成されていればそれを使って、なければ新しく作成します。テスト対象を書く
require 'rails_helper' RSpec.describe User, type: :model do describe '#able_to_send_mail?' do end # 他のメソッドがある場合は同じインデントで並べて書きます describe '他のメソッド' do end enddescribeでテスト対象のメソッド名を書きます。
他にもテスト対象のメソッドが追加された場合は同じインデントでテストを追加していきます。あり得るテストケースを書き出す
require 'rails_helper' RSpec.describe User, type: :model do describe '#able_to_send_mail?' do context 'nameとemailが存在する場合' do end context 'emailがnilの場合' do end context 'nameがnilの場合' do end end endcontextで先ほど網羅したメソッドの実行する時にあり得るテストケースを書き出します。コンテキストをネストして書くこともできますが、ネストが3重くらいになってくるような場合はメソッドの設計を見直すことを考えます。
日本語で書くか英語で書くかは全体で決まっていればどちらでもいいと思います。期待する結果を書き出す
require 'rails_helper' RSpec.describe User, type: :model do describe '#able_to_send_mail?' do context 'nameとemailが存在する場合' do it 'trueを返すこと' do end end context 'emailがnilの場合' do it 'falseを返すこと' do do end context 'nameがnilの場合' do it 'falseを返すこと' do end # 他にも期待する結果がある場合は書きます it '他の期待値' end end end enditを使って期待する結果を書き出します。
ここまで書けると後からテストを見た時にメソッドがどのような振る舞いをするかわかるようになってきました。テストコードを実装する
require 'rails_helper' RSpec.describe User, type: :model do describe '#able_to_send_mail?' do context 'nameとemailが存在する場合' do it 'trueを返すこと' do user = User.create(name: 'bob', email: 'bob@mail.com') expect(user.able_to_send_mail?).to eq true end end context 'emailがnilの場合' do it 'falseを返すこと' do user = User.create(name: 'bob', email: nil) expect(user.able_to_send_mail?).to eq false do end context 'nameがnilの場合' do it 'falseを返すこと' do user = User.create(name: nil, email: 'bob@mail.com') expect(user.able_to_send_mail?).to eq false end end end end期待する結果を検証するためのコードをexpectを使って書いていきます。
ここまでできれば一旦テストとしては完成です。テストのリファクタリング
require 'rails_helper' RSpec.describe User, type: :model do describe '#able_to_send_mail?' do subject { user.able_to_send_mail? } context 'nameとemailが存在する場合' do let(:user) { User.create(name: 'bob', email: 'bob@mail.com') } it { is_expected.to eq true } end end context 'emailがnilの場合' do let(:user) { User.create(name: 'bob', email: nil) } it { is_expected.to eq false } end context 'nameがnilの場合' do let(:user) { User.create(name: nil, email: 'bob@mail.com') } it { is_expected.to eq false } end end end重複を省いたり変数をletに書き出したりリファクタリングしました。
expect(user.able_to_send_mail?)
はsubjectでまとめて、ローカル変数だったuser
はletを使うようにしました。期待する値も真偽値のみなので自然言語で書かれた期待値は削除しました。
もっとも過度にリファクタリングしてコードの重複を省いた結果、コードを追うことが難しくなるようであれば後からメソッドの振る舞いを知るのが難しくなってしまうので、ある程度の重複は許容してもいいと考えています。まとめ
今回は私がどのように考えながらテストコードを書いていっているか振り返ってみました。
今年のフィードバックを糧に今後の新卒たちの研修を改善していきたいと思います。合わせて読みたい
第6回 Webアプリケーションのテスト
ホワイトボックステストにおけるカバレッジ(C0/C1/C2/MCC)について
【初心者向け】テストコードの方針を考える(何をテストすべきか?どんなテストを書くべきか?)
RSpecえかきうた
- 投稿日:2019-12-18T12:20:22+09:00
Rails6 のちょい足しな新機能を試す112(database.yml warning編)
はじめに
Rails 6 に追加された新機能を試す第112段。 今回は、
database.yml warning
編です。
Rails 6 では、複数データベースに対応していますが、database.yml
ファイルで複雑なERBが使われていると警告が出るようになっています。Ruby 2.6.5, Rails 6.0.1 で確認しました。 (Rails 6.0.0 でこの修正が入ったようです。)
$ rails --version Rails 6.0.1今回は、警告を出すために、 環境変数 DB によって、 MySQLデータベースかPostgreSQLデータベースにアクセスを切り変えるということで試してみたいと思います。
複数データベースの機能は使いません。Rails プロジェクトを作成する
$ rails new rails_sandbox $ cd rails_sandboxGemfile に mysql2 と pg を追加する。
MySQL と PostgreSQL どちらでも接続できるように、 Gemfile に mysql2 と pg を追加します。
Gemfilegem 'pg', '>= 0.18', '< 2.0' gem 'mysql2'bundle install を実行します。
$ bundle installdatabase.yml ファイルを編集する
database.yml
を変更します。MySQL 用の接続設定と PostgreSQL 用の接続設定を追加します。
環境変数によって、接続を切り変えられるようにします。
ここで、ポイントは、
<<: *<%= ENV.fetch("DB") { 'postgresql' } %>
となっている部分です。
この記述によって警告が出るようになります。config/database.ymlmysql: &mysql adapter: mysql2 encoding: utf8mb4 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: host: db_mysql postgresql: &postgresql adapter: postgresql encoding: unicode pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> host: db_postgresql user: <%= ENV.fetch("POSTGRES_USER") %> password: <%= ENV.fetch("POSTGRES_PASSWORD") %> development: <<: *<%= ENV.fetch("DB") { 'postgresql' } %> database: app_development ... test: <<: *<%= ENV.fetch("DB") { 'postgresql' } %> database: app_test ... production: <<: *<%= ENV.fetch("DB") { 'postgresql' } %> database: app_production ...スクリプトを作成する
確認のためだけなのですが、 database.yml を ERB でパースして結果を出力するスクリプトを書いておきます。
scripts/parse_database_yml.rbyml = ERB.new(File.read(Rails.root.join('config/database.yml'))) print yml.resultスクリプトを実行する
スクリプトを実行します。特にエラーもなくパースできています。
development:
の次の行が、<<: *postgresql
と処理できていることに注意してください。$ bin/rails runner scripts/parse_database_yml.rb Running via Spring preloader in process 104 mysql: &mysql adapter: mysql2 ... postgresql: &postgresql adapter: postgresql ... development: <<: *postgresql database: app_development ...db:create を実行する
db:create
を実行します。
Rails couldn't infer whether you are using multiple databases from your database.yml ... please simplify your ERB.
と警告が出ます$ bin/rails db:create Rails couldn't infer whether you are using multiple databases from your database.yml and can't generate the tasks for the non-primary databases. If you'd like to use this feature, please simplify your ERB. Created database 'app_development' Created database 'app_test'なぜ警告が出るのか
複数データベースに対応するために、 Rails は、 boot 完了する前に、
database.yml
を読んで、task を生成しているそうです。task 生成する時点では、 ERB を使っておらず DummyCompiler を使って解析しており、この DummyCompiler では処理できない場合に警告を出すようにしたということらしいです。どうすれば良いのか
結論としては、ERB の部分を警告が出ないように書き変えれば良いということになります。
では、一般的にどう書き変えれば良いのかは、筆者にもわかっていません。ワーニングを出ないように修正する
今回の場合は、以下のように書き変えることで警告が出ないようにすることができました。
default
を追加してif
文使って切り変えるようにして、development
などの環境では、<< *default
を使って ERB を使わないように修正しました。config/database.ymlmysql: &mysql adapter: mysql2 encoding: utf8mb4 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: host: db_mysql postgresql: &postgresql adapter: postgresql encoding: unicode pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> host: db_postgresql user: <%= ENV.fetch("POSTGRES_USER") %> password: <%= ENV.fetch("POSTGRES_PASSWORD") %> default: &default <% database = ENV.fetch("DB"){ 'postgresql' } %> <% if database == 'postgresql' %> <<: *postgresql <% else %> <<: *mysql <% end %> development: <<: *default database: app_development ... test: <<: *default database: app_test ... production: <<: *default database: app_production ...試したソース
試したソースは以下にあります。
https://github.com/suketa/rails_sandbox/tree/try112_warn_database_yml - 警告が出る方
https://github.com/suketa/rails_sandbox/tree/try112_warn_database_yml_fix - 警告が出ないように修正した方参考情報
- 投稿日:2019-12-18T11:41:34+09:00
.present?はnilかそうじゃないか、という場合は不要
キャンペーンコードから適用されるキャンペーンとプランに紐づくキャンペーンがあるんだけど、キャンペーンコードからのキャンペーンを配列の一番最初に表示させたかった時の実装。
辞書型(hash型)なのが注意。
key
でキャンペーンコードからのものを探して一旦削除してunshift
で先頭にpushしてるんだけどいまいち納得いってない。。もっといい方法ないのかな?修正前
order_base_decorator.rbdef order_campaigns c = options['campaigns'].to_a cc = c.find{|x| x["key"] == "campaign-code"} c.delete(cc) if cc.present? c.unshift(cc) if cc.present? if c.present? unless c.first == {} || cc return c.unshift({}).to_a end return c end return c end修正後
order_base_decorator.rbdef order_campaigns c = options['campaigns'].to_a cc = c.find{|x| x["key"] == "campaign-code"} # cのなかで見つからなかったらccはnil if cc c.delete(cc) c.unshift(cc) end if c.present? unless c.first == {} || cc return c.unshift({}).to_a end return c end return c end
- 投稿日:2019-12-18T09:55:11+09:00
【Ruby】NaNのNaNa不思議
これは Ruby Advent Calendar 2019の18日目の記事です。Otemachi.rb #17 - connpassで話した内容の大幅加筆修正版です。
実用性は皆無ですので、求めている方はブラウザバックしてください。
NaN (Not a Number) とは
みなさん NaN ってご存知でしょうか?
NaNとは Not a Number の略で、日本語では非数といいます。主に浮動小数点演算の結果が不定形だったり未定義だったりするときに返ってきます。
constant Float::NAN (Ruby 2.6.0 リファレンスマニュアル)
その数がNaNであるかどうかは、
Float#nan?
で調べることができます。NaNの仕様はIEEE 754で規定されているので、大抵のプログラミング言語には実装されています。
JavaScriptだとたまに見かけますが、RubyだとNaNを返すのではなく例外を発生させるケースが多い1ため、ほぼ見かけないですね。
NaNが返ってくるパターン
他にもあったらコメント欄などで教えて下さい。
Float::NAN
を代入するirb(main):001:0> nan = Float::NAN # => NaNそりゃそうだ
NaNと四則演算をする
Float::NAN + 1 # => NaN Float::NAN - 1 # => NaN Float::NAN * 1 # => NaN Float::NAN / 1 # => NaN上記のような四則演算に限らず、NaNと計算した結果はNaNになります。
つまり、NaNは感染します。
浮動小数点で不定形を計算する
「不定形」という言葉の意味、高校で理系数学をやった方はわかると思いますが、これを計算しようとするとNaNになります。
0/0.0
0/0.0 # => NaNこうなるのは浮動小数点数だけで、整数で
0/0
とするとZeroDivisionError
が返ってきます。この違いは、両者がもつ意味の違いではないかと思います。
整数の
0
は正確に0である一方で、浮動小数点数でx = 0.0
とすると0 <= x < 0.05
を満たす数を表す、という意味をもつ、という違いがあります。
0/0.0
はゼロ除算ではないのかもしれないけど、値を計算できないから非数となるのですね。
INFINITY/INFINITY
Float::INFINITY / Float::INFINITY # => NaN
Float
クラスの中では「無限大」を表すFloat::INFINITY
が定義されています。ただ無限大を無限大で割ってもどの値に収束するかわかりませんね。これもNaNです。
INFINITY - INFINITY
Float::INFINITY - Float::INFINITY # => NaNこちらもNaNになります。
NaNのNaNa不思議
ここからは、NaNのNaNa不思議、もとい変わった性質や挙動を見ていきましょう。
1. 自分どうしを
==
で比較するとfalse
が返ってくるFloat::NAN == Float::NAN # => falseRubyでこの挙動をする唯一の値ではないでしょうか。
これを利用した問題が、RubyKaigi 2019のエムスリーさんのブースで出題されてました。
難読Rubyコードクイズ問題と解説 in RubyKaigi 2019 - エムスリーテックブログ(Day3-3)
2.
truthy
であるFloat::NAN ? 'hoge' : 'piyo' # => "hoge" !!Float::NAN # => true「
nil
とfalse
以外はすべてtruthy
」というRubyのルールにしたがって、NaNもtrue
と判定されます。これはそこまで変わった性質じゃないですが、決して比較できない値がきっちりtrueと判定されるというのはなんだか不思議です。
3. 複素数に変換できる
Float::NAN.to_c # => (NaN+0i)お役所仕事という感じがします。
4. 複素数どうしで
0.0/0.0
をすると実部も虚部もNaNになるComplex(0.0, 0.0) / Complex(0.0, 0.0) # => (NaN+NaN*i)複素数つながりでもう一個。
複素数の割り算は分母の共役複素数を分母分子にかけて分母を実数にする、と教わりましたが、分母のゼロになにをかけてもやっぱりゼロです。
5. 数値なのにsingletonではない
前提として、 Rubyの数値はsingletonです2。
n = 1 m = 2 n.object_id == (m-1).object_id # => true計算しても、同じ値なら同じオブジェクトになります。
0xff.object_id == 255.object_id # => true (1.2e-0).object_id == (0.12*10).object_id # => trueこのように、表記が違ったり整数でなかったりしても同じ数値は同じオブジェクトです。処理系全体で同じ数値のオブジェクトは1つしか存在しないんですね。
でも、NaNの場合は違います。NaNに1足してもNaNですが、
nan.object_id == (nan+1).object_id # => falseやっぱり一味違いますね。
6. ハッシュのキーになることができるが、値が取り出せなくなることがある
h = {} h[:a] = 1 h[:a] # => 1 h[0/0.0] = 2 h[0/0.0] # => nilこうなると、普通の方法ではこの「2」は取り出せません。
取り出す方法は色々あると思いますが、一例。
h.to_a # => [[:a, 1], [NaN, 2]] h.to_a[1][1] # => 2ただし、
Float::NAN
をキーに使った場合や変数にNaNを入れた場合はちゃんと取り出すことができます。h = {} h[Float::NAN] = 3 h[Float::NAN] # => 3 n = 0/0.0 h[n] = 4 h[n] # => 4ハッシュのキーを比較するときには
Float#eql?
メソッドが使われているはず(参考: るりまのObject#eql?)なのですが、前述の通り NaNとの比較は無条件でfalseを返す ことになっている3ので、謎です。見た感じオブジェクトIDを比較している、つまり
BasicObject#equal?
で比較しているのですが、それらしいソースコードは見つけられず……なお、出典はこちらの記事です。
7. 配列に入れると
==
でtrue
を返すFloat::NAN == Float::NAN # => false [Float::NAN] == [Float::NAN] # => true配列以外のコンテナでも同様の挙動をします。
{ Float::NAN => 0 } == { Float::NAN => 0 } # => trueこちらも上記同様に
BasicObject#equal?
で比較していると思われる怪奇現象です。情報求む。なお、出典はこちらの記事です。
`Float::NAN` についての重箱の隅 - Qiita
まとめ
NaNの挙動はよくわかりません。
それではみなさん、良いお年を。
バージョン情報
$ ruby -v ruby 2.6.5p114 (2019-10-01 revision 67812) [x86_64-darwin19]参考文献
- NaN - Wikipedia
- 754-2008 - IEEE Standard for Floating-Point Arithmetic - IEEE Standard
- 難読Rubyコードクイズ問題と解説 in RubyKaigi 2019 - エムスリーテックブログ
- instance method Object#eql? (Ruby 2.6.0 リファレンスマニュアル)
- HashのキーをNaNにすると何が起きるか - Qiita
- `Float::NAN` についての重箱の隅 - Qiita
整数どうしのゼロ除算
0/0
や、負数の平方根Math.sqrt(-1)
など ↩ruby/numeric.c at master · ruby/rubyを読んでみましたが、該当部分を見つけられませんでした。。。 ので、正確な出典はありません。ご存じの方、教えて下さい ↩
- 投稿日:2019-12-18T02:38:15+09:00
Rails6でRSpecのコントローラテストをしようとするとエラーが発生する
はじめに
Everyday Rails - RSpecによるRailsテスト入門を見ながら、
勉強がてらアプリにRSpecを追加していましたが、
Rails6特有のエラーが発生しましたのでご紹介します。2019/12/18追記
Everyday Rails - RSpecによるRailsテスト入門 の著者の一人である、
@jnchito さんがRails6でRSpecを使用する際の変更点を挙げられた記事があります。Rails6でRSpecを使用する際の変更点が網羅されていますので、
ぜひご参照ください。【動画付き】Everyday RailsのサンプルアプリをRails 6で動かす際に必要なテストコードの変更点 - give IT a try
開発環境のバージョン
$ ruby -v ruby 2.6.4p104 (2019-08-28 revision 67798) [x86_64-darwin18] $ rails -v Rails 6.0.2
rspec-rails 3.9.0
を使用していました。発生したエラー
下記のようなコントローラのスペックファイルを書いて実行すると、
警告とエラーが発生しました。spec/controllers/static_pages_controller_spec.rbdescribe '#home' do it "正常なレスポンスを返す" do get :home expect(response).to be_success end end$ rspec spec/controllers/static_pages_controller_spec.rb StaticPagesController #home DEPRECATION WARNING: formats is deprecated and will be removed from Rails 6.1 (called from block (3 levels) in <top (required)> at /Users/amatsuki/programing/portfolio/study_roadmap/spec/controllers/static_pages_controller_spec.rb:6) DEPRECATION WARNING: ActionView::Template#initialize requires a locals parameter (called from block (3 levels) in <top (required)> at /Users/amatsuki/programing/portfolio/study_roadmap/spec/controllers/static_pages_controller_spec.rb:6) DEPRECATION WARNING: formats is deprecated and will be removed from Rails 6.1 (called from block (3 levels) in <top (required)> at /Users/amatsuki/programing/portfolio/study_roadmap/spec/controllers/static_pages_controller_spec.rb:6) DEPRECATION WARNING: ActionView::Template#initialize requires a locals parameter (called from block (3 levels) in <top (required)> at /Users/amatsuki/programing/portfolio/study_roadmap/spec/controllers/static_pages_controller_spec.rb:6) DEPRECATION WARNING: formats is deprecated and will be removed from Rails 6.1 (called from block (3 levels) in <top (required)> at /Users/amatsuki/programing/portfolio/study_roadmap/spec/controllers/static_pages_controller_spec.rb:6) DEPRECATION WARNING: ActionView::Template#initialize requires a locals parameter (called from block (3 levels) in <top (required)> at /Users/amatsuki/programing/portfolio/study_roadmap/spec/controllers/static_pages_controller_spec.rb:6) 正常なレスポンスを返す (FAILED - 1) Failures: 1) StaticPagesController#home 正常なレスポンスを返す Failure/Error: get :home ActionView::Template::Error: wrong number of arguments (given 2, expected 1) # ./spec/controllers/static_pages_controller_spec.rb:6:in `block (3 levels) in <top (required)>' # ------------------ # --- Caused by: --- # ArgumentError: # wrong number of arguments (given 2, expected 1) # ./spec/controllers/static_pages_controller_spec.rb:6:in `block (3 levels) in <top (required)>'原因
Rails6でのRSpecのバグのようです。
Issuesにもいくつか挙げられていました。github/rails - Warning formats is deprecated and will be removed from Rails 6.1
github/rspec-rails - Warning formats is deprecated and will be removed from Rails 6.1
github/rspec-rails - Changes In Rails 6 ActionView::Template Breaks EmptyTemplateHandler
rails-rspecのバージョンを4.0.0.beta3にする
先ほどのリンクで指摘されていたように、
rails-rspecのバージョンを4.0.0.beta2
にあげます。まずはコマンドでインストールできるバージョンの中に
4.0.0.beta2
があるか、
他に最新のバージョンはあるか確認します。
下記のgem query
はローカルまたはリモートリポジトリのgemの情報を取得するものです。$ gem query -rae --prerelease 'rspec-rails'今回使用したオプションの意味は下記のようになっています。
オプション 意味 -r リモートに指定 -a 全てのgemのバージョンを表示 -e 完全一致したgemを指定 --prerelease プレリリースバージョンも表示 gemコマンドの詳細はrubygems.orgのページを参照してください。
rubygems - Guidesgem queryコマンド実行
コマンドを実行してみると、
4.0.0.beta2
があることを確認できました。さらに、
4.0.0.beta3
があることも確認できました。(2019/12/18現在)
バージョンは高い方がよしとされていますので、今回は4.0.0.beta3
の方をインストールします。$ gem query -rae --prerelease 'rspec-rails' *** REMOTE GEMS *** rspec-rails (4.0.0.beta3, 4.0.0.beta2, 3.9.0, 3.8.3, 3.8.2, 3.8.1, 3.8.0, 3.7.2, 3.7.1, 3.7.0, 3.6.1, 3.6.0, 3.6.0.beta2, 3.6.0.beta1, 3.5.2, 3.5.1, 3.5.0, 3.5.0.beta4, 3.5.0.beta3, 3.5.0.beta2, 3.5.0.beta1, 3.4.2, 3.4.1, 3.4.0, 3.3.3, 3.3.2, 3.3.1, 3.3.0, 3.2.3, 3.2.2, 3.2.1, 3.2.0, 3.1.0, 3.0.2, 3.0.1, 3.0.0, 3.0.0.rc1, 3.0.0.beta2, 3.0.0.beta1, 2.99.0, 2.99.0.rc1, 2.99.0.beta2, 2.99.0.beta1, 2.14.2, 2.14.1, 2.14.0, 2.14.0.rc1, 2.13.2, 2.13.1, 2.13.0, 2.12.2, 2.12.1, 2.12.0, 2.11.4, 2.11.0, 2.10.1, 2.10.0, 2.9.0, 2.9.0.rc2, 2.8.1, 2.8.0, 2.8.0.rc2, 2.8.0.rc1, 2.7.0, 2.7.0.rc1, 2.6.1, 2.6.1.beta1, 2.6.0, 2.6.0.rc6, 2.6.0.rc4, 2.6.0.rc2, 2.5.0, 2.4.1, 2.4.0, 2.3.1, 2.3.0, 2.2.1, 2.2.0, 2.1.0, 2.0.1, 2.0.0, 2.0.0.rc, 2.0.0.beta.22, 2.0.0.beta.20, 2.0.0.beta.19, 2.0.0.beta.18, 2.0.0.beta.17, 2.0.0.beta.16, 2.0.0.beta.15, 2.0.0.beta.14.2, 2.0.0.beta.14.1, 2.0.0.beta.13, 2.0.0.beta.12, 2.0.0.beta.11, 2.0.0.beta.10, 2.0.0.beta.9.1, 2.0.0.beta.8, 2.0.0.beta.7, 2.0.0.beta.6, 2.0.0.beta.5, 2.0.0.beta.4, 2.0.0.beta.3, 2.0.0.beta.2, 2.0.0.beta.1, 2.0.0.a10, 2.0.0.a9, 2.0.0.a8, 2.0.0.a7, 2.0.0.a6, 2.0.0.a5, 2.0.0.a4, 2.0.0.a3, 2.0.0.a2, 1.3.4, 1.3.3, 1.3.3.rc, 1.3.2, 1.3.1, 1.3.0, 1.2.9, 1.2.7.1, 1.2.7, 1.2.6, 1.2.5, 1.2.4, 1.2.3, 1.2.2, 1.2.1, 1.2.0, 1.1.12, 1.1.11, 1.1.10, 1.1.9, 1.1.8, 1.1.7, 1.1.6, 1.1.5)bundleでインストール
bundleを使用しているので
Gemfile
を下記のように修正します。Gemfilegem 'rspec-rails', '4.0.0.beta3'そして、インストールします。
$ bundle
再度テストを実行
めでたしめでたし、、、と思いきやまたもやエラーが発生。。。
$ rspec spec/controllers/static_pages_controller_spec.rb StaticPagesController #home 正常なレスポンスを返す (FAILED - 1) Failures: 1) StaticPagesController#home 正常なレスポンスを返す Failure/Error: expect(response).to be_success expected #<ActionDispatch::TestResponse:0x00007ff5c5326f78 @mon_mutex=#<Thread::Mutex:0x00007ff5c5326be0>, @mo...ders:0x00007ff5c536d4f0 @req=#<ActionController::TestRequest:0x00007ff5c5327310 ...>>, @variant=[]>> to respond to `success?` # ./spec/controllers/static_pages_controller_spec.rb:7:in `block (3 levels) in <top (required)>' Finished in 0.11472 seconds (files took 5.48 seconds to load) 1 example, 1 failure Failed examples: rspec ./spec/controllers/static_pages_controller_spec.rb:5 # StaticPagesController#home 正常なレスポンスを返すRails6でbe_successメソッドは使えない
Rails6からは
be_success
メソッドは使用できなくなっているようです。。
be_success
をbe_successful
に置き換えるようにしている記事がいくつかありました。Rails 5.2 betaを使ったRSpec 実行時に be_success へされる非推奨警告
Gist - Replace 'be_success' to 'be_successful'.md
spec/controllers/static_pages_controller_spec.rbdescribe '#home' do it "正常なレスポンスを返す" do get :home - expect(response).to be_success + expect(response).to be_successful end end再々度テストを実行
やっと通りました。。。
$ rspec spec/controllers/static_pages_controller_spec.rb StaticPagesController #home 正常なレスポンスを返す Finished in 0.04691 seconds (files took 2.47 seconds to load) 1 example, 0 failuresめでたしめでたし。
最後に
これからはコントローラのテストはなくなって、
統合テスト等に置き換わっていくんですよね。コード量が少なくなるのはいいことですね。
- 投稿日:2019-12-18T01:42:20+09:00
2分探索っぽいものを実装した[Ruby]
はじめに
競技プログラミングをやっていたら、小さい順に並んだ無数の自然数の配列中(重複は無い)から、与えられたターゲットが配列のどの数とどの数の間にあるのか実装する必要があった。
無作為に0から探していくと実行時間切れになったので、2分探索っぽい実装をして効率的に探索を行おうとした。計算量
配列の全ての要素に対して探索を行う場合には、計算量が配列の要素数に比例して大きくなる。つまりオーダー記法で書くとO(N)
これに対して、二分探索を行うと、計算量はO(logN) (2分探索では対数の底は2)となるので、要素数が増えたときにも計算量の増加は比較的少なく済む。
これなら時間内に実行時間内に探索が終わりそう。
実装
とりあえず実装
色々と未熟な点があると思いますので、ご指摘があればコメントいただけるとありがたいです。
array = [1, 4, 6, 8, 20, 34, 87, 118, 234, 769, 1000, 8909] # 自然数の配列(ソート済み) target = 48 #ターゲットとなる自然数 outputs = "配列の範囲外です" # targetが配列の範囲内にある場合 if array.first <= target && target <= array.last while array.length >= 2 length = array.length # 配列の中央2つの間にあるか? if array[length / 2 - 1] <= target && target <= array[length / 2] outputs = array[length / 2 - 1].to_s + " " + array[length / 2].to_s break # 配列の前半にtargetがあるので配列の前半を切り取る elsif target < array[length / 2 - 1 ] array = array.take(length / 2) # 配列の後半にtargetがあるので配列の後半を切り取る else array = array.slice(length / 2 .. length - 1) end end end puts outputsarrayにソート済みの任意の自然数を、targetに与えられた自然数を代入する。
実行すると以下のようになる。
34 87確かに48は34と87の間なのでOK
ターゲットが配列の範囲外の場合(
target = 10000000
とか)には、配列の範囲外ですと出力される。
まとめ
アルゴリズム関連の知識が乏しいのでこれを機に色々勉強する。
計算量などについても、ぼんやりとしかわからないので調べたものをまたまとめようと思う。
- 投稿日:2019-12-18T01:26:14+09:00
renderが省略されてて眠れなくなったので調査
はじめに
この記事はエムスリーキャリア Advent Calendar 2019 18日目の記事です。
エニジニアリンググループのおどです。
新卒入社した会社を8月に辞め、エムスリーキャリアに転職しました。
転職を機にLaravelエンジニアからRailsエンジニアに転身。今回は利用するフレームワークがLaravelからRuby on Railsに変わり、つまずいたところを軽く深堀してみようと思います。
何に詰まったのか
入社して最初、簡単な文言修正のタスクからアサインされました。
「お、viewファイル見て文字直すだけとか簡単じゃん」
Laravelで開発をしてたときのようにroutingファイルを見て、controllerにたどり着きました。
home_controller.rbclass HomeController < ApplicationController def index end endどのファイルをレンダリングしているんだ…
気になって眠れない日々が始まりました。
Laravelはわかりやすかったな…
HomeController.php<?php namespace App\Http\Controllers; use App\Http\Controllers\Controller; class HomeController extends Controller { public function index() { return view('home.index'); } }調査
binding.pryを使ってトレースしていきたいと思います。
まず、railsではcontrollerのメソッドが終了すると、send_actionというメソッドが呼ばれます。
actionpack-5.2.4/lib/action_controller/metal/basic_implicit_render.rb# frozen_string_literal: true module ActionController module BasicImplicitRender # :nodoc: def send_action(method, *args) super.tap { default_render unless performed? } end def default_render(*args) head :no_content end end endcontrollerのメソッド内でrenderを1度も実行していないと、
performed?
がfalse
を返します。basic_implicit_renderをincludeした
action_controller/metal/implicit_render.rb
内に同名のdefault_renderが定義されていて、そこからrenderを呼び出します。ここで呼ばれるrenderは、自分でcontrollerのメソッド内で呼び出したrenderと同じです。(
action_controller/metal/instrumentation.rb
)action_controller/metal/instrumentation.rbdef render(*args) render_output = nil self.view_runtime = cleanup_view_runtime do Benchmark.ms { render_output = super } end render_output endさらに追っていくと、
abstract_controller/rendering.rb
内のrenderメソッドにたどり着きます。abstract_controller/rendering.rbdef render(*args, &block) options = _normalize_render(*args, &block) rendered_body = render_to_body(options) if options[:html] _set_html_content_type else _set_rendered_content_type rendered_format end self.response_body = rendered_body endrenderを省略した場合、この
render_to_body
メソッドに渡る引数が異なってきます。# renderを省略した場合 [1] pry(#<HomeController>)> options => {:prefixes=>["home", "application"], :template=>"index", :layout=>#<Proc:0x00007f481d31fa80@/myapp/vendor/bundle/ruby/2.6.0/gems/actionview-5.2.4/lib/action_view/layouts.rb:392>} # renderを省略しなかった場合 [1] pry(#<HomeController>)> options => {:action=>"index", :prefixes=>["home", "application"], :template=>"index", :layout=>#<Proc:0x00007f481ef20a90@/myapp/vendor/bundle/ruby/2.6.0/gems/actionview-5.2.4/lib/action_view/layouts.rb:392>}渡される引数を見ると、prefixesに呼び出したコントローラ名、templateにメソッド名が格納されているのが分かります。
renderを省略するとprefixesとtemplateから、省略しないとprefixesとactionからビューファイルの名前を組み立ててレンダリングしてくれているようです。わかったこと
- renderを省略すると呼び出しもとからファイル名を組み立ててレンダリングしてくれる
ちなみに
省略した場合に呼ばれるrenderは通常と同じrenderなので、引数だけを省略しても同じ挙動になりました。
home_controller.rbclass HomeController < ApplicationController def index render end end感想
たくさんのファイルが継承されていて、覚えられなくて大変でした。
これで安心してrenderを省略でき、ぐっすり眠れそうです。ご指摘や「ここ間違ってるよ!」、感想など募集中なので気軽にコメントください。
環境
Rails 5.2.4
ruby 2.6.5参考URL
こちらの記事でもっと詳しく追っていたので、さらに興味がある方はこちらも見てみてください。
Railsはどのようにテンプレートを見つけているか
- 投稿日:2019-12-18T00:50:16+09:00
ruby 学習アウトプット1
はじめに
ただの初学者のrubyシルバー取得に向けての学習アウトプットです。
文章もただただ、タコ殴りしてるだけなので、温かい目で見守っていただけると幸いです。今回の教材
公式擬似問題集
これは下記リンクからgithubに飛びます。
無料です。全50門解説もついてます。(ありがてぇ〜〜〜〜)
https://gist.github.com/sean2121/945035ef2341f0c39bf40762cd8531e0やってみて
全然だめでした。。。。。
ということで、凹んでられないので詰まった部分を復習していきたいと思います。以下復習
- 可変長引数の問題
#下記の実行結果は? def foo (a, *b) p a end foo(1,2,3,4) #結果は 1
*b
は可変長引数で引数を複数個設定できる。引数は配列として受け取られるので今回の場合#*bの中身も出力してみる def foo (a, *b) p a,b end foo(1,2,3,4) #結果 1 [2,3,4] #bは配列が出力される
size
メソッドputs({"members" => 193, "year" => 2014}.size) #結果 2
size
メソッドは要素の数を返す。今回はハッシュの要素数、配列の場合は配列の要素数。
clear
メソッドh = {1=>2, 3=>4} h.clear p h #=>{} ary = [1, 2] ary.clear p ary #=>[] #clearメソッドは空のハッシュ、配列を返すちなみに問題には
remove
,destroy
,empty?
の選択肢があったのですが、remove
,destroy
はNoMethodError
でempty?
はハッシュと配列をそのまま返してました。
chop
メソッド#ヘロ〜 s = "hello" puts s.chop #しかし!!結果は!! hell #地獄!!
chop
メソッドは最後の1文字を取り除いた新しい文字列を返しますが、末尾が\r\nで終わってる場合は2文字取り除きます。ちなみに
chomp
メソッドは末尾の改行コードを取り除きます。
rstrip
は文字列の末尾の空白文字を除去した新しい文字列を返します。
こんなにあるんか、、、、、、、、と気が滅入り、、、、、いや、がんばります。
- IOクラスの問題
File.open("foo.txt","r") do |io| puts io.gets puts io.read io.rewind p lines = io.readlines endこのコードのio.rewindの処理は何か?って問題でした。
答えはIO#rewindはファイルポインターを先頭に移動させます。
とのこと。
つか、IOクラスって何??って思いました。getとかは使っていたけど、IOクラスに関しては全くでした。頼りにしていたチェリー本でも網羅されていませんでした(泣)
なのでIOクラスはしっかり勉強しないとヤバ谷園ですね。()
- sort
odd = [1,3,5] even = [2,4,6] num = odd + even p num.sort [1,2,3,4,5,6] #ちなみにsortをつけないと [1,3,5,2,4,6]
sort
がないときの挙動も確かめときました。
- 正規表現の問題
/^[hc].*o$/i
この正規表現にマッチするものを2つ選べよ。A.Hello
B.holland
C.Cello
D.h35L320こちら先頭の文字がhかc(iオプションで大文字小文字無視)で.*で何かしら文字が連続してoで終わるってことで答えは。。。A,Cだ!!!!
ということなのですが、私、アルファベットのoを数字の0と読み間違えて、ハマってました(泣)復習してみて
2回目は流石に9割以上解けました。1回目に答えだけでなく1門1門調べて理由まで説明できるようにしました。まぁ、まだまだ、先は長いのでアール。。。。。
- 投稿日:2019-12-18T00:07:35+09:00
Rails のバグ報告テンプレートがすごい!
この記事は
ruby-jp の #rails チャンネルで知った Rails のバグ報告テンプレートが目からウロコだった話をします。bug_report_templates/active_record_gem.rb の存在を知らない方向けです。
概要
気軽に ActiveRecord の動作を確かめたい
ちょっと ActiveRecord の動作を確かめたい。でも手元にちょうといい Rails プロジェクトがない。rails new するのは面倒だ……。マイグレーションも書かなきゃ……。
そんなとき、Rails のリポジトリに含まれている次の Ruby スクリプトが役に立ちます。Rails のバグ報告テンプレートだそうです。
https://github.com/rails/rails/blob/v6.0.2/guides/bug_report_templates/active_record_gem.rb
active_record_gem.rb# frozen_string_literal: true require "bundler/inline" gemfile(true) do source "https://rubygems.org" git_source(:github) { |repo| "https://github.com/#{repo}.git" } # Activate the gem you are reporting the issue against. gem "activerecord", "6.0.0" gem "sqlite3" end require "active_record" require "minitest/autorun" require "logger" # This connection will do for database-independent bug reports. ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") ActiveRecord::Base.logger = Logger.new(STDOUT) ActiveRecord::Schema.define do create_table :posts, force: true do |t| end create_table :comments, force: true do |t| t.integer :post_id end end class Post < ActiveRecord::Base has_many :comments end class Comment < ActiveRecord::Base belongs_to :post end class BugTest < Minitest::Test def test_association_stuff post = Post.create! post.comments << Comment.create! assert_equal 1, post.comments.count assert_equal 1, Comment.count assert_equal post.id, Comment.first.post.id end end解説
このスクリプトは次のことを行っています。
- bundler/inline を使って Ruby スクリプト上で bundle install を行っている。
gemfile
メソッドの第 1 引数にtrue
を渡すことで、まだインストールしていない Gem をインストールする。- (Bundler のバージョンが 2.1.0 以上の場合)
gemfile
メソッドの第 2 引数にquiet: true
を渡すと bundle install 時のログ出力を抑制できる。BUNDLE_PATH
は無視する。つまり Gem は必ずグローバルにインストールされる。- ActiveRecord::Base.establish_connection の database オプションに
:memory:
を渡すことで、SQLite のデータベースをメモリ上に作成している。- ActiveRecord::Schema.define を使うことで、スクリプト内でテーブルを定義、作成している。
- スクリプト内でモデルを定義している。
- Minitest::Test で単体テストを実行している。
いろんなテクニックがシンプルに詰まっていて、いいスクリプトだなと思いました
参考
- Bundler
- bundler/inline should ignore BUNDLE_PATH and install gems to GEM_HOME by robuye · Pull Request #7154 · bundler/bundler
- bundler/inline での
BUNDLE_PATH
の扱いについて- SQLite
- In-Memory Databases
- インメモリのデータベースについて