vimでTDDしやすい環境を整えた
自分はコードを書くときにTDDをするので、vimから手軽にテストを回せる環境が必要でした。 なので、いままではvim-test + vim-dispatchを使用していたんですが、quickfixlistの挙動だったりがなんか微妙だなーと感じていたのでasyncrun.vimを使用して自分でいい感じの設定を作成することに成功しました。
実際の動作イメージ
vimrcでの設定
hi TestRed term=reverse ctermfg=252 ctermbg=52 guifg=#D9D9D9 guibg=#730B00 hi TestGreen term=bold ctermbg=22 guibg=#006F00 function! AsyncTestNearest() abort let g:async_test_running = 1 call popup_close(get(g:, 'test_bar_popup', 0)) exec "AsyncRun bundle exec rspec -b %:p:" . line(".") endfunction function! AsyncTestFile() abort let g:async_test_running = 1 call popup_close(get(g:, 'test_bar_popup', 0)) exec "AsyncRun bundle exec rspec %:p" endfunction function! AsyncTestAll() abort let g:async_test_running = 1 call popup_close(get(g:, 'test_bar_popup', 0)) exec "AsyncRun bundle exec rspec" endfunction function! AsyncTestLint() abort let g:async_test_running = 1 call popup_close(get(g:, 'test_bar_popup', 0)) exec "AsyncRun bundle exec rubocop" endfunction function! AsyncTestLintLocal() abort let g:async_test_running = 1 call popup_close(get(g:, 'test_bar_popup', 0)) exec "AsyncRun bundle exec rubocop %:p" endfunction let s:on_asyncrun_exit =<< trim END if !get(g:, 'async_test_running', 0) return endif cclose call popup_close(get(g:, 'test_bar_popup', 0)) if g:asyncrun_status == "failure" let g:test_bar_popup = popup_create("", #{line: 10000, minwidth: 10000, highlight: 'TestRed'}) botright cwindow 15 execute "normal \<c-w>p" else let g:test_bar_popup = popup_create("", #{line: 10000, minwidth: 10000, highlight: 'TestGreen'}) endif END let g:asyncrun_exit = join(s:on_asyncrun_exit, "\n")
完全にRSpec用となっていますが、簡単な修正で大体のテストコマンドに対応できるかなーと思います。
説明
Gifが全てなので細かい説明は省きますが、
:call AsyncTestNearest()
でカーソル上のテストを実行:call AsyncTestFile()
で現在のテストファイルを実行- テスト失敗時はvimの一番下が赤くなり、quickfixlistを表示する
- テスト成功時はvimの一番下が緑になり、quickfixlistを閉じる
みたいな感じです。
実現方法としてはAsyncRunが実行完了したときに実行される g:on_asyncrun_exit
でいろいろやっている感じです。
現在のモードが隠れてしまったりと欠点があるものの、いまの自分には完全にマッチしていて満足しています。
おわり
プラグイン化しようとも思いましたが、現状AsyncRunを使用している人にとってやりづらいだろうなーと思って、もうちょっといい解決方法を考えているので、思いついたらプラグインにしたいです。
HanamiのEntityのschemaに別Entityを使用する方法
結論
class User < Hanami::Entity attributes do # one attribute :post, Types::Entity(Post) # many attribute :posts, Types::Collection(Post) end end
経緯
HanamiでDDDっぽく書くために https://guides.hanamirb.org/entities/custom-schema/ に書いてあるとおりに、厳密に属性のチェックをしていました。
class User < Hanami::Entity attributes do attribute :id, Types::String attribute :name, Types::String attribute :created_at, Types::DateTime attribute :updated_at, Types::DateTime end end
そこで User has one post
みたいな関係が必要になりましたが、guideには別のEntityをschemaに記述する方法がありませんでした。そこでHanamiが内部的に使用している dry-types
のドキュメントを見てみると Types.instance(Range)
と記載があったのでこれを使用すればいけそうだなと感じ、以下のようなコードを試してみました。
class User < Hanami::Entity attributes do attribute :id, Types::String attribute :name, Types::String attribute :post, Types.Instance(Post) attribute :created_at, Types::DateTime attribute :updated_at, Types::DateTime end end
いけそうかな、と思いましたがエラーが出ました。
他に使えそうなAPIもないので、hamami/model
のソースコードを眺めていると https://github.com/hanami/model/blob/master/lib/hanami/model/types.rb にそれっぽいメソッド名のものが2つあったので試してみたところビンゴでした。
class User < Hanami::Entity attributes do attribute :id, Types::String attribute :name, Types::String attribute :post, Types::Entity(Post) attribute :posts, Types::Collection(Post) attribute :created_at, Types::DateTime attribute :updated_at, Types::DateTime end end
最悪 initialize
でチェックするかと思っていたので、スッキリした方法が見つかってよかったです。
VimでPRをレビューするためのプラグインを作っている
githubでレビューをしていていると、いつものエディタと違うことに違和感を感じることないですか?
自分の場合は
- フォント
- フォントサイズ
- ウィンドウサイズ
- 背景色
- シンタックスハイライト
- その他周りにあるUI
がいつもと違うので、全体的に違和感を感じてしまい、コードの違和感に気がつけないことが結構あります。 いつもと同じエディタでいつものdiffを使用してレビューできたらいいと思ったので、作り始めました。
diff取れるようになった pic.twitter.com/vIoNhnPJYB
— ippachi (@ippachi1218) 2020年5月9日
とりあえず名前は viview
ということにしてます。
現状はtweetにある通り、PRのリストを取得し、それを選択するとそのPRのdiffのあるファイル一覧をdiffで開くというものです。
今後機能追加として
コメント機能
- コメント表示
- コメント追加
レビュー完了
- comment
- request changed
- approved
あたりまではしたいなーと思ってます。 あとは、タブで開くと意外と見づらいことが判明したので、nerdtreeとかのファイラーのように、 左側にdiffのあるファイル一覧を表示して、そこからそれぞれのdiffへ飛べるようにしたいなーと思ってます。
もうちょっと完成してきたらgithubに上げる予定です。
FactoryBotを効果的に使用する
似た記事をQiitaに書いてますが、こっちをメインにする意味でも書き直そうと思います。
はじめに
ご存じの方も多いと思いますが、FactoryBotは主にテストを書く際に使用される、オブジェクトのインスタンス化を簡単にするためのものです。 ただ、FactoryBotは柔軟にオブジェクトを生成できてしまうため、よくメンテが辛かったり、何をテストしているのかがわからない状況になってしまうことが多くあるかと思います。 そうならないためのTipsをまとめようと思います。
外部キーを直指定しない
よくない例
FactoryBot.define do factory :post do title { 'FactoryBotを効果的に使用する' } user_id { 1 } end end
factoryの定義上で、 user_id
を 1
というマジックナンバーで定義してしまっています。
こうしてしまうと、このfactoryは id
が 1
のユーザーがいることが前提のfactoryになってしまい、テストをする際に id
が 1
のユーザーがいることを気にしないとダメなのはちょっと微妙ですね。
なので基本的にはマジックナンバーで指定するのではなく、別factoryを指定する方法があるのでそちらを使用しましょう。
改善例
FactoryBot.define do factory :user do name { 'ippachi' } end factory :post do title { 'FactoryBotを効果的に使用する' } user end end
このように書くと、factory :user
の定義を使用し、そのユーザーと紐付けてくれます。
参考: https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md#associations
factoryに定義している値をテストで直接使用しない
よくない例
Class User scope :adult, lambda do where('users.age >= ?', 20) end end FactoryBot.define do factory :user do title { 'FactoryBotを効果的に使用する' } age { 20 } end end describe 'adult scope' do let(:user) { create(:user) } it 'returns a user' do expect(User.adult).to eq [user] end end
例では User
モデルの adult
というスコープのテストを書いています。
その中で、 age
が20以上であるユーザーを絞り込んでいるのですが、ユーザーの age
はfactoryで定義されています。
これはあまり良くありません。
例えば、このサービスのメインのユーザーが18歳であることがあとから判明した場合にfactoryの定義を18に変えたほうが、今後追加されるテストが書きやすくなることは想像できます。
ただそれをするとこのテストは当然ですが落ちます。
なので、テストに使う値は、同じものを上書きする形になったとしても、しっかりと指定するようにしましょう。
改善例
describe 'adult scope' do let(:user) { create(:user, age: 20) } it 'returns a user' do expect(User.adult).to eq [user] end end
複数回生成してもエラーにならないようにする
よくない例
FactoryBot.define do factory :user do email { 'test@example.com' } # unique end end describe 'example' do it 'test' do create(:user) create(:user) end end
例ではユーザーのメールアドレスを定義していますが、これはユニークですので複数回生成すると、DBエラーが吐かれます。
単にユーザーが複数欲しいとき、例えば、ユーザーのスコープのテストのときにそれぞれ指定するのは面倒です。
そういうときは sequence
を使用しましょう。sequence
は複数回factoryを作成した際にそのデータにインデックスを付けたいときなどに使用します。
改善例
FactoryBot.define do factory :user do sequence(:email) { |i| "test#{i}@example.com" } end end describe 'example' do it 'test' do create(:user) create(:user) end end
参考: https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md#sequences
FactoryBotをモデルのUnitテストではあまり使用しないようにする
例えばモデルのUnitテストはrequestテストやE2Eテストと違い、そのモデルに注目したテストです。
そのテスト内でテスト対象のオブジェクトがどのように生成されたのかも大事な要素になります。
なのでfactoryを用いて生成せずに、できるだけ new
を使用して生成するようにしましょう。
他にもtraitを使うとかassociationなども色々ある気がしますが使い方を誤ると余計にメンテしづらくなるので、とりあえずこのくらいにしておきます。
hanamiで関連先のモデルにorderやlimitをかける方法
例えば User
モデルが Post
モデルを has_many
している場合に、find_with_posts
で posts
に対して order
や limit
をかけたい場合があると思います。
その方法は公式のDocumentに書いていなかったので、自分がやっている方法を共有します。
def UsersRepository associations do has_many :posts end def find_with_posts(user_id) aggregate(:posts).where( users[:id].qualified.is(user_id) ).node(:posts) do |posts| posts.order(posts[:created_at].qualified.desc) .limit(5) end.map_to(User).to_a end end
のような形で実現できます。 node
のブロック内で任意のSQLを発行できます。この node
メソッドの正体ですが、まずhanamiは ROM
で作られています。
実際 aggregate
の返り値は ROM::Relation::Graph
というクラスを返します。
これに関するドキュメントは以下のリンクにあります。
https://rom-rb.org/learn/core/5.2/combines/#adjusted-combine
まだhanamiの記事は少ないのでどんどん増やしていきたいですね。
Vueでpropsで渡されたものをコンポーネント内でさらにv-modelに渡したい場合
タイトルままですがVueのあるコンポーネントでv-modelで渡されたものを、さらにそのコンポーネント内で別のコンポーネントのv-modelに渡すなどした時に以下のようなwarningが出ます
Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders.
簡単に言えばv-modelで渡されたものを直接書き換えるなというエラーです。v-modelで渡されたものの変更は $emit
でおこなうので当然のメッセージです。
ただタイトルのようなことをしたい場合も多々あっていままでは以下のような手順で行っていました。
<my-component v-model="value" />
// MyComponent.vue <template> <sub-component v-model="subValue" /> </template> <script> export default { props: { value: { type: Object, required: true } }, data() { return{ subValue: this.value } } watch: { value() { this.subValue = this.value } } } </script>
非常に煩雑でわからづらいですね。ざっと説明すると
となっていました。やりたいことはさっと言葉で出ますが、コードは非常に複雑ですね。
解決策
また↑のコードを書こうとしたときにふと思いつき、試してみたコードがこちらです。
<my-component v-model="value" />
// MyComponent.vue <template> <sub-component :value="value" @input="e => { $emit('input', e) }" </template> <script> export default { props: { value: { type: Object, required: true } } } </script>
これで期待通りに動きました。sub-component
のイベントをそのまま上に流すことによって解決できました。
最初のコードよりもコード量も少なくよさそうなので、今後はこれを使っていこうと思います。