データアクセスのUnitTestについて考える

問題

DBに登録したり、ファイルに書いたり、保存するような処理のテストについて考える。

インターフェースを抽象化すると、下記のような感じになる。

public interface ItemRepository {
    // itemを保存する
    void save(Item item);
}

この場合、saveのユニットテストは「itemが保存されていること」になるが、どうやってテストしたらいいだろう?

考えられる選択肢

考えられる選択肢はいくつかある。

実体を見に行く 🤔

たとえば保存先がデータベースならデータベース、ファイルならファイルを直接参照しに行く形。

Spockであれば、Groovy.Sqlみたいなクラスを使って、テストDBにクエリを発行して結果を確認する。

ただこの手段だとどうしても実装量が増えてしまうし、テストのためのコードの品質が気になってくる。

また、実装のテストなのかインターフェースのテストなのかも曖昧になる。

実体を見に行く+ヘルパーを用意する 🤔

実体を参照するような処理をヘルパーなりライブラリなりに切り出して、ヘルパーの品質はヘルパーのテストなりで担保する方法。

実体を参照する処理は(ヘルパーに追い出したので)ある程度スッキリはする。

しかし実体を参照するために用意しているけど本当にやりたかったのは保存をするテストだったはずで、何をやりたかったのか分からなくなってくる。

取得のインターフェースを用意する 🤔

save(item) した後に findById(id) のように、取得のインターフェースを作成する。

しかしこうするとsaveのテストをしたかったはずなのにfindByIdを作成しなければならないし、findByIdのテストはどうするんだっけ?となってくる。

「ユニット」は「メソッド」ではない

結論から言うと、取得のインターフェースを提供するのがよさそうである。

粒度として対称性のある処理が必要になる(なりそう)なのであれば、それはモジュールが果たすべき役割ではないか。

createがあるならdeleteがあるだろうし、pushがあればpopがあるだろう、といったように。

「ユニット」を「メソッド」ではなく「モジュール」と考えると、「ユニットテスト」は「モジュールの整合性が取れていること(必要なインターフェースが揃っている)」を確認するものといえる。

実際の要求ではcreateメソッドだけが必要な場合もあるが、テストで結局同等の処理を書くなら、抽象度揃えたインターフェースを提供したほうが旨味もありそうである。

まとめ

  • ユニットはメソッドではなく「モジュール」と考える
  • ユニットテストはモジュールとしての整合性(必要なインターフェースが揃っているか)を確認する役割もある
  • やりすぎはよくないが、対称性のある処理について考慮し、必要に応じてモジュールに提供させる

「決断力」を読んだ

決断力 (角川oneテーマ21)

決断力 (角川oneテーマ21)

読んだきっかけ

将棋を指さない僕でも名前は知っている羽生さん。

読んだきっかけは↓のtweet

すごくいいなぁと思って、本とかないのかなと探してみたのがきっかけ。

この話も本書に登場した。

技術書関連以外の本を読んだのは久しぶりな気がする。

決断、その前に

タイトルも「決断力」だし、目次にも「直感の七割は正しい」といった話が出てくる。

ただ本を読んでいて思ったのはその前の準備、勉強の大切さ。

それがあるからこそ、実践ではシンプルに考えること、大局を見て判断することができるのだろう。

その一方で完璧な準備は存在しないとも思うので、決断のときの心構えが大切なのかもしれない。

最先端を学ぶ

最先端を知らないと、その時点で負けてしまう。

自分の経験や力を発揮する前に勝負が終わってしまう。

最先端を学ぶことから逃げてはいけない。

羽生さんの勉強法

羽生さんの勉強法として下記が紹介されていた。

  • アイディアを思い浮かべる
  • うまくいくか調べる
  • 実戦で実行する
  • 検証、反省する

実戦で試していく、というのが大事なんだなと。

目先の勝利にこだわらずに、長期的な視点に立った場合に、敢えて自分の得意ではない形で戦ったりするなど、常に挑戦している姿勢が素晴らしいと思った。

空白の時間を作る

頭の中に、空いたスペースがないと集中できない。

ぼんやりする時間を大切にする。

継続は力なり

無理をして途中でやめてしまうくらいなら、歩みを遅くしても構わない。

毎日、少しずつ続けることが大切。

まとめ

多くの気づきがあり、モチベーションがもらえました。

将棋を分からなくても楽しめると思うし、ところどころで別業界での喩え話も出てきたりする。
羽生さんの本でKISS(Keep it simple, stupid)の話が出てくるとは思わなかった。

どんな職種の人にも通じるものがありそう。

短くてサクッと読めるし、おすすめです。

MacにVagrantでAnsibleのお試し環境を用意する #vagrant #Ansible

VirtualBoxVagrantを最新化

$ brew cask install virtualbox
$ brew cask install vagrant

VagrantUbuntuの環境を作成

Ubuntu18.04のBoxを使う。

Vagrant box ubuntu/bionic64 - Vagrant Cloud

Boxの追加

$ vagrant box add ubuntu/bionic64

追加の確認

vagrant box list

Vagrantfileの作成

適当な作業ディレクトリで、Vagrantfileを作成する。

$ mkdir -p ansible-sandbox
$ cd ansible-sandbox
$ vagrant init ubuntu/bionic64

ubuntuの起動

$ vagrant up
  • ステータスは vagrant status
  • 停止は vagrant halt
  • ログインは vagrant ssh

sshできるようにする

sshの設定を確認する。

$ vagrant ssh-config

大体こんな感じ。(Hostはansible-sandboxに変更)

Host ansible-sandbox
  HostName 127.0.0.1
  User vagrant
  Port 2222
  UserKnownHostsFile /dev/null
  StrictHostKeyChecking no
  PasswordAuthentication no
  IdentityFile ansible-sandbox/.vagrant/machines/default/virtualbox/private_key
  IdentitiesOnly yes
  LogLevel FATAL

~/.ssh/config追記してsshしてみる。

$ ssh ansible-sandbox

inventoryファイルを作成する

inventoryファイルにUbuntuホストの情報を定義する。

$ vi sandbox-hosts

Ubuntu18.04にはPython2が入っていないので、ansible_python_interpreterも設定しておく。

[sandbox]
ansible-sandbox

[sandbox:vars]
ansible_python_interpreter=/usr/bin/python3

Ansibleを実行してみる

$ ansible ansible-sandbox -i sandbox-hosts -m command -a "uptime"

参考

pyenvを使って異なるバージョンのpythonを併用する #python

pythonの最新バージョンで動かないツールがあって、やむなく古いpythonを使うためにpyenvを入れてみた。

インストール

Macなのでbrewで入れる。

$ brew install pyenv

~/.bash_profile環境変数等を設定する。

#pyenv
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
if command -v pyenv 1>/dev/null 2>&1; then
  eval "$(pyenv init -)"
fi

使えるversionを確認する

すでにpythonが入っている場合、systemだけが表示されそう。

$ pyenv versions
* system

使いたいバージョンをインストールする

使いたいバージョンを探す。

3.6系とかならこんな感じ。

$ pyenv install --list | grep 3.6.*
  3.3.6
  3.6.0
  3.6-dev
  3.6.1
  3.6.2
  3.6.3
  3.6.4
  3.6.5
  3.6.6
  activepython-3.6.0

3.6.6をインストールしてみる。

$ pyenv install 3.6.6

インストールが完了すると、使えるVersionが増える。

$ pyenv versions
* system
  3.6.6

使うVersionを切り替える

system(3.7.0)から、3.6.6に切り替える。

$ python -V
Python 3.7.0
$ pyenv global 3.6.6
$ python -V
Python 3.6.6

特定ディレクトリの配下だけ使うバージョンを切り替える

特定のディレクトリ配下でだけ使用するバージョンを変更することも可能。

$ python -V
Python 3.7.0
$ mkdir -p ./hoge
$ cd hoge
$ pyenv local 3.6.6
$ python -V
Python 3.6.6
$ cd ../
$ python -V
Python 3.7.0

pyenv local を作ると .python-version というファイルが作られる。

中身は使用するバージョンが書いてあるだけ。

参考

メソッドを持ったKotlinのEnumをGroovy(Spock)から参照する #groovy #spock #kotlin

概要

  • KotlinはJava同様Enumに振る舞い(メソッド)を持たせることができる
  • Groovyから振る舞いを持ったEnum要素を直接参照するとエラーになる
  • Direct field access operator(@)経由で参照すればOK
  • EnumのValueOfでも参照できる

振る舞いを持たないEnumはうまくいく

enum class Shape {
    CIRCLE,
    TRIANGLE,
    SQUARE,
}

// Enumをフィールドに持つクラス
class Foo(val shape: Shape)
class FooTest extends Specification {
    def "KotlinのEnumを使ったテスト"() {
        given:
        def shape = Shape.CIRCLE

        when:
        def actual = new Foo(shape)

        then:
        actual.shape == Shape.CIRCLE
    }
}

振る舞いをもたせるとエラーになる

// 振る舞いを持ったEnum
enum class Shape {
    CIRCLE {
        override fun sign(): String = "○"
    },
    TRIANGLE {
        override fun sign(): String = "△"
    },
    SQUARE {
        override fun sign(): String = "□"
    };

    abstract fun sign(): String
}

class Foo(val name: String, val shape: Shape)

GroovyRuntimeExceptionが発生してしまう。

groovy.lang.GroovyRuntimeException: Could not find matching constructor for: Foo(java.lang.Class)

どうも振る舞いをもたせるとClassになるようだ。

コンパイル結果を見るとたしかにclassファイルができている。

  • Shape$CIRCLE.class
  • Shape$SQUARE.class
  • Shape$TRIANGLE.class

メソッドも呼び出せない

def "KotlinのEnumメソッドを呼び出すテスト"() {
    expect:
    Shape.CIRCLE.sign() == "○"
}

MissingMethodExceptionが発生してしまう。

Caused by: groovy.lang.MissingMethodException: No signature of method: static Shape$CIRCLE.sign() is applicable for argument types: () values: []

Enumインスタンス経由で呼び出す

Direct field access operator(@)を使って、要素のインスタンスを使うようにするとうまくいきます。

class FooTest extends Specification {
    def "KotlinのEnumを使ったテスト"() {
        given:
        def shape = Shape.@CIRCLE

        when:
        def actual = new Foo(shape)

        then:
        actual.shape == Shape.@CIRCLE
    }

    def "KotlinのEnumメソッドを呼び出すテスト"() {
        expect:
        Shape.@CIRCLE.sign() == "○"
    }
}

valueOf経由で呼び出す

valueOfを使うようにしてもOKです。

class FooTest extends Specification {
    def "KotlinのEnumを使ったテスト"() {
        given:
        def shape = Shape.valueOf('CIRCLE')

        when:
        def actual = new Foo(shape)

        then:
        actual.shape == Shape.valueOf('CIRCLE')
    }

    def "KotlinのEnumメソッドを呼び出すテスト"() {
        expect:
        Shape.valueOf('CIRCLE').sign() == "○"
    }
}

参考

MyBatisのMapKeyはValueがクラスじゃないといけないらしい #mybatis

MyBatisでリストで取得するのは簡単

@SelectProvider(type = HogeSqlBuilder::class, method = "selectList")
override fun findAll(): List<Hoge>

MapKey を使うとSelect結果をいい感じにMapにしてくれるMyBatis。

この場合、Hoge.id がKey、ValueHoge のMapにして返してくれる。

@MapKey("id")
@SelectProvider(type = HogeSqlBuilder::class, method = "selectMap")
override fun findAll(): Map<Long, Hoge>

しかしValueはクラスじゃないといけない

たとえば値は Hoge.name だけあればいいような場合に、ValueをStringにしたいのだがそれはできないようだ。

// これはできない
@MapKey("id")
@SelectProvider(type = HogeSqlBuilder::class, method = "selectNameMap")
override fun findAll(): List<id, String>

下記のような例外が発生する。 Stringに対してアクセサを呼び出そうとしてエラーになるようだ。

org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'id' in 'class java.lang.String'

対応策

おとなしくクラスを用意するか、リストで受けたあとに自分で詰め替えるとかでしょうね。。

参考

stackoverflow.com qiita.com

KotlinでMyBatisのSqlProviderを使う #kotlin #mybatis

概要

MyBatisのSQLXMLに書いたりアノテーションに文字列で渡す方法があるが、SqlProviderを使う方法を試してみた。

これが一番好きかもしれない。

ちなみにSpringBoot(mybatis-spring-boot-starter)を利用している。

SqlProviderの使い方

クエリの種類に応じてProviderアノテーションが用意されている。

今回はInsertを例にする

  • @InsertProvider
  • @UpdateProvider
  • @DeleteProvider
  • @SelectProvider
@Mapper
interface FooRepository {
    // メソッドにProviderのアノテーション付与
    // 実SQLを提供するクラスとメソッド名を渡す
    @InsertProvider(type = MySqlBuilder::class, method = "insert")
    fun save(foo: Foo)

    // SQLの提供クラス
    // インナークラスにしているが、アウタークラスでもOK
    class MySqlBuilder {
        companion object {
            // MyBatis側で利用されるので、JvmStaticアノテーションを付与
            @JvmStatic
            fun insert(foo: Foo): String = foo.run {
                // SQLの生成はMyBatisのSQLビルダークラスを使っている
                // が、極端な話文字列を返せばいいので究極好き勝手にSQLを書ける
                SQL().INSERT_INTO("foo")
                        .VALUES("id", "#{id}")
                        .VALUES("name", "#{name}")
                        .VALUES("age", "#{age}")
                        .toString()
            }
        }
    }
}

動的なSQLの生成

MyBatisのSQLビルダークラスは動的にSQLの組み立ても可能である。

SqlProvider側でインターフェースと同じ引数を受け取れるので、必要に応じて処理を埋め込む。

SQLビルダーは無名クラスにして組み立てるようにする。

class MySqlBuilder {
    companion object {
        @JvmStatic
        fun insert(careType: CareType): String = careType.run {
            // 動的に組み立てる場合は無名クラスにする
            object : SQL() {
                init {
                    INSERT_INTO("foo")
                    VALUES("id", "#{id}")
                    VALUES("name", "#{name}")

                    // 処理を埋め込むことも可能
                    if (name.isEmpty()) {
                        VALUES("name", "'default name'")
                    }

                    VALUES("age", "#{age}")
                }
            }.toString()
        }
    }
}

参考