ビジュアル回帰テストによるサービス信頼性の向上


この記事では、2022年1月の優雅な技術シンポジウムで視覚回帰テストを実施する方法についてより詳細に説明します.完成したコードをすぐに見たい場合は、https://github.com/bluestragglr/visual-regression-test-demoの画像を確認してください!
🤔 どこに使いますか。 ビジュアル回帰テストでは、コンパイルエラーなどの明示的なエラーに加えて、こっそり発生するスタイルエラーをキャプチャできます。忙しい平日にこっそり蓄積したスタイル関連のテクノロジー債を効率的に管理できます。

🧐 どこかで会ったことがあるようです。 この文章は優雅な兄弟科学技術ブログです。https://techblog.woowahan.com/7332/内容は投稿した文章と同じです。

視覚回帰テストとは?


視覚回帰テストは、レンダリング結果が既存の結果と同じかどうかをテストします.[レンダリング](Rendering)画面を保存し、テストを実行するたびに保存したイメージとレンダリング結果が異なるかどうかを確認して、既存の出力と同じ出力になるようにします.
この記事では、JestとPuppetterを使用してChrome環境での視覚回帰テストを作成します.すなわち、本明細書のプレゼンテーションが完了すると(下図のように)、レンダリング結果を比較テストできます.

構成テスト


プレゼンテーションアプリケーションの作成


通常の環境で作成されたテストを再現するには、CRAを使用してレスポンスアプリケーションを作成することから始めます.次のコマンドを使用して反応アプリケーションを生成してください.
$ npx create-react-app visual-regression-test-demo --template typescript
プロジェクトでタイプスクリプトを使用しない場合は、--template typescript構文を削除して実行します.既存のプロジェクトでテストを設定する場合は、この手順は無視できます.プロジェクトがタイプスクリプトプロジェクトでない場合は、タイプに関連する構文を削除するだけです.

インストール依存性


アプリケーションがCRAによって作成された場合は、プロジェクトディレクトリに移動し、テストのために他の依存性をインストールします.次のコマンドを実行してdevDependencyでインストールします.
$ yarn add -D @types/jest-image-snapshot cross-env jest jest-image-snapshot jest-puppeteer puppeteer ts-jest
これらのパッケージは構築に含まれる必要がないため、devDependencyとしてインストールされます.パケットマネージャとしてnpmを使用する場合、yarn add -D構文をnpm i --save-devに置き換えます.

テスト環境の構成


テスト環境を構成する前に、ディレクトリ構造を見てみましょう.CRAが作成した/srcディレクトリの下に/testディレクトリを作成し、次の構造構成テストを使用します.構成のテストを開始する前に、次のディレクトリとファイルを作成します.
  • コマンド
  • を追加
    最初にテストを実行するコマンドを追加します.package.jsonのscript領域に3つのコマンドを追加します.
    "testdev": "export PORT=4321 && react-scripts start",
    "test": "cross-env JEST_PUPPETEER_CONFIG=./src/test/e2e/jest-puppeteer.config.js jest --config=./src/test/e2e/jest.config.js",
    "test-update": "cross-env JEST_PUPPETEER_CONFIG=./src/test/e2e/jest-puppeteer.config.js jest --config=./src/test/e2e/jest.config.js --updateSnapshot",
    testdevは、$ react-script startを介して4321ポート上で応答アプリケーションにサービスを提供するスクリプトである.他のポートを使用する場合は、スクリプトとその後に表示されるプリファレンス・パラメータ・ファイルを変更できます.testはテストを実行するために使用され、test-updateはテストのためのスナップショットを更新するために使用される.各スクリプトは、後続の構成のプリファレンス・パラメータ・ファイルを参照して実行されます.
    作成
  • Jest設定(jest.config.ts)
  • 以下に示すように、/src/test/e2e/jest.config.jsでJestに関する設定を作成します.
    // /src/test/e2e/jest.config.js
    
    module.exports = {
      // 디렉토리 설정
      rootDir: '../../',
      roots: ['./test/e2e'],
      // 타임스크립트 컴파일
      transform: { '^.+\\.ts?$': 'ts-jest' },
      // 테스트 코드 특정
      testMatch: ['**/?(*.)+(spec|test).ts'],
      // 테스트코드를 찾지 않을 경로
      testPathIgnorePatterns: ['/node_modules/', 'dist'],
      // 테스트 타임아웃
      testTimeout: 100000,
      // 개별 테스트 결과 표시
      verbose: true,
      // 프리셋(puppeteer 사용)
      preset: 'jest-puppeteer',
      // 테스트 셋업 후 실행할 스크립트
      setupFilesAfterEnv: ['./test/e2e/jest.image.ts']
    }
  • JestとPuppetterを構成する環境:
  • 作成したばかりの優先パラメータファイルの横に、次のようにjest-puppeteer.config.jsファイルを作成します.すなわち、/src/test/e2e/jest-puppeteer-config.jsに設定ファイルが作成される.
    // /src/test/e2e/jest-puppeteer-config.js
    
    module.exports = {
      server: {
        // Jest 실행 시 서버 서빙을 위해 실행할 커맨드
        command: `npm run testdev`,
        // 서버 포트 번호 (package.json의 "testdev" 스크립트에 설정됨)
        port: 4321,
        // 로컬호스트이므로 https가 아닌 http 사용
        protocol: 'http',
        // 서버 실행 타임아웃
        launchTimeout: 120000,
        debug: true
      },
      launch: {
        // headless 모드
        headless: true,
        // 브라우저 실행 타임아웃
        timeout: 120000,
      }
    }
    💡 Q.タイプスクリプト環境を構成する理由config.jsとjst-ppuppeerconfig.jsはjavascriptで構成されていますか?
    A. jest.config.jsとjst-ppuppeerconfig.jsは変換とコンパイル後にブラウザで使用されるファイルではありません.Node.これはjsプロセスが直接取得して使用する値です.タイプスクリプトが無効なファイルですが、タイプスクリプトを作成したい場合は、ファイル構成を個別に変換する必要があります.ただし、タイプ設定を行う部分はほとんどないので、タイプ設定は推奨されません.
    構成
  • Jest-imal-snapshot設定
  • その後、/src/test/e2e/jest.image.ts'jest-image-snapshot'パッケージをロードし、使用可能に設定する.このファイルはjestです.config.js上のsetupFilesAfterEnvで設定したファイルは、環境構成が完了するとインポートされ、実行されます.これにより、既存のJestのexpect構文に存在しないパラメータtoMatchSnapshotを使用することができる.
    // /src/test/e2e/jest.image.ts
    
    import { toMatchImageSnapshot } from 'jest-image-snapshot'
    
    expect.extend({ toMatchImageSnapshot })
    定義
  • 画像コントラスト
  • 最後にjest image-snapshotパッケージを使用して画像と出力結果を比較する方法を構成します./src/test/e2e/imageComparison.tsの書類に次のように記入してください.
    import { MatchImageSnapshotOptions } from 'jest-image-snapshot'
    
    export const getSnapshotConfig: (
      imageName?: string
    ) => MatchImageSnapshotOptions = (imageName) => {
      return {
        // 비교 이미지를 배열할 방향
        diffDirection: 'horizontal',
        // 콘솔에 발생한 차이를 표시할지 여부: Base64 데이터라 의미 없음
        dumpDiffToConsole: false,
        // 비교 방법 'pixelmatch' | 'ssim'
        comparisonMethod: 'pixelmatch',
        // 스냅샷 이름 정의
        customSnapshotIdentifier: imageName,
        // 비교 이미지 출력 경로
        customDiffDir: 'src/test/e2e/tests/__image_snapshots__/__diff_output__/'
      }
    }
    ここまでフォローし続けると、環境構成が完了します.次に、テストを作成し、実際に動作しているかどうかをテストします.

    テストの作成と実行

  • ページ初期化および終了ユーティリティ構成
  • この手順では、個別のファイルを作成する必要はありませんが、複数のスキーマを構成するときに常に繰り返されるため、最初から個別のモジュールで作成するとより効果的です.ここで定義したinitalizeTest関数は、Puppetterを使用してブラウザインスタンスを実行し、ページにアクセスし、ページオブジェクトとページを終了するコールバックを返します.以下のコードは/src/test/e2e/initialize.tsによって記述される.
    // /src/test/e2e/initialize.ts
    
    import puppeteer from 'puppeteer'
    const HOST_BASE_URL = 'http://localhost:4321/'
    
    const initializeTest = async () => {
      // 크로미움 브라우저 실행
      const browser = await puppeteer.launch()
      // 크로미움 페이지 열기
      const page = await browser.newPage()
      // 페이지 뷰포트 사이즈 고정
      await page.setViewport({
        width: 1200,
        height: 800,
        deviceScaleFactor: 1
      })
    
      // 테스트용 페이지 로드
      const response: any = await page.goto(HOST_BASE_URL)
      // 페이지 정상로드 확인
      expect(response.status()).toBe(200)
      // 페이지 로드 완료 확인
      await page.waitForSelector('#root')
    
      return {
        page,
        // 종료 시 크로미움 프로세스 종료를 위한 콜백을 함께 반환
        async cleanUp() {
          await page.close()
          await browser.close()
        }
      }
    }
    
    export default initializeTest
    後でテストを作成すると、initializeTest関数は返されたpageオブジェクトを引き続き使用します.このオブジェクトはpuppeteerを使用してブラウザとインタラクティブなオブジェクトです.アクションをバインドできます(たとえば、しばらく待つか、要素を参照してクリックします).APIの詳細については、公式ドキュメント(https://pptr.dev/#?product=Puppeteer&version=v13.1.1&show=api-class-page)を参照してください.
    作成
  • テスト
  • やっと環境設定が完了しました!正常な動作を確認するために、テストを作成します.すべてのテストは/src/test/e2e/tests/の下に配置されています.
    最も簡単な例は、スクリーンをロードしてすぐにスクリーンを撮影し、テストを構成してスクリーンを保存または比較することです.
    // /src/test/e2e/tests.VisualRegression.test.ts
    
    import { getSnapshotConfig } from '../imageComparison'
    import initializeTest from '../initialize'
    
    it(`Visual Regression Test`, async () => {
      // Puppeteer 페이지 초기화 & 콜백함수 가져오기
      const { page, cleanUp } = await initializeTest()
    
      // 스크린샷을 찍어서
      const image = await page.screenshot({ fullPage: true })
    
      // Snapshot config 에 정의된 대로 비교
      const snapshotConfig = getSnapshotConfig()
      expect(image).toMatchImageSnapshot(snapshotConfig)
    
      await cleanUp()
    })
    テスト構成は複雑ですが、テストはこれで終わります.

    テストの実行


    すべての構成が完了しました.次のコマンドを使用してテストを実行します.
    $ yarn test
    初期構成が完了するとスナップショットがないため、イメージの保存時に次の成功メッセージが出力されます.サーバを解放してからクロミインスタンスを解放し、テストを実行するには時間がかかります.

    しかし、もう一度テストを実行したら?
    $ yarn test
    テストに失敗しました.

    実際、これはCRAによって作成されたアプリケーションのデフォルトCSSにアニメーションフレームが含まれていることによる動作です.diff outputフォルダをチェックすると、反応バッジの周囲に画素の違いがあり、テストに失敗したことを示します.

    テストが成功したことを確認するには、/src/App.cssファイルからアニメーションに関連するコードを削除し、スナップショットを更新してテストを再実行します.

    だから視覚回帰テスト..。本当に役に立ちますか?



    注釈とCSSデバッグは頭の痛い問題です.HTMLとCSSの特性のため、エラーはよく返されず、必要に応じて適切な分析を行い、レイアウトをソートして要素を描画します.横の要素も様々な側面効果を生み出しますそのため、フロントエンドの開発者はいつも「スクリーンが割れた」と聞いて開発しています.
    ただし、ビジュアル回帰テストを作成し、配置パイプラインに適切に挿入するだけで、この問題を大幅に解決できます.問題自体は消えませんが、少なくともスタイル部分でアイテムが破損しているかどうかを検出しやすくなります.私が実際のプロジェクトで行ったときも視覚回帰テストのおかげでした!(実例が気になるなら?👉  2022年1月に優雅な技術セミナーを見に行きます )
    スタイルに関連する技術的な借金が増え、1 pxごとの故障がジャンプ線になる前に、サービスの信頼性を高めるための視覚回帰テストを行いましょう.