Vitest で環境変数をモックしたいときは vi.stubEnv を使おう


こんにちは。フロントエンドエンジニアの辻です。

Gaji-Labo では Next.js や React をはじめとして、その時々のモダンなフロントエンド技術を積極的に採用しています。
もちろんテストフレームワークも時代に合わせて適切なモノを選定しています。
最近では Vitest を採用するプロジェクトが多いですね。

…ということで、今回は Vitest について書いていきます。

さて、テストコードを書いていると「特定の環境変数のときだけ、この処理が走るか確認したい」という場面に出くわすことがあります。
例えば「NODE_ENV が production のときだけXXを実行する」といったケースですね。

今回は、こういったケースに便利な Vitest の vi.stubEnv を紹介します!

まずは「直接書き換える」をやってみる

環境変数をモックしたいとき、真っ先に思いつくのは process.env を直接上書きする方法でしょう。

// テスト内で NODE_ENV を production にしたい…
process.env.NODE_ENV = 'production';

これで強引に上書きできちゃいますが、プロジェクトと上書き対象の環境変数の組み合わせによっては、エラーとなる場合があります。
例えば、Next.js では NODE_ENV が readonly になっていますので、型エラーが発生しますね。

declare namespace NodeJS {
  interface ProcessEnv {
    readonly NODE_ENV: 'development' | 'production' | 'test'
  }
}

また、Node.js の特性上 process.env に新しい環境変数を入れると、その変数は文字列扱いとなります。
そのため、そもそも環境変数が存在しない場合 = undefined を任意のタイミングで表現できません。

process.env.MODE = undefined
console.log(process.env.MODE, typeof process.env.MODE)
// ↑ undefined, string。つまり、MODE は undefined という文字列になる

どうにも環境変数を直接変更するのは筋が悪そうですね。
そもそも環境変数をソースコードから直接いじくり回すこと自体、むず痒くもあります。

さて、こういった課題に対して Vitest は vi.stubEnv を用意してくれています。

vi.stubEnv で環境変数をモックする

vi.stubEnv は、第一引数に環境変数名、第二引数にその値を指定すると、環境変数をモックできる関数です。
vi.stubEnv を一度呼ぶだけで process.env と import.meta.env の両方をまとめて書き換えてくれます。

import { vi } from 'vitest';

// MODE を update にモックする
vi.stubEnv("MODE", "update");

// process.env と import.meta.env の両方が書き換わる
console.log(process.env.MODE);     // update
console.log(import.meta.env.MODE); // update

試しに、環境変数に応じてログを切り分ける処理をテストしてみましょう。
「環境変数 NODE_ENV が production のときはログを出力しない」のテストコードを書いてみると、こうなりますね。

type Logger = {
  debug: (message: string) => void;
  info: (message: string) => void;
  warn: (message: string) => void;
  error: (message: string) => void;
};

export function createLogger(): Logger {
  const isProduction = process.env.NODE_ENV === 'production';

  return {
    debug(message: string) {
      if (!isProduction) {
        console.debug(message);
      }
    },
    info(message: string) {
      console.info(message);
    },
    warn(message: string) {
      console.warn(message);
    },
    error(message: string) {
      console.error(message);
    },
  };
}
import { describe, expect, it, vi } from 'vitest';
import { createLogger } from './logger';

describe('createLogger', () => {
  it('NODE_ENV が production ではデバッグログを出力しない', () => {
    vi.stubEnv('NODE_ENV', 'production');

    const logger = createLogger();
    const spy = vi.spyOn(console, 'debug').mockImplementation(() => {});

    logger.debug('これは出力されないはず');

    expect(spy).not.toHaveBeenCalled();
  });
});

モックの後始末は vi.unstubAllEnvs で

vi.stubEnv で書き換えた環境変数は、放っておくと他のテストにまで影響してしまいます。
テスト同士が干渉すると正しい結果を得られないので、きちんと元へ戻してあげましょう。

そのための関数が vi.unstubAllEnvs です。
vi.stubEnv を呼ぶ前の値に、まとめて復元してくれます。

また、vi.spyOn で書き換えたモック・スパイも放置すると他のテストに影響します。
環境変数の後始末と合わせて、vi.restoreAllMocks() も呼んでおくと安心です。

import { afterEach, vi } from 'vitest';

afterEach(() => {
  // stubEnv で書き換えた環境変数をすべて元に戻す
  vi.unstubAllEnvs();
  // spyOn で書き換えたモック・スパイをすべて元に戻す
  vi.restoreAllMocks();
});

毎回 afterEach に書くのが面倒であれば、vitest.config.ts の unstubEnvs / restoreMocks オプションを true にしておくと、各テストの前に自動で復元してくれます。
プロジェクト全体で統一したいときは、こちらを設定しておくと安心ですね。

import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    unstubEnvs: true,   // 各テストの前に自動で unstubAllEnvs を実行する
    restoreMocks: true, // 各テストの前に自動で restoreAllMocks を実行する
  }
});

vi.stubEnv の TIPS

せっかくですので、vi.stubEnv の TIPS も2点ほど紹介します。

PROD / DEV / SSR は真偽値で渡す

vi.stubEnv の第二引数は基本的に文字列ですが、PRODDEVSSR の 3 つは特別扱いになっており、真偽値で指定します。

// 文字列ではなく boolean を渡す
vi.stubEnv('PROD', true);
vi.stubEnv('DEV', false);

undefined を渡せば「未設定」を再現できる

先ほど「Node.js の特性上 process.env に新しい環境変数を入れると、その変数は文字列扱いとなります。」と書きましたが、vi.stubEnv は第二引数に undefined を渡すと、文字列化されずに環境変数を undefined として定義できます。
「環境変数が無いケースのフォールバック処理」などをテストしたいときに重宝します。

// HOGE_ENV が未設定の状態を再現する
vi.stubEnv('HOGE_ENV', undefined);

process.env.HOGE_ENV; // undefined

まとめ

以上、Vitest で環境変数をモックする vi.stubEnv の紹介でした。
環境変数まわりのテストを書く機会があれば、ぜひ vi.stubEnv を思い出してみてください。

参考ページ

Gaji-Labo では、こうしたテスト周りも実装・整備をしていくことで、プロジェクト開発の持続可能性を保つことも大切にしています。
今後もテストをはじめ、プロジェクト開発の知見を発信していきますので、ほかの記事もぜひご覧ください!

開発のお悩み、フロントエンドから解決しませんか?

あなたのチームのお悩みはなんですか?

「バックエンドエンジニアにフロントエンドまで任せてしまっている」
「デザイナーに主業務以外も任せてしまっている」
「すべての手が足りず細かいことまで手が回らない」

役割や領域を適切に捉えてカバーし、チーム全体の生産性と品質をアップするお手伝いをします。
フロントエンド開発に関わるお困りごとがあれば、まずは一度お気軽に Gaji-Labo にお声がけください。

オンラインでのヒアリングとフルリモートでのプロセス支援に対応しています。

リモートワーク対応のパートナーをお探しの場合もぜひ弊社にお問い合わせください!

お悩み相談はこちらから!

Gaji-Labo フロントエンドエンジニア向けご案内資料

今すぐの転職でなくてもOKです!まずはお話しませんか?

現在弊社では一緒にお仕事をしてくださるエンジニアさんやデザイナーさんを積極募集しています。まずはカジュアルな面談で、お互いに大事にしていることをお話できたらうれしいです。詳しい応募要項は以下からチェックしてください。

パートナー契約へのお問い合わせもお仕事へのお問い合わせも、どちらもいつでも大歓迎です。まずはオンラインでの面談でお話しましょう。ぜひお気軽にお問い合わせください!

話をしてみたい!

投稿者 Tsuji Atsuhiro

フロントエンドエンジニア。 DTP・Webデザイナーを経験した後、フロントエンドエンジニアに転向。HTML/CSS/JavaScriptを中心にWeb開発を担当してきました。 UI・UXに興味があり、デザイン・コーディング両面から考えられるデザインエンジニアを目指しています。 普段はマラソンやボクシングなどで体を動かしてます。