テストライブラリを用いたXSTATEのテスト


最近、私はこの新しいアプリケーションの中でいくつかの州の重いロジックを必要としました.
ロジックの実装はスムーズに、私は場所に私たちの新しい状態機械を持っていた時間内に正しく行った.
それからテストがやってきた.
私は、それが単位テストを書くようになったとき、少し立ち往生しました.理想的な世界では、私は単位テストにあまり依存していないでしょう.しかし、多くの会社がそうするように、私はテストトロフィーよりむしろテストピラミッドと整列するのを好みます.ユニットテストは私たちのために必要です.それで、私は医者を打ちました.
それでは、モデルベースのテストはとにかくですか?
私の頭を包む最初のものは、実際の単位テストの不足でした.モデルベースのテストでは、状態間の遷移へのロジックの中で小さなステップを実行する方法についての状態機械と命令に関する情報を与えることができます.
我々はこの情報を受け取り、論理を通してエンドツーエンドのパスを生成する.最後に、これらのパスを使って、我々の単体テストの生成をベースにします.次のようになります.
// State machine test

describe('StateMachine', () => {
  const testPlans = stateMachineModel.getShortestPathPlans();

  testPlans.forEach((plan) => {
    describe(plan.description, () => {
      afterEach(cleanup);
      plan.paths.forEach((path) => {
        it(path.description, async () => {
          await path.test(render(<TestComponent />));
        });
      });
    });
  });
});
まず、テストするコンポーネントが必要です.
一般的に、これらのビジュアルと相互作用するビジュアルコンポーネントを使用した状態機械は、ロジックを通して遷移を引き起こします.我々はテストのためのプロダクションのビジュアルには、ここでは、視覚的に変化し、ロジックがないしないようにする必要はありません.また、テストのための純粋なコンポーネントを作成するには、どのように我々の移行をトリガ簡素化することができます.
// State machine test

const TestComponent = () => {
  const [state, publish] = useMachine(stateMachine, {
    actions: {
      loadingEntryAction,
      userSubmitAction,
    },
  });

  return (
    <div>
      <p data-testid="current_state">{state.value}</p>
      <button
        onClick={() => {
          publish('SUBMIT');
        }}
      >
        SUBMIT
      </button>
      <button
        onClick={() => {
          publish('SUCCESS');
        }}
      >
        SUCCESS
      </button>
      <button
        onClick={() => {
          publish('FAILURE');
        }}
      >
        FAILURE
      </button>
    </div>
  );
};
だからここで私たちのシンプルなコンポーネントは、我々は現在の状態を表示し、我々はサポートの移行の種類ごとにボタンを持っている.私たちもインポートし、通常の反応コンポーネントでは、我々の状態マシンを使用します.
我々が正しいと主張すること.
ドキュメントを見てみましょう.
// XState Test Docs

const toggleMachine = Machine({
  id: 'toggle',
  initial: 'inactive',
  states: {
    inactive: {
      on: {
        /* ... */
      },
      meta: {
        test: async page => {
          await page.waitFor('input:checked');
        }
      }
    },
        /* ... */
});
私たちは私たちのプロダクションコードにテストロジックをハードコーディングしているように見えます.私はこれらの2つの世界を別々に保ちたい.
では、ステートマシンの初期化を行いましょう.
// State machine

import { Machine } from 'xstate';

export const stateMachine = Machine({
  id: 'statemachine',
  initial: 'IDLE',
  states: {
    IDLE: {
      on: { SUBMIT: { target: 'LOADING', actions: ['userSubmitAction'] } },
    },
    LOADING: {
      entry: ['loadingEntryAction'],
      on: {
        SUCCESS: 'SUCCESS',
        FAILURE: 'FAILURE',
      },
    },
    SUCCESS: {},
    FAILURE: {
      SUBMIT: { target: 'LOADING', actions: ['userSubmitAction'] },
    },
  },
});
状態機械の輸出と並行して、我々の状態機械のために論理の宣言を別々に輸出するためにこれを変えます.
// State machine

import { Machine } from 'xstate';

export const machineDeclaration = {
  id: 'statemachine',
  initial: 'IDLE',
  states: {
    IDLE: {
      on: { SUBMIT: { target: 'LOADING', actions: ['userSubmitAction'] } },
    },
    LOADING: {
      entry: ['loadingEntryAction'],
      on: {
        SUCCESS: 'SUCCESS',
        FAILURE: 'FAILURE',
      },
    },
    SUCCESS: {},
    FAILURE: {
      SUBMIT: { target: 'LOADING', actions: ['userSubmitAction'] },
    },
  },
};

export const stateMachine = Machine(machineDeclaration);
そして、テストの中で、テストコンポーネント内で実装されているCurrentRank状態を見て、正しい状態になっていると主張するロジックを追加できます.
// State machine test

machineDeclaration.states.idle.meta = {
  test: async ({ getByTestId }) => {
    expect(getByTestId('current_state')).toHaveTextContent('idle');
  },
};

machineDeclaration.states.loading.meta = {
  test: async ({ getByTestId }) => {
    expect(getByTestId('current_state')).toHaveTextContent('loading');
    expect(loadingEntryAction).toHaveBeenCalled();
  },
};

machineDeclaration.states.success.meta = {
  test: async ({ getByTestId }) => {
    expect(getByTestId('current_state')).toHaveTextContent('success');
  },
};

machineDeclaration.states.failure.meta = {
  test: async ({ getByTestId }) => {
    expect(getByTestId('current_state')).toHaveTextContent('failure');
  },
};
マシンモデルの形成
MachineDeclarationといくつかのイベントを使って新しいモデルを作りましょう.これらのイベントは、テストコンポーネント内の状態遷移をトリガーするアクションです.
// State machine test

const stateMachineModel = 
    createModel(xstate.createMachine(machineDeclaration)).withEvents({
      SUBMIT: {
        exec: ({ getByText }) => {
          fireEvent.click(getByText('SUBMIT'));
          expect(userSubmitAction).toHaveBeenCalled();
        },
      },
      SUCCESS: {
        exec: ({ getByText }) => {
          fireEvent.click(getByText('SUCCESS'));
        },
      },
      FAILURE: {
        exec: ({ getByText }) => {
          fireEvent.click(getByText('FAILURE'));
        },
      },
    });
アクションのアサート
我々は、我々の状態機械でイベントを引き起こす典型的な方法を使います.我々が2回、我々が行動に入るのを見ることができます.我々は前にこれらの主張を見ました、しかし、集中した観察をしましょう:
まず、テストコンポーネントで状態マシンを初期化する際に、XSTATEに渡します.
// State machine test
//...

const loadingEntryAction = jest.fn();
const userSubmitAction = jest.fn();

const TestComponent = () => {
  const [state, publish] = useMachine(stateMachine, {
    actions: {
      loadingEntryAction,
      userSubmitAction,
    },
  });

//...
XSTATEに渡すアサーション内でこれらの関数を使用して、エントリの関数をアサートすることができます.
// State machine test

machineDeclaration.states.loading.meta = {
  test: async ({ getByTestId }) => {
    expect(getByTestId('current_state')).toHaveTextContent('loading');
    expect(loadingEntryAction).toHaveBeenCalled();
  },
};
移行中に関数が呼び出されるとアサートするには、テストモデルでアサーションを追加することができます.
// State machine test

const stateMachineModel = 
    createModel(xstate.createMachine(machineDeclaration)).withEvents({
      SUBMIT: {
        exec: ({ getByText }) => {
          fireEvent.click(getByText('SUBMIT'));
          expect(userSubmitAction).toHaveBeenCalled();
        },
      },
      //...
    });
最後に
すべてを一緒に結ぶことは私のために少しの時間を要したので、私は自分自身を思い出させるためにそれを書き留めたかったです、そして、うまくいけば他の誰でも彼らのXstate状態機械を単位テストするのを見ているのを助けました.私のための鍵は、これらの小さな部品のそれぞれを理解し、それらを使用してJESTと反応テストライブラリの組み合わせではなく、Pitpeteer.
You can see this example in its entirety here.
いつものように、これは私がこの目標を達成するために見つけた方法です.あなたがどんな考えか意見を持っているならば、手を伸ばしてください.