【Jest】globalオブジェクトを使ったコンポーネントのテストのやり方

## カスタムglobalオブジェクトを定義してそれを使うコンポーネントのテスト

例えばこんな感じで`hogeProperty`をglobalで定義した

```index.html
<script>
  window.hogeProperty = {
    hogeProp: "hoge",
    hogeFn(arg1 = "", arg2 = "") {
      console.log(`${arg1}_${arg2}`)
    }
  };
</script>
```

globalオブジェクトを使うコンポーネント

```useGlobalProperty.tsx
/**
 * hogeボタンをクリックすると`hogeProperty`の`hogeFn`を実行するコンポーネント 
 */
export default function UseGlobalProperty() {
  // globalオブジェクト呼ぶとtsエラー出るので制御
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  // @ts-ignore
  const { hogeProp, hogeFn } = hogeProperty;

  function clickHandler() {
    hogeFn(hogeProp, 'bar');
  }

  return (
    <>
      <button onClick={clickHandler} type='button'>
        {hogeProp}ボタン
      </button>
    </>
  );
}
```

globalオブジェクトを使うコンポーネントのテストコード

```useGlobalProperty.spec.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import UseGlobalProperty from './useGlobalProperty';

describe('UseGlobalProperty', () => {
  test('hogeボタンクリック時、hogeFnが実行される', () => {
    // NOTE: テスト中のglobalプロパティを定義する
    this.global.hogeProperty = {
      hogeProp: 'hoge', // ←使わないので書かなくても良い
      hogeFn(arg1 = '', arg2 = '') {
        console.log(`${arg1}_${arg2}`); // ←使わないので関数の中身は書かなくても良い
      },
    };

    // NOTE: globalオブジェクト呼ぶとtsエラー出るので制御
    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore
    const { hogeProperty } = global;
    const { hogeProp } = hogeProperty;

    // NOTE: hogeFnが呼ばれたかをテストする為にspyOnする
    const hogeFnSpy = jest.spyOn(hogeProperty, 'hogeFn');

    render(<UseGlobalProperty />);

    fireEvent.click(screen.getByText(`hogeボタン`));

    // 1回だけ呼ばれたかテスト
    expect(hogeFnSpy).toHaveBeenCalledTimes(1);
    // 引数が正しいかテスト
    expect(hogeFnSpy).toHaveBeenCalledWith('hoge', 'bar');
  });
});
```

## document.cookieのテスト

document.cookieに値をセットするコンポーネント

```useDocumentCookie.tsx
/**
 * setCookieボタンクリックするとcookieに値を設定するコンポーネント
 */
export default function UseDocumentCookie() {
  function setCookie(): void {
    // eslint-disable-next-line no-restricted-globals
    const { hostname } = location;
    document.cookie = `hogeKey=anyValue; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/; domain=${hostname}`;
  }

  return (
    <>
      <div>
        <button onClick={setCookie} type='button'>
          setCookieボタン
        </button>
      </div>
    </>
  );
}
```

document.cookieに値をセットするコンポーネントのテストコード

```useDocumentCookie.spec.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import UseDocumentCookie from './useDocumentCookie';

describe('UseDocumentCookie', () => {
  test('setCookieボタンクリック時、cookieが設定される', () => {
    // NOTE: これがないとテスト実行でクッキー書き換えられない
    Object.defineProperty(this.global.document, 'cookie', {
      writable: true,
    });

    render(<UseDocumentCookie />);

    fireEvent.click(screen.getByText('setCookieボタン'));

    const { cookie } = document;
　　　　　　　　// cookieの値が期待通り設定されたかテスト
    expect(cookie).toEqual(
      `hogeKey=anyValue; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/; domain=${hostname}`
    );

    // NOTE: このテストでcookieの値変更されて他のテストにも影響するので戻しておく
    Object.defineProperty(document, 'cookie', {
      value: undefined,
    });

    // NOTE: 一応戻す
    Object.defineProperty(this.global.document, 'cookie', {
      writable: false,
    });
  });
});
```


## テスト時のglobalオブジェクトの定義はCustomEnvironmentにまとめる

globalプロパティの定義はCustomEnvironmentにまとめた方が可読性とか良さげ
また`jest.config.js`の`testEnvironment`が`node`、 特定のテストだけ`jsdom`環境にする必要がある時とかはCustomEnvironmentを作成する。

https://jestjs.io/ja/docs/configuration#testenvironment-string

```custom-js-dom-environment.ts
// eslint-disable-next-line import/no-extraneous-dependencies,import/no-import-module-exports
import JSDOMEnvironment from 'jest-environment-jsdom';

class CustomJSDOMEnvironment extends JSDOMEnvironment {
  async setup(): Promise<void> {
    await super.setup();

    this.global.hogeProperty = {
      hogeProp: 'hoge',
      hogeFn(arg1 = '', arg2 = '') {
        // eslint-disable-next-line no-console
        console.log(`${arg1}_${arg2}`);
      },
    };

    Object.defineProperty(this.global.document, 'cookie', {
      writable: true,
    });
  }

  async teardown(): Promise<void> {
    Object.defineProperty(this.global.document, 'cookie', {
      writable: false,
    });
    await super.teardown();
  }
}

module.exports = CustomJSDOMEnvironment;
```

CustomEnvironmentを使うテストコード

```useGlobalProperty.spec.tsx
/**
 * @jest-environment ./src/test/custom-js-dom-environment.ts
 */
// ↑ファイルの先頭に記述。custom-js-dom-environment.tsがあるパスを書く

import { render, screen, fireEvent } from '@testing-library/react';
import UseGlobalProperty from './useGlobalProperty';

describe('UseGlobalProperty', () => {
  test('hogeボタンクリック時、hogeFnが実行される', () => {
    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore
    const { hogeProperty } = global;
    const { hogeProp } = hogeProperty;

    const hogeFnSpy = jest.spyOn(hogeProperty, 'hogeFn');

    render(<UseGlobalProperty />);

    fireEvent.click(screen.getByText(`hogeボタン`));

    expect(hogeFnSpy).toHaveBeenCalledTimes(1);
    expect(hogeFnSpy).toHaveBeenCalledWith('hoge', 'bar');
  });
});
```

```useDocumentCookie.spec.tsx
/**
 * @jest-environment ./src/test/custom-js-dom-environment.ts
 */

import { render, screen, fireEvent } from '@testing-library/react';
import UseDocumentCookie from './useDocumentCookie';

describe('UseDocumentCookie', () => {
  afterEach(() => {
    // 他のテストも考えるとafterEachに書くのがベター
    Object.defineProperty(document, 'cookie', {
      value: undefined,
    });
  });

  test('setCookieボタンクリック時、cookieが設定される', () => {
    render(<UseDocumentCookie />);

    fireEvent.click(screen.getByText('setCookieボタン'));

    const { cookie } = document;

    expect(cookie).toEqual(
      `hogeKey=anyValue; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/; domain=${hostname}`
    );
  });
});
```

## 全てのコード、バージョン情報等はこちら

https://github.com/yamatoto/jest-sample
`https://github.com/yamatoto/jest-sample/tree/master/src/views/useGlobalProperty`