24時間でClojureテストスクリプトを書き換える

Habrahabrの読者に、CircleCIの創設者からの記事「24時間でClojureでテストスイートを書き換える」の無料翻訳を提供します。







画像






このストーリーは、 CircleCIテストスイート (14,000行)を24時間で別のテストライブラリに自動的に変換するコンパイラーを作成した方法に関するものです。







これまでのところ、このテストスイートはおそらく世界最大のClojureの1つです。 サーバーコードは100%Clojureです。これには、140個のファイルに14,000行、5,000個のアサーションで構成されるテストが含まれます。 並列化しない場合、実行には40分かかります。







この冒険の初めに、すべてのテストは、 RSpecに似たBDDテスト用のライブラリであるMidje作成されました。 私たちはMidjeに特に満足していなかったため、おそらく最も広く使用されているテスト用ライブラリであるclojure.testに切り替えることにしました。 clojure.test



よりシンプルで魔法が少なく、ツールとプラグインのエコシステムがより発達しています。







明らかに、5000のテストを手で書き換えることは実用的ではありません。 代わりに、Clojureに組み込まれているメタプログラミング機能を使用して、Clojureを使用してそれらを自動的に書き換えることにしました。







Clojureはホモ-イコニックです-これは、任意のコードをデータ構造として表現できることを意味します。 当社の翻訳者は、各テストファイルをClojureデータ構造に変換します。 次に、コードを変換し、結果をディスクに書き込みます。 記録されたら、テストを実行できます。テストに合格すると、ファイルをバージョン管理システムに自動的に追加し直すこともできます。これはすべて、REPLを離れることなく行われます。







読書



変換全体の鍵は、 read



機能です。 read-string



は、Clojureに組み込まれた関数で、Clojureコードを含む文字列を受け取り、それをデータ構造として返します。 ソースファイルをロードするときに、コンパイラが同じ関数を使用します。 例: (read-string "[1 2 3]")



[1 2 3]



を返します。







read



を使用して、テストコードを通常のClojureコードで変更できる大きなネストされたリストに変換します。







変換



私たちのテストはmidje



で書かれてmidje



、それらをclojure.test



に変換したいと考えていclojure.test



midje



を使用したテスト例:







 (ns circle.foo-test (:require [midje.sweet :refer :all] [circle.foo :as foo])) (fact "foo works" (foo x) => 42)
      
      





clojure.test



を使用して変換されたバージョン:







 (ns circle.foo-test (:require [clojure.test :refer :all])) (deftest foo-works (is (= 42 (foo x))))
      
      





変換には以下の置換が含まれます。









変換は、深さのあるツリーへの単純なウォークです。







 (defn munge-form [form] (let [form (-> form (replace-midje-sweet) (replace-foo) ...)] (cond (or (list? form) (vector? form)) (-> form (replace-fact) (replace-arrow) (replace-bar) ... (map munge-form))) :else form))
      
      





動作->



、RubyやJQuery、またはBashのパイプでのチェーンに似ています。関数呼び出しの計算結果を引数として次の関数呼び出しに渡します。







最初の部分(let [form ...])



はClojure形式を取り、各変換関数をそれに適用します。 2番目の部分は、他のClojureの式と関数を表すフォームのリストを取り、それらを再帰的に変換します。







置換機能では興味深いプロセスが行われます。 これらはすべて次のようになります。







 (if (this-form-is-relevant? form) (some-transformation form) form)
      
      





つまり、送信されたフォームが置換基準を満たしているかどうかを確認し、満たす場合は必要に応じて変換します。 たとえば、 replace-midje-sweet



は次のようになります。







 (defn replace-midje-sweet [form] (if (= 'midje.sweet form) 'clojure.test form))
      
      





矢印



Midjeのテストの構文全体は「矢印」を中心に展開します。これは、MidjeがBDDスタイルのテストの宣言的性質を強化するために使用する非イデオロギーの構成です。 簡単な例:







 (foo 42) => 5
      
      





(foo 42)



が5を返すことを確認します。







どの矢印が使用され、どのタイプが矢印の反対側にあるかによって、多数の異なる動作が異なります。







 (foo 42) => map?
      
      





上記の例でmap?



する場合 関数である場合、この関数を式の左側に適用した結果がtrue(真-nilまたはfalseと等しくない)であるかどうかがチェックされます。 Clojureでは、これは次のようになります。







 (map? (foo 42))
      
      





Midjeシューティングのいくつかの例:







 (foo 42) => falsey (foo 42) => map? (foo 42) => (throws Exception) (foo 42) =not=> 3 (foo 42) => #"hello world" ;; regex (foo 42) =not=> "hello"
      
      





交換用矢印



実際の変換では、約40のcore.matchルールが使用されます。 しかし、それらはすべて次のようになります。







 (match [actual arrow expected] [actual '=> 'truthy] `(is ~actual) [actual '=> expected] `(is (= ~expected ~actual) [actual '=> (_ :guard regex?)] `(is (re-find ~contents ~actual)) [actual '=> nil] `(is (nil? ~actual)))
      
      





(Clojureの専門家向け:読みやすさを向上させるために、上記のマクロで多くの〜 '文字を省略しました。実際の外観を確認するには、ソースを参照してください。)







ほとんどの変換は非常に簡単です。 ただし、 contains



フォームの場合contains



さらに複雑になりcontains









 (foo 42) => (contains {:a 1}) (foo 42) => (contains [:a :b] :gaps-ok) (foo 42) => (contains [:a :b] :in-any-order) (foo 42) => (contains "hello")
      
      





最後のケースは特に興味深いです。 表現のために







 (foo 42) => (contains "hello")
      
      





テストが成功する場合、まったく異なる2つの状況があります。 (foo 42)



hello要素を含むリストを返すか、サブストリングhelloを含む文字列を返す場合があります。







 "hello world" => (contains "hello") ["foo" "hello" "bar"] => (contains "hello")
      
      





一般に、 contains



フォームは自動変換のために複雑です。 いくつかのケースでは、実行時に追加情報が必要です(最後の例として) (contains [:a :b] :in-any-order)



など、Clojureの多くのcontains



ケースにcontains



実装がありません。すべてのcontains



ケースを無視することにしました。 それらを自動的に翻訳しようとする代わりに、次のような「失敗した」ルールを使用します。







 [actual arrow expected] (is (~arrow ~expected ~actual))
      
      





(foo 42) => (contains bar)



(is (=> (contains bar) (foo 42)))



変換します。 Midjeからの矢印関数の定義はロードされないため、このコードはコンパイルされません。手動で修正できます。







ランタイムタイプ情報



自動変換にはさらに別の問題がありました。 2つの式がある場合:







 (let [bar 3] (foo) => bar
      
      





そして







 (let [bar clojure.core/map?] (foo) => bar
      
      





Midje矢印の解釈は、右側の式に依存します。この式は、実行時にのみ(問題なく)決定できます。 bar



文字列、数値、リスト、マップなどのデータに解決される場合、Midjeは同等性をチェックします。 しかし、 bar



が関数に解決される場合、Midje その関数を呼び出します(is (= bar (foo)))



vs (is (bar (foo)))



90%のソリューションは、元のテストの名前空間を( require



)接続し、変換プロセス中に機能を解決します。







 (defn form-is-fn? [ns f] (let [resolved (ns-resolve ns f)] (and resolved (or (fn? resolved) (and (var? resolved) (fn? @resolved)))))))
      
      





ほとんどの場合、これは正常に機能しますが、ローカル変数がグローバル変数とオーバーラップすると問題が発生します。







 (let [s [1 2 3] count (count s)] (foo s) => count)
      
      





この場合、 (is (= count (foo s)))



必要ですが、 (is (= count (foo s)))



が取得されますが、これは間違っています。 ローカル環境では、 count



は数値であり、 (3 [1 2 3])



はエラーを引き起こします。 幸い、この問題を解決するには環境内のローカル変数の定義を備えた本格的なコンパイラーを作成する必要があるため、そのような状況はほとんどありませんでした。







テスト実行



変換コードが作成されたとき、それが機能するかどうかを理解する必要がありました。 なぜなら 実行時にREPLでコードを実行します。 clojure.test



関数を使用してテストを実行するだけです(変換後)。







clojure.test



の実装は、変換プロセスと計算プロセスをリンクするのに役立ちます。 すべてのテスト関数はREPLから呼び出すことができ、さらに(clojure.test/run-all-tests)



でも意味のある値-合格したテストと失敗したテストの数を含むマップ( map



)を返します。







 {:pass 61, :test 21, :error 0, :fail 0}
      
      





REPLでテストを実行する機能により、プロセスが非常に便利になり、コンパイラとテストを変更して、すぐにフィードバックを受け取ることができます。







読書



ただし、すべてがそれほど単純に機能するわけではありません。







「リーダー」(Clojureでは、 read



機能を実装するコンパイラーの一部を指す用語)は、主にコンパイラーが使用するために、ソースファイルをデータ構造に変換するように設計されています。 コメントを削除し、マクロを展開します。これらの行を返すには、すべての差分を手動で確認する必要があります。 幸いなことに、テストにはほんのわずかしかありませんでした。 プログラミングスタイルでは、通常、コメントよりもdocstringを好み、少数のファイルにマクロを分離するため、これはあまり影響しませんでした。







くぼみ



新しいコードで慣用句をインデントするのに十分なライブラリが見つかりませんでした。 clojure.pprint



を使用しclojure.pprint



。これはおそらく利用可能な最高のライブラリですが、これはあまりうまくいきません。 このプロジェクトの一部としてそのようなライブラリを書きたいとは思っていなかったので、いくつかのファイルは非慣用的なスペースとインデントでディスクに書き戻されました。 ファイルを直接操作しているので、手動で修正できます。 そうでない場合、これには慣用的なフォーマットを理解し、データ読み取り段階でファイルと行のメタデータを考慮するツールが必要になります。







テストケースを書き直してからこの記事を公開するまでに大きな遅延がありました。 この間、 rewrite-cljがリリースされました 。 私はそれを使用しませんでしたが、一見すると、私たちがそんなに不足していたものがあります。







結果



テストファイルの約40%が私たちの介入なしに合格しました。これは、このソリューションをどれだけ高速に作成したかを考えると、本当に素晴らしいことです。 残りのファイルでは、テストアサーションの約90%が変換され、合格しました。 すべてのファイルのアサーションの合計94%が自動的に変換されました-素晴らしい結果です。







コードは、GitHubのこちらにあります 。 使用するかどうかをお知らせください 。 なぜなら 特にコメントとマクロのため、制御されていない変換にはお勧めしません。 このコードは、制御されたプロセスの一部としてCircleCIでうまく機能しました







翻訳者から。 助けてくれてありがとう: comercSourcechort409およびartemyarulin

タイトル画像ソース








All Articles