フロントエンドテクノロジーは急速に発展しており、日を追うごとに変化していなくても、新しいフレームワークや新しいテクノロジーが毎年導入されています。Tubi の製品フロントエンド コード ウェアハウスは 2015 年に構築され、8 年以上使用されています。良いニュースは、長年にわたる React コミュニティの発展を受けて、Tubi の基本フレームワークの選択のほとんどが、コミュニティで人気のあるベスト プラクティスに従っていることです。コア フレームワークと依存バージョンは、基本的に最新の安定バージョンに更新されているか、更新される予定です。
これは主に、Tubi の小規模で美しいフロントエンド チームが強力な技術的自走力を備えていることと、チーム管理者がエンジニア文化と技術インフラの構築を非常に重視しており、積極的に貢献していることによるものです。チーム全体の時間の少なくとも 20% 機能開発以外に必要な技術的な最適化とアップグレードを実行します。
この記事で紹介する Enzyme から React Testing Library (RTL) への移行は、2022 年末に行われる予定です。これは、Tubi のフロントエンドにとって重要であり、かなりの量の作業を必要とするコード移行プロジェクトの 1 つです。
移行の動機
Tubi では、たとえそれがコミュニティによって推奨されたテクノロジーの選択であっても、まずその必要性と価値を評価し、合意に達した後にのみさらなるアクションを実行する必要があります。Enzyme から RTL への移行に戻ると、主な理由は 4 つあります。
まず、Airbnb は正式に Enzyme を積極的に保守していなくなり、最新の React 18 をサポートする予定はありません。Tubi は最新の React 18 にアップグレードすることを決定し、すべての UI テストを Enzyme の代替テストを見つけて移行する必要がありました。
第 2 に、統合テストに重点を置く RTL の設計哲学により、チームは保守可能なテスト コードをより簡単かつ効率的に作成できるようになります。
第三に、RTL は、ユーザーによる実際の使用の観点からテスト ケースを作成することを奨励しているため、その API 設計コンセプトには、Web アクセシビリティへの注意や強調など、UI テストの多くのベスト プラクティスが導入されています。
第 4 に、RTL の成熟したアクティブなコミュニティと軽量の実装メカニズムにより、テスト フレームワークの予測可能な長期的な活力が保証されます。
2 番目の点に関しては、よく知られたテスト ピラミッド モデルに関連して、RTL の著者である Kent C. Dods が、次の図に示すような、対応するテスト トロフィー モデル (Test Trophy) を提案しました。Kent 氏は、API 呼び出しをシミュレートし、結果を返してアプリケーションを統合するために、UI 統合テスト (統合テスト) を作成することに主な焦点を当てるべきであると考えています。Angular を使用して、アプリケーションの機能をあらゆる方向でテストし、半分の努力で結果が出る。
画像ソース: testingjavascript.com/
たとえば、ページのレンダリングをテストする場合、レンダリングされることが予想される UI 要素をテストするだけでなく、ページ データの読み込み、UI 要素の表示、潜在的なユーザー インタラクションと権限の制御も当然カバーします。言い換えれば、UI テスト ケースを通じて、Redux の状態管理、イベント ディスパッチ、データ編成に関連する下位レベルの機能とロジックに自然に触れることができ、本来は単体テストで検出する必要があったコード機能や分岐条件を簡単にカバーできるようになります。 。したがって、UI 統合テストはテスト カバレッジとテスト実行速度の両方を考慮しており、フロントエンド テスト コードを作成するためのより効果的で推奨される方法です。
3 番目のポイントで述べた RTL 高度なテストの概念は、以下のカウンター テストの例で完全に実証されています。酵素テストは通常、コンポーネントの特定の実装の詳細に関連付けられています。
この例では、Enzyme は、increment
ボタンに対応するクラス名を通じて自動インクリメント ボタンを特定し、コンポーネントの内部状態に格納されているカウントの変化をチェックして、自動インクリメント関数が正しく実行されたかどうかを判断します。RTL は、 このようなセマンティック API を通じて Web アクセシビリティ仕様に対応する要素を取得し、ユーザーに表示されるインターフェイスの変更を検出することによって、カウンタ増分関数が期待どおりに実行されることを検証しますgetByLabelText
。getByRole
したがって、RTL テストには 2 つの明らかな利点があります。
1. UI 実装の詳細から切り離すことでテスト コードがより堅牢になり、将来実装の詳細が変更されてもテスト コードが失敗することはありません。
2. Web アクセシビリティ指向の API 設計により、開発者は UI コンポーネントを実装する際に Web アクセシビリティの仕様に従うことが推奨されます。
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { shallow } from 'enzyme';
import React from 'react';
import Counter from './Counter';
describe('Enzyme tests', () => {
it('should increment by 1', () => {
const wrapper = shallow(<Counter />);
const instance = wrapper.instance();
expect(instance.state.count).toBe(0);
wrapper.find('button.increment').simulate('click');
expect(instance.state.count).toBe(1);
});
});
describe('RTL tests', () => {
it('should increment by 1', () => {
render(<Counter />);
const countLabel = screen.getByLabelText('count');
expect(countLabel).toHaveTextContent(0);
userEvent.click(screen.getByRole('button', { name: 'Increment' }));
expect(countLabel).toHaveTextContent(1);
});
});
作者:隔壁正在装修真羡慕
链接:https://juejin.cn/post/7260024054066085946
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
さらに、RTL と Enzyme の npm ダウンロード統計も、 RTL が Enzyme に取って代わり、React フロントエンド プロジェクトのテスト フレームワークとして最適な選択肢になったことを明確に反映しています。したがって、私たちは Enzyme の RTL への移行を正式に決定しました。
移行計画
現実的な移行計画と戦略を策定するには、移行プロジェクトの規模と全体像を理解することが重要です。Tubi 製品のフロントエンド コード ウェアハウスのコード テスト カバレッジ率は 92% という高さです。移行プロジェクト開始前の統計によると、Enzyme に依存する UI テスト ファイルが合計 440 以上あります (関連するコードの総行数は 100,000 行を超えるはずです)。移行がすぐに完了しない場合、新しい酵素テストが作成されるにつれて、このワークロードは増加し続けます。
したがって、新しい機能やコンポーネントの開発は、RTL を使用してできるだけ早くテストする必要がありますが、同時にストック酵素テストについては、段階的な移行を実現するための段階的なソリューションを見つける必要があります。以下の図に示すように、分割統治型の移行パスが現れます。
実現可能性を検証する
RTL が新機能の新しい UI テスト要件に適合するかどうかをさらに検証するために、実際の戦闘から開始し、当時開発していたワールドカップ イベント ページに RTL を適用することにしました。検証のためにこのページを選択する場合は、最初は難しく、次に簡単にするという原則に基づいています。このランディング ページは非常に複雑であるため、レスポンシブ UI、ディープ リンクの呼び出し、API モック、ユーザー インタラクション、ページ ジャンプなどの多くの複雑なシナリオのテストが必要になります。RTL が複雑なシナリオでのテストを十分に満足できるのであれば、比較的単純な UI テストはそれほど問題ではないと考えています。
最終結果は満足のいくもので、RTL は上記のテスト シナリオに完全に適合できます。酵素テストと比較して、RTL テストの実装はより簡潔かつ効率的であり、テストの実行時間は大幅に変わりません。同時に、Reduxやreact-intlなどのサードパーティライブラリの初期化に適応したカスタムレンダリング(render)メソッドもカプセル化し、Nockを使用してAPIの応答結果をAPIレベルでシミュレートし ます 。これらすべてにより、テクノロジの選択と依存関係を可能な限り維持することに基づいて、公式 RTL が推奨するプラクティスとコンセプトに比較的厳密に従うことができます。これらの成功した肯定的なフィードバックにより、RTL テストへの移行に対するチームの自信が強化されました。
Lint と進行状況統計スクリプトの導入
分割統治戦略を継続し、既存のテストの大規模な移行の前に、カスタム ESlint ルールを実装し、それを Github CI に追加して、新しい酵素テストの追加を停止するというコンセンサスが確実に実装されるようにしました。下の画像では、Lint ルールが startDate
2022-12-16 に設定されています。これは、その日付以降に導入された新しい酵素テストによって Lint エラーが発生することを意味します。
同時に、移行の進捗状況を簡単に把握し、移行されていないコードをすぐに見つけるために、 rtl-migrate
プロジェクトの開始時に移行を支援する npm コマンドを実装しました。以下の図に示すように、実行すると、yarn run rtl-migrate
酵素テストの全体的な移行の進行状況と、選択したフォルダーの移行の概要を取得できます。実行すると、ディレクトリ内の移行されていないすべてのテスト ファイルがyarn run rtl-migrate -p src/ott --type=enzyme
出力されます。src/ott
さらに、このコマンドには、テスト ファイルの中心的な貢献者を見つけ出し、ファイル サイズに応じてフィルタリングおよび並べ替える機能も実装されており、移行タスクを最適な開発者に割り当てるためのデータ参照が提供されます。記事の長さに限りがあるので、ここでは一つ一つ紹介しません。
Codemod による自動移行
初期分析によると、移行するテスト ファイルは 440 個以上ありました。最も楽観的な見積もりがファイルあたり平均わずか 250 行に基づいているとしても、移行全体に含まれるテスト コードの書き換え量は 110,000 行以上のコードになります。これは間違いなく大規模で、長期にわたる可能性のあるプロジェクトです。構築期間を可能な限り短縮するために、私たちはjscodeshiftに基づいて独自の Codemods スクリプトを構築することにしました。これにより、典型的な Enzyme コード パターンを可能な限り RTL テストに自動的に移行し、手動移行のコストを節約できます。
オープンソース コミュニティで要件を満たす既製の Enzyme 移行スクリプトが見つからなかったため、Codemods スクリプトを自分で構築することにしました。同時に、プロジェクトの独自のカプセル化と Enzyme の拡張を考慮すると、より多くの Enzyme テストをできるだけ柔軟かつ正確に RTL テストに自動的に変換したい場合は、完全に制御できる Codemods スクリプトを構築することが不可欠です。
もちろん、Codemod がすべてのテスト ケースを移行することを期待しているわけではありません。実際的な期待は、Codemod が最小限の労力でコード リポジトリ内の最も一般的なテスト ケースとコード パターンを優先的にカバーすることです。テクノロジーの選択と意思決定では、投入産出比と最終的なメリットも測定する必要があります。バランスとトレードオフは、技術的な意思決定におけるキーワードとなることがよくあります。特に移行のテストなどの 1 回限りのジョブの場合、包括的な Codemods スクリプトを実装することは本来の意図ではありません。重要なのは、Codemod の自動移行によって得られる時間の節約が、Codemod の開発にかかる労力を上回るようにする必要があるということです。
したがって、私たちはまず酵素テストで最も一般的なコード パターンとそのバリアントを特定し、それらが自動的に変換できるように対応する Codemod を実装しました。このプロセスは、私たちがよく言う 28 番目の原則に沿っています。つまり、Codemod の予定された目標の 80% は 20% のコストで完了し、残りの 20% を磨き上げるために時間の 80% を投資し続けることができます。機能の説明。移行作業の 1 回限りの投資を考慮すると、珍しいテスト パターンに対する Codemod レベルでの自動移行サポートには固執しません。
具体的な実装に戻ると、本質的に、Codemod はまず特定のコード フラグメントを抽象構文ツリーに変換し、次に特定の構文ツリーの構造を認識して変更し、その後新しいコード フラグメントを再生成します。したがって、酵素テストの移行など、複雑な機能を備えた codemod を構築する場合は、Codemod 関数を逆アセンブルして整理することが特に重要です。そうしないと、複雑さが継続的に蓄積され、最終的に Codemod の保守が困難になります。実装の初めに、 rtl-codemod
コード構成の関係とトランスフォーマー間の通信保存メカニズムを慎重に設計しました。具体的には以下の通りである。
ここでのキーワードはデカップリングであり、各トランスフォーマーは、互いに干渉することなく独立して実行できる、独立した目的を持ったプラグインまたはミドルウェアとして想像できます。Codemods によるコードの最終的な変更は、次のコードの順次実行の結果です。これらのプラグイン。を設計する rtl-codemod
際に、マイクロトランスフォーマーの概念であるモーションをさらに導入し、2層のプラグインアーキテクチャを構築し、トランス機能のさらなる分解と柔軟な組み立てを実現しました。
Codemods 実行エントリの中核として、transform は各トランスフォーマーの実行順序の構成のみを担当します。その実装は次のように大まかに簡略化されます。
const transform: Transform = (file, api, options) => {
const j = api.jscodeshift;
const { source } = file;
const ast = j(source);
// NOTE: The order of motions is important. Some motions need to be
// applied before others.
applyMotions(j, ast, [
...renderMotions,
...snapshotMotions,
...findTextMotions,
...inTheDocumentMotions,
...userEventMotions,
// More transform-related motions...
]);
return toSource(ast, options.toSourceOptions);
};
さらに、スクリプト プログラムとして、トランスフォーマーの実行時結果と中間計算出力は、永続ストレージを介して他のトランスフォーマーによって特定のストレージから読み取られることができます。つまり、ストレージは、トランスが通信やデータ共有を実現するための媒体とも捉えることができます。
詳細については、オープンソースで公開されている合理化されたサンプル実装を参照してください: github.com/nickqi-tubi…
妥協と適応
技術的な決定を下すとき、多くの場合、次善の妥協策を選択する必要がありますが、これは完全に悪いことではありません。それどころか、既存のコードやパターンにうまく適応するために積極的に妥協することは、多くの場合、実用的で賢明な選択となります。
酵素移行を例に挙げます。
私たちは、コンポーネントの期待されるインスタンス値をチェックするために、酵素テストでこのユニークな API を多用しています wrapper.instance()
。これは、酵素テストで一般的かつ公式に推奨されているモードの 1 つです。対照的に、RTL は実装の詳細についてテスト ケースを設計することを明示的に推奨しません。そのため、RTL はコンポーネント インスタンスや内部状態を取得するための API を決して公開しません。
RTL 公式推奨事項
コンポーネントのリファクタリング (機能ではなく実装の変更) によってテストが中断され、あなたとチームの速度が低下しないように、テストには実装の詳細が含まれないようにしたいと考えています。
RTL の設計哲学と推奨事項は確かに理にかなっています。しかし、テスト移行のタスクに戻ると、すでに Enzyme プラクティスに基づいたコードが少なくとも 100,000 行あることを考えると、Enzyme wrapper.instance()
への依存関係を削除するには、多数のテストを完全に書き直す必要があり、間違いなく実装が大幅に増加します。テスト移行にはコストがかかり、困難です。実際的には、RTL テストがコンポーネント インスタンスと内部状態の既存のテストに適応できるように、RTL 用のブリッジを提供する必要があります。
上記の理由から、 renderWithInstance
以下に示すようにユーティリティ メソッドを実装しました。
function renderWithInstance(
passedComponentOptions,
renderOptions
) {
let incorrectlyUseInstancePattern;
let WithExtendedClass;
const { extendingClass } = passedComponentOptions;
try {
if (extendingClass.prototype.isReactComponent) {
WithExtendedClass = class WrapperInstance extends extendingClass {
constructor(props) {
super(props);
incorrectlyUseInstancePattern = this;
}
};
} else {
WithExtendedClass = class WrapperInstance {
constructor(_props: any) {
incorrectlyUseInstancePattern = this;
}
};
Object.setPrototypeOf(WithExtendedClass, extendingClass);
}
} catch (e) {
throw new Error(`Problem extending passed 'extendingClass'.\n${e.stack}`);
}
const renderResult = render(
<WithExtendedClass {...passedComponentOptions.props} />,
{
wrapper: getWrapper(renderOptions),
...renderOptions,
}
);
return {
...renderResult,
incorrectlyUseInstancePattern,
};
}
作者:隔壁正在装修真羡慕
链接:https://juejin.cn/post/7260024054066085946
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
移行された RTL テストでこれを呼び出すことでrenderWithInstance
、カスタム RTL レンダリング メソッドが引き続きコンポーネント インスタンス オブジェクトを公開し、既存の検出条件を可能な限り再利用できるようになります。
it('should call handleUpdateA11y when activeIdx changes', () => {
const updateA11ySpy = jest.spyOn(
incorrectlyUseInstancePattern, 'handleUpdateA11y'
);
const {
incorrectlyUseInstancePattern
} = renderWithInstance({
extendingClass: AlertModal,
props: getProps()
});
incorrectlyUseInstancePattern.componentDidUpdate(
{}, { activeIdx: 1 }
);
expect(updateA11ySpy).toHaveBeenCalled();
});
作者:隔壁正在装修真羡慕
链接:https://juejin.cn/post/7260024054066085946
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
時間とコストのオーバーヘッド
Tubi フロントエンド チームによる Enzyme から RTL へのテスト移行には約 2 か月半かかりました。この期間中に、109 件の関連開発タスク (ストーリー) を作成および完了し、合計 466 個のテスト ファイルと 100,000 行を超えるテスト コードを移行しました。プロジェクトのバーンダウン チャートは次のとおりです。
このうち、緑色は、ストーリーの完了統計が急激に低下していることを示しています。これは、ショートカット システムでは、対応するコードが実際に本番環境にリリースされた後でのみ完了としてカウントされるためですが、実際の移行作業は日々進んでおり、そうではありません。一晩中。
バーンダウン チャートから、Tubi はすでに 2022 年 9 月からこの移行作業の検討を開始していることがわかります。この時は、概念実証のための事前準備作業とチーム内での共有ディスカッションだけを作成しました。11 月末に、クリスマスと新年の間のオンライン コードの凍結を利用して、テスト移行を正式にスケジュールしました。
2022 年 11 月末から 12 月中旬まで、フルタイムのフロントエンド エンジニア 1 人半近くを投資しました (移行プロジェクトを担当する 2 人のフロントエンド エンジニアは、それぞれの時間の約 70% を移行プロジェクトの推進に費やしました)。移行)、移行を支援するための codemod と進行状況レポートを構築するスクリプト ツールと lint ツール。同時に、移行の難しさを理解し、対応する解決策を見つけるために、コード ベース内のいくつかの典型的なテストを手動で移行することも試みました。
2022 年 12 月末、補助ツールの改善と移行の難易度の制御に伴い、Tubi 製品のフロントエンド コード ウェアハウスで長年働いてきた他のエンジニアを対象に RTL テストと移行トレーニングを実施しました。 。その後、移行する必要があるテスト ファイルは、関連するコードの習熟度と作業負荷に応じて均一に割り当てられました。2023 年 2 月中旬に、すべての移行作業が実際に完了しており、バーンダウン チャートには、2 月末の公開時にマージされたテスト コードが完了したものとして記録されます。
体験談とまとめ
ほとんどのエンジニア (特にフロントエンド エンジニア) は、テクノロジーのトレンドを把握し、最新かつ最も優れたテクノロジーを使用したいと考えていますが、既存のコード ウェアハウスの場合、フレームワークと依存関係の移行にかかるコストは無視できません。成熟したエンジニアリングチームは、インプットとアウトプットの比率と必要性を明確にし、メリットとデメリットのバランスをとった上で、合理的な意思決定を行う必要があります。移行を進める際には、漸進的なアプローチを模索することが重要な戦略であると考慮する必要があります。もちろん、古いコードと新しいコードを分割して征服するための差別化されたアプローチは、多くの場合、迅速に移行するために必要な妥協です。
さらに、この記事で説明されている Codemod や進捗レポート スクリプトなどのツールも、移行を支援するために事前開発が必要です。移行は 1 回限りの作業であることが多いことを考慮すると、補助ツールの開発への投資には 28 対 20 の原則が依然として適用され、網羅的ではなくほとんどのニーズを満たすためにできるだけ少ない時間を費やす必要があると考えています。
RTL テスト移行プロジェクトを再度主導する機会がある場合、次の 2 つの点でまだ改善の余地があります。
1. Codemodの範囲を事前に定義する必要がある
私たちは 28 の原則を強調し、ワンタイム Codemod ツールへの過剰投資を避けてきましたが、完璧な自動化ツールを追求するエンジニアの性質により、予想よりも Codemod に多くの時間を費やすことになり、コード パターンとコードのサポートが比較的珍しいことがわかりました。それらの亜種。チームの計画と評価の意思決定プロセスを事前に導入すると、この問題を効果的に回避できます。
2. プロジェクトの進行を加速するために、チームの参加を早期に導入する必要があります
プロジェクトの後半では、Codemods が比較的安定するまで待ってから、さらにチーム メンバーを迎え入れました。振り返ってみると、このウォーターフォール型の開発プロセスにより、プロジェクト サイクル全体が長期化しました。さらに良いことに、移行をテストするための典型的なコード パターンを事前に決定し、チームを事前トレーニングします。Codemods が完全に完了していない場合でも、フロントエンド チーム全体を動員して、Codemods 計画でサポートされないコード パターンをできるだけ早く手動で移行し、プロジェクトを並行して進めることができます。
全体として、このテスト移行により、Tubi フロントエンド チームが最新の React 18 にアップグレードするための障害が解消されました。わずか 2 か月で、466 個のテスト ファイルと 100,000 行を超えるテスト コードの移行を完了しました。これは間違いなく賞賛に値する成果です。同時に、React Testing Library の恩恵を受けて、UI テスト コードの実装におけるチームの効率と Web アクセシビリティの重視が大幅に向上しました。これは、技術プロジェクトへの投資の価値と利点も裏付けています。
Nick QI、Tubi スタッフ ソフトウェア エンジニア
トゥビへようこそ
同様のプロジェクトに興味がある場合は、Tubi への参加を歓迎します。