将来を見据えたウェブ・アプリケーションの構築:The Codestのエキスパート・チームによる洞察
The Codestが、最先端技術を駆使してスケーラブルでインタラクティブなウェブアプリケーションを作成し、あらゆるプラットフォームでシームレスなユーザー体験を提供することにどのように秀でているかをご覧ください。The Codestの専門知識がどのようにデジタルトランスフォーメーションとビジネス...
Codestは主にRubyのショップですが、私たちが構築している多くのプロジェクトの1つはJavaScriptです。これはクライアント・サイド・ライブラリで、かなり難しい環境で動作する。非常に古いものも含めて、現存するほとんどすべてのブラウザをサポートしなければならないし、その上、たくさんの外部スクリプトやサービスとやりとりする。とても楽しい。
上記のような要件には、クライアントサイドでは通常存在しない一連の課題が伴う。 プロジェクトそして、それらの問題の1つがテストに関係している。もちろん、私たちは非の打ちどころのないユニットテストのセットを持っており、CI/CD環境で非常に大きなブラウザ/オペレーティング・システムの組み合わせに対してそれらを実行しているが、それだけではうまくいかない可能性をすべて探ることはできない。
我々のエコシステムの包括的なアーキテクチャのため、いくつかの外部ライブラリーは我々のライブラリーと一緒にロードされることに依存している。 コードできないし、どうしようもない。というのも、そのようなライブラリは興味深い課題を提示しているからだ:
これは、単体テストが十分でない理由を明確に示している。ある外部ライブラリの公開APIの一部を、そのAPIのドキュメントで発見した内容に基づいてモックアップし、それに対して単体テストを実行したとしよう。それで何が証明できるだろうか?
外部ライブラリのAPIで動作するということだ」と言いたくなるかもしれないが、それは悲しいかな間違いだ。外部ライブラリのパブリックAPIのサブセットと正しく相互作用するという意味でしかなく、それも私たちがモックアップしたバージョンのみである。
もしライブラリが文字通り私たちの足元から変わってしまったら?もし野放しになっているときに、奇妙な反応が返ってきて、文書化されていない別のコード・パスにぶつかったら?私たちはそれを防ぐことができるのだろうか?
100%ではない。しかし、私たちのコードに起こるかもしれない一般的な例を使って、すべてが想定通りに動くことを合理的に確認することはできる。単体テストは、私たちのコードが内部的に正しく動作することを保証し、統合テストは、私たちがコントロールできないライブラリと正しく「会話」することを保証する必要がある。それも、ライブラリのスタブではなく、実際の生きたライブラリとだ。
のために利用可能な統合テストフレームワークのひとつを使えばいい。 JavaScriptシンプルなHTMLページを作成し、私たちのライブラリとリモート・ライブラリの呼び出しをいくつか投げて、それをうまく使ってみる。しかし、CI/CD環境によって生成されたコールでリモートサービスのエンドポイントを氾濫させたくはない。それは、いくつかの統計情報を混乱させ、いくつかのものを壊してしまう可能性がある。
しかし、これほど複雑な統合テストは可能なのだろうか?私たちはRubyが何よりもまず好きなので、専門知識を頼りに、Rubyプロジェクトでリモート・サービスとの統合テストを通常どのように行うかについて考え始めた。例えば ビデオデッキ 何が起きているかを一度記録し、必要なときにいつでもテストに再生し続けることができる。
内部的には、vcrはリクエストをプロキシすることでこれを実現している。これが私たちのハッとした瞬間だった。私たちは、"本当の "インターネット上の何もヒットしないはずのすべてのリクエストを、いくつかのスタブ・レスポンスにプロキシする必要があった。そうすれば、その受信データは外部ライブラリに渡され、コードは通常通りに実行される。
複雑そうなプロトタイプを作るとき、時間を節約する方法としてRubyに頼ることがよくある。私たちは、(おそらく)JavaScriptでもっと複雑なものを作ることにコミットする前に、プロキシのアイデアがどの程度うまくいくかを確認するために、RubyでJavaScript用のテストハーネスのプロトタイプを作ることにした。その結果、驚くほどシンプルであることが判明した。実際、とてもシンプルなので、この記事で一緒に作ってみようと思う。
もちろん、私たちは「本物」を扱うわけではない。私たちが作っているものを少し説明するだけでも、ブログ記事の範囲をはるかに超えている。私たちは、問題のライブラリの代わりとなるものを素早く簡単に作り、その後Rubyの部分にもっと集中することができる。
まず、我々が扱っている外部ライブラリの代わりになるものが必要だ。外部サービスにコンタクトし、あちこちでイベントを発生させ、そして何よりも、簡単な統合を念頭に置いて構築されていないことが重要だ🙂。
使うのはこんな感じだ:
/* global XMLHttpRequest, Event */
const URL = 'https://poloniex.com/public/?command=returnTicker'
const METHOD = 'GET'
module.exports = {
fetch: function () {
var req = new XMLHttpRequest()
req.responseType = 'json'
req.open(METHOD, URL, true)
var doneEvent = new Event('example:fetched')
req.onreadystatechange = function (aEvt) {
if (req.readyState === 4) {
if (req.status === 200) {
this.data = req.response
} else {
this.error = true
}
window.dispatchEvent(doneEvent)
}
}.bind(this)
req.send(null)
},
error: false,
data: {}
}
このAPIはサンドボックスを公開しないし、レートも制限されているので、テストで実際にヒットさせるべきでないものの典型例となる。
通常扱うスクリプトは簡単にバンドルできるようにNPMで利用できないことをほのめかしたが、これは実際にはNPM互換のモジュールであることにお気づきだろう。このデモンストレーションでは、ある特定の動作を示すだけで十分であり、ここではむしろ、単純化しすぎることを犠牲にしてでも、説明しやすさを重視したい。
さて、ライブラリーの代わりになるものも必要だ。ここでも要件はシンプルにする。「外部」ライブラリーを呼び出して、その出力で何かをする必要がある。テスト可能な」部分をシンプルに保つために、コンソールへのロギングと、グローバルに利用可能な配列へのロギングの両方を行うようにします。
window.remote = require('remote-calling-example')
window.failedMiserably = true
window.logs = [].
関数ログ (メッセージ) {
window.logs.push(メッセージ)
console.log(message)
}
window.addEventListener('example:fetched', function () {)
if (window.remote.error) {
log('[EXAMPLE] リモートフェッチに失敗しました')
window.failedMiserably = true
} else {
log('[EXAMPLE] リモート・フェッチ成功')
log([EXAMPLE] BTC to ETH: ${window.remote.data.BTC_ETH.last})。
}
})
window.remote.fetch()
また、意図的に動作は驚くほどシンプルにしている。このままでは、実際に興味深いコードパスが2つしかないので、ビルドが進むにつれて雪崩をうって仕様に振り回されることはないだろう。
簡単なHTMLページを作ってみよう:
<code> <!DOCTYPE html>
<html>
<head>
<title>ページ例</title>
<script type="text/javascript" src="./index.js"></script>
</head>
<body></body>
</html>
このデモでは、HTMLとJavaScriptを次のようにバンドルする。 小包非常にシンプルである。 ウェブアプリ bundler。手っ取り早くサンプルをまとめたり、ナプキンで思いついたクラスのアイデアをハックしたり。Webpackを設定するほうが、書きたいコードを書くよりも時間がかかるような単純なことをするときには、Parcelは最高だ。
また、邪魔にならないので、もう少しテストされたものに切り替えたいときに、Parcelからほとんど後退する必要がない。しかし、注意点として、Parcelは開発が進んでいるため、問題が発生する可能性があります。 Node.js.結論:まだ生産パイプラインの一部にする必要はないが、それでも試してみてほしい。
これでテストハーネスを構築できる。
スペック・フレームワーク自体には スペック.開発環境では、ヘッドレスでない実際のChromeを使用してテストします。 ワチアー (そして信頼できる相棒watir-rspec)。プロキシには パフ・ビリー そして ラック を追加した。最後に、スペックを実行するたびにJavaScriptビルドを再実行したい。 コカイン.
そのため、スペック・ヘルパーはこの単純な例でさえも、少々...複雑になっている。それを分解して見てみよう。
Dir['./spec/support/*/.rb'].each { |f| require f }.
TEST_LOGGER = Logger.new(STDOUT)
RSpec.configureを行う |config|
config.before(:suite) { Cocaine::CommandLine.new('npm', 'run build', logger: TEST_LOGGER).run }.
config.include Watir::RSpec::Helper
config.include Watir::RSpec::Matchers
config.include ProxySupport
config.order = :random
ブラウザサポート.configure(config)
終了
Billy.configureは|c|を行う
c.cache = false
c.cacherequestheaders = false
c.persistcache = false
c.recordstubrequests = true
c.logger = Logger.new(File.expandpath('../log/billy.log', FILE))
終了
スイート全体の前に、cocaineを通してカスタムビルドコマンドを実行している。TEST_LOGGER定数は少しやりすぎかもしれないが、ここではオブジェクトの数をあまり気にしていない。もちろん、specをランダムな順番で実行するので、watir-rspecのすべてのグッズを含める必要がある。また、Billyがキャッシュを行わず、ロギングを広範囲に行うように設定する必要がある。 spec/log/billy.log
.リクエストが実際にスタブされているのか、ライブサーバーに当たっているのか(おっと!)わからない場合、このログはまさに金です。
あなたの鋭い目は、すでにProxySupportとBrowserSupportを見つけたことでしょう。私たちのカスタムグッズはこの中にあると思うかもしれない...まさにその通りだ!まずはBrowserSupportが何をするのか見てみましょう。
まずは、次のことを紹介しよう。 テンプブラウザ
:
クラス TempBrowser
def get
ブラウザ ||= Watir::Browser.new(web_driver)
終了
def kill
if @browser.close if @browser
ブラウザ = nil
終了
プライベート
def web_driver
Selenium::WebDriver.for(:chrome, options: オプション)
終了
def オプション
Selenium::WebDriver::Chrome::Options.new.tap do |options|.
options.addargument '--auto-open-devtools-for-tabs'を追加します。
options.addargument "--proxy-server=#{Billy.proxy.host}:#{Billy.proxy.port}"
終了
終了
終了
コールツリーを逆算すると、Chrome用のSeleniumブラウザオプションセットをセットアップしていることがわかる。ChromeインスタンスにPuffing Billyインスタンスを通してすべてをプロキシするように指示します。もう一つのオプションは、持っていると便利なものです。 ヘッドレス をクリックすると、検査ツールが自動的に開きます。一日にCmd+Alt+Iを数え切れないほど使わなくてすむ。
これらのオプションでブラウザをセットアップしたら、Watirにそれを渡す。これで 殺す
メソッドは、TempBrowserインスタンスを捨てることなく、必要であればドライバーの停止と再起動を繰り返すことができる、ちょっとした砂糖のようなものです。
これでrspecのサンプルに2つのスーパーパワーを与えることができる。まず、粋な ブラウザ
ヘルパー・メソッドを使用します。また、超繊細な作業をしている場合は、特定の例でブラウザを再起動する便利なメソッドも利用できる。もちろん、テストスイートが終わったらブラウザを終了させることもできます。
モジュール BrowserSupport
def self.browser
ブラウザ ||= TempBrowser.new
終了
def self.configure(config)
config.around(:each)で|example|を実行する。
ブラウザサポート.ブラウザ.kill if example.metadata[:clean]
ブラウザサポート.ブラウザ.get
ブラウザ.クッキー.クリア
ブラウザドライバ.manage.timeouts.implicit_wait = 30
example.実行
終了
config.after(:suite) do
ブラウザサポート.ブラウザ.kill
終了
終了
終了
ブラウザとスペックヘルパーをセットアップし、プロキシへのリクエストのプロキシを開始する準備ができました。しかし、まだセットアップしていない!しかし、ヘルパーメソッドをいくつか用意して、数千のキーストロークを節約する方が良いでしょう。それが プロキシサポート
はそうする。
私たちのテストセットアップで使っているのは、もう少し複雑なものだが、一般的なアイデアはこんな感じだ:
frozenstringliteral: true
require 'json'
モジュール ProxySupport
HEADERS = {
'Access-Control-Allow-Methods' => 'GET'、
'Access-Control-Allow-Headers' => 'X-Requested-With, X-Prototype-Version, Content-Type'、
'Access-Control-Allow-Origin' => '*'.
フリーズ
def stubjson(url, file)
Billy.proxy.stub(url).andreturn({」を返します。
body: open(file).read、
code:200,
ヘッダ:HEADERS.dup
})
終了
def stubstatus(url, status)
Billy.proxy.stub(url).andreturn({
body: ''、
code: status、
ヘッダ:HEADERS.dup
})
終了
def stubpage(url, file)
Billy.proxy.stub(url).andreturn(
body: open(file).read、
content_type: 'text/html'、
code:200
)
終了
def stubjs(url, file)
Billy.proxy.stub(url).andreturn(
body: open(file).read、
content_type: 'application/javascript'、
code:200
)
終了
終了
私たちはスタブ(半券)を作ることができる:
簡単な例ならこれで十分だ。例といえば......いくつか設定しておこう!
まず、プロキシのためにいくつかの "ルート "を配線する必要がある:
let(:pageurl) { 'http://myfancypage.local/index.html' }.
let(:jsurl) { 'http://myfancypage.local/dist/remote-caller-example.js' }.
let(:pagepath) { './dist/index.html' }.
let(:jspath) { './dist/remote-caller-example.js' }.
を行う前に
stubpage pageurl, pagepath
stubjs jsurl, jspath
終了
rspecの観点からは、ここでの相対パスはメインのプロジェクト・ディレクトリを参照していることに注意する価値がある。 ディスト
ディレクトリにある。これらの スタブ
ヘルパーは便利だ。
また、"偽 "のウェブサイトを ローカル
TLD。そうすることで、万が一何か問題が発生しても、暴走したリクエストがローカル環境から逃げ出すことはない。一般的なプラクティスとして、絶対に必要な場合を除き、少なくともスタブでは「本物の」ドメイン名を使わないことをお勧めする。
ここでもう一つ注意すべきことは、同じことを繰り返さないということである。プロキシのルーティングがより複雑になり、より多くのパスとURLを持つようになると、このセットアップを共有コンテキストに抽出し、必要に応じて単純に含めることに本当の価値があるようになるだろう。
これで、"良い "パスがどのようにあるべきかを特定することができる:
コンテキスト '正しい応答で' do
やる前に
stubjson %r{http://poloniex.com/public(.*)}, './spec/fixtures/remote.json'
goto pageurl
Watir::Wait.until { browser.execute_script('return window.logs.length === 2') } を実行する。
終了
it '適切なデータをログに記録' do
expect(browser.execute_script('return window.logs')).to(
eq(['[EXAMPLE] Remote fetch successful', '[EXAMPLE] BTC to ETH: 0.03619999'])
)
終了
終了
とてもシンプルでしょう?リモートAPIからのJSONレスポンスをフィクスチャでスタブし、メインURLに移動し、そして...待つ。
例えばJavaScriptのイベントを確実に待つことはできないので、スクリプトが私たちがアクセスできるオブジェクトを私たちが興味を持つ状態に移動させるまで、少しズルをして "待つ "必要がある。欠点は、(バグなどで)そのステートが来なかった場合、watir waiterがタイムアウトするのを待つ必要があることだ。このため、スペックにかかる時間が少し長くなる。それでもスペックは確実に失敗する。
ページが興味のある状態で「安定」した後、ページのコンテキストでさらにいくつかのJavaScriptを実行することができる。ここでは、public配列に書き込まれたログを呼び出し、それらが期待したものであるかどうかをチェックする。
余談だが、リモート・リクエストをスタブ化することが本当に有効なのはここだ。コンソールにログされるレスポンスは、リモートAPIから返される為替レートに依存しているため、ログの内容が変化し続けると、確実にテストすることができない。もちろん、それを回避する方法はありますが、あまりエレガントではありません。
もうひとつ、"失敗 "ブランチをテストしてみよう。
コンテキスト 'with failed response' do
する前に
スタブステータス %r{http://poloniex.com/public(.*)}, 404
goto pageurl
Watir::Wait.until { browser.execute_script('return window.logs.length === 1') } }.
終了
it 'logs failure' do
expect(browser.execute_script('return window.logs')).to(
eq(['[EXAMPLE]リモート取得に失敗しました'])
)
終了
終了
上記とよく似ているが、異なるのは、レスポンスが404のHTTPステータスコードを返すようにスタブし、異なるログを期待する点である。
それではスペックを確認してみよう。
% バンドル exec rspec
シード63792でランダム化
I, [2017-12-21T14:26:08.680953 #7303] INFO -- : コマンド :: npm run build
リモート呼び出し
正しいレスポンスで
適切なデータを記録
失敗したレスポンス
失敗のログ
23.56秒で終了(ファイルのロードに0.86547秒かかった)
2例、失敗0
うっほー!
JavaScriptをRubyと統合テストする方法について簡単に説明した。当初はその場しのぎ的なものだと考えていたが、今では小さなプロトタイプにかなり満足している。もちろん、まだ純粋なJavaScriptソリューションを検討していますが、その間に、私たちが実際に遭遇した非常に複雑な状況を再現し、テストするためのシンプルで実用的な方法を手に入れました。
同じようなものを自分で作ろうと考えているのであれば、制限がないわけではないことに注意すべきである。例えば、あなたがテストしているものが本当にAJAXを多用するものである場合、Puffing Billyは応答するのに長い時間がかかります。また、いくつかのSSLソースをスタブする必要がある場合は、より多くの微調整が必要になります。私たち独自のユースケースに対処するための最良の方法を探求し、探し続けます。