二郎系npmを公開しました

フィヨルドブートキャンプには「npmを作って公開する」というプラクティスがあります。 みんな思い思いに自分が作りたいnpmを作っていたので自分も好きなものをテーマに作りました。

今回作成したnpmのページになります。

www.npmjs.com

入力した住所の近くにあるラーメン二郎/二郎系の店を表示するnpmです。

以下のAPIを使用しました。

やっていることはシンプルです。

  1. 住所を緯度経度に変換
  2. 緯度経度を基準に店を検索
  3. ヒットした店と住所の距離を取得する

ラーメン二郎と検索した結果、ラーメン二郎と二郎系どちらも含む結果が返ってきます。 これは良くないので、二郎のみを表示する処理を書きました。 二郎か二郎系か判断する方法として自分で二郎の配列を用意する予定でした。

ja.wikipedia.org

ラーメン二郎と検索した結果、名前がラーメン二郎XX店である場所が「ラーメン二郎」ということが分かったので、最終的に配列は用意せずに済みました。

検索の精度をあげたり、表示できる件数を多くしたりと改良できる点はあるので、暇な時にやっていきたいです。

6ヶ月ぶりにプロフィール画像を変更した

フィヨルドブートキャンプのメンターの jnchitoさんのツイートがきっかけ

変える前に「自分のプロフィール画像どう思いますか?」とフィヨルド生に聞いてみたところ以下のような感想だった。

  • どこ向いてるのかわからない
  • 服が変
  • なぜ室内?

これはまずい..

新しいプロフィール画像はこの反省を生かして

  • 前をむく
  • 白い服をきる
  • 日が当たる屋外で撮影

とした。 兄に一眼レフで撮ってもらった。 しばらくはこのプロフィール画像でやっていきたい。

はじめて学ぶソフトウェアのテスト技法を読んだ

フィヨルドブートキャンプのプラクティスには、自分で作成したRailsアプリのテストコードを書くプラクティスがあります。 実際にテストコードを書く前に、どのようなテスト技法があるか、TDDとは何か、test-unitについて学習します。 テスト技法について勉強するために、はじめて学ぶソフトウェアのテスト技法を読んだので、まとめていきたいと思います。

www.amazon.co.jp

どのようなテストの種類があるのか?

ブラックボックステスト

用件や仕様に基づいて行うテスト。プログラミング詳細の知識がなくてもテストできる。 ブラックボックステストを行うには仕様から期待する値、期待しない値を選択し、それぞれの入力に対する期待する値を決める。 仕様からサブセットの組み合わせを決定することで、総当たりで入力するデータを組みわせて検証しなくても良い。

ホワイトボックステスト

ソフトウェアの実装、構造に基づいて行うテスト。プログラミングの知識が必要となる。

目指すべきテストケースとは?

全ての可能性を予想してそれらをテストすることは不可能。 最小の時間と労力でほとんどのエラーを検出できるテストケースを目指す。

テストのレベル

単体テスト

ソフトウェアの最小単位をテストする。 何を最小単位とするかはプログラミング言語によって異なる。

統合テスト

単体テストでテストした複数のプログラムを統合させた時に動くかどうかテストする。

システムテスト

もっとも高いレベルの統合時(ソフトウェアが完成した時)に起きる欠陥に焦点を当ててテストする。

受け入れテスト

発注者がソフトウェアを受け入れて、ベンダーに代金を支払うか検証するテスト。 発注者の意図通りに動くか確かめる。

ブラックボックステスト

同値レベルテスト

以下のようなコードがあったとする

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

  • 全ペア方式  macの場合以下の記事が参考になった。 qiita.com

ペア構成テストはあくまで「全てのペア」の組み合わせをテストするので、シングルモード欠陥、ダブルモード欠陥以外の欠陥はカバーできない。よって特殊な条件がある場合テストを追加する必要がある。

状態遷移テスト

  • 状態遷移図を書くことによって、状態、次の動作、イベントを明確にできる。
  • 状態遷移図と合わせて状態遷移表を使うことで抜け漏れなく遷移を確認できる。

ドメイン分析テスト

  • 複数の変数を同時にテストするための技法。
  • 境界値が不正確に定義、実装された条件を見つけることができる。

ユースケーステスト

  • アクターがどうシステムを使うのシナリオのこと。アクターはユーザーを指すことが多い。シナリオとはユーザーとシステムの相互作用のこと。
  • ユースケーステストはユーザーの視点から定義される。システムの視点からではない。
  • 主成功シナリオに対して少なくとも1つのテストを書く。

ホワイトボックステスト

制御フローテスト

  • モジュール内の実行パスを識別して、それらのパスを網羅するテストを作成する。
  • 制御フローグラフを元にパスを分析する。

データフローテスト

  • 最初に値を代入せずに参照するミスを確認するためデータフローテストを行う。
  • モジュール内の実行パスを識別した後、各パスの使用、未定義の変数のペアをテストする。

テストのパラダイム

スクリプトテスト

  • ソフトウェア開発手法の一つのウォーターフォールモデルの一部分として世に登場した。
  • スクリプトテストとは公式なシステム要求に基づいてテストを計画すること。
  • 再現性、監査性、客観性を担保できる。

探索的テスト

  • 探索的テストとは製品を開発しながらテストの設計と実行を行うこと。
  • 実行すべきてテストケースが事前に決定できず、一つ前のテストの結果を元に次のテストを考えなければならない場合、探索的テストを選択する。

テストの計画

  • スクリプトテストを採用したからといって、探索的テストを取り入れてはいけないということではない。逆も然り。
  • 計画が現在進行形のプロセスである場合、現在の計画は暫定的なものであることを認め、新しい知識や情報が入った時は計画を見直すべきである。

支援方法

欠陥の分類

  • 欠陥を分類することでテストの方向性、重要度を決めることができる。
  • 分類せずにテストすることもできる。

テストの終了判定

テストを終了するか決める時の5つの基準

  • 事前に決めたカバレッジの目標値を達成した
  • 欠陥検出率が、事前に決めた基準以下に下がった
  • 次の欠陥を見つけるのに要する限界コストが、その欠陥で生じると予想される損害額より大きくなった
  • プロジェクトチームが、製品をリリースしても良いという意見に達した
  • 上司による「いいから出荷しろ」の一言

感想

まだまともにテストコードを書いたことがないのでなんのこっちゃと感じる内容が多かったですが、新しいことを学んでいる時によく発生する現象だと割り切って読み進めました。 ひとつ残念な点を挙げるとしたら、章末問題の答えが載っていないことです。 本書を読んで一番よかったと思う点は、正しいテストとの向き合い方を学べたことです。 闇雲、気まぐれにテストを書くのではなく、「最小の時間と労力でほとんどのエラーを検出できるテスト」をかけるエンジニアを目指したいと思いました。 そしてこの本には「最小の時間と労力でほとんどのエラーを検出できるテスト」を書くためのテクニックが詰まっています。 どんなテストを書けばいいんだ?と迷った時、このテストは何のためにあるんだろうと疑問に思った時、読み返したい一冊です。

ユーザーフォローの関連付けを自分の言葉で説明してみる

Rails Tutorial 14章でお馴染みのユーザーフォロー機能。 ユーザー同士の多対多と関係するコードについて、自分の言葉で説明してみたいと思います。 尚、実装例や全ての解説とコードは以下の記事にまとまっていますので、とりあえず動かしたいという方はそちらを読んでください。

qiita.com

ユーザーフォロー概観

  • 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レベルではどのようなデータが保存されているか確認します。(手書きのメモが汚いです すんません..)

f:id:mashoo1101:20200811163710p:plain とてもシンプルですね。 ユーザーはユーザーをたくさん持っていて、ユーザーはユーザーをたくさん持っている状態を表しています。 DBレベルではデータを保存するだけなのでこの実装で問題ないのですが、Rails側でデータを取り出したいとなった時にひとつ困ることがあります。 「ユーザーはユーザーをたくさん持っていて、ユーザーはユーザーをたくさん持っている」これだけだと、ユーザー間の関係性をうまく表現できません。フォロワーとフォローされる側を明確にする必要があります。 よって以下のコードを追記します。

class Relationship < ApplicationRecord
  belongs_to :following, class_name: 'User'
  belongs_to :follower, class_name: 'User'
end

この記述により、Railsからは以下のようにデータを扱えるようになりました。

f:id:mashoo1101:20200811163954p:plain

「フォロワーはたくさんのフォローしているユーザーを持っている」&「フォローしているユーザーはたくさんのフォローされているユーザーを持っている」という状態を表現できました。 この記述により最後に実装する「ユーザーの取得」で、適切なユーザーを取得できるようになります。 (あくまでRailsから操作できる仮想的なモデル名であり、DBに保存される情報はコード追記前と変わりません)

Relationshipから適切なデータを取得する

最終目標である「自分がフォローしているユーザー、フォローされているユーザーを取得」するには、中間テーブルであるRelationshipsを使えば書けそうです。 現在の中間テーブルの中身は以下のようになっています。

f:id:mashoo1101:20200811163612p:plain

この中から自分をフォローしているユーザーとの関係(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_relationshipspassive_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すると...

f:id:mashoo1101:20200707180703p:plain

無事表示できました🎊