フロントエンドテストの標準とスタイルガイドライン

GitLab でフロントエンドのコードを開発しているときに出会うテストスイートには 2 種類あります。 JavaScript のユニットテストとインテグレーションテストには Karma と Jasmine、Jest を使い、e2e (エンドツーエンド) のインテグレーションテストには Capybara と RSpec feature tests を使います。

すべての新機能について、ユニットテストと機能テストを書く必要があります。 ほとんどの場合、機能テストにはRSpecを使うべきです。

リグレッションテストは、バグが将来再発しないように、バグ修正のために書かれるべきです。

GitLabでの一般的なテストプラクティスについては、テスト標準とスタイルガイドラインのページをご覧ください。

Vue.jsのテスト

Vueコンポーネントのテストに関するガイドをお探しの場合は、このセクションにすぐにジャンプできます。

冗談

フロントエンドのテストをJestテストフレームワークに移行し始めました(対応するエピックも参照してください)。

Jest テストは/spec/frontend/ee/spec/frontend にあります。

注:

ほとんどの例には、Jestの例とKarmaの例があります。 Karmaの例は、コードの中で何が起こっているかの説明としてのみ見てください。 Jestの例は、あなたが従うべきものです。

カルマ・テスト・スイート

GitLab がJestに切り替わっても、私たちのアプリケーションには Karma のテストが残っています。KarmaJasmineをテストフレームワークとして使うテストランナーです。 Jest も Jasmine を基盤として使っているので、見た目はよく似ています。

Karmaのテストはspec/javascripts/ 、EEの/ee/spec/javascripts

app/assets/javascripts/behaviors/autosize.jsは対応するspec/javascripts/behaviors/autosize_spec.js ファイルを持っているかもしれません。

CI環境では、これらのテストはヘッドレスブラウザで実行され、Notificationのような特定のAPIにアクセスできないことに注意してください。

KarmaよりJestを使うべき時は?

既存の Karma テストファイル (spec/javascriptsにあります) を更新する必要がある場合、spec 全体を Jest に移行する必要はありません。変更をテストするために Karma spec を更新するだけでも問題ありません。 おそらく、別のマージリクエストで Jest に移行する方が適切でしょう。

新しいテストファイルを作成する場合は、Jestで作成する必要があります。 これにより、私たちの移行をサポートすることができ、Jestを使用することが好きになると思います。

JestはKarmaで経験した多くの問題を解決し、より良い開発者体験を提供しますが、予期せぬ問題が発生する可能性があります(特にブラウザ固有の機能に対するテスト)。

カルマとの違い

  • Jest はブラウザではなく Node.js 環境で動作します。 ブラウザで Jest テストを実行するためのサポートは計画中です
  • JestはNode.js環境で動作するため、デフォルトではjsdomを使用します。 以下の制限事項も参照してください。
  • JestはWebpackのローダーやエイリアスにアクセスできません。 Jestが使用するエイリアスは、独自の設定で定義されます。
  • setTimeoutsetInterval への呼び出しはすべてモックされます。Jest Timer Mocksも参照してください。
  • rewire Jestはモックモジュールをサポートしているので、モックモジュールは必要ありません。 手動モックも参照してください。
  • Jest ではコンテキストオブジェクトがテストに渡されることはありません。 つまり、たとえばbeforeEach()it() の間でthis.something を共有してもうまくいかないということです。 その代わりに、必要なコンテキストで (const /letを使って) 共有変数を宣言する必要があります。
  • 以下のような場合、Jest のテストは失敗します:
    • 模倣されない要求
    • 未処理のプロミス拒否。
    • Vue などのライブラリからの警告を含むconsole.warnへの呼び出し。

jsdomの限界

上述したように、Jestはテストの実行にブラウザの代わりにjsdomを使用します。 これには次のような制限があります:

ブラウザで Jest テストを実行するサポートに関するイシューも参照してください。

Jestテストのデバッグ

yarn jest-debug を実行すると、Jest がデバッグモードで実行され、Jest のドキュメントに記述されているようなデバッグ/インスペクションが可能になります。

タイムアウトエラー

Jest のデフォルトのタイムアウトは/spec/frontend/test_setup.jsで設定されています。

この時間を超えると、テストは失敗します。

テストのパフォーマンスを改善できない場合は、setTestTimeoutを使用して、特定のテストのタイムアウトを増やすことができます。

import { setTestTimeout } from 'helpers/timeout';

describe('Component', () => {
  it('does something amazing', () => {
    setTestTimeout(500);
    // ...
  });
});

各テストの性能は環境に依存することを忘れないでください。

何をどのようにテストするか

モックやスパイのようなJest特有のワークフローについて詳しく説明する前に、Jestで何をテストすべきかを簡単に説明します。

ライブラリをテストしないでください

ライブラリは、JavaScript開発者にとって不可欠なものです。 一般的なアドバイスとしては、ライブラリ内部をテストするのではなく、ライブラリが何をすべきか知っていて、それ自身でテストカバレッジを持っていることを期待することです。 一般的な例としては、次のようなものがあります。

import { convertToFahrenheit } from 'temperatureLibrary'

function getFahrenheit(celsius) {
  return convertToFahrenheit(celsius)
}

getFahrenheit 関数のテストは意味がありません。なぜなら、この関数の内部では、ライブラリ関数を呼び出す以外に何もしていないからです。

Vueの世界を少し覗いてみましょう。 VueはGitLabのJavaScriptコードベースの重要な部分です。 Vueコンポーネントの仕様を書くときによくあるのが、Vueが提供する機能をテストしてしまうことです。 Vueが提供する機能が一番テストしやすいように見えるからです。

// Component
{
  computed: {
    hasMetricTypes() {
      return this.metricTypes.length;
    },
}

対応するスペックは以下の通り。

 describe('computed', () => {
    describe('hasMetricTypes', () => {
      it('returns true if metricTypes exist', () => {
        factory({ metricTypes });
        expect(wrapper.vm.hasMetricTypes).toBe(2);
      });

      it('returns true if no metricTypes exist', () => {
        factory();
        expect(wrapper.vm.hasMetricTypes).toBe(0);
      });
    });
});

hasMetricTypes computed propをテストすることは当然のことのように思えますが、computedプロパティがmetricTypesの長さを返しているかどうかをテストすることは、Vueライブラリ自体をテストすることになります。 テストスイートが追加されるだけで、これには何の価値もありません。 よりよい方法は、ユーザがそれとインタラクトする方法でテストすることです。 おそらくテンプレートを通してです。

この種のテストは、ロジックの更新を必要以上に壊れやすく面倒なものにするだけなので、気をつけましょう。 これは他のライブラリにも当てはまります。

フロントエンドのユニットテストのセクションに、さらにいくつかの例があります。

モックをテストしないでください

モックを使うのであれば、モックはテストをサポートすべきですが、テストの対象にはなりません。

悪い:

const spy = jest.spyOn(idGenerator, 'create')
spy.mockImplementation = () = '1234'

expect(idGenerator.create()).toBe('1234')

よかったです:

const spy = jest.spyOn(idGenerator, 'create')
spy.mockImplementation = () = '1234'

// Actually focusing on the logic of your component and just leverage the controllable mocks output
expect(wrapper.find('div').html()).toBe('<div id="1234">...</div>')

ユーザーをフォロー

コンポーネントが多い世界では、ユニットテストとインテグレーションテストの境界線はかなり曖昧になりがちです。 最も重要なガイドラインは次のとおりです:

  • 複雑なロジックが将来壊れるのを防ぐために、単体でテストすることに実際の価値がある場合は、クリーンなユニットテストを書きます。
  • そうでない場合は、できるだけユーザーの流れに近い形で仕様を書くようにしてください。

たとえば、手動でメソッドを呼び出してデータ構造もしくは計算されたプロパティを検証するよりも、ボタンをクリックするために生成されたマークアップを使い、それに応じて変更されたマークアップを検証するほうがベターです。 テストが通過して誤ったセキュリティ感覚を提供する一方で、ユーザーフローを誤って壊す可能性が常にあります。

一般的な慣行

このガイドに従っていないことにつまずいたら、すぐに修正するのが理想的です。

DOM 要素のクエリ方法

テスト内で DOM 要素をクエリする場合は、 要素を一意的かつ意味的にターゲットにするのがベストです。 これが実現不可能な場合もあります。 このような場合は、テスト属性を追加してセレクタを簡略化するのが最適です。

@vue/test-utilsを使ったコンポーネントテストでは、子コンポーネントのクエリをコンポーネント自身を使って行うのが好ましいでしょう。 これにより、特定の振る舞いをそのコンポーネントの単体テストでカバーできるようになります。 そうでない場合は、 を使うようにしましょう:

  • のようなセマンティック属性(正しくセットアップされているname ことも確認 name)。
  • data-testid 属性 (@vue/test-utils](https://github.com/vuejs/vue-test-utils/issues/1498#issuecomment-610133465)のメンテナーが推奨する[)
  • a Vueref (@vue/test-utilsを使用している場合)

例:

it('exists', () => {
    // Good
    wrapper.find(FooComponent);
    wrapper.find('input[name=foo]');
    wrapper.find('[data-testid="foo"]');
    wrapper.find({ ref: 'foo'});

    // Bad
    wrapper.find('.js-foo');
    wrapper.find('.btn-primary');
    wrapper.find('.qa-foo-component');
    wrapper.find('[data-qa-selector="foo"]');
});

テスト目的で.js-* クラスを追加することはお勧めしません。 他に実行可能なオプションがない場合にのみ行ってください。

.qa-* クラスやdata-qa-selector 属性は、QA エンドツーエンドテスト以外のテストには使用しないでください。

ユニットテストの命名

特定の関数/メソッドをテストするために記述テストブロックを書くときは、 メソッド名を記述ブロック名として使用してください。

// Good
describe('methodName', () => {
  it('passes', () => {
    expect(true).toEqual(true);
  });
});

// Bad
describe('#methodName', () => {
  it('passes', () => {
    expect(true).toEqual(true);
  });
});

// Bad
describe('.methodName', () => {
  it('passes', () => {
    expect(true).toEqual(true);
  });
});

テストの約束

Promises をテストするときは、常にテストが非同期であることと、リジェクトが処理されることを確認する必要があります。 テストスイートでasync/await 構文を使えるようになりました:

it('tests a promise', async () => {
  const users = await fetchUsers()
  expect(users.length).toBe(42)
});

it('tests a promise rejection', async () => {
  expect.assertions(1);
  try {
    await user.getUserName(1);
  } catch (e) {
    expect(e).toEqual({
      error: 'User with 1 not found.',
    });
  }
});

また、Promiseチェーンも使えます。この場合、done コールバックと、エラーが発生した場合のdone.fail 。 以下に例を示します:

// Good
it('tests a promise', done => {
  promise
    .then(data => {
      expect(data).toBe(asExpected);
    })
    .then(done)
    .catch(done.fail);
});

// Good
it('tests a promise rejection', done => {
  promise
    .then(done.fail)
    .catch(error => {
      expect(error).toBe(expectedError);
    })
    .then(done)
    .catch(done.fail);
});

// Bad (missing done callback)
it('tests a promise', () => {
  promise.then(data => {
    expect(data).toBe(asExpected);
  });
});

// Bad (missing catch)
it('tests a promise', done => {
  promise
    .then(data => {
      expect(data).toBe(asExpected);
    })
    .then(done);
});

// Bad (use done.fail in asynchronous tests)
it('tests a promise', done => {
  promise
    .then(data => {
      expect(data).toBe(asExpected);
    })
    .then(done)
    .catch(fail);
});

// Bad (missing catch)
it('tests a promise rejection', done => {
  promise
    .catch(error => {
      expect(error).toBe(expectedError);
    })
    .then(done);
});

時間を操る

例えば、X秒ごとに実行される繰り返しイベントなどです。 ここでは、そのようなコードに対処するための戦略をいくつか紹介します:

setTimeout() /setInterval() 申請中

Jestでは、これはデフォルトで行われています(Jest Timer Mocksも参照してください)。 Karmaでは、Jasmineのモッククロックを使うことができます。

const doSomethingLater = () => {
  setTimeout(() => {
    // do something
  }, 4000);
};

冗談で

it('does something', () => {
  doSomethingLater();
  jest.runAllTimers();

  expect(something).toBe('done');
});

カルマの中で

it('does something', () => {
  jasmine.clock().install();

  doSomethingLater();
  jasmine.clock().tick(4000);

  expect(something).toBe('done');
  jasmine.clock().uninstall();
});

検査待ち

テストが継続する前に、アプリケーションで何かが起こるのを待つ必要があることがあります。setTimeoutの使用は避けてください。なぜなら、待つ理由が不明確になり、Karma の内部で 0 よりも大きな時間を使用すると、テストスイートが遅くなるからです。 代わりに、次のいずれかのアプローチを使用してください。

プロミスとAjaxコール

Promise が解決されるのを待つハンドラ関数を登録します。

const askTheServer = () => {
  return axios
    .get('/endpoint')
    .then(response => {
      // do something
    })
    .catch(error => {
      // do something else
    });
};

冗談で

it('waits for an Ajax call', async () => {
  await askTheServer()
  expect(something).toBe('done');
});

カルマの中で

it('waits for an Ajax call', done => {
  askTheServer()
    .then(() => {
      expect(something).toBe('done');
    })
    .then(done)
    .catch(done.fail);
});

もしPromiseにハンドラを登録できない場合、例えばそれが同期的な Vue のライフサイクルフックで実行される場合、waitForヘルパーを参照するか、保留中のPromiseをすべてフラッシュしてください:

冗談で

it('waits for an Ajax call', () => {
  synchronousFunction();
  jest.runAllTicks();

  expect(something).toBe('done');
});

Vueレンダリング

Vue コンポーネントが再レンダリングされるまで待機するには、同等のVue.nextTick() またはvm.$nextTick()のいずれかを使用します。

冗談で

it('renders something', () => {
  wrapper.setProps({ value: 'new value' });

  return wrapper.vm.$nextTick().then(() => {
    expect(wrapper.text()).toBe('new value');
  });
});

カルマの中で

it('renders something', done => {
  wrapper.setProps({ value: 'new value' });

  wrapper.vm
    .$nextTick()
    .then(() => {
      expect(wrapper.text()).toBe('new value');
    })
    .then(done)
    .catch(done.fail);
});

イベント

アプリケーションからイベントが発生し、それをテスト内で待機する必要がある場合は、 アサーションを含むイベントハンドラを登録します:

it('waits for an event', done => {
  eventHub.$once('someEvent', eventHandler);

  someFunction();

  function eventHandler() {
    expect(something).toBe('done');
    done();
  }
});

Jestでは、Promise

it('waits for an event', () => {
  const eventTriggered = new Promise(resolve => eventHub.$once('someEvent', resolve));

  someFunction();

  return eventTriggered.then(() => {
    expect(something).toBe('done');
  });
});

テストの分離の確保

テストは通常、テスト対象のコンポーネントのセットアップと分解を繰り返す必要があるパターンで設計されます。 これは、beforeEachafterEach フックを利用することで行われます。

  let wrapper;

  beforeEach(() => {
    wrapper = mount(Component);
  });

  afterEach(() => {
    wrapper.destroy();
  });

最初にこれを見ると、コンポーネントが各テストの前にセットアップされ、テストが終わると分解され、テスト間の分離が行われるのではないかと思うでしょう。

しかし、destroy メソッドは、wrapper オブジェクトで変更されたすべてのものを削除するわけではないので、これは完全には正しくありません。 機能コンポーネントの場合、destroy はレンダリングされた DOM 要素のみをドキュメントから削除します。

各テストできれいなラッパーオブジェクトとDOMが使用されていることを確実にするために、コンポーネントの分解はむしろ以下のように行われるべきです:

  afterEach(() => {
    wrapper.destroy();
    wrapper = null;
  });

destroy](https://vue-test-utils.vuejs.org/api/wrapper/#destroy)の[Vue Test Utils ドキュメントも参照してください。

Jestのベストプラクティス

GitLab 13.2 で導入されました

プリミティブ値の比較ではtoEqual よりもtoBe を優先。

Jest にはtoBetoEqual のマッチャがあります。toBeObject.isを使用して値を比較するので、toEqualを使用するよりも (デフォルトでは) 高速です。 後者はプリミティブな値に対しては最終的にObject.isを使用するようにフォールバックしますが、 複雑なオブジェクトで比較が必要な場合にのみ使用すべきです。

例:

const foo = 1;

// good
expect(foo).toBe(1);

// bad
expect(foo).toEqual(1);

もっとふさわしいマッチャーがいい

Jest にはtoHaveLengthtoBeUndefined のような便利なマッチャが用意されており、 テストを読みやすくしたりエラーメッセージをわかりやすくしたりすることができます。マッチャの一覧はドキュメントを参照ください。

例:

const arr = [1, 2];

// prints:
// Expected length: 1
// Received length: 2
expect(arr).toHaveLength(1);

// prints:
// Expected: 1
// Received: 2
expect(arr.length).toBe(1);

// prints:
// expect(received).toBe(expected) // Object.is equality
// Expected: undefined
// Received: "bar"
const foo = 'bar';
expect(foo).toBe(undefined);

// prints:
// expect(received).toBeUndefined()
// Received: "bar"
const foo = 'bar';
expect(foo).toBeUndefined();

toBeTruthy の使用は避けてください。toBeFalsy

Jest には次のようなマッチャーも用意されています:toBeTruthytoBeFalsy。これらのマッチャーはテストを弱くし、偽陽性の結果を出すので、使うべきではありません。

例えば、expect(someBoolean).toBeFalsy() は、someBoolean === nullのときにパスし、someBoolean === falseのときにパスします。

トリッキーtoBeDefined matcher

Jest にはtoBeDefined というトリッキーなマッチャーがあり、このマッチャーは偽陽性のテストを生成する可能性があります。 与えられた値をundefined に対してのみ検証するからです。

// good
expect(wrapper.find('foo').exists()).toBe(true);

// bad
// if finder returns null, the test will pass
expect(wrapper.find('foo')).toBeDefined();

の使用は避けてください。setImmediate

setImmediateの使用は避けるようにしてください。setImmediate は、I/Oが完了した後にコールバックを実行するためのアドホックなソリューションです。 また、Web APIの一部ではないため、ユニットテストではNodeJS環境をターゲットにしています。

setImmediateの代わりに、jest.runAllTimers またはjest.runOnlyPendingTimers を使って、保留中のタイマーを実行します。後者は、コード内にsetInterval がある場合に便利です。覚えておいてください:Jestのコンフィギュレーションでは、偽のタイマーを使っています。

工場

TBU

Jestによるモッキング戦略

スタブとモッキング

Jasmineはスタブ機能とモッキング機能を提供します。 KarmaとJestでの使い方に微妙な違いがあります。

スタブやスパイはよく同義語で使われます。 Jestでは、.spyOn メソッドのおかげでとても簡単です。公式ドキュメントより難しいのはモックで、関数や依存関係にも使えます。

手動モジュールモック

手動モックは、Jest 環境全体のモジュールをモックするために使われます。 これは非常に強力なテストツールで、テスト環境で簡単に消費できないモジュールをモックすることで、単体テストを単純化するのに役立ちます。

警告: モックがすべての spec で一貫して適用される必要がない場合 (つまり、いくつかの spec でしか必要とされない場合) は、手動モックを使わないでください。 代わりに、関連する spec ファイルでjest.mock(..)(あるいは同様のモック関数) を使うことを検討してください。

手動モックはどこに置くべきですか?

Jest は手動モジュールモックをサポートしており、ソースモジュールの隣にある__mocks__/ ディレクトリにモックを配置することができます (例:app/assets/javascripts/ide/__mocks__)。このようなことはしないでください。テストに関連するコードはすべて一か所 (spec/ フォルダ) にまとめたいのです。

node_modules パッケージに対して手動モックが必要な場合は、spec/frontend/__mocks__ フォルダを使用してください。以下はmonaco-editor](https://gitlab.com/gitlab-org/gitlab/blob/b7f914cddec9fc5971238cdf12766e79fa1629d7/spec/frontend/mocks/monaco-editor/index.js#L1)パッケージに対する[Jest モックの例です。

CEモジュールに手動モックが必要な場合は、spec/frontend/mocks/ce

  • spec/frontend/mocks/ce のファイルは、対応するCEモジュールをapp/assets/javascriptsからモックし、ソース・モジュールのパスをミラーリングします。
    • 例:spec/frontend/mocks/ce/lib/utils/axios_utils はモジュール~/lib/utils/axios_utilsをモックします。
  • EEモジュールのモッキングはまだサポートしていません。
  • ソースモジュールが存在しないモックが見つかった場合、テストスイートは失敗します。 仮想」モック、つまりソースモジュールと 1 対 1 の関連付けを持たないモックは、まだサポートされていません。

手動モックの例

  • mocks/axios_utils - このモックは役に立ちます。モックされていないリクエストをテストに通したくないからです。 また、axios.waitForAllのようなテストヘルパーを注入することもできます。
  • __mocks__/mousetrap/index.js - このモジュールはwebpackが理解できるAMDフォーマットを使っていますが、jest環境とは互換性がないため、このモックが役に立ちます。 このモックは動作を削除せず、es6互換のラッパーを提供するだけです。
  • __mocks__/monaco-editor/index.js - MonacoパッケージはJest環境では全く互換性がないため、このモックが役に立ちます。 実際、webpackはこのパッケージを動作させるために特別なローダーを必要とします。 このモックは単純にこのパッケージをJestで消費できるようにします。

モックを軽く保つ

グローバル・モックはマジックであり、技術的にはテスト・カバレッジを低下させます。 モックが有益であると判断された場合:

  • モックは短く、集中して。
  • なぜそれが必要なのか、モックにトップレベルのコメントを残してください。

その他のモッキング技術

利用可能なモッキング機能の完全な概要については、Jestの公式ドキュメントを参照してください。

フロントエンド・テストの実行

フロントエンドのテストを実行するには、以下のコマンドが必要です:

  • rake frontend:fixtures (再)備品を生成します。
  • yarn test はテストを実行します。
  • yarn jest は Jest テストだけを実行します。

備品が変わらない限り、yarn test で十分です(時間の節約にもなります)。

ライブテストとフォーカステスト – Jest

テストスイートで作業している間、これらの仕様をウォッチモードで実行し、保存のたびに自動的に再実行するようにするとよいでしょう。

# Watch and rerun all specs matching the name icon
yarn jest --watch icon

# Watch and rerun one specifc file
yarn jest --watch path/to/spec/file.spec.js

また、--watch フラグなしで、いくつかのフォーカステストを実行することもできます。

# Run specific jest file
yarn jest ./path/to/local_spec.js
# Run specific jest folder
yarn jest ./path/to/folder/
# Run all jest files which path contain term
yarn jest term

ライブテストと集中テスト – Karma

カルマでも似たようなことはできますが、コストがかかります。

yarn run karma-start で Karma を実行すると、JavaScript のアセットがコンパイルされ、http://localhost:9876/ でサーバーが実行され、そこに接続したブラウザー上でテストが自動的に実行されます。一度に複数のブラウザーでその URL を入力し、それぞれのテストを並行して実行させることができます。

Karma が実行されている間、あなたが行った変更は即座にテストスイート全体の再コンパイルと再テストを引き起こします。 そのため、あなたの変更によってテストが壊れたかどうかを即座に確認することができます。Jasmine のフォーカスされたテストや除外されたテスト (fdescribexdescribeを使用します) を使用することで、特定の機能の作業中に必要なテストのみを Karma に実行させることができます。

また、引数--filter-spec または short-fで実行テストをフィルタリングすることで、特定のフォルダやファイルに対してのみ Karma を実行することも可能です:

# Run all files
yarn karma-start
# Run specific spec files
yarn karma-start --filter-spec profile/account/components/update_username_spec.js
# Run specific spec folder
yarn karma-start --filter-spec profile/account/components/
# Run all specs which path contain vue_shared or vie
yarn karma-start -f vue_shared -f vue_mr_widget

グロブを引用符で囲むと、シェルがグロブを複数の引数に分割することがあります:

# Run all specs named `file_spec` within the IDE subdirectory
yarn karma -f 'spec/javascripts/ide/**/file_spec.js'

フロントエンドのテストフィクスチャ

HAML テンプレート (app/views/) に追加されるコード、もしくはバックエンドに Ajax リクエストを行うコードには、バックエンドから HTML もしくは JSON を要求するテストがあります。 これらのテストのためのフィクスチャは次の場所にあります:

  • spec/frontend/fixtures/CEでテストを実行するためのものです。
  • ee/spec/frontend/fixtures/EEでテストを実行するためのものです。

フィクスチャー・ファイル

  • Karmaテストスイートはjasmine-jqueryによって提供されます。
  • spec/frontend/helpers/fixtures.jsを使ってください。

以下は、Karma と Jest の両方で動作するテストの例です:

it('makes a request', () => {
  const responseBody = getJSONFixture('some/fixture.json'); // loads spec/frontend/fixtures/some/fixture.json
  axiosMock.onGet(endpoint).reply(200, responseBody);

  myButton.click();

  // ...
});

it('uses some HTML element', () => {
  loadFixtures('some/page.html'); // loads spec/frontend/fixtures/some/page.html and adds it to the DOM

  const element = document.getElementById('#my-id');

  // ...
});

HTML と JSON フィクスチャは RSpec を使ってバックエンドのビューとコントローラから生成されます (spec/frontend/fixtures/*.rbを参照)。

それぞれのフィクスチャについて、response 変数の内容が出力ファイルに保存されます。 この変数は、テストがtype: :request もしくはtype: :controllerとマークされている場合に、自動的に設定されます。フィクスチャはbin/rake frontend:fixtures コマンドを使って再生成されますが、例えばbin/rspec spec/frontend/fixtures/merge_requests.rbのように、個別に生成することもできます。 新しいフィクスチャを作成するとき、(ee/)spec/controllers/ もしくは(ee/)spec/requests/のエンドポイントに対応するテストを見ることは、しばしば意味があります。

データ駆動テスト

RSpec のパラメータ化されたテストと同様に、Jest はデータ駆動型のテストをサポートしています:

  • test.eachit.eachへのエイリアス)を使用した内部テスト。
  • describe.eachを使用したテストグループ。

各オプションは、データ値の配列あるいはタグつきテンプレートリテラルをとります。

使用例:

// function to test
const icon = status => status ? 'pipeline-passed' : 'pipeline-failed'
const message = status => status ? 'pipeline-passed' : 'pipeline-failed'

// test with array block
it.each([
    [false, 'pipeline-failed'],
    [true, 'pipeline-passed']
])('icon with %s will return %s',
 (status, icon) => {
    expect(renderPipeline(status)).toEqual(icon)
 }
);
// test suite with tagged template literal block
describe.each`
    status   | icon                 | message
    ${false} | ${'pipeline-failed'} | ${'Pipeline failed - boo-urns'}
    ${true}  | ${'pipeline-passed'} | ${'Pipeline succeeded - win!'}
`('pipeline component', ({ status, icon, message }) => {
    it(`returns icon ${icon} with status ${status}`, () => {
        expect(icon(status)).toEqual(message)
    })

    it(`returns message ${message} with status ${status}`, () => {
        expect(message(status)).toEqual(message)
    })
});

ゴッチャ

JavaScriptによるRSpecエラー

デフォルトでは、RSpecのユニットテストはヘッドレスブラウザでJavaScriptを実行せず、単にrailsによって生成されたHTMLの検査に依存します。

インテグレーションテストを正しく実行するために JavaScript に依存している場合、テストの実行時に JavaScript が有効になるように spec を設定する必要があります。 これを行わないと、spec runner からあいまいなエラーメッセージが表示されます。

rspec テストで JavaScript ドライバを有効にするには、JavaScript を有効にする必要がある個々の Spec、または複数の Spec を含むコンテキストブロックに:js を追加します:

# For one spec
it 'presents information about abuse report', :js do
  # assertions...
end

describe "Admin::AbuseReports", :js do
  it 'presents information about abuse report' do
    # assertions...
  end
  it 'shows buttons for adding to abuse report' do
    # assertions...
  end
end

フロントエンドテストレベルの概要

フロントエンドのテストレベルに関する主な情報は、テストレベルのページにあります。

フロントエンド開発に関連するテストは、以下の場所にあります:

  • spec/javascripts/カルマテスト用
  • spec/frontend/Jest テスト用
  • spec/features/RSpec テスト用

RSpec は完全な機能テストを実行し、Jest と Karma ディレクトリはフロントエンドのユニットテストフロントエンドのコンポーネントテストフロントエンドのインテグレーションテストを含みます。

spec/javascripts/ にあるすべてのテストは最終的にspec/frontend/ に移行されます(#52483も参照)。

2018年5月以前は、features/ 、Spinachによって実行される機能テストも含まれていました。これらのテストは2018年5月にコードベースから削除されました(#23036)。

Vue コンポーネントのテストに関する注意事項も参照してください。

テストヘルパー

Vuexヘルパー:testAction

公式ドキュメントにあるように、アクションのテストを簡単にするヘルパーを用意しています:

testAction(
  actions.actionName, // action
  { }, // params to be passed to action
  state, // state
  [
    { type: types.MUTATION},
    { type: types.MUTATION_1, payload: {}},
  ], // mutations committed
  [
    { type: 'actionName', payload: {}},
    { type: 'actionName1', payload: {}},
  ] // actions dispatched
  done,
);

spec/javascripts/ide/stores/actions_spec.jsspec/javascripts/ide/stores/actions_spec.jsの例をご覧ください。

アクシオス・リクエストが終わるまでお待ちください

spec/frontend/mocks/ce/lib/utils/axios_utils.js にある Axios Utils モックモジュールには、HTTP リクエストを生成する Jest テスト用のヘルパーメソッドが 2 つ含まれています。 これらは、リクエストの Promise に対するハンドルを持っていない場合、例えば Vue コンポーネントがそのライフサイクルの一部としてリクエストを行う場合などに非常に便利です。

  • waitFor(url, callback):url へのリクエストが終了(成功または失敗)した後にcallback を実行します。
  • waitForAll(callback)callback 保留中のリクエストがない場合は、 callback次のティックで実行されます。

どちらの関数も、.then() または.catch() ハンドラが実行できるように、リクエスト終了後の次のティックでcallback を実行します(setImmediate()を使用)。

古いブラウザでのテスト

リグレッションの中には、特定のブラウザのバージョンにのみ影響するものがあります。 FirefoxまたはBrowserStackを使用して、以下の手順で特定のブラウザをインストールし、テストすることができます:

ブラウザスタック

BrowserStackでは、1200以上のモバイルデバイスやブラウザをテストすることができます。ライブアプリから直接使用することも、クローム拡張機能をインストールして簡単にアクセスすることもできます。 GitLabの共有1PasswordアカウントのEngineeringvaultに保存された認証情報でBrowserStackにサインインします。

ファイアフォックス

macOS

古いバージョンの Firefox は、リリースの FTP サーバーhttps://ftp.mozilla.org/pub/firefox/releases/からダウンロードできます:

  1. ウェブサイトからバージョンを選択し、この場合は50.0.1
  2. macフォルダに移動します。
  3. お好きな言語を選択し、DMGパッケージをダウンロードしてください。
  4. アプリケーションをApplications フォルダ以外のフォルダにドラッグ&ドロップします。
  5. アプリケーション名をFirefox_Oldのように変更してください。
  6. アプリケーションをApplications フォルダに移動します。
  7. ターミナルを開き、/Applications/Firefox_Old.app/Contents/MacOS/firefox-bin -profilemanager を実行して、その Firefox バージョン専用の新しいプロファイルを作成します。
  8. プロファイルの作成が完了したら、アプリを終了し、通常通り再度実行してください。 これで古いバージョンのFirefoxが動作するようになりました。

テストドキュメントに戻る