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

GitLabでフロントエンドのコードを開発する際に遭遇するテストスイートは2種類あります。JavaScriptのユニットテストとインテグレーションテストにはJestを使い、e2e(エンドツーエンド)のインテグレーションテストにはCapybaraを使ったRSpecの機能テストを使います。

ユニットテストと機能テストは、すべての新しい機能に対して書く必要があります。ほとんどの場合、機能テストにはRSpecを使うべきです。機能テストの始め方についての詳細は、機能テストの始め方 を参照ください。

リグレッションテストは、バグ修正のために書くべきです。

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

Vue.js のテスト

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

Jest

フロントエンドのユニットテストとインテグレーションテストを書くためにJestを使います。Jest テストは/spec/frontend/ee/spec/frontend in EE にあります。

jsdomの制限

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

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

Jest テストのデバッグ

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

タイムアウトエラー

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

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

テストのパフォーマンスを改善できない場合は、テストスイート全体のタイムアウトを増やすことができます。jest.setTimeout

jest.setTimeout(500);

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

を使うか、あるいは第三引数をit

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

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

テスト固有のスタイルシート

RSpec のインテグレーションテストを容易にするために、2 つのテスト専用スタイルシートがあります。これらを使って、アニメーションを無効にしてテストの速度を向上させたり、Capybaraのクリックイベントのターゲットとなる要素を見えるようにしたりすることができます:

  • app/assets/stylesheets/disable_animations.scss
  • app/assets/stylesheets/test_environment.scss

テスト環境は可能な限り本番環境と一致させる必要があるため、これらの使用は最小限にとどめ、必要な場合にのみ追加してください。

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

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

ライブラリをテストしない

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

import { convertToFahrenheit } from 'temperatureLibrary'

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

getFahrenheit 関数をテストするのは意味がありません。なぜなら、その関数の内部ではライブラリ関数を呼び出しているだけだからです。

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

// Component script
{
  computed: {
    hasMetricTypes() {
      return this.metricTypes.length;
    },
}
<!-- Component template -->
<template>
  <gl-dropdown v-if="hasMetricTypes">
    <!-- Dropdown content -->
  </gl-dropdown>
</template>

hasMetricTypes computed propをテストすることは当然のことのように思えます。しかし、computedプロパティがmetricTypes の長さを返しているかどうかをテストすることは、Vueライブラリ自体をテストすることになります。これには、テストスイートに追加する以外に価値はありません。ユーザーがコンポーネントとインタラクトする方法でコンポーネントをテストするほうがよいのです。

// Bad
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);
    });
  });
});

// Good
it('displays a dropdown if metricTypes exist', () => {
  factory({ metricTypes });
  expect(wrapper.findComponent(GlDropdown).exists()).toBe(true);
});

it('does not display a dropdown if no metricTypes exist', () => {
  factory();
  expect(wrapper.findComponent(GlDropdown).exists()).toBe(false);
});

この種のテストは、ロジックの更新を必要以上に壊れやすく面倒なものにするだけなので、気をつけましょう。これは他のライブラリにも当てはまります。wrapper.vm のプロパティをチェックしているのであれば、一度立ち止まって、代わりにレンダリングされたテンプレートをチェックするようにテストを考え直すべきでしょう。

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

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

もうひとつのよくある失敗例は、モックが動作しているかどうかをスペックで検証してしまうことです。モックを使うのであれば、モックはテストをサポートすべきですが、テストの対象にはすべきではありません。

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

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

// Good: 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 要素をクエリする場合は、その要素を一意的かつ意味的にターゲットにするのが一番です。

DOM Testing Library を使って、ユーザーが実際に見ているものをターゲットにするのが望ましいでしょう。テキストで選択する場合は、アクセシビリティのベストプラクティスを実施するのに役立つbyRole クエリを使用するのがベストです。findByRole やその他のDOM Testing Library クエリはshallowMountExtended あるいはmountExtendedを使用するときに使用できます。

Vue コンポーネントのユニットテストを記述する場合、子コンポーネントの動作の複雑さに対処するよりも、ユニットテストが包括的な値のカバレッジに集中できるように、コンポーネントごとに子コンポーネントをクエリするのが賢明です。

上記のどちらも実現できないこともあります。このような場合は、テスト属性を追加してセレクタを単純化するのが最良の方法です。可能なセレクタの一覧は次のとおりです:

  • のようなセマンティック属性(適切に設定されているname ことも確認 nameします)。
  • data-testid 属性 (@vue/test-utils](https://github.com/vuejs/vue-test-utils/issues/1498#issuecomment-610133465)のメンテナーが推奨する[) オプションで、shallowMountExtended またはmountExtended
  • Vueref (@vue/test-utils を使用する場合)
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'

const wrapper = shallowMountExtended(ExampleComponent);

it('exists', () => {
  // Best (especially for integration tests)
  wrapper.findByRole('link', { name: /Click Me/i })
  wrapper.findByRole('link', { name: 'Click Me' })
  wrapper.findByText('Click Me')
  wrapper.findByText(/Click Me/i)

  // Good (especially for unit tests)
  wrapper.findComponent(FooComponent);
  wrapper.find('input[name=foo]');
  wrapper.find('[data-testid="my-foo-id"]');
  wrapper.findByTestId('my-foo-id'); // with shallowMountExtended or mountExtended – check below
  wrapper.find({ ref: 'foo'});

  // Bad
  wrapper.find('.js-foo');
  wrapper.find('.btn-primary');
});

data-testid 属性にはkebab-case を使用してください。

テストのためだけに.js-* クラスを追加することは推奨されません。他に実行可能なオプションがない場合にのみ、これを実行してください。

子コンポーネントのクエリ

Vue コンポーネントを@vue/test-utils でテストする場合、DOM ノードをクエリする代わりに子コンポーネントをクエリするという方法もあります。これは、テスト対象の動作の実装の詳細が、そのコンポーネントの個別のユニットテストでカバーされていることを前提としています。テスト対象のコンポーネントの期待される振る舞いをテストが確実にカバーする限り、DOM クエリとコンポーネントクエリのどちらに強くこだわる必要はありません。

使用例:

it('exists', () => {
  wrapper.findComponent(FooComponent);
});

ユニットテストの命名

特定の関数/メソッドをテストするためにテストブロックを記述する場合、 メソッド名を記述ブロック名として使用します。

悪い

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

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

良い

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 () => {
  await expect(user.getUserName(1)).rejects.toThrow('User with 1 not found.');
});

テスト関数からプロミスを返すこともできます。

donedone.fail コールバックは、プロミスを扱うときには使わないでください。使用すべきではありません。

悪い

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

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

良い

// verifying a resolved promise
it('tests a promise', () => {
  return promise
    .then(data => {
      expect(data).toBe(asExpected);
    });
});

// verifying a resolved promise using Jest's `resolves` matcher
it('tests a promise', () => {
  return expect(promise).resolves.toBe(asExpected);
});

// verifying a rejected promise using Jest's `rejects` matcher
it('tests a promise rejection', () => {
  return expect(promise).rejects.toThrow(expectedError);
});

時間を操る

時間に敏感なコードをテストしなければならないことがあります。たとえば、X 秒ごとに繰り返し実行されるイベントなどです。そのような場合の対処法をいくつか紹介します:

setTimeout() アプリケーション内部で /setInterval()

アプリケーション自身が何らかの待ち時間がある場合、その待ち時間をモックで待ちます。Jestでは、これはデフォルトですでに行われています(Jest Timer Mocksも参照してください)。

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

を参照してください:

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

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

Jestでの現在地のモック

note
以前のテストが後のテストに影響しないように、window.location.href の値はテストごとにリセットされます。

window.location.href に特定の値を指定する必要がある場合は、setWindowLocation ヘルパーを使用します:

import setWindowLocation from 'helpers/set_window_location_helper';

it('passes', () => {
  setWindowLocation('https://gitlab.test/foo?bar=true');

  expect(window.location).toMatchObject({
    hostname: 'gitlab.test',
    pathname: '/foo',
    search: '?bar=true',
  });
});

ハッシュだけを変更したい場合は、setWindowLocation ヘルパーを使うか、window.location.hash に直接代入します:

it('passes', () => {
  window.location.hash = '#foo';

  expect(window.location.href).toBe('http://test.host/#foo');
});

テストがwindow.location のメソッドがコールされたことを保証する必要がある場合は、useMockLocationHelper ヘルパーを使用します:

import { useMockLocationHelper } from 'helpers/mock_window_location_helper';

useMockLocationHelper();

it('passes', () => {
  window.location.reload();

  expect(window.location.reload).toHaveBeenCalled();
});

テストでの待機

テストは、アプリケーションで何かが起こるのを待ってから続けなければならないことがあります。

これは避けるべきです:

  • setTimeout というのも、待つ理由が不明確になってしまうからです。さらに、私たちのテストではフェイクなので、その使い方はトリッキーです。
  • setImmediate なぜなら、Jest 27以降ではサポートされなくなったからです。詳しくはこのエピックを見てください。

プロミスと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');
});

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

を参照してください:

it('waits for an Ajax call', async () => {
  synchronousFunction();

  await waitForPromises();

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

Vueのレンダリング

nextTick() を使って、Vue コンポーネントが再レンダリングされるまで待ちます。

を参照してください:

import { nextTick } from 'vue';

// ...

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

  await nextTick();

  expect(wrapper.text()).toBe('new value');
});

イベント

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

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

  someFunction();

  return new Promise((resolve) => {
    function expectEventHandler() {
      expect(something).toBe('done');
      resolve();
    }
  });
});

Jest では、このためにPromise を使うこともできます:

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

  someFunction();

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

gon オブジェクトを操作します。

gon (window.gon) はバックエンドからのデータを渡すために使われるグローバルオブジェクトです。あなたのテストがこのオブジェクトの値に依存している場合は、直接このオブジェクトを変更することができます:

describe('when logged in', () => {
  beforeEach(() => {
    gon.current_user_id = 1;
  });

  it('shows message', () => {
    expect(wrapper.text()).toBe('Logged in!');
  });
})

gon テストが分離されていることを保証するために、すべてのテストでリセットされます。

テストが確実に分離されていること

テストは通常、テスト対象のコンポーネントを繰り返しセットアップする必要があるパターンで設計されます。これは、beforeEach フックを使用することで実現できます。

物件例

  let wrapper;

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

enableAutoDestroy を使うと、手動でwrapper.destroy() を呼び出す必要がなくなります。しかし、いくつかのモック、スパイ、フィクスチャは破棄する必要があり、afterEach フックを活用することができます。

物件例

  let wrapper;

  afterEach(() => {
    fakeApollo = null;
    store = null;
  });

ローカルのみのアポロのクエリと突然変異のテスト

バックエンドに追加する前に新しいクエリや変異を追加するには、@client ディレクティブを使用します。例えば

mutation setActiveBoardItemEE($boardItem: LocalBoardItem, $isIssue: Boolean = true) {
  setActiveBoardItem(boardItem: $boardItem) @client {
    ...Issue @include(if: $isIssue)
    ...EpicDetailed @skip(if: $isIssue)
  }
}

このような呼び出しのテストケースを書く際には、 リゾルバを使用して正しいパラメータで呼び出されるようにします。

例えば、ラッパーを作成する際に、リゾルバがクエリや変異にマッピングされていることを確認します。ここでモックしている変異はsetActiveBoardItem です:

const mockSetActiveBoardItemResolver = jest.fn();
const mockApollo = createMockApollo([], {
    Mutation: {
      setActiveBoardItem: mockSetActiveBoardItemResolver,
    },
});

以下のコードでは、4つの引数を渡さなければなりません。番目の引数はモックするクエリの入力変数のコレクションです。ミューテーションが正しい引数で呼び出されることをテストします:

it('calls setActiveBoardItemMutation on close', async () => {
    wrapper.findComponent(GlDrawer).vm.$emit('close');

    await waitForPromises();

    expect(mockSetActiveBoardItemResolver).toHaveBeenCalledWith(
        {},
        {
            boardItem: null,
        },
        expect.anything(),
        expect.anything(),
    );
});

Jest のベストプラクティス

GitLab 13.2 で導入されました

プリミティブ値を比較する際にtoEqual よりtoBe を優先

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

例:

const foo = 1;

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

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

より適切なマッチングを希望

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

例:

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 マッチャー

Jest にはtoBeDefined matcher というトリッキーなものがあります。なぜなら、与えられた値をundefined に対してのみ検証するからです。

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

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

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

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

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

非決定的な仕様を避ける

非決定性は、薄っぺらで脆い仕様の温床です。そのようなスペックはCIパイプラインを壊してしまい、他の貢献者の作業の流れを妨げてしまいます。

  1. テスト対象の共同作業者(Axios、Apollo、Lodashヘルパーなど)やテスト環境(Dateなど)が、システム間や時間経過に関わらず一貫した動作をするようにしましょう。
  2. テストに集中し、「余分な作業」をしないようにします(たとえば、個々のテストでテスト対象を不必要に複数回作成するなど)。

Date 決定論の偽造

Date はJest環境ではデフォルトでフェイクされます。つまり、Date() またはDate.now() を呼び出すと、決定論的な固定値が返されます。

デフォルトの偽の日付を本当に変更する必要がある場合は、どのdescribe ブロック describe内でもuseFakeDate を呼び出すことができます:

import { useFakeDate } from 'helpers/fake_date';

describe('cool/component', () => {
  // Default fake `Date`
  const TODAY = new Date();

  // NOTE: `useFakeDate` cannot be called during test execution (that is, inside `it`, `beforeEach`, `beforeAll`, etc.).
  describe("on Ada Lovelace's Birthday", () => {
    useFakeDate(1815, 11, 10)

    it('Date is no longer default', () => {
      expect(new Date()).not.toEqual(TODAY);
    });
  });

  it('Date is still default in this scope', () => {
    expect(new Date()).toEqual(TODAY)
  });
})

同様に、本物のDate クラスを使う必要がある場合は、describe ブロック内でuseRealDate をインポートして呼び出します:

import { useRealDate } from 'helpers/fake_date';

// NOTE: `useRealDate` cannot be called during test execution (that is, inside `it`, `beforeEach`, `beforeAll`, etc.).
describe('with real date', () => {
  useRealDate();
});

Math.random 決定論の偽造

被験者がMath.random に依存する場合、 を偽物に置き換えることを検討してください。

beforeEach(() => {
  // https://xkcd.com/221/
  jest.spyOn(Math, 'random').mockReturnValue(0.4);
});

ファクトリー

TBU

Jestを使ったモッキング戦略

スタビングとモッキング

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

手動モジュールモック

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

警告:モックがすべての仕様で一貫して適用されるべきではない場合 (つまり、一部の仕様でしか必要とされない場合) は、手動モックを使用しないでください。代わりに、関連する 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/__helpers__/mocks に置き、frontend/test_setup (またはfrontend/shared_test_setup) に次のような行を追加してください:

// "~/lib/utils/axios_utils" is the path to the real module
// "helpers/mocks/axios_utils" is the path to the mocked implementation
jest.mock('~/lib/utils/axios_utils', () => jest.requireActual('helpers/mocks/axios_utils'));

手動モックの例

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

モックは軽量に

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

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

その他のモック・テクニック

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

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

フィクスチャを生成する前に、実行中の GDK インスタンスがあることを確認してください。

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

  • rake frontend:fixtures (再)フィクスチャを生成します。フィクスチャを必要とするテストを実行する前に、フィクスチャが最新であることを確認してください。
  • yarn jest は Jest テストを実行します。

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

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

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

# Watch and rerun one specific 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

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

フロントエンドフィクスチャはバックエンドコンテナからのレスポンスを含むファイルです。これらのレスポンスは HAML テンプレートから生成された HTML か JSON ペイロードのどちらかです。これらのレスポンスに依存するフロントエンドのテストでは、バックエンドのコードとの正しいインテグレーションを検証するためにフィクスチャを使うことがよくあります。

フィクスチャの使用

JSON もしくは HTML フィクスチャをインポートするには、test_fixtures エイリアスを使ってimport してください。

import responseBody from 'test_fixtures/some/fixture.json' // loads tmp/tests/frontend/fixtures-ee/some/fixture.json

it('makes a request', () => {
  axiosMock.onGet(endpoint).reply(200, responseBody);

  myButton.click();

  // ...
});

フィクスチャの生成

テストフィクスチャを生成するコードは

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

を実行することでフィクスチャを生成できます:

  • bin/rake frontend:fixtures すべてのフィクスチャを生成するには
  • bin/rspec spec/frontend/fixtures/merge_requests.rb 特定のフィクスチャを生成する(この場合、merge_request.rb)

生成されたフィクスチャはtmp/tests/frontend/fixtures-ee にあります。

フィクスチャのダウンロード

GitLab CIでフィクスチャを生成し、パッケージレジストリに保存します。

scripts/frontend/download_fixtures.sh スクリプトは、これらのフィクスチャをダウンロードして、内部で使うために抽出するためのものです:

# Checks if a frontend fixture package exists in the gitlab-org/gitlab
# package registry by looking at the commits on a local branch.
#
# The package is downloaded and extracted if it exists
$ scripts/frontend/download_fixtures.sh

# Same as above, but only looks at the last 10 commits of the currently checked-out branch
$ scripts/frontend/download_fixtures.sh --max-commits=10

# Looks at the commits on the local master branch instead of the currently checked-out branch
$ scripts/frontend/download_fixtures.sh --branch master

新しいフィクスチャの作成

それぞれのフィクスチャについて、response 変数の内容を出力ファイルで見つけることができます。たとえば、spec/frontend/fixtures/merge_requests.rb"merge_requests/diff_discussion.json" という名前のテストは、出力ファイルtmp/tests/frontend/fixtures-ee/merge_requests/diff_discussion.jsonを生成します。テストがtype: :request またはtype: :controllerとマークされている場合、response 変数が自動的に設定されます。

新しいフィクスチャを作成するとき、(ee/)spec/controllers/(ee/)spec/requests/ にあるエンドポイントに対応するテストを見ることは、しばしば意味があります。

GraphQL クエリフィクスチャ

get_graphql_query_as_string ヘルパーメソッドを使って、GraphQL クエリの結果を表すフィクスチャを作成できます。たとえば

# spec/frontend/fixtures/releases.rb

describe GraphQL::Query, type: :request do
  include GraphqlHelpers

  all_releases_query_path = 'releases/graphql/queries/all_releases.query.graphql'

  it "graphql/#{all_releases_query_path}.json" do
    query = get_graphql_query_as_string(all_releases_query_path)

    post_graphql(query, current_user: admin, variables: { fullPath: project.full_path })

    expect_graphql_errors_to_be_empty
  end
end

これはtmp/tests/frontend/fixtures-ee/graphql/releases/graphql/queries/all_releases.query.graphql.json にある新しいフィクスチャを作成します。

前述したようにtest_fixtures エイリアスを使って Jest テストで JSON フィクスチャをインポートできます。

データ駆動型テスト

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

  • test.each (it.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)
 }
);
note
テンプレート・リテラル・ブロックは、スペック出力にプリティ・プリントが必要ない場合のみ使用してください。例えば、空の文字列、ネストされたオブジェクトなどです。

例えば、空の検索文字列と空でない検索文字列の違いをテストする場合、pretty print オプションを使用した配列ブロック構文の使用が好ましいでしょう。そうすれば、空の文字列('')と空でない文字列('search string')の違いが spec の出力に表示されます。一方、テンプレートリテラルブロックでは、空文字列はスペースとして表示され、開発者を混乱させる可能性があります。

// bad
it.each`
    searchTerm | expected
    ${''} | ${{ issue: { users: { nodes: [] } } }}
    ${'search term'} | ${{ issue: { other: { nested: [] } } }}
`('when search term is $searchTerm, it returns $expected', ({ searchTerm, expected }) => {
  expect(search(searchTerm)).toEqual(expected)
});

// good
it.each([
    ['', { issue: { users: { nodes: [] } } }],
    ['search term', { issue: { other: { nested: [] } } }],
])('when search term is %p, expect to return %p',
 (searchTerm, expected) => {
    expect(search(searchTerm)).toEqual(expected)
 }
);

// 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

非同期インポートによる Jest テストのタイムアウト

モジュールが実行時に他のモジュールを非同期にインポートする場合、これらのモジュールは実行時に Jest ローダによってトランスパイルされなければなりません。これによってJest がタイムアウトする可能性があります。

このイシューに遭遇した場合、Jestがコンパイル時にそれをコンパイルしてキャッシュするようにモジュールをeager importすることを検討してください。

次の例を考えてみましょう:

// the_subject.js

export default {
  components: {
    // Async import Thing because it is large and isn't always needed.
    Thing: () => import(/* webpackChunkName: 'thing' */ './path/to/thing.vue'),
  }
};

Jestはthing.vue モジュールを自動的にトランスパイルせず、そのサイズによってはJestがタイムアウトする可能性があります。このモジュールをeagerly importすることで、Jestにトランスパイルとキャッシュをさせることができます:

// the_subject_spec.js

import Subject from '~/feature/the_subject.vue';

// Force Jest to transpile and cache
// eslint-disable-next-line no-unused-vars
import _Thing from '~/feature/path/to/thing.vue';
note
テストのタイムアウトを無視しないでください。テストのタイムアウトを無視してはいけません。この機会に本番環境の webpack バンドルとチャンクを分析し、非同期インポートに本番環境の問題がないことを確認してください。

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

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

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

  • spec/frontend/Jest テスト用
  • spec/features/RSpec テスト用

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

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

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

テストヘルパー

テストヘルパーはspec/frontend/__helpers__にあります。 新しいヘルパーを導入する場合は、そのディレクトリに配置してください。

Vuexヘルパー:testAction

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

// prefer using like this, a single object argument so parameters are obvious from reading the test
await testAction({
  action: actions.actionName,
  payload: { deleteListId: 1 },
  state: { lists: [1, 2, 3] },
  expectedMutations: [ { type: types.MUTATION} ],
  expectedActions: [],
});

// old way, don't do this for new tests
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,
);

Axiosのリクエストが終わるまで待ちます

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

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

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

shallowMountExtendedmountExtended

shallowMountExtended およびmountExtended ユーティリティは、利用可能なDOM Testing Library のクエリの先頭にfind またはfindAllをつけることで、そのクエリを実行する機能を提供します。

import { shallowMountExtended } from 'helpers/vue_test_utils_helper';

describe('FooComponent', () => {
  const wrapper = shallowMountExtended({
    template: `
      <div data-testid="gitlab-frontend-stack">
        <p>GitLab frontend stack</p>
        <div role="tablist">
          <button role="tab" aria-selected="true">Vue.js</button>
          <button role="tab" aria-selected="false">GraphQL</button>
          <button role="tab" aria-selected="false">SCSS</button>
        </div>
      </div>
    `,
  });

  it('finds elements with `findByTestId`', () => {
    expect(wrapper.findByTestId('gitlab-frontend-stack').exists()).toBe(true);
  });

  it('finds elements with `findByText`', () => {
    expect(wrapper.findByText('GitLab frontend stack').exists()).toBe(true);
    expect(wrapper.findByText('TypeScript').exists()).toBe(false);
  });

  it('finds elements with `findAllByRole`', () => {
    expect(wrapper.findAllByRole('tab').length).toBe(3);
  });
});

spec/frontend/alert_management/components/alert_details_spec.jsで例を確認してください。

古いブラウザでのテスト

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

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が使えるようになります。

スナップショット

Jest スナップショットテストは、与えられたコンポーネントの HTML 出力に予期せぬ変更が加えられるのを防ぐ便利な方法です。他のテスト方法 (vue-tests-utils を使った要素のアサーションなど) では必要なユースケースをカバーできない場合にのみ使うようにしましょう。GitLab でスナップショットテストを使うには、いくつかのガイドラインに注意する必要があります:

  • スナップショットをコードとして扱う
  • スナップショットファイルをブラックボックスと思わないでください。
  • スナップショットの出力に注意を払ってください。通常、生成されたスナップショットファイルを他のコードと同じように読みます。

スナップショットテストは、テストされる項目に投入されたものの生のString 表現を保存する単純な方法だと考えてください。これはコンポーネント、ストア、生成された出力の複雑な部分などの変更を評価するために使われます。推奨されるDo's and Don'ts 。スナップショットテストは非常に強力なツールですが、ユニットテストを置き換えるものではなく、補足するものです。

Jestは、スナップショットを作成する際に留意すべきベストプラクティスに関する素晴らしいドキュメントを提供しています。

スナップショットはどのように機能するのですか?

スナップショットは、関数呼び出しの左側でテストされるように要求されたものを文字列化したものです。つまり、文字列の書式に何らかの変更を加えると、結果に影響を与えます。この処理は、シリアライザーを活用して自動変換ステップで行われます。Vueでは、適切なシリアライザを提供するvue-jest パッケージを活用することで、この処理はすでに行われています。

仕様の結果が、生成されたスナップショットファイルの内容と異なる場合は、テストスイートのテストが失敗することで通知されます。

詳細は Jests 公式ドキュメントhttps://jestjs.io/docs/snapshot-testingを参照ください。

長所と短所

長所

  • 重要なHTML構造が誤って変更されないよう、適切な警告を表示します。
  • セットアップの容易さ

短所

  • vue-tests-utils 、要素を見つけ、その存在を直接主張することによって提供される明確さやガードレールに欠けています。
  • 意図的にコンポーネントを更新する場合、不必要なノイズが発生します。
  • バグのスナップショットを取得するリスクが高く、イシューを修正する際にテストが失敗するため、テストが不利になります。
  • スナップショットには意味のあるアサーションや期待値がないため、推論や置換が難しい
  • GitLab UI のような依存ライブラリと一緒に使うと、テスト対象のコンポーネントの HTML が変更されたときにテストが壊れやすくなります。

いつ

スナップショットを使うのは次のような場合です。

  • 重要なHTML構造を保護し、誤って変更しないようにします。
  • 複雑なユーティリティ関数のJSオブジェクトまたはJSON出力のアサーション

使用しない場合

スナップショットは次のような場合には使用しないでください。

  • 代わりにvue-tests-utils を使ってテストを書くことができます。
  • コンポーネントのロジックのアサーション
  • データ構造の出力の予測
  • リポジトリの外にUI要素がある(GitLabのUIバージョン更新を思い浮かべてください)

使用例

おわかりのように、スナップショットテストの短所は、一般的に長所をはるかに上回ります。このことをよりよく説明するために、このセクションではスナップショットテストを使いたくなるようないくつかの例と、なぜそれが良くないパターンなのかを示します。

例1 - 要素の可視性

要素の可視性をテストする場合、vue-tests-utils (VTU) を使って指定されたコンポーネントを見つけ、それから VTU ラッパーで基本的な.exists() メソッドを呼び出すことを推奨します。こうすることで、より読みやすく、より弾力的なテストが可能になります。以下の例を見ると、スナップショットのアサーションが、あなたが何を期待しているのかがわからないことに注意してください。私たちは、it の記述に完全に頼ってコンテキストを与え、スナップショットが目的の動作をキャプチャしているという前提に立っています。

<template>
  <my-component v-if="isVisible" />
</template>

悪いこと

it('hides the component', () => {
  createComponent({ props: { isVisible: false }})

  expect(wrapper.element).toMatchSnapshot()
})

it('shows the component', () => {
  createComponent({ props: { isVisible: true }})

  expect(wrapper.element).toMatchSnapshot()
})

良い

it('hides the component', () => {
  createComponent({ props: { isVisible: false }})

  expect(findMyComponent().exists()).toBe(false)
})

it('shows the component', () => {
  createComponent({ props: { isVisible: true }})

  expect(findMyComponent().exists()).toBe(true)
})

それだけでなく、間違ったpropをコンポーネントに渡し、間違ったvisibilityを持つことを想像してみてください: スナップショットテストは、イシューを持つHTMLをキャプチャしているのでまだパスします。

例2 - テキストの存在

vue-test-utils メソッドwrapper.text() を使用すれば、 コンポーネント内のテキストを見つけるのはとても簡単です。しかし、フォーマットやHTMLのネストによって返される値に一貫性のないスペースが多い場合、スナップショットを使用したくなるケースがあります。

このようなインスタンスでは、スナップショットを使用して空白を無視するよりも、文字列を個別にアサーションして複数のアサーションを行うほうがよいでしょう。DOM レイアウトを変更すると、たとえテキストが完璧にフォーマットされていたとしてもスナップショットテストに失敗してしまうからです。

<template>
  <gl-sprintf :message="my-message">
    <template #code="{ content }">
      <code>{{ content }}</code>
    </template>
  </gl-sprintf>
  <p> My second message </p>
</template>

悪いこと

it('renders the text as I expect', () => {
  expect(wrapper.text()).toMatchSnapshot()
})

良い

it('renders the code snippet', () => {
  expect(findCodeTag().text()).toContain("myFunction()")
})

it('renders the paragraph text', () => {
  expect(findOtherText().text()).toBe("My second message")
})

例3 - 複雑なHTML

非常に複雑な HTML がある場合は、それを全体としてとらえるのではなく、 特定のセンシティブで意味のあるポイントをアサーションすることに集中すべきです。スナップショットテストの価値は、開発者が変更するつもりのない HTML 構造を誤って変更してしまったことを警告することです。複雑なHTMLの出力でよくあることですが、変更の出力が読みにくい場合、何かが変更されたという信号自体で十分なのでしょうか?もしそうだとしたら、スナップショットなしで実現できるのでしょうか?

複雑な HTML 出力の良い例はGlTable です。スナップショットテストは、行や列の構造をキャプチャできるので、良いオプションのように感じるかもしれませんが、その代わりに、期待するテキストをアサートしたり、行や列の数を手動で数えてみるべきです。

<template>
  <gl-table ...all-them-props />
</template>

悪いこと

it('renders GlTable as I expect', () => {
  expect(findGlTable().element).toMatchSnapshot()
})

良い

it('renders the right number of rows', () => {
  expect(findGlTable().findAllRows()).toHaveLength(expectedLength)
})

it('renders the special icon that only appears on a full moon', () => {
  expect(findGlTable().findMoonIcon().exists()).toBe(true)
})

it('renders the correct email format', () => {
  expect(findGlTable().text()).toContain('my_strange_email@shaddyprovide.com')
})

より冗長になりましたが、GlTable の内部実装が変更されても、テストが壊れることはありません。また、リファクタリングやテーブルの追加を行う際に、何が重要であるかを他の開発者(あるいは半年後の自分自身)に伝えることができます。

スナップショットの取り方

it('makes the name look pretty', () => {
  expect(prettifyName('Homer Simpson')).toMatchSnapshot()
})

このテストが初めて実行されると、.snap ファイルが作成されます。このようなファイルです:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`makes the name look pretty`] = `
Sir Homer Simpson the Third
`

これで、このテストを呼び出すたびに、新しいスナップショットが以前に作成されたバージョンに対して評価されるようになります。これは、スナップショットファイルの内容を理解し、それを注意深く扱うことが重要であるという事実を強調すべきです。スナップショットの出力が大きすぎたり複雑すぎたりして読み取れない場合、スナップショットはその価値を失います。これは、スナップショットをマージリクエストレビューで評価可能な、あるいは絶対に変更されないことが保証された、人間が読み取れる項目に隔離しておくことを意味します。同じことがwrapperselements

it('renders the component correctly', () => {
  expect(wrapper).toMatchSnapshot()
  expect(wrapper.element).toMatchSnapshot();
})

上記のテストは2つのスナップショットを作成します。どちらのスナップショットがコードベースの安全性にとってより価値があるかを判断することが重要です。つまり、どちらかのスナップショットが変更された場合に、 コードベースが壊れている可能性があるかどうかということです。これは、基礎となる依存関係の何かが知らないうちに変更された場合に、予期しない変更をキャッチするのに役立ちます。

機能テストを始めましょう

機能テストとは

white-box testing とも呼ばれる機能テストは、ブラウザを起動し、Capybaraヘルパーを持つテストです。つまり、テストは

  • ブラウザ内で要素を見つけること。
  • その要素をクリックします。
  • APIを呼び出します。

機能テストは実行コストが高いです。テストを実行する前に、この種のテストが本当に必要なのかを確認すべきです。

私たちの機能テストはすべてRuby で書かれていますが、ユーザー向けの機能を実装しているため、JavaScript のエンジニアによって書かれることがよくあります。そのため、以下のセクションでは、RubyCapybaraの予備知識がないことを前提に、これらのテストをいつ、どのように使うかについて明確なガイドラインを示します。

機能テストを使うとき

機能テストは、以下のような場合に使用します:

  • 複数のコンポーネントにまたがるテスト。
  • ユーザーがページ間を移動する必要があります。
  • フォームを送信し、他の場所で結果を見ること。
  • ユニットテストとして行う場合、偽のデータとコンポーネントで膨大な数のモックとスタブを行うことになります。

機能テストは、テストしたいときに特に便利です:

  • 複数のコンポーネントが正常に動作していることをテストしたい場合。
  • 複雑なAPIとのやりとり。フィーチャーテストは API と相互作用するので、動作は遅くなりますが、モックやフィクスチャを使う必要はありません。

機能テストを使わない場合

jestvue-test-utils などのユニットテストを使うべきでしょう。機能テストは実行コストがかなり高くなります。

ユニットテストを使うべきです:

  • 実装しているビヘイビアが1つのコンポーネントにまとまっている場合。
  • 他のコンポーネントの動作をシミュレートすることで、目的の効果をトリガーすることができます。
  • 仮想 DOM で UI 要素を選択して、必要なエフェクトをトリガーすることができます。

また、新しいコードのビヘイビアが複数のコンポーネントを一緒に動作させる必要がある場合は、コンポーネントツリーの上位でビヘイビアをテストすることを検討する必要があります。例えば、ParentComponent というコンポーネントがあるとします:

  <script>
  export default{
    name: ParentComponent,
    data(){
      return {
        internalData: 'oldValue'
      }
    },
     methods:{
      changeSomeInternalData(newVal){
        this.internalData = newVal
      }
     }
  }
  </script>
  <template>
   <div>
    <child-component-1 @child-event="changeSomeInternalData" />
    <child-component-2 :parent-data="internalData" />
   </div>
  </template>

この例では:

  • ChildComponent1 イベントが発生します。
  • ParentComponentinternalData の値を変更します。
  • ParentComponent 小道具をChildComponent2 に渡します。

ユニットテストの代わりに

  • ParentComponent ユニットテストファイルの内部から、期待されるイベントをchildComponent1
  • propがchildComponent2

それから、それぞれの子コンポーネントはイベントが発せられたときとpropが変更されたときに何が起こるのかをユニットテストします。

この例はより大規模でより深いコンポーネントツリーにもあてはまります。可能であれば、ユニットテストを使い、機能テストの余分なコストを避けることは間違いなく価値があります:

  • 子コンポーネントを自信を持ってマウントできます。
  • イベントを発信したり、仮想 DOM 内の要素を選択します。
  • 希望するテスト動作を取得します。

テストの作成場所

機能テストはspec/features フォルダにあります。あなたが機能を追加しようとしているページをテストできる既存のファイルを探す必要があります。そのフォルダーの中で、あなたのセクションを見つけることができます。例えば、パイプラインページに新しい機能テストを追加したい場合、spec/features/projects/pipelines を探し、ここに書きたいテストが存在するかどうかを確認します。

機能テストの実行方法

  1. 動作するGDK環境があることを確認してください。
  2. gdk start コマンドでgdk 環境を起動してください。
  3. ターミナルで
 bundle exec rspec path/to/file:line_of_my_test

このコマンドの前に、WEBDRIVER_HEADLESS=0 を付けることもできます。このコマンドは、あなたのコンピュータ上で実際のブラウザを開いてテストを実行します。

テストの書き方

基本的なファイル構造

  1. すべての文字列リテラルを変更不可に

すべての機能テストでは、最初の行を

# frozen_string_literal: true

これはすべてのRuby ファイルにあり、すべての文字列リテラルを変更不可能にします。パフォーマンス上の利点もありますが、このセクションの範囲を超えています。

  1. インポート依存性。

必要なモジュールをインポートしてください。ほとんどの場合、常にspec_helper を必要とするでしょう:

require 'spec_helper'

その他の関連モジュールをインポートしてください。

  1. Jest の最初の describe ブロックで行うのと同じように、RSpec のグローバルスコープを作成してテストを定義します。

そして、一番最初のRSpec スコープを作成します。

RSpec.describe 'Pipeline', :js do
  ...
end

Rubyのすべてがそうであるように、これは実際にはclass 。つまり、一番上に、テストに必要なモジュールをinclude 。たとえば、RoutesHelpers を含めると、より簡単にナビゲートできるようになります。

RSpec.describe 'Pipeline', :js do
  include RoutesHelpers
  ...
end

このように実装すると、次のようなファイルになります:

# frozen_string_literal: true

require 'spec_helper'

RSpec.describe 'Pipeline', :js do
  include RoutesHelpers
end

シードデータ

各テストは独自の環境にあるため、ファクトリを使用して必要なデータをシードする必要があります。パイプラインの例を続けるために、メインパイプラインページ (/namespace/project/-/pipelines/:id/ ルート) に移動するテストが必要だとします。

ほとんどの機能テストでは、少なくともユーザーを作成する必要があります。サインインする必要がない場合はこのステップを省略できますが、一般的なルールとして、特に匿名ユーザーが見る機能をテストする場合を除き、常にユーザーを作成する必要があります。こうすることで、セクションの変更に応じて新しい権限レベルを変更したりテストしたりするために、必要に応じてテストで編集できる権限レベルを明示的に設定しておくことができます。ユーザーを作成するには:

  let(:user) { create(:user) }

新しく作成されたユーザーを保持する変数が作成されます。spec_helper をインポートしたので、create を使うことができます。

しかし、このユーザーは単なる変数なので、まだ何もしていません。そのため、仕様書のbefore do ブロックで、このユーザーでサインインし、すべての仕様書が認証済みユーザーで始まるようにすることができます。

  let(:user) { create(:user) }

  before do
    sign_in(user)
  end

ユーザーを得たので、パイプラインページで何かをアサートする前に、他に何が必要かを見てみましょう。ルート/namespace/project/-/pipelines/:id/ を見ると、プロジェクトとパイプラインが必要であることがわかります。

そこで、プロジェクトとパイプラインを作成し、それらをリンクします。通常ファクトリでは、子要素は引数として親要素を必要とします。この場合、パイプラインはプロジェクトの子です。そのため、プロジェクトを最初に作成し、パイプラインを作成するときにプロジェクトを引数として渡すことで、パイプラインをプロジェクトに “バインド “することができます。パイプラインの所有者はユーザーです。例えば、プロジェクトとパイプラインを作成します:

  let(:user) { create(:user) }
  let(:project) { create(:project, :repository) }
  let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id, user: user) }

同じように、ビルドファクトリーを使用して親パイプラインを渡すことで、ジョブ(ビルド)を作成することができます:

  create(:ci_build, pipeline: pipeline, stage_idx: 10, stage: 'publish', name: 'CentOS')

すでに存在するファクトリーはたくさんあるので、必要なものが利用可能かどうかを確認するために、他の既存のファイルを見てください。

visit メソッドを使用して、引数としてパスを渡すことで、ページに移動することができます。Railsは自動的にヘルパーパスを生成するので、ハードコードされた文字列の代わりにこれらを使うようにしてください。これらはルートモデルを使って生成されるので、パイプラインに移動したい場合は

  visit project_pipeline_path(project, pipeline)

UIをナビゲートしたり非同期で呼び出したりするときは、ページのインタラクションを実行する前に、wait_for_requests

要素の相互作用

エレメントを見つけて相互作用させるには、さまざまな方法があります。ベストプラクティスについては、UIテストのセクションを参照してください。

ボタンをクリックするには、ボタンの中にある文字列でclick_button を使います:

  click_button 'Text inside the button element'

リンクをたどりたい場合は、click_link

  click_link 'Text inside the link tag'

fill_in を使って入力/フォーム要素を埋めることができます。第一引数はセレクタ、第二引数はwith: で、これは渡す値です。

  fill_in 'current_password', with: '123devops'

また、find セレクタと、send_keys の組み合わせで、前のテキストを削除せずにフィールドにキーを追加したり、set で入力要素の値を完全に置き換えたりすることもできます。

アクションのより包括的なリストは、feature tests actionsdocumentationにあります。

アサーション

ページ内で何かをアサートするには、常にpage 変数に pageアクセスすることができますpage 。変数は自動的に定義され、実際にはページドキュメントを意味 pageします。これは、セレクタやコンテンツのような特定のコンポーネントを持つことをpage 期待できることを意味 pageします。以下にいくつかの例を示します:

  # Finding a button
  expect(page).to have_button('Submit review')
  # Finding by text
  expect(page).to have_text('build')
  # Finding by `href` value
  expect(page).to have_link(pipeline.ref)
  # Find by data-testid
  # Like CSS selector, this is acceptable when there isn't a specific matcher available.
  expect(page).to have_css('[data-testid="pipeline-multi-actions-dropdown"]')
  # Finding by CSS selector. This is a last resort.
  # For example, when you cannot add attributes on the desired element.
  expect(page).to have_css('.js-icon-retry')
  # You can combine any of these selectors with `not_to` instead
  expect(page).not_to have_button('Submit review')
  # When a test case has back to back expectations,
  # it is recommended to group them using `:aggregate_failures`
  it 'shows the issue description and design references', :aggregate_failures do
    expect(page).to have_text('The designs I mentioned')
    expect(page).to have_link(design_tab_ref)
    expect(page).to have_link(design_ref_a)
    expect(page).to have_link(design_ref_b)
  end

また、サブブロックを作成することもできます:

  • 主張する箇所を絞り込み、意図しない別の要素が見つかるリスクを減らすことができます。
  • 要素が正しい境界の中で見つかるようにしてください。
  page.within('[data-testid="pipeline-multi-actions-dropdown"]') do
    ...
  end

matchers の一覧は、feature tests matchersのドキュメントを参照ください。

フィーチャーフラグ

デフォルトでは、YAML の定義や GDK で手動で設定したフラグにかかわらず、すべての機能フラグは有効になります。機能フラグが無効になっているときをテストするには、手動でフラグをスタブしなければなりません。理想的には、before do ブロックのなかでスタブします。

  stub_feature_flags(my_feature_flag: false)

ee 機能フラグをスタブする場合、次のようにします:

  stub_licensed_features(my_feature_flag: false)

デバッグ

WEBDRIVER_HEADLESS=0 というプレフィックスを付けて spec を実行すると、実際のブラウザを開くことができます。しかし、スペックはすぐにコマンドを実行してしまうので、見て回る暇はありません。

この問題を避けるには、Capybaraに実行を停止させたい行にbinding.pry 。そうすれば、標準的な使い方でブラウザの中にいることになります。ある要素を見つけられない理由を理解するには、次のようにします:

  • 要素を選択してください。
  • コンソールとネットワークタブを使用します。
  • ブラウザコンソール内でセレクタを実行します。

Capybaraが動作しているターミナル内部で、next 、テストを一行ずつ実行することもできます。こうすることで、ひとつひとつのインタラクションをチェックし、何がイシューの原因になっているかを確認することができます。

ChromeDriverの更新

Selenium 4.6 以降、ChromeDriver はselenium-webdriver gem に付属するSelenium Manager によって自動的に管理されます。chromedriverを手動で同期する必要はなくなりました。


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