二郎系npmを公開しました
フィヨルドブートキャンプには「npmを作って公開する」というプラクティスがあります。 みんな思い思いに自分が作りたいnpmを作っていたので自分も好きなものをテーマに作りました。
今回作成したnpmのページになります。
入力した住所の近くにあるラーメン二郎/二郎系の店を表示するnpmです。
以下のAPIを使用しました。
やっていることはシンプルです。
- 住所を緯度経度に変換
- 緯度経度を基準に店を検索
- ヒットした店と住所の距離を取得する
ラーメン二郎と検索した結果、ラーメン二郎と二郎系どちらも含む結果が返ってきます。 これは良くないので、二郎のみを表示する処理を書きました。 二郎か二郎系か判断する方法として自分で二郎の配列を用意する予定でした。
ラーメン二郎と検索した結果、名前がラーメン二郎XX店である場所が「ラーメン二郎」ということが分かったので、最終的に配列は用意せずに済みました。
検索の精度をあげたり、表示できる件数を多くしたりと改良できる点はあるので、暇な時にやっていきたいです。
6ヶ月ぶりにプロフィール画像を変更した
フィヨルドブートキャンプのメンターの jnchitoさんのツイートがきっかけ
採用希望のプロフィール写真、写り方によって第一印象がガラッと変わるので、左のような雑なスナップ写真ではなく、右のようにお金払って近所の写真屋さんで撮ってもらうぐらいの投資はしてもいいと思うの。(ちなみにどっちも2012年頃の僕です) pic.twitter.com/KC23DVwraT
— Junichi Ito (伊藤淳一) (@jnchito) 2020年10月16日
変える前に「自分のプロフィール画像どう思いますか?」とフィヨルド生に聞いてみたところ以下のような感想だった。
- どこ向いてるのかわからない
- 服が変
- なぜ室内?
これはまずい..
新しいプロフィール画像はこの反省を生かして
- 前をむく
- 白い服をきる
- 日が当たる屋外で撮影
とした。 兄に一眼レフで撮ってもらった。 しばらくはこのプロフィール画像でやっていきたい。
ターミナルからブラウザを起動する
.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
すると...
無事表示できました🎊