ActiveRecordのtakeメソッドがテスタブルだった
Railsにおいてある特定個数の要素がほしいときに以下のようなコードをよく書くと思います。
Article.most_recently.limit(5)
ただこのように書いてしまうと、 limit
はrailsのメソッドのためテストコードが書きにくくなってしまいます。具体的な例を上げると、これをコントローラーのユニットテストをしようとしたときにスタブにするコードが以下のようなものになります。
let(:most_recently) { double('most_recently', limit: [double('article')]) } before do allow(Article).to recevive(:most_recently) { most_recently } end
most_recently
メソッドの出力として、limit
メソッドを持つスタブを登録して、その limit
で配列を返しています。ただ自分の場合は most_recently
の時点で pure rubyの配列として扱い出したいので limit
なんてものは介したくありません。
そこでrailsって take
メソッドもあったよなーと思い実行してみるとちゃんと limit
もつけてくれてました。
Article.most_recently.take(5) Article.most_recently.limit(5).to_a
railsの take
のソースコードを見ると1行目のコードを実行すると、2行目と同じ処理をしていました。
take
を引数付きで呼ぶと、その引数で limit
をつけてSQLを発行した結果を返してくれています。これを使えば先程のテストコードは以下のようにできます。
before do allow(Article).to recevive(:most_recently) { [double('article')] } end
limit
をpure rubyにもある take
に置き直したことで、スタブにしなくて良くなりました。個人的にはすごく嬉しい発見です。
ただ to_a
していることから分かる通り、返り値が ActiveRecord_Relation
ではなく Array
なのでそこには注意してください。
Arch linuxでdockerをrootlessモードで動かす
どこかでdockerがrootlessモードを追加するみたいなことは聞いてましたが、いつの間にか追加されていたようです。 なのでrootlessモードで動かしてみようと思います
※実験的な機能なので本番環境などで使用する場合は注意してください
環境
❯ lsb_release -a LSB Version: 1.4 Distributor ID: Arch Description: Arch Linux Release: rolling Codename: n/a ❯ docker -v Docker version 19.03.5-ce, build 633a0ea838
前提条件
newuidmap
とnewgidmap
が必要なみたいです。筆者は入ってました。入ってない人はAURにあると思うので入れましょう。/etc/subuid and /etc/subgid should contain at least 65,536 subordinate UIDs/GIDs for the user. In the following example, the user testuser has 65,536 subordinate UIDs/GIDs (231072-296607).
とありますが、よくわかりません。とりあえずリンク先を参考に以下のようにしました。今度調べます。
sudo touch /etc/subuid /etc/subgid sudo usermod -v 100000-165535 -w 100000-165535 vagrant
第14回 LXCの構築・活用 [2] ― コンテナを作成・構築する:LXCで学ぶコンテナ入門 -軽量仮想化環境を実現する技術|gihyo.jp … 技術評論社
- archlinux固有の前提条件として
Add kernel.unprivileged_userns_clone=1 to /etc/sysctl.conf (or /etc/sysctl.d) and run sudo sysctl --system
が必要みたいなので言われたとおりにやります
❯ sudo touch /etc/sysctl.d/sysctl.conf ❯ sudo echo kernel.unprivileged_userns_clone=1 >> /etc/sysctl.d/sysctl.conf ❯ sudo sysctl --system * Applying /usr/lib/sysctl.d/10-arch.conf ... fs.inotify.max_user_instances = 1024 fs.inotify.max_user_watches = 524288 * Applying /usr/lib/sysctl.d/50-coredump.conf ... kernel.core_pattern = |/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %h * Applying /usr/lib/sysctl.d/50-default.conf ... kernel.sysrq = 16 kernel.core_uses_pid = 1 net.ipv4.conf.all.rp_filter = 2 net.ipv4.conf.all.accept_source_route = 0 net.ipv4.conf.all.promote_secondaries = 1 net.core.default_qdisc = fq_codel fs.protected_hardlinks = 1 fs.protected_symlinks = 1 fs.protected_regular = 1 fs.protected_fifos = 1 * Applying /usr/lib/sysctl.d/50-pid-max.conf ... kernel.pid_max = 4194304 * Applying /etc/sysctl.d/sysctl.conf ... kernel.unprivileged_userns_clone = 1
最後の行を見てると成功してるっぽいですね
インストール
前提条件ができたので早速インストールしてみます
❯ curl -fsSL https://get.docker.com/rootless | sh # Docker binaries are installed in /home/vagrant/bin # WARN: dockerd is not in your current PATH or pointing to /home/vagrant/bin/dockerd # Make sure the following environment variables are set (or add them to ~/.bashrc):\n export PATH=/home/vagrant/bin:$PATH export DOCKER_HOST=unix:///run/user/1000/docker.sock # # To control docker service run: # systemctl --user (start|stop|restart) docker #
お、なにかでましたね。 書かれているとおりにやりましょう。
sudo echo export PATH=/home/vagrant/bin:$PATH >> ~/.zshrc sudo echo export DOCKER_HOST=unix:///run/user/1000/docker.sock >> ~/.zshrc source ~/.zshrc systemctl --user start docker
これで終わりっぽいですね 早速試してみましょう
❯ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
お、動いた imagesを確認してみます
❯ docker images REPOSITORY TAG IMAGE ID CREATED SIZE
あれ、なにもない。sudo docker images の方はどうなってるでしょうか
❯ sudo docker images REPOSITORY TAG IMAGE ID CREATED SIZE <none> <none> 5c617ac3beaf 2 days ago 496MB composer latest d103d14ad00a 2 days ago 165MB <none> <none> cab68bb4c619 4 days ago 495MB <none> <none> 087354b9e59b 4 days ago 486MB <none> <none> b8ddc88295c1 6 days ago 496MB <none> <none> 237c179d71c0 7 days ago 496MB <none> <none> 52a40879c553 12 days ago 495MB <none> <none> 0c6d0038c226 2 weeks ago 353MB postgres latest 4a82a16ee75c 2 weeks ago 394MB ruby 2.6.5-alpine 3304101ccbe9 2 months ago 50.9MB cfcommunity/slack-notification-resource latest a43829b960aa 6 months ago 20.7MB node 11.15.0-alpine f18da2f58c3d 6 months ago 75.5MB
sudo docker
だと色々ありますね。どうやらリソース自体が独立したものになっているようです。
次にhello worldしてみましょう
❯ time docker run hello-world Unable to find image 'hello-world:latest' locally latest: Pulling from library/hello-world 1b930d010525: Pull complete Digest: sha256:4fe721ccc2e8dc7362278a29dc660d833570ec2682f4e4194f4ee23e415e1064 Status: Downloaded newer image for hello-world:latest Hello from Docker! This message shows that your installation appears to be working correctly. To generate this message, Docker took the following steps: 1. The Docker client contacted the Docker daemon. 2. The Docker daemon pulled the "hello-world" image from the Docker Hub. (amd64) 3. The Docker daemon created a new container from that image which runs the executable that produces the output you are currently reading. 4. The Docker daemon streamed that output to the Docker client, which sent it to your terminal. To try something more ambitious, you can run an Ubuntu container with: $ docker run -it ubuntu bash Share images, automate workflows, and more with a free Docker ID: https://hub.docker.com/ For more examples and ideas, visit: https://docs.docker.com/get-started/ real 0m7.560s user 0m0.033s sys 0m0.012s
sudoなしで動きました。どうやら成功してるみたいです
sudo docker run hello-world
で試してみましょう。
❯ time sudo docker run hello-world Unable to find image 'hello-world:latest' locally latest: Pulling from library/hello-world 1b930d010525: Pull complete Digest: sha256:4fe721ccc2e8dc7362278a29dc660d833570ec2682f4e4194f4ee23e415e1064 Status: Downloaded newer image for hello-world:latest Hello from Docker! This message shows that your installation appears to be working correctly. To generate this message, Docker took the following steps: 1. The Docker client contacted the Docker daemon. 2. The Docker daemon pulled the "hello-world" image from the Docker Hub. (amd64) 3. The Docker daemon created a new container from that image which runs the executable that produces the output you are currently reading. 4. The Docker daemon streamed that output to the Docker client, which sent it to your terminal. To try something more ambitious, you can run an Ubuntu container with: $ docker run -it ubuntu bash Share images, automate workflows, and more with a free Docker ID: https://hub.docker.com/ For more examples and ideas, visit: https://docs.docker.com/get-started/ real 0m8.331s user 0m0.019s sys 0m0.031s
同じような感じで成功しました。 では実際に使用しているdocker-compose.ymlファイルで試してみます。
まずはrootless dockerから
❯ time docker-compose build --parallel real 3m13.673s user 0m0.946s sys 0m0.278s
次にいつものdockerです
❯ time sudo docker-compose build --parallel real 2m40.807s user 0m0.908s sys 0m0.149s
ある程度差が有るみたいですが、十分実用できそうです
まとめ
sudoなし、かつdockerグループにユーザーを追加することなくdockerコマンドを使用することに成功しました。一旦rootless dockerをメインに使ってみたいと思います。 もし使用する場合は、いろいろな制限がまだ有るみたいなので、そのへんにも注意して使用しましょう。
※追記 実際に使ってみるとパーミッションエラーなど、いろいろ詰まりそうだったので、やっぱりメインで使うのはやめとこうと思います
RailsでFileをActionDispatch::Http::UploadedFileに変換する方法
Railsで開発しているときに、生のRubyのFileオブジェクトからRalisのフォームから送信されたActionDispatch::Http::UploadedFileに変換したいときがあったのでそのときに解決した方法です。
require 'mime/types' File.open(path) do |file| filename = File.basename(file.path), ActionDispatch::Http::UploadedFile.new(filename: filename, type: MIME::Types.type_for(filename).first.to_s, tempfile: file)) end
vimで直前に編集していたファイルに戻る方法 <C-6> or <C-^>
タイトルのままですがずっとしたかった機能がredditのどこかのスレッドで見つけたので。
:e foo.txt
の後に :e bar.txt
を開いて <C-6>か<C-^> で foo.txt
に戻れます。
ちなみに自分は <C-6> ではできませんでした。
もっと詳しく知りたい方は :h CTRL-^
を参照してください。
開発環境をArch Linuxにした
自分の開発マシンはMacなんですが開発自体はほとんどをvirualbox上のubuntuでしています。 なんとなくarchlinuxを試してみたかったので入れてみました。
vagrant init 'general/arch' vagrant up
でいれました。
所感
省メモリ
ubuntuのときの消費メモリを覚えてないのでなんとも言えませんが、デフォルトだとめちゃ省メモリですね。htopで起動しているプロセスを見たらubuntuよりも遥かに少ないのでそのせいかもしれません。
vimが速い(気がする)
vimが速い気がします。ものすごく満足です。
yay(pacman) なんでもある
とりあえず yay -S <パッケージ名>
で叩けば大体入りますね。ubuntuだとちょくちょくソースからビルドが必要だったり、go getでないとだめだったりした気がします。あと速い。
速い
他の部分でも言ってますが、大体のものが速い(気がする)。
デーモン勝手に起動しない
ubuntuだと apt install
したら大体のデーモンは立ち上がってた気がしますが、archだと自分で立ち上げないとだめなんですね。面倒ですが、勝手に立ち上がるよりはいいかなーと思ってます。
おわり
しばらくメインの開発環境として使っていこうと思います。
テストを書くタイミング
「テストは時間がないから後で」
こんな言葉を聞くことは最近は減ったかもしれません。しかし今もテストは往々にして後回しにされる、そもそも書かれない、などといったことがやはり多くあるように感じます。言い換えればアプリケーションコードと比べて明らかに軽視されがちです。そんな後回しにされがちなテストコードにも書かれるのに適したタイミングというものがあります。
これから書くのはすべて私の個人的な考えになるので、必ずこれが正しいとは思わないでください。
テストを書くのに適したタイミング
結論からいうと、直前 か 直後 です。ついでに一緒に言ってしまいますが、テストを書くのに適した人間は 実装者本人 です。殆どの場合はこれが適したタイミングと人間になると思います。
直前
おそらく最も適していると思うタイミングがここです。「なんだ、TDDしろって言いたいのか」と思ったかもしれませんが、そういうことではありません。ついでに説明しておくと実装の前にテストを書くことがTDDというわけではないということだけ言っておきます。
あるメソッドや機能を作る場合に当然ですがまず存在するのはコードではありません。目的 です。後に説明しますが、この目的に近いほど適していると考えています
直後
次に適していると思うのが実装直後です。もう少し具体的に言うならば、目的に沿ったコードを書いた直後です。
なぜ直前、直後なのか
実装後、例えば1週間後にテストコードを書くとしましょう。その開発者はその1週間の間に同じプロジェクトの別メソッドのコード、別プロジェクトのコードなどを当然書くでしょう。そして1週間前に実装した自分のコードのテストを書くとします。そのときに開発者はそのメソッドを実装した目的を正しく覚えているでしょうか?そんなことを覚えていなくてもコードを見ればテストは書けると思うかもしれません。そこで1つ大げさかもしれない例をあげてみます。あるメソッドがありそのメソッドが下の図のような状態になっているとします。
目的と実装がずれていますね。このコードはおそらくバグを引き起こすでしょう。ここで、テストコードを追加することにします。この実装のコードを見て書いたテストは「a,bの積を返す」ことをテストするテストコードになるでしょう。つまり、目的とは違ったテストをパスするテストを追加することになります。これほど単純な例だと実感がわかないかもしれませんが、1週間のうちに無数のコードを見て、書いた結果、1週間前のコードが何をしているかはわかるが、なんのために存在するかはわからなくなってしまうものです。 今回の例でいうと、「a,bの積を返しているメソッド」ことはわかるが「a,bの和を返すためのメソッド」であることは忘れてしまっているという状態です。これが1週間ならまだいいですが、1ヶ月後、2ヶ月後になると最悪で、なにをしているかすらわからなくなってしまいます。こうならないうちにテストを書くのはできるだけ早いほうがいいのです。つまり直前が最も適していて、時間が経てば立つほど基本的にテストコードの質は下がっていきます。
実装者本人
もう分かると思いますが、テストコードを書くのに適した人間は多くの場合は実装者本人です。これも同じ理由で、実装者本人が目的を一番理解している人間のハズだからです。他人の書いたコードが何を目的としているかを正しく理解するのは無理ではないですが、それを理解するよりも最も理解しているであろう人間がテストコードを書いたほうが効率的です。
おわり
思ったことを書きなぐったので、拙い部分も多いと思いますが、結局言いたいのはテストはできるだけ 早く 実装者本人 が書きましょうということでした。
VeeValidateで子供のコンポーネントまでvalidateする
※3.0.3が出てるので、できればそちらを使いましょう。
バージョン
- VeeValidate: 2.2.15
VeeValidateで下のコードのようなバリデーションを行おうと思ったときにうまく動きませんでした。
<!-- EmailField.vue --> <template> <div> <input v-model="user.email" v-validate="'required|email'" name="email"> </div> </template> <script> export default { props: { user: { type: Object, required: true } } }; </script>
<!-- App.vue --> <template> <div id="app"> <form> <input v-model="user.name" v-validate="'required'" name="name"> <email-field :user="user"></email-field> <button @click="submit">submit</button> </form> </div> </template> <script> import EmailField from "./components/EmailField"; export default { name: "App", components: { EmailField }, data() { return { user: { name: "", email: "" } }; }, methods: { submit() { this.$validator.validate().then(valid => { if (valid) { window.console.log("success"); } else { window.console.log("failure"); } }); } } }; </script>
具体的に何がうまく行かなかったかというとemailが不正な値でもconsoleにsuccessが表示されてしまいました。
どうやら this.$validator.validate()
は自分の要素だけを検証してPromiseを返すようです。ということで下のようにして解決しました。
<template> <div id="app"> <form> <input v-model="user.name" v-validate="'required'" name="name"> <email-field :user="user" ref="emailField"></email-field> <button @click="submit">submit</button> </form> </div> </template> <script> import EmailField from "./components/EmailField"; export default { name: "App", components: { EmailField }, data() { return { user: { name: "", email: "" } }; }, methods: { submit() { Promise.all([ this.$validator.validate(), this.$refs.emailField.$validator.validate() ]) .then(valid => { if (valid.every(e => e)) { window.console.log("success"); } else { window.console.log("failure"); } }); } } }; </script> <style> </style>
変更点は <email-field>
にref="emailField"
をつけたのと、submit()
の中身です。
Promise.all()
を使用してそれぞれの要素で validate()
メソッドを呼びそれらがすべて成功したときのみを成功とすることで正しく処理できました。
書き終わったあとに気づきましたが VeeValidate の version3 出てたので新しく作る場合は新しいものを使いましょう。