Journey

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

ActiveRecordのtakeメソッドがテスタブルだった

Railsにおいてある特定個数の要素がほしいときに以下のようなコードをよく書くと思います。

Article.most_recently.limit(5)

ただこのように書いてしまうと、 limitrailsのメソッドのためテストコードが書きにくくなってしまいます。具体的な例を上げると、これをコントローラーのユニットテストをしようとしたときにスタブにするコードが以下のようなものになります。

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

railstakeソースコードを見ると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モードで動かしてみようと思います

docs.docker.com

※実験的な機能なので本番環境などで使用する場合は注意してください

環境

❯ 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

前提条件

  • newuidmapnewgidmap が必要なみたいです。筆者は入ってました。入ってない人は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つ大げさかもしれない例をあげてみます。あるメソッドがありそのメソッドが下の図のような状態になっているとします。

f:id:ippachi1218:20190912015326p:plain

目的と実装がずれていますね。このコードはおそらくバグを引き起こすでしょう。ここで、テストコードを追加することにします。この実装のコードを見て書いたテストは「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 出てたので新しく作る場合は新しいものを使いましょう。