Railsのルーティングの細かいところについて
最近細かく引っかかっていたので。
URLヘルパのnew, editの修飾子は前に付く
xxxx_new_pathかnew_xxxx_pathかよく迷っていたけど、
new_xxxx_path, edit_xxxx_path
のほうが正解。namespaceで
# routes.rb namespace 'admin' do resources xxxxs end
となって、xxxxの前にadminが付くようになっても
new_admin_xxxx_path, edit_admin_xxxx_path
とnew, editが前に付く。
namespaceとscopeの作るパスとアクションの対応の違い
# routes.rb namespace 'admin' do resources xxxxs end
とすると、
コントローラのクラスのアクション | パス名 | 名前付きルーティングヘルパー |
---|---|---|
Admin::XxxxsController#index | admin/xxxxs | admin_xxxxs_path |
という風になるが、
# routes.rb scope 'admin' do resources xxxxs end
とすると、
コントローラのクラスのアクション | パス名 | 名前付きルーティングヘルパー |
---|---|---|
XxxxsController#index | admin/xxxxs | xxxxs_path |
という風に、コントローラや名前付きルーティングヘルパーはそのままでパスだけ admin
がつくようになる。
配列でのパス指定
<%= link_to 'Edit Xxxx', edit_xxxx_path(@xxxx) %>
というのを
<%= link_to 'Edit Xxxx', [:edit, @xxxx] %>
のように書ける。これは、
form_for [:admin, @article]
のように、名前空間の中に入っているパスをform_forで指定したい場合に覚えておくと便利な気がする。
参考
テスト中のオブジェクトに実際の動作をさせないで返り値を指定する
特定のオブジェクトにあるメソッドの中身を実行せず結果だけ返して欲しい場合
allowとreceiveを使ってスタブの指定をします。
allow(some_object).to receive(:method_name).and_return(return_value)
特定のクラスのインスタンス全てにあるメソッドの中身を実行せず結果だけ返して欲しい場合
allow_any_instance_of(ClassName).to receive(:method_name).and_return(return_value)
以下のようなコントローラのアクションに対するテストなどでテストコードではないところで生成するインスタンスに対して
# コントローラのコード def index api_client = SomeApiClient.new(token) @result = api_client.search # 外部APIへリクエストを飛ばす end
# テストコード allow_any_instance_of(SomeApiClient).to receive(:search).and_return(["foo", "bar"])
といった具合に使います。
引数があるメソッドの場合に引数を指定したい場合
メソッドの呼び出しに伴う引数を指定したい場合はwithを使います。
allow(some_object).to receive(:method_name).with(arg).and_return(return_value)
複数の引数を指定したい場合は
allow(some_object).to receive(:method_name).with(arg1, arg2, ...).and_return(return_value)
どの引数を受け取ってもいい場合は
allow(some_object).to receive(:method_name).with(anything()).and_return(return_value)
この引数にはハッシュの一部だけを指定したりいろんなバリエーションがあります。
例外を起こしたい場合
and_raiseを使います。
allow(some_object).to receive(:method_name).and_raise(SomeError.new)
参考
ActiveRecordで関連しているモデルに対してメソッドを定義する
Userと1対多関連の関係にあるCommentというモデルがあるとする。
あるUserに関連したコメントに対して user.commentes.some_method
のようにスコープやメソッドを定義したいという場合があって、その場合以下のように書ける。
class User < ApplicationRecord has_many :comments do # has_many関連を定義した部分にブロックを渡すとuser.comments.shortest_commentのように書ける def shortest_comment self.order(Arel.sql('length(contents) asc')).first end end end
class Comment < ApplicationRecord belongs_to :user def self.longest_comment # クラスメソッドでallを使うと呼び出された時点でのhas_many関連などの上にメソッドチェインをつなげることができる all.order(Arel.sql('length(contents) desc')).first end end
確認用のテストコード
RSpec.describe Comment, type: :model do let(:user) { create(:user) } let!(:comment1) { create(:comment, contents: 'あ' * 3, user: user)} let!(:comment2) { create(:comment, contents: 'あ' * 5, user: user)} let!(:another_user_comment) { create(:comment, contents: 'あ' * 7, user: create(:user)) } let!(:other_user_comment) { create(:comment, contents: 'あ' * 1, user: create(:user)) } describe '.longest_comment' do it 'userのコメントの中で一番長いコメントが返ってくる' do expect(user.comments.longest_comment.contents).to eq comment2.contents end end describe '.shortest_comment' do it 'userのコメントの中で一番短いコメントが返ってくる' do expect(user.comments.shortest_comment.contents).to eq comment1.contents end end end
参考
attributes_forでテスト用のハッシュを取得するときはFactoryに定義された属性しか取り出されない
下準備としてモデルのSchemeとFactoryの定義
# schema.rb ActiveRecord::Schema.define(version: 2020_11_23_064724) do create_table "users", force: :cascade do |t| t.string "name" t.integer "age" t.string "mail" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false end end
# factories/user.rb FactoryBot.define do factory :user do name { 'テストユーザー' } mail { 'test@example.com' } # age { 15 } end end
テストでattributes_forを使ってみる
require 'rails_helper' RSpec.describe UsersController do # attributes_forはおそらくコントローラのテストでパラメータの組み立てに使う describe 'attributes_forとbuild(:user).attributesの違い' do it 'attributes_forで取り出されるハッシュにはFactoryに定義した属性しか含まれない' do expect(attributes_for(:user)).to eq({:name=>"テストユーザー", :mail=>"test@example.com"}) # ageやcreated_atなどの属性はFactoryに記述されていないのでattributes_forで取り出したハッシュには含まれない end it 'build(:user).attributesにはFactoryに定義した値以外も含まれる' do expect(build(:user).attributes).to eq({"id"=>nil, "name"=>"テストユーザー", "age"=>nil, "mail"=>"test@example.com", "created_at"=>nil, "updated_at"=>nil}) # buildでインスタンスを作ってから、attributesメソッドで作るハッシュにはFactoryに定義されていないものもあわせて全属性が含まれている end end end
参考
今週のGo
週5以上活動していればよい感じで。
- 以下のようなコードを動かしながら、関数値gの中でその値が定義された関数f内の変数Aを利用すると、関数値gは変数Aの参照を持っているのだなぁ、という確認をしていた
var once sync.Once onceFunc := func() { once.Do(func() { fmt.Println("A") }) } func main() { var once sync.Once firstFunc := func() { once.Do(func() { fmt.Println("A") }) } secondFunc := func() { once.Do(func() { fmt.Println("A") }) } firstFunc() // 1回しか実行されない secondFunc() } // $ go run sync.go // A
func main() { var cnt int firstFunc := func() { cnt++ fmt.Println("A: ", cnt) } secondFunc := func() { cnt++ fmt.Println("B: ", cnt) } firstFunc() secondFunc() } // $ go run sync.go // A: 1 // B: 2 // それぞれの関数値で値を定義した時点でのコピーを持っているなら、ここは1のはず
- 定数を見てて知らない概念がいてひよる
- コメント読むの好き
// We add : because we include :port as part of host.
のように一つずつ値について説明してあるやつとか
- 値を直接入力させず、値を設定するメソッドを置いている
// 値を直接入力させず、値を設定するメソッドを置いている // Development sets the Client to use the APNs development push endpoint. func (c *Client) Development() *Client { c.Host = HostDevelopment return c } // Production sets the Client to use the APNs production push endpoint. func (c *Client) Production() *Client { c.Host = HostProduction return c }
- golintのusage が結構愚直に書いてあった
func usage() { fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) fmt.Fprintf(os.Stderr, "\tgolint [flags] # runs on package in current directory\n") fmt.Fprintf(os.Stderr, "\tgolint [flags] [packages]\n") fmt.Fprintf(os.Stderr, "\tgolint [flags] [directories] # where a '/...' suffix includes all sub-directories\n") fmt.Fprintf(os.Stderr, "\tgolint [flags] [files] # all must belong to a single package\n") fmt.Fprintf(os.Stderr, "Flags:\n") flag.PrintDefaults() }
- 分岐について自分個人は早期returnしたくなるところで
if { 短い } else { 長い }
func main() { flag.Usage = usage flag.Parse() if flag.NArg() == 0 { lintDir(".") } else {
- 関数の中で関数を定義したり、varブロックでまとめて変数を定義したり
- 関数、必要とあれば100行以上書くこともあるのか(テスト厚めなパッケージ読んで再考したい。。)
context-aware
https://www.nttpc.co.jp/yougo/%E3%82%B3%E3%83%B3%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88%E3%83%BB%E3%82%A2%E3%82%A6%E3%82%A7%E3%82%A2%E3%83%8D%E3%82%B9.html- チャンネルの写経が必要
- 生のerrorを見て自パッケージで扱いやすい型に包み直す関数が関数としてでなく変数として関数内に定義
uerr := func(err error) error { // the body may have been closed already by c.send() if !reqBodyClosed { req.closeBody() } method := valueOrDefault(reqs[0].Method, "GET") var urlStr string if resp != nil && resp.Request != nil { urlStr = resp.Request.URL.String() } else { urlStr = req.URL.String() } return &url.Error{ Op: method[:1] + strings.ToLower(method[1:]), URL: urlStr, Err: err, } }
- switchに式を与えず case に 式を書くのも普通にやるようだ
func unhex(c byte) byte { switch { case '0' <= c && c <= '9': return c - '0' case 'a' <= c && c <= 'f': return c - 'a' + 10 case 'A' <= c && c <= 'F': return c - 'A' + 10 } return 0 }
今週のGo
- defer FILO/panicになっても実行される
- recoverはpanicが起こる関数と同じgoroutineの中に置く
- deferと対になる処理について
- errorに判定を生やす標準ライブラリの実装例
ログにもありますが、試したGoのバージョンは1.7.3です
deferはFILOでpanicになっても実行される
package main import "fmt" func main() { defer func() { fmt.Println("1st in") }() defer func() { fmt.Println("2nd in") }() arr := []int{1, 2, 3} fmt.Printf("%v", arr[100]) // raise panic }
2nd in 1st in panic: runtime error: index out of range goroutine 1 [running]: panic(0x8efc0, 0xc42000a0d0) /usr/local/Cellar/go/1.7.3/libexec/src/runtime/panic.go:500 +0x1a1 main.main() /Users/yoshidanozomi/study/gopl/study/defer.go:10 +0x93 exit status 2
panicをrecoverで拾えるスコープはgoroutineの中
panicの情報をrecoverで拾える範囲がよくわかってなかったのですが、同じgoroutineで実行される範囲のようです。
package main import "fmt" func main() { defer func() { if err := recover(); err != nil { fmt.Println("err:", err) } }() panic("test") }
err: test
package main import "fmt" import "sync" func main() { var wg sync.WaitGroup wg.Add(1) go func () { defer wg.Done() // recoverはdeferの中でしか実行できない // 同じgoroutineの中であれば、 // recoverを書くのはpanicが起こる関数の中でなくてもよい defer func() { if err := recover(); err != nil { fmt.Println("err:", err) } }() recoverTest() }() wg.Wait() } func recoverTest() { panic("test") }
err: test
package main import "fmt" import "sync" func main() { var wg sync.WaitGroup wg.Add(1) // deferをpanicが起こるgoroutineの外に置いた場合 defer func() { if err := recover(); err != nil { fmt.Println("err:", err) } }() go func () { defer wg.Done() recoverTest() }() wg.Wait() } func recoverTest() { panic("test") }
$ go run defer.go panic: test goroutine 5 [running]: panic(0x89e00, 0xc42000a2c0) /usr/local/Cellar/go/1.7.3/libexec/src/runtime/panic.go:500 +0x1a1 main.recoverTest() /Users/woshidan/study/gopl/study/defer.go:28 +0x6d main.main.func2(0xc42000a2b0) /Users/woshidan/study/gopl/study/defer.go:21 +0x4a created by main.main /Users/woshidan/study/gopl/study/defer.go:22 +0x9a exit status 2
deferと対になる処理について
go fmt
で整形してくれるわけではないですが、Go Blog Defer, Panic, and Recoverあたりを見るに*1、
dst, err := os.Create(dstName) if err != nil { return } defer dst.Close()
のように、後始末が行われてほしい処理の近くにおくのがよいかもです。
errorに判定を生やす標準ライブラリの実装例
net/http ライブラリの実装例 :eyes: .
https://golang.org/src/net/net.go?h=Temporary#L375
// An Error represents a network error. type Error interface { error Timeout() bool // Is the error a timeout? Temporary() bool // Is the error temporary? }
Go、割と雑に一つのファイルに長く処理が書いてあったりして、復習してる間にhttpのコード眺めててひやっとしたり、いままで違う種類のプログラミングなので迷わしい。。なので慣れるまでもうちょっと読む量増やしてこ、ということで現場からは以上です。
*1:まあ、古いですがががが