スクリーンショットを使用した単体テスト:音の壁を破ります。 レポートの書き起こし

スクリーンショットを使用してレイアウトの回帰をテストするのは流行です。誰も驚かないでしょう。 私たちは長い間、この種のテストを自宅で導入したいと考えていました。 サポートとアプリケーションの使いやすさの質問は常に混乱を招きましたが、それは大部分がソリューションの帯域幅でした。 私はそれが使いやすく、動作が速いものであることを望んでいました。 既製のソリューションは適合しなかったため、私たちは独自のことをすることを約束しました。







カットの下で、その結果、解決したタスク、およびスクリーンショットでのテストがテストの完了にかかった合計時間に実質的に影響しないことを確認した方法を説明します。 この投稿は、 HolyJS 2017 Moscowで配信されたレポートの転写です。 リンクでビデオを見ることができ、下のスライドを読んで見ることができます。













みなさん、こんにちは、私の名前はローマです。 私はAvitoで働いています。 私は、オープンソース、いくつかのプロジェクトの作成者を含む多くのことに従事しています: CSSTreebase.jsremplCSSOメンテナーなど







今日は、スクリーンショットを使用して単体テストについて説明します。 このレポートは、エンジニアリングソリューションの検索に関するストーリーです。 すべての機会にレシピを提供するわけではありません。 しかし、私は思考の方向を共有します:すべてをうまくやるためにどこへ行くか。







自転車は必ずしも悪いわけではありません。 私を知っている人は、たくさんの既製があるにもかかわらず、私はしばしば新しいことをしようとすることを覚えています。 これは何につながりますか? あきらめなければ、解決策を見つけることができますが、探していた場所ではありません。







本日、スクリーンショットのテーマを発表したので、コードを最適化するだけでなく、テストを加速できると言います。 問題はそれだけではないかもしれません。 そして、実験して、面白い動きと解決策を得ることができます。







問題が発生した場合、現代のフロントエンドベンダーは通常、すぐにNPM、StackOverflowにアクセスし、既製のソリューションを使用しようとします。 ただし、npm installが役立つとは限りません。 「冒険主義の精神は私たちの中に消えてしまった」:私たちは自分自身で何かを深めようとすることはめったにありません。













修正します。







単体テスト:ツール



テストは異なる場合があります:ユニット、機能、統合...このレポートでは、レイアウトのリグレッションをテストするコンポーネントまたはブロックのユニットテストについて説明します。







私たちはこのトピックに長い間取り組んだかったのですが、すべてが手に届きませんでした。 シンプルで安価で高速なソリューションが必要でした。







オプションは何ですか?









特定の理由により、サービスは私たちに合いません。外部のサービスを使用したくない、すべてを内部にしたいのです。







完成したツールはどうですか? それらはいくつかありますが、通常はURLを歩いて特定のブロックを「クリック」することに焦点を合わせています。 これは私たちにはあまり適していませんでした。スクリーンショットを撮って、コンポーネントとブロック、それらの状態をテストしたかったのです。







Yandex Geminiツールは便利ですが、宇宙船のように見えます。 始めて設定するのは難しく、たくさんのコードを書く必要があります。 おそらくこれは問題ではありません。 しかし、私にとっての問題は、readmeから簡単なテストを行い、それを100回コピーすると、この図が得られたことです。100個の282x200イメージが約2分間チェックされます。 これは非常に長い時間です。







その結果、彼らは自分のことを始めました。 これについては、今日のレポートになります。 先に進みます:何が起こったのかを示します。













そのため、Reactにある種のテストマークアップコンポーネントをtoMatchSnapshotImage()



、スクリーンショットを撮り、「マジック」メソッドtoMatchSnapshotImage()



を呼び出す1行を追加します。 つまり、テストに1行追加します。特に、スクリーンショットでコンポーネントのステータスを確認します。







数字で:サイズが800x600の2つの同一のスクリーンショットを比較すると、ソリューションでは約0ミリ秒かかります。 スクリーンショットが少し異なり、異なるピクセルを数える必要がある場合、約100ミリ秒かかります。 参照スクリーンショットの「ベース」を初期化するとき、スクリーンショットの更新、写真の取得、スクリーンショットごとに約25ミリ秒かかります。 それがたくさんであろうと少しであろうと、私たちは後で見るでしょう。







現在のレイアウトからスクリーンショットを取り、標準と比較できる決定を下す場合、何をする必要がありますか? まず、必要なスタイルとリソースを備えたコンポーネントの静的レイアウトを取得し、それをすべてブラウザーにロードし、スクリーンショットを撮って、参照スクリーンショットと比較します。 それほど難しくありません。













マークアップ生成



マークアップの生成から始めましょう。 いくつかのステップに分かれています。 最初に、HTMLコンポーネントを生成します。 次に、どの従属部分であるかを決定します。彼が使用するスタイル、必要な画像の種類などです。 これらすべてを単一のHTMLドキュメントに入れようとしています。このドキュメントには、ローカルリソースやファイルへのリンクはありません。







HTML生成



HTMLの生成は、使用しているスタックに大きく依存しています。 私たちの場合、これはReactです。 既製のreact-dom / serverライブラリを使用します。これにより、必要なHTMLである静的な文字列を生成できます。













つまり、 react-dom/server



を接続し、 renderToStaticMarkup()



メソッドを呼び出しreact-dom/server



renderToStaticMarkup()



を取得します。







CSS生成



さらに進む:CSS生成。 すでにHTMLがありますが、多くのスタイルやその他のリソースがまだ残っている可能性があります。 これらはすべて収集する必要があります。 ここでの行動計画は何ですか? まず、コンポーネントで接続され使用されているファイルを見つける必要があります。 CSSファイルを変換して、リソースへのリンクが含まれないようにします。 つまり、リソースへのリンクを見つけて、それらをCSS自体にインライン化します。 次に、すべてを接着します。







ここでも、解決策はスタックに依存します。 このケースでは、 Jestをテストランナー、 Babelを使用してJavaScriptおよびCSSモジュールを変換し、スタイルを記述します。







開始するには、CSSファイル検索を実行します。













CSSモジュールは、CSSがJavaScriptで通常のモジュールとして接続されていることを意味します。つまり、 import



またはrequire()



使用します。







技術的には、そのような呼び出しをすべてインターセプトし、要求されたパスを保持するように変換する必要があります。







これを行うために、Babelのプラグインを作成しました。 JestにはJavaScript変換をカスタマイズする機能があります(Jestを使用している場合は、すでにこれを行っている可能性があります)。 transform



設定を使用して、ルールに一致するリソースを変換するスクリプトが追加されます。 この場合、JavaScriptファイルが必要です。













スクリプトは、 babel-jest



を使用してトランスフォーマーを作成します。 他の設定に、独自のプラグインを追加する必要があります。これは必要なことを行います。













プラグインタスクは2つの部分で構成されています。 最初に、 require()



れるすべてのimport



に対して検索が行われるため、CSS接続を探す方が簡単です。 その後、すべてのrequire()



特別な関数に置き換えられます:













このような関数は、パスを格納するためにグローバル配列を初期化し、この配列に新しいパスを追加して、置換された元のエクスポートを返します。 プラグインコードは52行です。 ソリューションは簡素化できますが、これまでは必要ありませんでした。







コンポーネントのHTMLマークアップを生成するとき、 includedCssModules



配列には、 require()



介してrequire()



されたすべてのパスが含まれます。 残っているのは、パスをこれらのファイルのコンテンツに変換することだけです。







CSS処理



この段階では、すべてのCSSファイルを調べて、それらのリソースへのリンクを見つけてインライン化する必要があります。 また、ダイナミクスをオフにする必要もあります。アニメーションまたは一部のダイナミックパーツを使用すると、結果が異なる場合があり、予測できない瞬間にスクリーンショットを撮ることができます。







インラインリソース



リソースをインライン化するために、別のプラグインを作成しました。 (既製のものを使用できますが、この場合、独自のものを作成する方が簡単であることが判明しました)。













それはすべてどのように見えますか? プラグインをjest-transform



追加したことを覚えていますか? 話はここでも同じです。CSSモジュール用の特別なプラグイン、つまりbabel-jest



css-modules-transform



のみを使用しcss-modules-transform



。これは、CSSプリプロセスをカスタマイズする機能を備えています。







したがって、 processCss



でプラグインへのパスを追加し、プラグイン自体を記述します。 CSSTreeパーサーが使用されます。 それは私がその著者だというだけではありません;)-それは高速で詳細であり、例えば複雑なRegExp



なしでパスとURLを検索することを可能にします。 また、エラー耐性があります。CSSに理解できない部分がある場合、何も壊れず、これらの部分だけが組み立てられないままになります。 しかし、これはめったに起こりません。













プラグインはURLでCSSを検索し、インラインリソースに置き換えます。







ここで何が起こっていますか? 最初の行でASTを取得します。つまり、CSS文字列をツリーに解析します。 次に、このツリーを一周し、 Url



タイプのノードを見つけ、そこから値を選択して、インライン化するファイルへのパスとして使用します。 最後に、 translate



を呼び出すだけです。つまり、変換されたツリーを文字列に変換します。













インラインリソースの実装は、見かけほど複雑ではありません。









それだけです! インラインリソースがあります。 説明されている機能は、必要なすべてを実行する26行のコードです。







独自のソリューションを作成するのに役立つものは他にあります。たとえば、それを拡張できます。たとえば、後でアニメーションGIFの静的画像への変換を追加しました。 しかし、それについては後で。







ダイナミクスを取り除く



次のステップは、ダイナミクスを取り除くことです。 アニメーションをフリーズする方法とそれはどこで発生しますか?







ダイナミクスは次の場所に表示されます。









同じ結果が常に得られるように、このすべてを「切断」しようとします。







CSSの移行



すべてのtransitions-delay



およびtransition-duration



ゼロにします。













この場合、すべてのtransition



が最終状態にあることが保証されます。







CSSアニメーション



CSSアニメーションでも同じことを行います。













ここでは、このハックを見ることができます:













animation-delay: –0.0001s



値に注意してくださいanimation-delay: –0.0001s



。 実際のところ、これがないと、Safariではアニメーションに最終状態がありません。







そして最後:アニメーションを最後まで駆動しました(最終状態)が、アニメーションは遷移を繰り返すことができるという点で異なります。 したがって、 animation-play-state



paused



設定して、 animation-play-state



paused



ます。 したがって、アニメーションは一時停止、つまり再生が停止します。













キャリッジ



次の瞬間は、フィールドでの運送です。 問題は点滅することです。ある時点で垂直線が表示され、ある時点で-いいえ。 これは、結果のスクリーンショットに影響する場合があります。







ここ数か月で、 caret-color



などの機能がブラウザーに登場しました。最初はChromeで、次にFirefoxとSafari(テクノロジープレビュー)でした。 キャリッジを「オフ」にするには、キャリッジを透明にします(色をtransparent



設定します)。 したがって、キャリッジは常に非表示になり、結果には影響しません。







他のバージョンのブラウザの場合、他のバージョンを作成する必要がありますが、これはスクリーンショットに使用する場合のみです。













GIF



GIFの場合、状況はもう少し複雑です。 タスクは、アニメーションGIFから1つの静的フレームを残すことです。 私はこのためのモジュールを見つけようとし、それを入れて問題を忘れようとしました。 その結果、写真のサイズを変更したり、パレットを変更したり、複数の画像からGIFを作成したり、逆にアニメーションGIFから一連の画像を作成したりする多くのライブラリを見つけました。 しかし、アニメーションGIFを静的にするパッケージは見つかりませんでした。 私は自分で書かなければなりませんでした。







ライブラリを2時間検索した後、私はGIF形式がどれほど複雑かを調べることにしました。 私はVickiを読んで、89年目から仕様を公​​開しました。













GIFはいくつかのブロックで構成されています。最初に、画像のサイズを説明する署名と、インデックス付きの色の表があります。 次に、ブロックは順番に移動します。グラフィックスを担当する画像記述子ブロックと、パレット、テキスト、コメント、著作権などを保存できる拡張ブロックです。 ファイルの最後にはTrailerがあります。これは、GIFが終わったという特別なブロックです。







したがって、これらのブロックを通過して、最初を除くすべての画像記述子ブロックをフィルタリング(削除)する必要があります。 必要なコードを含むGistへのリンクを次に示します。 数時間で書いてデバッグしましたが、完璧に機能していましたが、問題は見つかりませんでした。







結論:GIF画像は静的、アニメーションはオフ、すべてのCSSパスがあります。 接着剤のままです。 もっとシンプルにできるものがあるように思えますか?







CSSスプライシング



Jestの仕組みを見てみましょう。 通常、並行して実行され、テストを実行する複数のスレッドを実行します。 各テストファイルはいずれかのストリームで起動され、各ファイルは別のコンテキストであり、他のコンテキスト間でデータを移動しません。 そして問題は、CSSファイルのソースコードにアクセスするCSS変換がテストのコンテキスト外にあり、このコンテンツにアクセスできないことです。 CSSはファイルからも読み取ることができません。CSSはすでに変換されており、JavaScript自体に何らかの環境、コンテキスト、ワーカーで保存されているためです。







テスト間でCSSを調べる方法は? 小さなハックを作りました。 各ワーカーはJSON形式の一時ファイルを作成します。キーはCSSへのパスであり、値自体は既にCSSに変換されています。 各スレッドはこのファイルを読み取り、そこから必要なものを取得し、テストのコンテキスト内で連結します。













ここで、一時ファイルを読み取り、JSONで解析し、必要なコンテンツを追加します。 ファイル名がキー、CSSが変換された値です。 そして、変換されたマップを書き戻します。













スクリーンショットのCSSを生成するとき、このファイルから読み取り、 includedCssModules



(CSSパスの配列)を使用し、必要なファイルのコンテンツを取得してjoin()



ます。







すべてを一緒に収集することが残っています。







最終組立









最終的なHTMLを生成します。 最初に、ダイナミクス(アニメーション)をオフにするスタイルを設定します。 2番目のスタイルでは、見つかったすべての接着されたCSSが接続します。 各テストにはこれらのスタイルの独自のセットがあります。コンポーネントのrequire()



を行うrequire()



、リストにある依存関係がプルアップされるためです。 その結果、使用されるCSSファイルのみが接続され、プロジェクト内のすべてのCSSは接続されません。 HTML、それぞれ以前に受け取った-これはコンポーネント自体のコードです。







その結果、目標を達成しました。 適切な状態のHTMLコンポーネントと、必要なCSSを生成できます。







これで、すべてのマークアップが組み立てられ、アニメーションがオフになります-すべてがスクリーンショットを撮る準備ができました。 ソリューションは完璧ではありませんが、より良くすることができますが、よりエレガントで安定したソリューションを得るには、Jest、Babel、CSSモジュールなどをさらに掘り下げる必要があります。 しかし、全体として、これは私たちに合っており、先に進むことができます。







スクリーンショット



今日、ブラウザでスクリーンショットを撮るのはとても簡単です。 数年前、これは難しい作業になる可能性があり、複雑なソリューションを使用する必要がありました。 現在、GUIなしで実行されるヘッドレスブラウザーがあり、任意のコードをダウンロードして、スクリーンショットの撮影など、その動作を確認できます。







また、最新のブラウザーはすべてWebDriverをサポートしています。 たとえば、Seleniumを使用する場合、すべてが比較的簡単です。 そのような環境向けのテストの記述を簡素化するライブラリー、ヘルパーがあります。







この場合、単一のブラウザを使用して簡単な比較を行いました。 ブラウザー間の比較を行う必要はありませんでしたが、Chromeヘッドレスを実行できる特別なライブラリーであるPuppeteerを使用し、それを操作するための適度に便利なインターフェースを提供しました。 スクリーンショットを撮る主なコードは次のとおりです。













Puppeteerがここに接続され、ブラウザーが起動します。スクリーンショットを撮る必要がある場合、 screenshot()



関数をHTMLで呼び出します。 この関数は、新しいページを作成し、送信されたHTMLを挿入し、スクリーンショットを撮り、ページを閉じて、スクリーンショットの結果を提供します。 動作します。 簡単です。 しかし、それはそれほど単純ではないことが判明しました。







実際、コードをローカルで実行すると、すべてが正常に機能します。 参照イメージと新しいイメージがあります。これは、参照を作成した同じシステムで、同じブラウザーバージョンで新しいイメージを作成するためです。 しかし、WindowsではなくMacではなく、Linux、独自のバージョンのChrome、独自のアンチエイリアスルール、フォントなどが存在しないCIでこれらすべてを実行し始めたとき、画像は異なっていました。 つまり、彼らは異なる結果を得始めました。







どうする いくつかの解決策があります。 いくつかの解決策は、数学の助けを借りてこの違いを克服しようとします。 ピクセルごとに比較するのではなく、ピクセルと隣接するピクセルを比較します。つまり、特定の許容値を使用した厳密でない比較です。 これは高価で、どういうわけか奇妙です。ピクセルごとに比較したいと思います。







別のソリューションの方向に進みました。HTMLコードでPOSTリクエストを送信できる外部マイクロサービスを作成し、出力で必要な画像、スクリーンショットを取得します。







私たちが得た利点は何ですか? テストが実行されるマシンに依存せず、ブラウザのバージョンを更新、変更できます。マイクロサーバーの側には常に同じブラウザがあり、同じ結果が得られます。







また、新しいプロジェクトのローカル設定は必要ありません。 ブラウザを起動したり、Puppeteerなどを設定したりする必要はありません。POSTリクエストを作成して画像を取得するだけです。 ネットワークコストはありますが、それはさらに速く、奇妙なことに十分です。 サービスにリクエストを送信します。キャッシュとウォームアップされたブラウザの両方があり、非常に迅速に画像を提供します。 さらに、PNGは十分に小さく、よく揺れ、ネットワークトラフィックはそれほど大きくありません。







短所も。 サービスはいつでも低下する可能性があるため、「健康」を監視する必要があります。 単純なページにアクセスしたとしても、ブラウザは大量のメモリを「消費」できることを誰もが知っています。 サービスは突然あふれ、対処できず、リソースが限られています。 また、サービスがクラッシュすると、画像をレンダリングできません-テストは失敗します。







したがって、全員が同時にスクリーンショットをチェックする(実行する)ことを決定した場合、大きな負荷があるため、長時間待たなければならないか、サービスが応答しなくなることがあります。 後者は多かれ少なかれ解決されています。複数のサービスインスタンスを作成して負荷を分散できるローカルクラウドがあります。 それにもかかわらず、そのような問題が存在します。 サービスの死はどのように見えますか?













動作するサービスの特定のインスタンスがあり、限られた量のメモリ(この場合は1 GB)があり、利用可能なすべてのメモリを「消費」して応答を停止できます。 このような場合、再起動のみが役立ちます。







マイクロサービスを使用したソリューションには別の側面があります。 コードでスクリーンショットを撮ると、コードだけでなく、URLと特定のセレクターでもスクリーンショットを提供するようにサービスに教えるというアイデアが生まれました。 この場合のサービスの動作:ページに移動し、ページの完全なスクリーンショットを取得するか、渡されたセレクターが一致するブロックを取得します。 これは、テストにまったく関係のない他のタスクにとって便利で便利であることがわかりました。 たとえば、現在実験中です:ページのスクリーンショット、サービスの説明、サイトの一部をドキュメントに挿入し、画像のサービスへのURL( <img>



)を使用してナレッジベースに挿入します。 ドキュメントにアクセスすると、常に実際のスクリーンショットが表示されます。 そして、それらを常に更新する必要はありません。 これは非常に興味深いソリューションであり、それ自体が判明しました。







スクリーンショットサービスのURLを画像として使用し、ページまたはブロックのスクリーンショットを取得できるメソッドのアプリケーションは、ドキュメント化だけでなく、他のタスクにも非常に役立ちます。 たとえば、サイトの機能的なグラフを作成できます。グラフの各ブロックには、サイトのロールアウトごとに更新されるページまたはブロックのスクリーンショットがあります。







画像比較



そのため、スクリーンショットを使用したテストに直接移行します。 コードを取得し、このコードからスクリーンショットを取得しましたが、それらを比較する必要があります。







Jestを使用してコンポーネントをテストします。 これは重要です。

Jestにはキラー機能があります-スナップショットの比較。













オブジェクト、いくつかのデータのマークアップを行い、このデータに対してtoMatchSnapshot()



メソッドを呼び出すことができます。 メソッドの仕組み:









toMatchSnapshot()



メソッドを使用すると、コンポーネントのマークアップ(HTML)が変更されたかどうかを確認でき、スナップショットの比較、更新、保存などのためにコードを記述する必要はありません。 魔法!







しかし、画像比較に戻ります。 バイナリ画像がありますが、これは文字列表現ではありません。 Jestで画像を比較するための組み込みツールはまだありません。 このトピックに関する GitHubのチケットがあります 。彼らはプルリクエストを待っています。 たぶん、時間をかけて自分でやるでしょう。 しかし、現時点ではAmerican Expressのプラグインjest-image-snapshotがあります。 バイナリイメージの比較をすぐに開始するのに適しています。 次のようになります。













このモジュールをプラグインexpect



jest-image-snapshotからtoMatchImageSnapshot()



expect



新しいtoMatchImageSnapshot()



メソッドでexpect



拡張expect



ます。













. — , , , .







, toMatchImageSnapshot()



. , toMatchSnapshot()



. , . , , , , ( — ), .







? , . , . Jest. ( , Jest : , , . CI, , , , ). , jest-image-snapshot



, , , , CI, CI , .







: , , , , .







— . 800600, 1,5-2 . ? , 300 . , , . 300 — .







jest-image-snapshot



. — . , 300 ( : « ») 10-20 , . 4,5 . , , . , Jest, 12-15 .







? , jest-image-snapshot



, blink-diff , , PNG.







, , 800600. — 480 . — . — 4 (RGB ). 2 . , , 4 . 300 — 300 4 , .







, Garbage Collector , . , . : .













. , blink-diff , , 1,5-2 (800x600). — pixelmatch . . , jest-image-snapshot



( , ). looks-same Gemini, , pixelmatch .







? : ( ) . . PNG , (, 800x600 , 2 , ). , .







? , !













node.js – crypto



, . , md5



sha1



, . , hex



, .







. 4 58 . , . 137 2 . , , .







. — overkill. , . ? , , ( ) . , . :













Buffer equals()



( compare()



, –1, 1 0, ). , , , equals()



. : 4 — 3 , 137 — 148 . 50-70 , .









, , . . , . , ? , , .













, : , PNG, , . GZIP, , , , .







, ( actualImage



expectedImage



), fast-png . Uint32Array . , , . , , . , , . actual.data



, , , Uint32Array, . , , , . : , , , , .







, , (: count / (width * height)



).







, . 800600 ~100 , , , .







diff-



: ? ?







, . , , . diff- .













, . , alloc()



, ( ). Uint32Array. . , , . . — , . , . — . , .







?













, — , , , — , — 255. . , . . gray



( ) . .







: , . , , , .













( actual



, expected



diff



) Buffer.concat()



, . fast-png, PNG. , , , .













, ( ), — diff: , . ? .







? , , 250 ( , diff-). . , .







. JS, / PNG, / GZIP . , . WebAssembly , . , , , .







: diff-, . diff ! , Git. , . , GitHub , . BitBucket (Stash), :









, . diff? , () . .







Jest, , expand



. , , . Jest , . — --expand



, . : expand



, , , diff-.







?



? , . . これはどういう意味ですか? , , . , , . , , , , ? , , .







, HTML , . :









. , , . , . -, .







, , , ( ), ( CI, ). , . , , . , , .







: ? — . どうして? , PNG , , . GIF , PNG «»?







, PNG. , PNG . , GIF. GIF , PNG 4 : , , .







() PNG, . , . . なに? , , , . — , .







, . – . , . (, ). , ( ), , – . ( ), . , .







( ), , . 45-50 , - , 12-15 . 300 (800x600). .







: , - -, 300 . , ( — ). .







GIT



, , – Git. Git, , .







, Git, VCS, . , ( ). , Git , . Git.







Git — GIT LFS . , Git. 仕組みは次のとおりです。













Git , : GIT LFS, . . pull , . push, , Git . git push/pull .







CI, beyond the frontend



: 12-15 , , CI . . CI , : 14 , 4 . : 700+ , 300+ . , — 3,5 . . , - , , devops-, , , ? , TeamCity , .







CI : , git, git checkout, , -, eslint-, stylelint- . . « », , , 3,5 . , , , , . , 30 .







結果



- 12-15 . 300 (800600). CI — 20-30 . : , , . 3,5 , , . 20-30 .







Jest. , : , , expand, , (, ). : , , Jest. , , jest-image-snapshot



, , .







— « », , . , , , Open Source.







まとめ



Babel, CSS Modules, Jest. , , , , . , , - , .













: 11 , . . 328 : .







: , , . , : , . , - — .







? , Jest , GIF, PNG, . Buffer API, TeamCity, - .







: - . . , .







それだけです よろしくお願いします!








All Articles