👌

一日一処: Vitestで同じファイル内にある関数をMock化することはできない

2024/02/29に公開

Vitest

Jestも人気だが、最近はViteの使用からVitestを選択するプロジェクトも増えてきているだろう。ただ、Jestほど明確に解説しているページなどもなく困ることが多い。そのうちの一つとして、Mock化があるだろう。

関数のテストで同じファイル内の関数をMock化できない

例えば、この以下のようなファイルに関数があるとする。

main.ts
export function getData() {
  return { value: 2024 }
}

export function showData() {
  const data = getData()
  console.log(data.value)
}

この時、showData関数のテストをする場合、getDataをMock化して、結果が任意の状態になることをテストすることもあるだろう。まずは、このgetDataをMock化せずにテストするとこうなる。

main.test.ts
import { describe, expect, test, vi } from 'vitest';
import { showData } from '../src/main.ts';

describe('showData', () => {
  const logger = vi.spyOn(console, 'log');
  test('output data', () => {
    showData();
    expect(logger).toHaveBeenCalledWith(2024);
  });
});

内部でconsole.logを用いているため、これはspyOnで到達するか確認した。続いて、前述したgetDataを同じ方法でMock化してみよう。

main.test.ts
import { describe, expect, test, vi } from 'vitest';
import { showData } from '../src/main.ts';
import * as Main from '../src/main.ts';

describe('showData', () => {
  const logger = vi.spyOn(console, 'log');
  const getter = vi.spyOn(Main, 'getData')
  test('output data', () => {
    const value = 2000
    getter.mockReturnValue({ value })
    showData();
    expect(logger).toHaveBeenCalledWith(value);
    // failed
    // AssertionError: expected "log" to be called with arguments: [ 2000 ]
  });
});

これは失敗する。console.logと同様にspyOnを行ったにも関わらず。これは、vi.mockでも同様だ。シンプルなものだが、躓く部分でもある。理由としては、同一ファイル内の関数をテストするときに、同じファイルにある別の関数はMock化できないという制約だ。これは、関数を別のファイルに移すことで解決する。理由については、IssueにてJestの機能と比較され、同様にMock化できない理由として、JSへのトランスパイルが影響しているため、不可能だと回答されていた。
さらに、VitestのDiscussionに具体的な質問もあった。回答の得られたトピックだが、そのメンテナーの回答が清々しかった。

You cannot

そう、これだけ。前述のIssueの回答者と同じ人の回答だ。推測ではあるが、同じような質問が多く、端的な回答になったのではと考える。他の情報もなく、「できない」と伝えるだけの内容になったのかもしれない。このDiscussionの回答には、他にも興味深いやり取りがあった

This is a test-only problem, and having to re-organized the production code to make the tests "work" seems like a framework/language limitation.

とあり、ある質問者が「Vitest自体の課題であり、テストのためにファイルを分割したりコードを再編成することは、制限をかけているようのものだ」とのこと。たしかに。ただし、これは、同メンテナーに否定されている。

I don’t believe this is a test framework problem. If language doesn’t allow something, then don’t write code that doesn’t work - it’s that simple.

このメンテナーの考え方は、とても好きだ。「Vitestの問題ではない。許可されてない方法でコードを書くな。簡単だろ?」たしかにそうだ。(その後、質問者から「でもそのためだけにファイル分割するのいやだなぁ」みたいな回答があった)
実際のところ、テストしづらい状況ということは、コードそのものに課題があったり、関係性の強い関数同士が本来は別々のファイルに定義されるべきだったなど、再考の余地はあると思っている。

ファイルを分離してMock化

同じファイルに居続けさせるより、機能を明確に分けたりすることで、ファイルは分割できるかもしれない。状況によっては、クラスを用いたほうがMock化が容易なため、関数だけで処理を記述しない選択肢もある。もし、ファイルを分割した場合もMock化については、以下に記しておく。

getter.ts
export function getData() {
  return { value: 2024 };
}
main.ts
import { getData } from './getter.js';

export function showData() {
  const data = getData();
  console.log(data.value);
}
main.test.ts
import { describe, expect, test, vi } from 'vitest';
import { showData } from '../src/main.ts';
import * as Getter from '../src/getter.ts';

describe('showData', () => {
  const logger = vi.spyOn(console, 'log');
  const getter = vi.spyOn(Getter, 'getData')
  test('output data', () => {
    const value = 2000
    getter.mockReturnValue({ value })
    showData();
    expect(logger).toHaveBeenCalledWith(value);
    // passed
  });
});

テストコードは同じ(importやspyOnが若干異なるが)で、ファイルを分けるだけで動作するのであれば、この方法を選択したほうが簡単で、早いだろう。シンプルであることが一番だが、これを振り返った時、メンテナーのdon’t write code that doesn’t workという言葉は、はやり素敵だ。

Discussion