メドピア開発者ブログ

集合知により医療を再発明しようと邁進しているヘルステックカンパニーのエンジニアブログです。読者に有用な情報発信ができるよう心がけたいので応援のほどよろしくお願いします。

メドピア開発合宿でVue.jsテストライブラリ「vue-function-tester」を作った話

涙の数だけ強くなるフロントエンドエンジニア村上(@pipopotamasu)です。

先週の水木金とメドピア恒例の開発合宿 in 熱海に行ってきたので、そこで作ったVue.jsのテストライブラリ「vue-function-tester」を紹介したいと思います。

github.com

f:id:ec0156hx39:20191121182751j:plain
atami

vue-function-testerとは

vue-function-testerは、Vue.jsの「メソッドの単体テストライブラリ」として作りました。
もっと具体的に言うと、Vue.jsのmethods, lifecycle hooks, computed, watchをテストするためのライブラリです。
※ちなみに「vue-method-tester」にしなかったのはmethodsプロパティと被ってややこしくなるのでやめました

Get started

長々と解説するよりは実際のコードをみて行きましょう。
テスト対象のコンポーネントとして、簡単な検索ボックスのサンプルコードを用意しました。

// SearchBox.vue
export default Vue.extend({
  data () {
    return {
      query: '',
      error: ''
    }
  },
  methods: {
    search () {
      this.error = ''
      if (this.query === '') {
        this.error = "Please input."
      } else {
        this.$emit('search', this.query)
        this.query = ''
      }
    }
  }
})

上記のsearchメソッドに対して、vue-function-testerでテストを書くと以下のようになります
(※ jest依存ライブラリなのでjestのテストコードになります。余談ですがメドピアのフロントエンドはjestでバシバシテストコードが書かれています。)

# SearchBox.spec.js
import SearchBox from "@/components/SearchBox.vue";
import { methods } from "vue-function-tester";

describe("search()", () => {
  const { search } = methods(SearchBox);

  describe("no query", () => {
    it("show error", () => {
      expect(search().run({ query: "", error: "" }).error).toBe("Please input.");
    });
  });

  describe("with query", () => {
    it("emits query", () => {
      const result = search().run({ query: "test", error: "" });
      expect(result.$emit).toBeCalledWith("search","test");
      expect(result.query).toBe("");
    });
  });
});

上記のようにsearchメソッドに対するテストを書くことができます。 lifecycle hooksやcomputedのテストに関しては以下をご覧ください。

https://github.com/pipopotamasu/vue-function-tester#lifecycle-hooks https://github.com/pipopotamasu/vue-function-tester#computed

vue-function-testerを作った背景

ただなんとなく作りたかっただけです。深い理由はありません。

(上記だとブログの長さ的に寂しくなりすぎるので、理由を無理やり捻り出しました。)

vue-function-testerを使わなくてもVueコンポーネントのメソッドのテストを書くことは可能です。 例えば...

  • vue-test-utilsを使用する
  • メソッド内の処理を別の関数として切り出してそちらをテストする
  • VueオブジェクトもしくはVueコンストラクタからメソッドの参照を取り出しテストする

ざっと考えついただけ3つの方法がありますが、どれも良い面もあればそうでない面もあり、個人的にときめくものではありませんでした。

vue-test-utils

皆さんご存知の通り、既存のVue.jsのテストライブラリとして「vue-test-utils」という有名なライブラリがあります。
とても便利なライブラリで私ももちろん愛用させてもらっています。

しかしこれは「Vueコンポーネント」のテストにフォーカスしたライブラリです。 常日頃開発している身としては、「Vueコンポーネントのメソッド」にフォーカスしてテストを書きたい時にちょくちょく出くわします。
その場合、vue-test-utilsだとテストコードが冗長になってしまうことがあります。

  • テストの度に毎回mountが必要
  • mount時に初期設定が必要な場合がある
    • テストしたいメソッドとは関係のないpropsを用意する
    • created hooks用の設定
    • etc...

等々、Vueコンポーネントのメソッドテストをときめく感じに書きたいという欲を満たすことができませんでした。

VueオブジェクトもしくはVueコンストラクタからメソッドの参照を取り出しテストする

上記のサンプルコードを例にすると以下のような感じになります。

# SearchBox.spec.js
import SearchBox from "@/components/SearchBox.vue";

describe("search()", () => {
  const { search } = (SearchBox as any).options.methods;

  describe("no query", () => {
    it("show error", () => {
      const context = {
        query: "",
        error: ""
      }
      search.call(context);
      expect(context.error).toBe("Please input.");
    });
  });

  describe("with query", () => {
    it("emits query", () => {
      const context = {
        query: "test",
        error: "",
        $emit: jest.fn()
      }
      search.call(context);
      expect(context.$emit).toBeCalledWith("search", "test");
      expect(context.query).toBe("");
    });
  });
});

テスト自体は普通に書けるのですが、若干冗長さを感じてしまいます。え、私だけ...? 特に上記のコードで冗長と感じるのは以下の2点です。

  • thisのcontextを別変数で宣言->メソッド実行->context変数の検証と、3ステップ踏まないといけない。
  • $emitのモック作成。methodsやcomputedのsetterのテストを書いているとそこそこの頻度で$emitが出てくるのに毎回モックするのが面倒。

Vueコンポーネントのメソッドにフォーカスするという部分では問題ないのですが、上記の点でときめかない点が残りました。

メソッド内の処理を別の関数として切り出してそちらをテストする

そもそも別関数として切り出すというやり方です。
関数のインターフェースをcontextベース(this)ではなく、引数・返り値ベースでinput/outputを実装すればとてもテストしやすい関数になります。
(※ contextベースだと結局、contextを別変数で宣言->メソッド実行->context変数の検証と、3ステップ踏まないといけない)

export function sum(lfs, rhs) {
  return lfs + rhs;
}

export default Vue.extend({
  data() {
    return {
      lfs: 1,
      rhs: 2,
      result: 0
    };
  },
  methods: {
    sum() {
      this.result = sum(this.lfs, this.rhs);
    }
  }
});

簡単すぎる例ですが、上記のsum関数なら容易にテストできます。
特にロジックが長くなってくると、Vueコンポーネントのメソッドとして定義しておくとコンポーネント全体の視認性が悪化するので上記のような分割はとても良い手段です。

ただテストのために全てのロジックを外部の関数に切り出すのはどうでしょうか? そもそも切り出すのがめんどくさいですし、切り出した関数のテストが正でも対象のVueコンポーネントのメソッドが正常に動くとは限りません。

こちらの手段も若干ときめかない部分が残りました。

vue-function-testerのときめくポイント

ではどうすればときめくのか、それはコードを短く書くことではないかと自問自答し、以下のコードを短くするときめきポイントを実装しました。

ときめきポイントその1: メソッドチェーン

以下のようにメソッドチェーンで繋げることで、ワンライナーで検証できるようにしました。

import { methods } from "vue-function-tester";
const { search } = methods(SearchBox);

expect(search().run({ query: "", error: "" }).error).toBe("Please input.");

ときめきポイントその2: alias

さらにaliasを貼り文字数を削減。

import { methods } from "vue-function-tester";
const { search } = methods(SearchBox);

// 通常
expect(search().run({ query: "", error: "" }).error).toBe("Please input.");
// aliasその1
expect(search().r({ query: "", error: "" }).error).toBe("Please input.");
// aliasその2
expect(search.run({ query: "", error: "" }).error).toBe("Please input.");
// aliasその3
expect(search.r({ query: "", error: "" }).error).toBe("Please input.");

※メソッドに引数を与えなければならない時はaliasその2とその3は使えません

ときめきポイントその3: $emit

使用頻度の高い$emitを事前にモック化

import { methods } from "vue-function-tester";
const { search } = methods(SearchBox);

expect(search.r({ query: "test", error: "" }).$emit).toBeCalledWith("search","test");

終わりに

このようにメドピアの開発合宿では自作のライブラリを作ったり、他のOSSにPRを送ったり、使ったことのない技術を使って何かしらのサービスを作ってみたり等々、参加者それぞれが自由に課題を設定しコードを書きます(年2回、水木金の2泊3日)。

f:id:ec0156hx39:20191122141942j:plain

今回で14回目の開発合宿となりましたが、まだまだ今後も続けていくので開発合宿をしたくなったら是非メドピアへ!


(☝︎ ՞ਊ ՞)☝︎是非読者になってください


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html