のぶLab.

流しのソフトウェアエンジニアの雑記帳. Android, Scala, Clojure, Ruby on Railsなど

Luminus, Reagentな環境でSpecljを導入

ClojureRSpecのような読みやすいテストを書くためのフレームワークとしてSpecljというものがあります。

ここではprofileにcljsを選択して作成したLuminus projectでClojure, ClojureScriptのspecを実行できるようにしていきます。

前提

前述したLuminus projectは以下のように作成されたprojectを指します

lein new luminus foo +cljs
cd foo
tree .
.
├── Procfile
├── README.md
├── env
│   ├── dev
│   │   ├── clj
│   │   │   └── foo
│   │   │       ├── dev.clj
│   │   │       └── repl.clj
│   │   └── cljs
│   │       └── foo
│   │           └── dev.cljs
│   └── prod
│       └── cljs
│           └── foo
│               └── prod.cljs
├── project.clj
├── resources
│   ├── docs
│   │   └── docs.md
│   ├── public
│   │   ├── css
│   │   │   └── screen.css
│   │   ├── img
│   │   └── js
│   └── templates
│       └── home.html
├── src
│   └── foo
│       ├── core.clj
│       ├── handler.clj
│       ├── layout.clj
│       ├── middleware.clj
│       ├── routes
│       │   └── home.clj
│       └── session.clj
├── src-cljs
│   └── foo
│       └── core.cljs
└── test
    └── foo
        └── test
            └── handler.clj

この方法で作成すると、React.jsのClojureScriptインターフェイスreagentやClojureScriptの変更を自動でブラウザに送信するfigwheelなど便利なlibraryを導入してくれます。 (これらの詳しい説明についてはネット上にたくさん転がっているので、それらをご覧ください。)

また、この方法で作成したディレクトリはsrc, src-cljsの2種類のソースコードを格納するディレクトリがあって今ひとつな気がするので、 ここではspecの環境構築と併せて以下のようなディレクトリ構成にしていきます。

src
├──clj
└──cljs
spec
├──clj
└──cljs

ClojureのSpec

まずclojure向けのspecを構成していきます 現在src直下にcljファイルの格納されているfooディレクトリがあるので、これを以下のようにclj配下に持っていきます

src
└──clj
   └── foo

ついでにspecファイルを格納するディレクトリも追加しておきます 以下のような構成でディレクトリを作成してください。

spec
├──clj
│ └── foo
└──cljs
   └── foo

次にproject.cljを編集してSpecljの依存関係とtest pathを追加します

...

:plugins [[lein-ring "0.9.1"]
            [lein-environ "1.0.0"]
            [lein-ancient "0.6.0"]
            [ragtime/ragtime.lein "0.3.8"]
            [lein-cljsbuild "1.0.4"]
            [speclj "3.2.0"]]   ; <- これを追加

:source-paths ["src/clj"]  ; <- これを追加
:test-paths ["spec/clj"]  ; <- これを追加


:dev {:dependencies [[ring-mock "0.1.5"]
                        [ring/ring-devel "1.3.2"]
                        [pjstadig/humane-test-output "0.7.0"]
                        [leiningen "2.5.1"]
                        [figwheel "0.2.5"]
                        [weasel "0.6.0"]
                        [speclj "3.2.0"]  ; <- これを追加
                        [com.cemerick/piggieback "0.1.6-SNAPSHOT"]]
...

一旦ライブラリをインストールしておきましょう$ lein deps

続いてspec/clj/foo/core_spec.cljというファイルを作成して、以下のようなダミーのテストを記述します

(ns foo.core-spec
  (:require [speclj.core :refer :all]
            [foo.core :refer :all]))

(describe "Truth"

  (it "is true"
    (should true))

  (it "is not false"
    (should-not false)))

(run-specs)

これで以下のコマンドを実行するとSpecljによるテストが実行されるはずです

$lein spec spec/clj

ClojureScriptのSpec

続いてClojureScriptのSpecです ClojureScriptのspecにはphantomjsが必要ですのでインストールしておいてください。

またここでは PhantomJS 1.9.8での動作を想定しています

(※ PhantomJS 2.0ではうまく動作しないようです。)

luminusによってClojureScriptはsrc-cljsに格納されているのでこれをsrc/cljsに移動します。 (src-cljsのままでいい場合はこの作業は不要です)

src
├──clj
│ └── foo
└──cljs
   └── foo

ディレクトリを移動したらproject.cljでsrc-cljsとなっている部分は全てsrc/cljsと変更しておいてください。

次にproject.cljにテスト向けのbuild情報を記述します。

devのprofile情報に対して以下のように変更してください

:cljsbuild {:test-commands {"spec" ["phantomjs" "bin/speclj" "resources/public/js/app_test.js"]}  ; <- これを追加
                     :builds {:app {:source-paths ["env/dev/cljs"]}
                              :test {:source-paths ["src/cljs" "spec/cljs"]    ; <- これを追加
                                     :compiler {:output-to     "resources/public/js/app_test.js"   ; <- これを追加
                                                :output-dir    "resources/public/js/test"   ; <- これを追加
                                                :source-map    "resources/public/js/test.js.map"   ; <- これを追加
                                                :externs ["react/externs/react.js"]   ; <- これを追加
                                                :optimizations :whitespace   ; <- これを追加
                                                :pretty-print  false}}}}   ; <- これを追加

project rootにbin/specljというファイルを作成して以下のように記述します (※ Speclj公式のtutorialに対してReactの使用も考慮に入れた変更を加えています)

#! /usr/bin/env phantomjs

var fs = require("fs");
var webpage = require('webpage').create();
var system = require('system');
var url = phantom.args[0];

webpage.onConsoleMessage = function (x) {
  system.stdout.write(x)
};

webpage.injectJs("env/test/js/polyfill.js");
// note the polyfill file, this is added to play nice with React

webpage.injectJs(url);

var result = webpage.evaluate(function () {
    speclj.run.standard.armed = true;
    return speclj.run.standard.run_specs(
        cljs.core.keyword("color"), true
    );
});

phantom.exit(result);

env配下にtest/js/polyfill.jsというファイルを作成し以下のように編集します。 (phantomjsでcomponentを扱うために必要みたい(適当))

if (!Function.prototype.bind) {
  Function.prototype.bind = function(oThis) {
    if (typeof this !== 'function') {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }

    var aArgs   = Array.prototype.slice.call(arguments, 1),
        fToBind = this,
        fNOP    = function() {},
        fBound  = function() {
          return fToBind.apply(this instanceof fNOP && oThis
                 ? this
                 : oThis,
                 aArgs.concat(Array.prototype.slice.call(arguments)));
        };

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();

    return fBound;
  };
}

spec/cljs/foo/core_spec.cljsを作成して以下のようにダミーのspecを記述します

(ns foo.core-spec
  (:require-macros [speclj.core :refer [describe it should should-not run-specs]])
  (:require [speclj.core]
            [foo.core :as my-core]))

(describe "Truth"

  (it "is true"
    (should true))

  (it "is not false"
    (should-not false)))

(run-specs)

以下のようにClojureScriptをcompileして、testタスクを実行したらSpecljによるClojureScript向けのspecが実行されるはずです

$ lein cljsbuild once
$ lein cljsbuild spec

これでclojureでの快適なTDD/BDD環境が整いました!

こんなエラーが出たら

以下のようなエラーが出たらPhantomjsがReactのcomponent操作などに対応できていない可能性があります。

TypeError: 'undefined' is not a function (evaluating 'ReactElementValidator.createElement.bind(
      null,
      type
    )')

おそらくtest/js/polyfill.jsかbin/specljの設定がうまくできていないのでしょう。

polyfill.js or bin/specljを見なおして見てください。