Journey

技術に関することと覚書と

vimでTDDしやすい環境を整えた

自分はコードを書くときにTDDをするので、vimから手軽にテストを回せる環境が必要でした。 なので、いままではvim-test + vim-dispatchを使用していたんですが、quickfixlistの挙動だったりがなんか微妙だなーと感じていたのでasyncrun.vimを使用して自分でいい感じの設定を作成することに成功しました。

実際の動作イメージ

f:id:ippachi1218:20201010233739g:plain
動作イメージ

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を使用してレビューできたらいいと思ったので、作り始めました。

とりあえず名前は 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_id1 というマジックナンバーで定義してしまっています。 こうしてしまうと、このfactoryは id1 のユーザーがいることが前提のfactoryになってしまい、テストをする際に id1 のユーザーがいることを気にしないとダメなのはちょっと微妙ですね。 なので基本的にはマジックナンバーで指定するのではなく、別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_postsposts に対して orderlimit をかけたい場合があると思います。 その方法は公式の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>

非常に煩雑でわからづらいですね。ざっと説明すると

  1. propsで渡されたものをdataで適当な変数に代入
  2. そのdataを次のv-modelに入れる
  3. valueの変更に追従できるようにwatchを設定

となっていました。やりたいことはさっと言葉で出ますが、コードは非常に複雑ですね。

解決策

また↑のコードを書こうとしたときにふと思いつき、試してみたコードがこちらです。

<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 のイベントをそのまま上に流すことによって解決できました。 最初のコードよりもコード量も少なくよさそうなので、今後はこれを使っていこうと思います。

Github trend素振り的なものをやっていく

Github trend素振り的なものがあるらしいんのでやってみる。 具体的には、githubトレンドにあるものを、動かしてみるものらしいです。

ものを動かし始めることって結構ハードル高いので、それの耐性がつくのと、文字通りOSSのトレンドにおいていかれないようにできそうです。 ハードル低めで、動かしてみるところまでが基本で、面白かったらなんか作ろうと思います。