RSpecに慣れたエンジニアがJestを始めてみた話

対象読者

  • RSpecをそれなりに書いたことがあるがフロントエンドテストは未経験なエンジニア
  • カラリアの開発に興味があるエンジニア
  • 香りで世界を彩ることに興味があるエンジニア

もし、私たちの開発チームに興味を持った方がいましたら、 ぜひカジュアル面談からお話しましょう!!

はじめに

こんにちは、ハイリンクでプロダクト開発エンジニアをやっていますタイガです。 この記事では、フロントエンドのテストに初めて挑戦した感想をお伝えしたいと思います。

正直なところ、新しいテストフレームワークを学ぶことには少し抵抗というか腰が重いという気持ちがありました。 新しい構文を覚えたり、テストの書き方を一から学んだりするのは大変そうだな、という印象があったためです。

ちなみに私自身はRails歴は5年くらいで、Rspec歴も同じくらい。 フロントエンドテスト及びJestは初めて触るので、まさに「RSpecをそれなりに書いたことがあるがフロントエンドテストは未経験なエンジニア」ということです。

結論から申し上げると、RSpecに慣れたエンジニアであれば、その知識を活かしてJestを使ったフロントエンドテストをスムーズに始めることができそうでした。

背景

おかげさまで、私たちが運営しているカラリアというサービスも、多くの方にご利用いただいています。ユーザー数が増えるにつれ、サービスに求められる機能も多様化してきました。 今では香水だけではなく、ルームフレグランスやボディアケア商品などの商品も数多く取り扱っています。

さて、様々な機能が増えていく中でユーザーインターフェースの部分、つまりフロントエンドの実装が複雑となっていきました。 特に同じページ内で条件分岐が複雑になってくると、全ての操作や表示が正しく動作しているかを人の手で確認するのは非効率であり、ミスも起こりがちです。 安心してコードを変更することが少しずつ難しくなってきたと感じます。

チームではすでにJestを使ってフロントエンドの自動テストを行っていましたが、このような背景から私自身もテストを書けるようになる必要が出てきたと強く感じました。 そこで、Jestを使ったテストを学び始めることにしたので、その過程で学んだことを共有したいと思った次第です。

公式ドキュメントを見て構文を学ぶ

Jestを学び始めるにあたって、具体的にどういうテストを書くのかを理解するために、まずは公式ドキュメントを見てみました。 公式ドキュメントのはじめにの章にあるサンプルコードには以下のように書かれています。

2つの数値を加算する関数のテストを書くことから始めてみましょう。 
まずsum.js ファイルを作成します。

function sum(a, b) {
  return a + b;
}
module.exports = sum;

その後、sum.test.js というファイルを作成します。
このファイルに実際のテストが含まれます。

const sum = require('./sum');

test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
});

このシンプルなサンプルコードを見たとき、「RSpecに似てる」と感じました。 testメソッドでテストケースを定義し、expect で結果を検証する構造は、 RSpecitブロックやexpectと非常に似ています。

Rspecだと以下のように書くかと思います。

it 'adds 1 + 2 to equal 3' do
  expect(1 + 2).to eq(3)
end

この時点でJestのテストはRSpecと非常に似た構造をしているため、RSpecに慣れているエンジニアにとっては、すぐに馴染むことができるのではないかと感じました。

RSpecとJestの関係

そもそもJestはJasmineというテストフレームワークの上に構築されています。そして、このJasmine自体がRSpecの影響を強く受けているため、JestとRSpecの構文が非常に似ているのだと考えられます。

Jasmineの背景については、公式ドキュメントにも次のように記載されています。

Enter Jasmine
Jasmine is our dream JavaScript testing framework. It's heavily influenced by, and borrows the best parts of, ScrewUnit, JSSpec, JSpec, and of course RSpec.

Jasmineは、JavaScriptのテストフレームワークとして開発されましたが、その設計にはRSpecの他にScrewUnit、JSSpec、JSpecなどの影響を受けているとのこと。

また、Jestの公式ドキュメントにも次のように記載されています。

Familiar Approach: Built on top of Jasmine test framework, a familiar BDD testing environment

これは、JestがBehavior-Driven Development (BDD) 環境を提供するJasmineの上に構築されていることを示しています。結果として、JestとRSpecの構文が似ているのだと思われます。

詳しくは、以下のリンクで確認できます。

UIテストを小さく試す

Jestを学び始めた理由として、手動での確認だけでは網羅的にテストをすることが困難になってきたことを背景の章で述べました。

この記事ではAPIレスポンスに基づいてUIの表示を検証する自動テストをJestで書いてみたいと思います。

今回はユーザーのステータス(アクティブ/非アクティブ)に応じて異なるメッセージを表示するコンポーネントを例にしましょう。 APIレスポンスによってUIが変わるのですが、テストではAPIをモックしその動作を検証します。

以下は、APIからユーザーの状態を取得し、その結果に応じて異なるメッセージを表示するコンポーネントです。

// UserStatus.tsx
import useSWR from 'swr';

interface UserStatusResponse {
  status: 'active' | 'inactive';
}

function UserStatus() {
  const { data, error } = useSWR<UserStatusResponse>('/api/user/status');

  if (error) return <div>エラーが発生しました</div>;

  return data?.status === 'active' ? (
    <div>ユーザーはアクティブです</div>
  ) : (
    <div>ユーザーは非アクティブです</div>
  );
}

export default UserStatus;

次にテストです。

// UserStatus.spec.tsx
import { render, screen } from '@testing-library/react';
import useSWR from 'swr';

import UserStatus from './UserStatus';

// useSWRをモックする
jest.mock('swr');

describe('UserStatus', () => {
  it('アクティブユーザーの場合、対応するメッセージが表示される', async () => {
    // useSWRがアクティブユーザーのデータを返すようにモック
    (useSWR as jest.Mock).mockReturnValue({
      data: { status: 'active' },
      error: null,
    });

    render(<UserStatus />);

    // ユーザーがアクティブであるメッセージが表示されるか確認
    expect(screen.getByText('ユーザーはアクティブです')).toBeInTheDocument();
  });

  it('非アクティブユーザーの場合、対応するメッセージが表示される', async () => {
    // useSWRが非アクティブユーザーのデータを返すようにモック
    (useSWR as jest.Mock).mockReturnValue({
      data: { status: 'inactive' },
      error: null,
    });

    render(<UserStatus />);

    // ユーザーが非アクティブであるメッセージが表示されるか確認
    expect(screen.getByText('ユーザーは非アクティブです')).toBeInTheDocument();
  });

  it('APIエラー時、エラーメッセージが表示される', async () => {
    // useSWRがエラーを返すようにモック
    (useSWR as jest.Mock).mockReturnValue({
      data: null,
      error: new Error('API Error'),
    });

    render(<UserStatus />);

    // エラーメッセージが表示されるか確認
    expect(screen.getByText('エラーが発生しました')).toBeInTheDocument();
  });
});

実際に実行してみます。

無事通りました。

RSpecでも allow を使って外部への通信をモックすることが可能ですが、Jestでも同様にjest.mock() を使って外部への通信をモックすることが可能です。

RSpec:

let(:api_client) { instance_double('ApiClient') }

allow(api_client).to receive(:get_user_status).and_return({ status: 'active' })

Jest:

jest.mock('swr');

(useSWR as jest.Mock).mockReturnValue({
  data: { status: 'active' },
  error: null,
});

さいごに

今回、Jestを使ったフロントエンドテストの初歩的な部分に触れてみましたが、RSpecに慣れているエンジニアであれば、フロントエンドテストに対する抵抗感は思ったよりも少ないと感じました。特に構文の似ている部分が多く、既存の知識を活かしてスムーズに導入することができました。

これからJestを学ぼうとしているバックエンドエンジニアの方々にとって、このブログが少しでも参考になれば幸いです。ぜひ、フロントエンドテストに挑戦してみてください!

もし、私たちの開発やサービスに興味を持った方がいましたら、ぜひカジュアル面談からお話しましょう!

herp.careers