ターミナルからブラウザを起動する
.bashrc
に以下を追記。
alias chrome='open -a "Google Chrome"' alias firefox='open -a "Firefox"' alias safari='open -a "Safari"'
chrome index.html
みたいに使える。
はじめて学ぶソフトウェアのテスト技法を読んだ
フィヨルドブートキャンプのプラクティスには、自分で作成したRailsアプリのテストコードを書くプラクティスがあります。 実際にテストコードを書く前に、どのようなテスト技法があるか、TDDとは何か、test-unitについて学習します。 テスト技法について勉強するために、はじめて学ぶソフトウェアのテスト技法を読んだので、まとめていきたいと思います。
どのようなテストの種類があるのか?
ブラックボックステスト
用件や仕様に基づいて行うテスト。プログラミング詳細の知識がなくてもテストできる。 ブラックボックステストを行うには仕様から期待する値、期待しない値を選択し、それぞれの入力に対する期待する値を決める。 仕様からサブセットの組み合わせを決定することで、総当たりで入力するデータを組みわせて検証しなくても良い。
ホワイトボックステスト
ソフトウェアの実装、構造に基づいて行うテスト。プログラミングの知識が必要となる。
目指すべきテストケースとは?
全ての可能性を予想してそれらをテストすることは不可能。 最小の時間と労力でほとんどのエラーを検出できるテストケースを目指す。
テストのレベル
単体テスト
ソフトウェアの最小単位をテストする。 何を最小単位とするかはプログラミング言語によって異なる。
統合テスト
単体テストでテストした複数のプログラムを統合させた時に動くかどうかテストする。
システムテスト
もっとも高いレベルの統合時(ソフトウェアが完成した時)に起きる欠陥に焦点を当ててテストする。
受け入れテスト
発注者がソフトウェアを受け入れて、ベンダーに代金を支払うか検証するテスト。 発注者の意図通りに動くか確かめる。
ブラックボックステスト
同値レベルテスト
以下のようなコードがあったとする
def sake age = ARGV[0].to_i if age < 20 puts '飲んじゃダメ!' else puts '飲んでいいよ' end end sake $ ruby sake.rb 20 飲んでいいよ $ ruby sake.rb 10 飲んじゃダメ!
このコードが正しく動くテストを書く場合、n~19までの値全てをテストする必要はない。 この範囲のテストに必要なのは20未満の値ひとつのみ。12を選んでも1を選んでもテストという観点から見れば同じ値である。 ここでいうテストの範囲のこと「同値クラス」と呼ぶ。 「同値クラス」を使ったテストを書くことで、テストケースの数を減らすことができる。
同値クラステストを行うときは、1回に1つの無効値をテストするようにする。1つの無効値に絞ることで、システムが正しく無効値を認識したか確認することができる。
異常値が引数に渡されるパターンもテストするべきなのだろうか?
答えはインターフェースの事前条件によって異なる。
事前条件とはモジュール、メソッドが正常に動作するために期待される条件のこと。
ちなみに事後条件とはメソッド、モジュールが何をするかということ。
事前条件で受け付けるデータがメソッド、モジュール外でしっかり定義されている場合、異常値のテストは実行しなくても問題ない。このようなテストのことを「契約によるテスト」と呼ぶ。
逆にどんな入力でも受け付けるメソッド、モジュールの場合は異常値のテストも実行しなければならない。
上記のプログラムを例にとって考えてみる。
メソッドsake
内のage
変数はコマンドラインから入力された値を代入しており、どんな入力でも受け付ける状態。
なのでsake
メソッドには異常値が入力された場合の処理、テストを書かないといけない。
境界値テスト
- データの境界に注目して行うテスト
- 境界値、境界値のすぐ下、すぐ上のデータをテストする
- 比較演算子の打ち間違いなどにより境界は欠陥ができやすい
デシジョンテーブルテスト
デシジョンテーブルは、条件の組み合わせに基づいて、条件と対応するアクションを表形式で表したもの。 抜け漏れなく全ての組み合わせをテーブルに格納する必要がある。 テストだけでなく複雑な仕様を確認するのに有効活用できる。
ペア構成テスト
全ての変数の全て値の全ての組み合わせを検証するのではなく、全てのペアの組み合わせを検証する。 ソフトウェアの欠陥のほとんどはシングルモード欠陥、ダブルモード欠陥のいずれかに分類される。 ペア構成テストはシングルモード欠陥、ダブルモード欠陥をカバーする最小の組み合わせを提供する。 直交表か全ペア方式を使ってペアを作成する。
直交表 数字の入った二次元の配列で配列から任意の2列を選ぶと、別のペアをどう選んでも値の全てのペアの組み合わせができる。 qiita.com
ペア構成テストはあくまで「全てのペア」の組み合わせをテストするので、シングルモード欠陥、ダブルモード欠陥以外の欠陥はカバーできない。よって特殊な条件がある場合テストを追加する必要がある。
状態遷移テスト
- 状態遷移図を書くことによって、状態、次の動作、イベントを明確にできる。
- 状態遷移図と合わせて状態遷移表を使うことで抜け漏れなく遷移を確認できる。
ドメイン分析テスト
- 複数の変数を同時にテストするための技法。
- 境界値が不正確に定義、実装された条件を見つけることができる。
ユースケーステスト
- アクターがどうシステムを使うのシナリオのこと。アクターはユーザーを指すことが多い。シナリオとはユーザーとシステムの相互作用のこと。
- ユースケーステストはユーザーの視点から定義される。システムの視点からではない。
- 主成功シナリオに対して少なくとも1つのテストを書く。
ホワイトボックステスト
制御フローテスト
- モジュール内の実行パスを識別して、それらのパスを網羅するテストを作成する。
- 制御フローグラフを元にパスを分析する。
データフローテスト
- 最初に値を代入せずに参照するミスを確認するためデータフローテストを行う。
- モジュール内の実行パスを識別した後、各パスの使用、未定義の変数のペアをテストする。
テストのパラダイム
スクリプトテスト
探索的テスト
- 探索的テストとは製品を開発しながらテストの設計と実行を行うこと。
- 実行すべきてテストケースが事前に決定できず、一つ前のテストの結果を元に次のテストを考えなければならない場合、探索的テストを選択する。
テストの計画
- スクリプトテストを採用したからといって、探索的テストを取り入れてはいけないということではない。逆も然り。
- 計画が現在進行形のプロセスである場合、現在の計画は暫定的なものであることを認め、新しい知識や情報が入った時は計画を見直すべきである。
支援方法
欠陥の分類
- 欠陥を分類することでテストの方向性、重要度を決めることができる。
- 分類せずにテストすることもできる。
テストの終了判定
テストを終了するか決める時の5つの基準
- 事前に決めたカバレッジの目標値を達成した
- 欠陥検出率が、事前に決めた基準以下に下がった
- 次の欠陥を見つけるのに要する限界コストが、その欠陥で生じると予想される損害額より大きくなった
- プロジェクトチームが、製品をリリースしても良いという意見に達した
- 上司による「いいから出荷しろ」の一言
感想
まだまともにテストコードを書いたことがないのでなんのこっちゃと感じる内容が多かったですが、新しいことを学んでいる時によく発生する現象だと割り切って読み進めました。 ひとつ残念な点を挙げるとしたら、章末問題の答えが載っていないことです。 本書を読んで一番よかったと思う点は、正しいテストとの向き合い方を学べたことです。 闇雲、気まぐれにテストを書くのではなく、「最小の時間と労力でほとんどのエラーを検出できるテスト」をかけるエンジニアを目指したいと思いました。 そしてこの本には「最小の時間と労力でほとんどのエラーを検出できるテスト」を書くためのテクニックが詰まっています。 どんなテストを書けばいいんだ?と迷った時、このテストは何のためにあるんだろうと疑問に思った時、読み返したい一冊です。
ユーザーフォローの関連付けを自分の言葉で説明してみる
Rails Tutorial 14章でお馴染みのユーザーフォロー機能。 ユーザー同士の多対多と関係するコードについて、自分の言葉で説明してみたいと思います。 尚、実装例や全ての解説とコードは以下の記事にまとまっていますので、とりあえず動かしたいという方はそちらを読んでください。
ユーザーフォロー概観
- 1ユーザーはたくさんのユーザーをフォローでき、たくさんのユーザーによってフォローされる
- このような関係のことを自己結合多対多と呼ぶ
- 多対多なので、中間テーブル(Relationships)が必要
ゴール
irb(main):002:0> user.followings => #<ActiveRecord::Associations::CollectionProxy [#<User id: 3, email: "testuser-3@example.com", created_at: "2020-08-11 00:05:15", updated_at: "2020-08-11 00:05:15", address: nil, postal_code: nil, bio: nil, name: "testuser-3", uid: "d36fa9e5-c0ba-41b0-a386-1122be7e8bde", provider: "">, #<User id: 4, email: "testuser-4@example.com", created_at: "2020-08-11 00:05:16", updated_at: "2020-08-11 00:05:16", address: nil, postal_code: nil, bio: nil, name: "testuser-4", uid: "4a93e555-40ec-4308-a3d5-c2a59ae03c2e", provider: "">]> irb(main):004:0> user.followers => #<ActiveRecord::Associations::CollectionProxy [#<User id: 2, email: "testuser-2@example.com", created_at: "2020-08-11 00:05:15", updated_at: "2020-08-11 00:05:15", address: nil, postal_code: nil, bio: nil, name: "testuser-2", uid: "a927f181-595e-4e74-9c29-11367612cb85", provider: "">, #<User id: 3, email: "testuser-3@example.com", created_at: "2020-08-11 00:05:15", updated_at: "2020-08-11 00:05:15", address: nil, postal_code: nil, bio: nil, name: "testuser-3", uid: "d36fa9e5-c0ba-41b0-a386-1122be7e8bde", provider: "">]>
自分がフォローしているユーザー、フォローされているユーザーを取得できるようにします。
ユーザーモデルの切り分け
Railsのコードを書く前に、DBレベルではどのようなデータが保存されているか確認します。(手書きのメモが汚いです すんません..)
とてもシンプルですね。 ユーザーはユーザーをたくさん持っていて、ユーザーはユーザーをたくさん持っている状態を表しています。 DBレベルではデータを保存するだけなのでこの実装で問題ないのですが、Rails側でデータを取り出したいとなった時にひとつ困ることがあります。 「ユーザーはユーザーをたくさん持っていて、ユーザーはユーザーをたくさん持っている」これだけだと、ユーザー間の関係性をうまく表現できません。フォロワーとフォローされる側を明確にする必要があります。 よって以下のコードを追記します。
class Relationship < ApplicationRecord belongs_to :following, class_name: 'User' belongs_to :follower, class_name: 'User' end
この記述により、Railsからは以下のようにデータを扱えるようになりました。
「フォロワーはたくさんのフォローしているユーザーを持っている」&「フォローしているユーザーはたくさんのフォローされているユーザーを持っている」という状態を表現できました。 この記述により最後に実装する「ユーザーの取得」で、適切なユーザーを取得できるようになります。 (あくまでRailsから操作できる仮想的なモデル名であり、DBに保存される情報はコード追記前と変わりません)
Relationshipから適切なデータを取得する
最終目標である「自分がフォローしているユーザー、フォローされているユーザーを取得」するには、中間テーブルであるRelationshipsを使えば書けそうです。 現在の中間テーブルの中身は以下のようになっています。
この中から自分をフォローしているユーザーとの関係(relationship)を表す列を取得するにはどうすれば良いでしょうか? 答えは 「follower_idの値が自分のidの値と同じ列」を取得するよう指定する必要があります。 逆に自分がフォローしているユーザーとの関係(relationship)を表す列を取得するには、「following_idの値が自分のidの値と同じ列」を取得するよう指定する必要があります。 UserをFollowerとFollowingに分けたように上記二つに名前を付けてあげましょう。 自分をフォローしているユーザーとの関係を「passive_relationships」、自分がフォローしているユーザーとの関係を「active_relationships」とします。 コードは以下のようになります。
class User < ApplicationRecord has_many :active_relationships, class_name: 'Relationship', foreign_key: :following_id, dependent: :destroy has_many :passive_relationships, class_name: 'Relationship', foreign_key: :follower_id, dependent: :destroy end
「follower_idの値が自分のidの値と同じ列」を取得する指定はforeign_key
オプションで行います。
dependent: :destroy
はユーザーが削除された時、relationshipも削除するという意味になります。
ユーザーの取得
active_relationships
とpassive_relationships
を通してフォロー中/フォロワーのユーザーを取得します。
followings
を例に説明します。自分がフォローしているユーザーとの関係性を表すactive_relationshipsから、自分にフォローされているユーザーを取得します。
コードは以下のようになります
class User < ApplicationRecord has_many :followings, through: :active_relationships, source: :follower has_many :followers, through: :passive_relationships, source: :following end
「ユーザーモデルの切り分け」で記述した以下のコードによってどちらのユーザーを取ってくるかsource
を指定できるのです!!
class Relationship < ApplicationRecord belongs_to :following, class_name: 'User' belongs_to :follower, class_name: 'User' end
まとめ
既にユーザーフォローに関する素晴らしい記事は存在しますが、自分の言葉でまとめてみると理解度も上がるなと感じました。 疑問点、間違いの指摘等ありましたらコメントお願いします。
Error: Duplicate column name を解決する
rails db:migration
した時に、Error: Duplicate column name "fuga"
というエラーが出た時の解決方法。
エラーにある通り、fugaというカラムはダブってまっせ〜ということです。
まず現在のDBのバージョンを確認。
$ rails db:migrate:status database: db/development.sqlite3 Status Migration ID Migration Name -------------------------------------------------- up 20200707094922 Create books up 20200708020232 Add picture to books up 20200718144222 Devise create users drop 20200718161232 Add column to users up 20200802141002 Add omniauth columns to users up 20200802143752 Add index to users up 20200808064935 Create relationships up 20200809072542 Add not null to relationship
Add column to users
に対応するマイグレーションファイルが実行できていない。
次にdb/schema.rb
を確認。
ActiveRecord::Schema.define(version: 2020_08_09_072542) do # 省略 create_table "users", force: :cascade do |t| t.string "name" end end
既にUserのnameというカラムは作成されていることを確認できる。
つまりマイグレーションの実行結果は既にDBに反映されているため、同じマイグレーションを実行することはできないということ。
よって、マイグレーションファイルを以下のように修正し、rails db:migrate
する。
class AddColumnsToUsers < ActiveRecord::Migration[6.0] def change # 何も書かない end end
これで全てのマイグレーションを実行できた。
$ rails db:migrate:status database: db/development.sqlite3 Status Migration ID Migration Name -------------------------------------------------- up 20200707094922 Create books up 20200708020232 Add picture to books up 20200718144222 Devise create users up 20200718161232 Add column to users up 20200802141002 Add omniauth columns to users up 20200802143752 Add index to users up 20200804145226 Create active storage tablesactive storage up 20200808064935 Create relationships up 20200809072542 Add not null to relationship
修正したマイグレーションファイルは、元の状態に戻しておく。
Rails6 Webpacker::Manifest::MissingEntryErrorを解決する
環境
起きたこと
rails g controller index home
して、ページにアクセスしたところ以下のエラーが出た。
=> Booting Puma => Rails 6.0.3.2 application starting in development => Run `rails server --help` for more startup options Puma starting in single mode... * Version 4.3.5 (ruby 2.7.1-p83), codename: Mysterious Traveller * Min threads: 5, max threads: 5 * Environment: development * Listening on tcp://127.0.0.1:3000 * Listening on tcp://[::1]:3000 Use Ctrl-C to stop Started GET "/hello/index" for ::1 at 2020-07-07 16:46:25 +0900 (1.6ms) SELECT sqlite_version(*) Processing by HelloController#index as HTML Rendering hello/index.html.erb within layouts/application Rendered hello/index.html.erb within layouts/application (Duration: 2.3ms | Allocations: 189) /Users/mashio/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/sprockets-rails-3.2.1/lib/sprockets/rails/helper.rb:355: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call /Users/mashio/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/sprockets-4.0.2/lib/sprockets/base.rb:118: warning: The called method `[]' is defined here [Webpacker] Compiling... [Webpacker] Compilation failed: yarn run v1.22.4 info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command. error Command "webpack" not found. Completed 500 Internal Server Error in 2640ms (ActiveRecord: 0.0ms | Allocations: 9336) ActionView::Template::Error (Webpacker can't find application in /Users/mashio/Desktop/fjord/rails/helloworld/public/packs/manifest.json. Possible causes: 1. You want to set webpacker.yml value of compile to true for your environment unless you are using the `webpack -w` or the webpack-dev-server. 2. webpack has not yet re-run to reflect updates. 3. You have misconfigured Webpacker's config/webpacker.yml file. 4. Your webpack configuration is not creating a manifest. Your manifest contains: { } ): 6: <%= csp_meta_tag %> 7: 8: <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> 9: <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> 10: </head> 11: 12: <body>
error Command "webpack" not found.
ここが気になる..
一度rails webpacker:install
してちゃんとインストールされるのか、確認したところ気になる部分を見つけた。
error browserslist@4.13.0: The engine "node" is incompatible with this module. Expected version "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7". Got "13.6.0" error Found incompatible module.
nodeのバージョンが原因でエラーが出ている。
nodebrew
を使って、別のバージョンのnodeをインストールし有効化してみる。
$ nodebrew install v12.0.0 $ nodebrew use v12.0.0
再度webpackerをインストールすると、errorが出ずにインストールできる。
そしてrails s
すると...
無事表示できました🎊
さくらVPS上のPostgreSQLにsshポートフォワーディングで接続する
なぜポートフォワーディングで接続するのか
先日MacからさくらVPSにあるPostgreSQLのデータベースに接続してみました。が、 外部接続の設定が誰でも接続できる設定にした(サブネットマスクが0)のがずっと気になっていました。
ポートフォワーディングを使うことで、安全に接続できることを知ったのでまとめてみます。
ポートフォワーディングとは
ポートフォワーディングとは、IPネットワーク上のある機器が、自らのIPアドレスのTCPやUDPの特定のポート番号への通信を、別のアドレスの特定のポートへ自動的に転送すること。
これを使うことで、MacからVPSのPostgrSQLへ直接接続しているかのように操作できます。 現時点で自分が知っている接続方法を挙げてみます。
VPSにSSH接続し、PostgreSQLに接続する(Mac--->VPS--->PostgreSQL)
PostgreSQLに外部からの通信を受け付ける設定をして、Macから直接PostgreSQLに接続する(Mac--->PostgreSQL)
ポートフォワーディングの設定をし、MacからPostgreSQLに接続する (Mac--->(VPS)--->PostgreSQL)
今回試したのは3つ目のポートフォワーディングを使った接続です。 ポートフォワーディングはどちらかというと1のSSHに近いなとやってみて思いました。 この記事のリモートフォワードのイラストがとてもわかりやすかったです。 https://qiita.com/mechamogera/items/b1bb9130273deb9426f5
localhostとは
設定ファイルでよく見るlocalhostについて調べました。
loaclhostは自身を表すホスト名のことです。loaclhostを使うとTCP/IP ソケットの場合以下のように接続できます。
psql -h localhost postgres -p 5432
なんでTCP/IPを使うかというと、ユニックスドメインソケットによる接続はPostgreSQL が動いているマシンに直接ログイン(例えばSSH)しなければならないので、今回のような外部接続の場合、TCP/IPを使うことになります。
以下の記事の説明はとてもわかりやすかったです。
http://www.hizlab.net/app/pgsec.html
トンネルを掘る⛏
コマンド一本でポートフォワーディングの設定は終わります。 ポートフォワーディングを設定することを、「トンネルを掘る」と呼ぶそうです。 わかりやすい...! 実行したコマンドは以下の通りです。
ssh -N -L [ローカル側で転送に使用するPort)]:[PostgreSQLのHostName]:[PostgreSQLが解放しているPort(デフォルトだと5432)] -i [さくらVPSの秘密鍵])] -p [VPSが解放しているPort] [VPSのUser]@[VPSのIPアドレス]
PostgreSQLのHostNameはlocalhostを指定します。
VPSのポート、ユーザー名、ホスト名は普段SSHで使っているのでOKです。
これで接続の準備が整いました。-f
オプションをつけるとバックグラウンドで実行できます。
接続するには以下のコマンドを実行します。
psql -h localhost -p [ローカル側で転送に使用するPort] -U [ユーザー名] -d [DB名]
接続完了!
不要な設定を削除する
接続はできたものの、不要な設定が残っていないか見直してみました。 外部接続に関するファイルは2つあります。
/etc/postgresql/12/main/postgresql.conf
listen_addresses = 'localhost'
listen_address = '*'
は「何でもOK!どうぞどうぞ」な設定なので変更。
listen_addresについて
クライアント認証 は誰がサーバにアクセス可能かをきめ細かく制御するのに対し、listen_addressesはどのインターフェイスが接続を試みるかを制御します。
listen_addressにどのインターフェースを受け付けるかのざっくりとした設定を書き、クラインアント認証(pg_hba.conf)でより詳細な設定を書くようです。
ちなみに、listen_addressで設定されていないことによって弾かれた時のメッセージと、listen_addressでは許可されているが、pg_hba.confでは設定されていない時に表示されるメッセージが違うことを確認しました。 - listen_addressで設定されていない時のメッセージ
- pg_hba.confで設定されていない時のメッセージ
/etc/postgresql/12/main/pg_hba.conf
# Database administrative login by Unix domain socket # TYPE DATABASE USER ADDRESS METHOD # "local" is for Unix domain socket connections only local all all peer # IPv4 local connections: host all all 127.0.0.1/32 md5 # IPv6 local connections: host all all ::1/128 md5 # Allow replication connections from localhost, by a user with the # replication privilege. local replication all peer host replication all 127.0.0.1/32 md5 host replication all ::1/128 md5
hostの設定はlocalhost(自身のIPアドレス)のみとし、ポートフォワーディングで使用する接続以外は受け付けないようにしました。
psql -U ユーザー名 -d postgres -h さくらVPSのホスト名
みたいな接続は受け付けません。
ユニックスドメインソケットの設定と、リプレーションの設定はそのままにしておきました。
参考記事(状況が全く同じだったためとても参考になりました) https://qiita.com/rjge/items/d9ec5eb463a0ce24cb8
感想
設定ファイルの意味などを調べつつ動作確認をしたので時間がかかりましたが、自分が何をしているのか&してきたのかをだいたい理解できました。 また何でpeer認証なんてあるんだ?なぜpostgresっていうユーザーがOSのユーザーとして作成されるんだという疑問も解決しました。 「UNIX ドメインソケット経由の場合、アクセス制御はOSレベルに任せることで、安全に運用できるから」となりました。 逆にOSレベルにユーザー管理任せないでどうするの?と考えたところ、そっか!OSに任せた方が都合いいかと納得しました。 読んでてSSHとあんまり変わらないじゃん!と思った方... ポートフォワーディングの方がかっこいいと僕は思います!
プロセスとジョブの違い
コマンドを実行する時、内部の動きは以下のようになる。
シェルからコマンドを実行
カーネルはディスクから実行ファイルを読み出してメモリに入れる
メモリ内容に従ってCPUがプログラムを実行する
- プロセス
- カーネルから見た処理の単位。 メモリ上で実行状態にあるプログラムのことを「プロセス」と呼ぶ。 「プロセス」にはLinuxのシステム全体で一意のIDがつけられる。
- ジョブ
- シェルから見た処理の単位(使う人間側から見た時の単位)。 シェルのコマンドラインに入力している1つの行が、1つのジョブに該当する。 ジョブはシェルごとにジョブ番号を持っている。
プロセスIDはlinuxのシステム全体で一意のIDだが、ジョブ番号は「シェル」の中で一意のIDなので、複数ターミナルを立ち上げている場合、同じジョブ番号を他のシェルから確認できる。(重複する)