コンテンツへスキップ

モック

テストを作成する際には、内部または外部サービスの「偽の」バージョンを作成する必要があるのは時間の問題です。これは一般的にモックと呼ばれています。Vitestは、そのviヘルパーを通して、これを支援するためのユーティリティ関数を提供します。import { vi } from 'vitest'でインポートするか、グローバルにアクセスできます(グローバル設定有効になっている場合)。

警告

実行間でモックの状態変更を元に戻すために、常に各テスト実行の前または後にモックをクリアまたは復元することを忘れないでください!詳細については、mockResetのドキュメントを参照してください。

すぐに始めたい場合は、APIセクションを確認してください。そうでない場合は、読み進めてモックの世界を深く掘り下げていきましょう。

日付

テスト時に一貫性を確保するために、日付を制御する必要がある場合があります。Vitestはタイマーとシステム日付を操作するために@sinonjs/fake-timersパッケージを使用します。具体的なAPIの詳細についてはこちらを参照してください。

js
import { , , , , ,  } from 'vitest'

const  = [9, 17]

function () {
  const  = new ().()
  const [, ] = 

  if ( >  &&  < )
    return { : 'Success' }

  return { : 'Error' }
}

('purchasing flow', () => {
  (() => {
    // tell vitest we use mocked time
    .()
  })

  (() => {
    // restoring date after each test run
    .()
  })

  ('allows purchases within business hours', () => {
    // set hour within business hours
    const  = new (2000, 1, 1, 13)
    .()

    // access Date.now() will result in the date set above
    (()).({ : 'Success' })
  })

  ('disallows purchases outside of business hours', () => {
    // set hour outside business hours
    const  = new (2000, 1, 1, 19)
    .()

    // access Date.now() will result in the date set above
    (()).({ : 'Error' })
  })
})

関数

関数のモックは、スパイとモックの2つの異なるカテゴリに分類できます。

特定の関数が呼び出されたかどうか(および可能性のある引数が渡されたかどうか)を確認する必要がある場合のみ、スパイを使用すれば十分です。これには、vi.spyOn()詳細はこちら)を直接使用できます。

ただし、スパイは関数についてスパイするのに役立つだけで、それらの関数の実装を変更することはできません。関数の偽の(またはモックされた)バージョンを作成する必要がある場合は、vi.fn()詳細はこちら)を使用できます。

関数のモックにはTinyspyをベースとして使用していますが、jestと互換性を持たせるための独自のラッパーがあります。vi.fn()vi.spyOn()は同じメソッドを共有しますが、vi.fn()の戻り値のみが呼び出し可能です。

js
import { , , , ,  } from 'vitest'

function ( = .. - 1) {
  return .[]
}

const  = {
  : [
    { : 'Simple test message', : 'Testman' },
    // ...
  ],
  , // can also be a `getter or setter if supported`
}

('reading messages', () => {
  (() => {
    .()
  })

  ('should get the latest message with a spy', () => {
    const  = .(, 'getLatest')
    (.()).('getLatest')

    (.()).(
      .[.. - 1],
    )

    ().(1)

    .(() => 'access-restricted')
    (.()).('access-restricted')

    ().(2)
  })

  ('should get with a mock', () => {
    const  = .().()

    (()).(.[.. - 1])
    ().(1)

    .(() => 'access-restricted')
    (()).('access-restricted')

    ().(2)

    (()).(.[.. - 1])
    ().(3)
  })
})

その他

グローバル

vi.stubGlobalヘルパーを使用して、jsdomまたはnodeに存在しないグローバル変数をモックできます。グローバル変数の値をglobalThisオブジェクトに入れます。

ts
import {  } from 'vitest'

const  = .(() => ({
  : .(),
  : .(),
  : .(),
  : .(),
}))

.('IntersectionObserver', )

// now you can access it as `IntersectionObserver` or `window.IntersectionObserver`

モジュール

モジュールのモックは、他のコードで呼び出されるサードパーティライブラリを観察し、引数、出力、または実装の再宣言をテストできます。

詳細なAPIの説明については、vi.mock() APIセクションを参照してください。

自動モックアルゴリズム

コードが、このモジュールに関連付けられた__mocks__ファイルまたはfactoryがないモックされたモジュールをインポートしている場合、Vitestはモジュール自体を呼び出してすべてのエクスポートをモックすることによってモジュールをモックします。

次の原則が適用されます

  • すべての配列は空になります
  • すべてのプリミティブとコレクションは同じままです
  • すべてのオブジェクトは深く複製されます
  • クラスとそのプロトタイプすべてのインスタンスは深く複製されます

仮想モジュール

Vitestは、Viteの仮想モジュールのモックをサポートしています。Jestで仮想モジュールを扱う方法とは異なります。vi.mock関数にvirtual: trueを渡す代わりに、Viteにモジュールが存在することを伝える必要があります。そうでないと、解析中に失敗します。これを行う方法はいくつかあります。

  1. エイリアスの提供
ts
// vitest.config.js
export default {
  test: {
    alias: {
      '$app/forms': resolve('./mocks/forms.js')
    }
  }
}
  1. 仮想モジュールを解決するプラグインの提供
ts
// vitest.config.js
export default {
  plugins: [
    {
      name: 'virtual-modules',
      resolveId(id) {
        if (id === '$app/forms')
          return 'virtual:$app/forms'
      }
    }
  ]
}

2番目のアプローチの利点は、動的に異なる仮想エントリポイントを作成できることです。複数の仮想モジュールを1つのファイルにリダイレクトした場合、それらのすべてがvi.mockの影響を受けます。そのため、一意の識別子を使用してください。

モックの落とし穴

同じファイルの他のメソッド内で呼び出されるメソッドへの呼び出しをモックすることは不可能であることに注意してください。たとえば、このコードでは

ts
export function foo() {
  return 'foo'
}

export function foobar() {
  return `${foo()}bar`
}

直接参照されているため、外部からfooメソッドをモックすることはできません。そのため、このコードはfoobar内のfoo呼び出しには影響しませんが(他のモジュールでのfoo呼び出しには影響します)、

ts
import { vi } from 'vitest'
import * as mod from './foobar.js'

// this will only affect "foo" outside of the original module
vi.spyOn(mod, 'foo')
vi.mock('./foobar.js', async (importOriginal) => {
  return {
    ...await importOriginal<typeof import('./foobar.js')>(),
    // this will only affect "foo" outside of the original module
    foo: () => 'mocked'
  }
})

foobarメソッドに実装を直接提供することで、この動作を確認できます。

ts
// foobar.test.js
import * as mod from './foobar.js'

vi.spyOn(mod, 'foo')

// exported foo references mocked method
mod.foobar(mod.foo)
ts
// foobar.js
export function foo() {
  return 'foo'
}

export function foobar(injectedFoo) {
  return injectedFoo !== foo // false
}

これは意図的な動作です。通常、モックがこのような方法で使用されている場合、コードの不良の兆候です。コードを複数のファイルにリファクタリングするか、依存性注入などの手法を使用してアプリケーションアーキテクチャを改善することを検討してください。

js
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { Client } from 'pg'
import { failure, success } from './handlers.js'

// handlers
export function success(data) {}
export function failure(data) {}

// get todos
export async function getTodos(event, context) {
  const client = new Client({
    // ...clientOptions
  })

  await client.connect()

  try {
    const result = await client.query('SELECT * FROM todos;')

    client.end()

    return success({
      message: `${result.rowCount} item(s) returned`,
      data: result.rows,
      status: true,
    })
  }
  catch (e) {
    console.error(e.stack)

    client.end()

    return failure({ message: e, status: false })
  }
}

vi.mock('pg', () => {
  const Client = vi.fn()
  Client.prototype.connect = vi.fn()
  Client.prototype.query = vi.fn()
  Client.prototype.end = vi.fn()

  return { Client }
})

vi.mock('./handlers.js', () => {
  return {
    success: vi.fn(),
    failure: vi.fn(),
  }
})

describe('get a list of todo items', () => {
  let client

  beforeEach(() => {
    client = new Client()
  })

  afterEach(() => {
    vi.clearAllMocks()
  })

  it('should return items successfully', async () => {
    client.query.mockResolvedValueOnce({ rows: [], rowCount: 0 })

    await getTodos()

    expect(client.connect).toBeCalledTimes(1)
    expect(client.query).toBeCalledWith('SELECT * FROM todos;')
    expect(client.end).toBeCalledTimes(1)

    expect(success).toBeCalledWith({
      message: '0 item(s) returned',
      data: [],
      status: true,
    })
  })

  it('should throw an error', async () => {
    const mError = new Error('Unable to retrieve rows')
    client.query.mockRejectedValueOnce(mError)

    await getTodos()

    expect(client.connect).toBeCalledTimes(1)
    expect(client.query).toBeCalledWith('SELECT * FROM todos;')
    expect(client.end).toBeCalledTimes(1)
    expect(failure).toBeCalledWith({ message: mError, status: false })
  })
})

リクエスト

VitestはNode.jsで実行されるため、ネットワークリクエストのモックは困難です。Web APIは使用できないため、ネットワーク動作を模倣する必要があります。Mock Service Workerを推奨します。これにより、RESTGraphQLの両方のネットワークリクエストをモックでき、フレームワークに依存しません。

Mock Service Worker(MSW)は、テストが行うリクエストをインターセプトすることによって機能します。そのため、アプリケーションコードを変更せずに使用できます。ブラウザ内では、Service Worker APIを使用します。Node.jsおよびVitestでは、@mswjs/interceptorsライブラリを使用します。MSWの詳細については、導入をお読みください。

設定

セットアップファイルで以下のように使用できます。

js
import { , ,  } from 'vitest'
import {  } from 'msw/node'
import { , ,  } from 'msw'

const  = [
  {
    : 1,
    : 1,
    : 'first post title',
    : 'first post body',
  },
  // ...
]

export const  = [
  .get('https://rest-endpoint.example/path/to/posts', () => {
    return .json()
  }),
]

const  = [
  .query('ListPosts', () => {
    return .json(
      {
        : {  },
      },
    )
  }),
]

const  = (..., ...)

// Start server before all tests
(() => .listen({ : 'error' }))

//  Close server after all tests
(() => .close())

// Reset handlers after each test `important for test isolation`
(() => .resetHandlers())

onUnhandleRequest: 'error'を使用してサーバーを設定すると、対応するリクエストハンドラーがないリクエストがあるたびにエラーがスローされます。

MSWを使用する完全な動作例があります。React Testing with MSW

その他

MSWには他にも多くの機能があります。Cookieやクエリパラメータにアクセスしたり、モックエラーレスポンスを定義したり、他にも多くのことができます!MSWでできることのすべてを見るには、ドキュメントをお読みください。

タイマー

タイムアウトまたは間隔を含むコードをテストする場合、テストがタイムアウトするまで待つのではなく、「偽の」タイマーを使用してsetTimeoutsetIntervalへの呼び出しをモックすることでテストを高速化できます。

詳細なAPIの説明については、vi.useFakeTimers APIセクションを参照してください。

js
import { , , , , ,  } from 'vitest'

function () {
  (, 1000 * 60 * 60 * 2) // 2 hours
}

function () {
  (, 1000 * 60) // 1 minute
}

const  = .(() => .('executed'))

('delayed execution', () => {
  (() => {
    .()
  })
  (() => {
    .()
  })
  ('should execute the function', () => {
    ()
    .()
    ().(1)
  })
  ('should not execute the function', () => {
    ()
    // advancing by 2ms won't trigger the func
    .(2)
    ()..()
  })
  ('should execute every minute', () => {
    ()
    .()
    ().(1)
    .()
    ().(2)
  })
})

チートシート

情報

以下の例では、vivitestから直接インポートされています。 設定globalstrueに設定すると、グローバルにも使用できます。

〜したい

methodをスパイする

ts
const instance = new SomeClass()
vi.spyOn(instance, 'method')

エクスポートされた変数をモックする

js
// some-path.js
export const getter = 'variable'
ts
// some-path.test.ts
import * as exports from './some-path.js'

vi.spyOn(exports, 'getter', 'get').mockReturnValue('mocked')

エクスポートされた関数をモックする

  1. vi.mockを使用した例

警告

vi.mock呼び出しはファイルの先頭に巻き上げられることに注意してください。常にすべてのインポートの前に実行されます。

ts
// ./some-path.js
export function method() {}
ts
import { method } from './some-path.js'

vi.mock('./some-path.js', () => ({
  method: vi.fn()
}))
  1. vi.spyOnを使用した例
ts
import * as exports from './some-path.js'

vi.spyOn(exports, 'method').mockImplementation(() => {})

エクスポートされたクラスの実装をモックする

  1. vi.mock.prototypeを使用した例
ts
// some-path.ts
export class SomeClass {}
ts
import { SomeClass } from './some-path.js'

vi.mock('./some-path.js', () => {
  const SomeClass = vi.fn()
  SomeClass.prototype.someMethod = vi.fn()
  return { SomeClass }
})
// SomeClass.mock.instances will have SomeClass
  1. vi.mockと戻り値を使用した例
ts
import { SomeClass } from './some-path.js'

vi.mock('./some-path.js', () => {
  const SomeClass = vi.fn(() => ({
    someMethod: vi.fn()
  }))
  return { SomeClass }
})
// SomeClass.mock.returns will have returned object
  1. vi.spyOnを使用した例
ts
import * as exports from './some-path.js'

vi.spyOn(exports, 'SomeClass').mockImplementation(() => {
  // whatever suites you from first two examples
})

関数から返されたオブジェクトをスパイする

  1. キャッシュを使用した例
ts
// some-path.ts
export function useObject() {
  return { method: () => true }
}
ts
// useObject.js
import { useObject } from './some-path.js'

const obj = useObject()
obj.method()
ts
// useObject.test.js
import { useObject } from './some-path.js'

vi.mock('./some-path.js', () => {
  let _cache
  const useObject = () => {
    if (!_cache) {
      _cache = {
        method: vi.fn(),
      }
    }
    // now every time that useObject() is called it will
    // return the same object reference
    return _cache
  }
  return { useObject }
})

const obj = useObject()
// obj.method was called inside some-path
expect(obj.method).toHaveBeenCalled()

モジュールの部分をモックする

ts
import { mocked, original } from './some-path.js'

vi.mock('./some-path.js', async (importOriginal) => {
  const mod = await importOriginal<typeof import('./some-path.js')>()
  return {
    ...mod,
    mocked: vi.fn()
  }
})
original() // has original behaviour
mocked() // is a spy function

現在の日付をモックする

Dateの時間をモックするには、vi.setSystemTimeヘルパー関数を使用できます。この値は、異なるテスト間で自動的にリセットされません。

vi.useFakeTimersを使用すると、Dateの時間も変更されることに注意してください。

ts
const mockDate = new Date(2022, 0, 1)
vi.setSystemTime(mockDate)
const now = new Date()
expect(now.valueOf()).toBe(mockDate.valueOf())
// reset mocked time
vi.useRealTimers()

グローバル変数をモックする

globalThisに値を代入するか、vi.stubGlobalヘルパーを使用してグローバル変数を設定できます。vi.stubGlobalを使用する場合、unstubGlobals設定オプションを有効にするか、vi.unstubAllGlobalsbeforeEachフックで手動で呼び出さない限り、異なるテスト間で自動的にリセットされません。

ts
vi.stubGlobal('__VERSION__', '1.0.0')
expect(__VERSION__).toBe('1.0.0')

import.meta.envをモックする

  1. 環境変数を変更するには、新しい値を代入するだけです。

警告

環境変数の値は、異なるテスト間で自動的にリセットされません。

ts
import { beforeEach, expect, it } from 'vitest'

// you can reset it in beforeEach hook manually
const originalViteEnv = import.meta.env.VITE_ENV

beforeEach(() => {
  import.meta.env.VITE_ENV = originalViteEnv
})

it('changes value', () => {
  import.meta.env.VITE_ENV = 'staging'
  expect(import.meta.env.VITE_ENV).toBe('staging')
})
  1. 値を自動的にリセットする場合は、unstubEnvs設定オプションを有効にして(またはvi.unstubAllEnvsを手動でbeforeEachフックで呼び出して)、vi.stubEnvヘルパーを使用できます。
ts
import { expect, it, vi } from 'vitest'

// before running tests "VITE_ENV" is "test"
import.meta.env.VITE_ENV === 'test'

it('changes value', () => {
  vi.stubEnv('VITE_ENV', 'staging')
  expect(import.meta.env.VITE_ENV).toBe('staging')
})

it('the value is restored before running an other test', () => {
  expect(import.meta.env.VITE_ENV).toBe('test')
})
ts
// vitest.config.ts
export default {
  test: {
    unstubAllEnvs: true,
  }
}

MITライセンスで公開。