📝

Vitest の assert を使って不要な if 文を避ける tips

に公開3

zod などで作成した schema に対して Vitest で Unit テストを記述する際に以下のような if 文を書くことがあります。

import { z } from 'zod';

export const userSchema = z.object({
  name: z.string().min(1, "Name is required"),
});
import { z } from 'zod';
import { test, expect } from 'vitest';

test('parseに失敗すること', () => {
  const result = userSchema.safeParse({
    name: ""
  });

  expect(result.success).toBe(false);
  // expect.toBeでは型の絞り込みがされないため、if文でresult.successがfalseであることを絞り込む
  if (result.success) {
    throw new Error();
  }

  const tree = z.treeifyError(result.error);
  expect(tree.properties?.name?.errors[0]).toBe("Name is required");
});

result.error! を使用して絞り込みをしないようにすることも可能ですが、lint ルールなどで ! の使用を禁止している場合はできません。
そのような時に Vitest の assert を使用します。

assertを使用したコード

if 文の箇所を assert に変えることで型を絞ることができます。

import { z } from 'zod';
import { test, assert, expect } from 'vitest';

test('parseに失敗すること', () => {
  const result = userSchema.safeParse({
    name: ""
  });

  expect(result.success).toBe(false);
  // result.successがfalseとして型が絞り込まれる
  assert(!result.success);

  const tree = z.treeifyError(result.error);
  expect(tree.properties?.name?.errors[0]).toBe("Name is required");
});

assert に失敗した場合は以下のようなエラーが発生します。

assert と expect の使い分け

assert(!result.success); を行うことで expect(result.success).toBe(false); が不要になったと思う方もいるかもしれません。しかし、assert に失敗した場合、エラーが発生するため、テストログがみづらくなってしまいます。

そのため、assert でエラーが発生しまう可能性がある場合は前段で expect を挟むのが良いでしょう。以下は expect が失敗した時のスクショです。

さまざまな assert

Vitest の assert は122種類あります(実際には chai の assert をそのまま再exportしているだけなので chai の assert ですが)。

assert.isFalse

https://vitest.dev/api/assert.html#isfalse

対象の値が false であることを判定する assert です。

import { assert, test } from 'vitest'

const testPassed = false

test('assert.isFalse', () => {
  assert.isFalse(testPassed)
})

先ほどのサンプルコードでは assert(!result.success); と書いて、result.success が false であることを判定していましたが、assert.isFalse を使うことで assert.isFalse(result.success); と書けるため、可読性が向上します。

assert.operator

https://vitest.dev/api/assert.html#operator

第二引数に演算子を書いてそれで評価する assert です(いつ使うんでしょう)。

import { assert, test } from 'vitest'

test('assert.operator', () => {
  assert.operator(1, '<', 2, 'everything is ok')
})

内部実装はどうなっているのか見てみたらとても愚直で微笑みました。

https://github.com/chaijs/chai/blob/d63c74ece14407b538c119e2e147388e98b7f401/lib/chai/interface/assert.js#L2059-L2085

assert.changes

https://vitest.dev/api/assert.html#changes

対象のオブジェクトのプロパティが変更されたことを判定する assert です(いつ使うんでしょう)。

import { assert, test } from 'vitest'

test('assert.changes', () => {
  const obj = { val: 10 }
  function fn() { obj.val = 22 };
  assert.changes(fn, obj, 'val')
})

サンプルコードでは test ブロック内で作成した objfn を使っているため、実務上での使いどころは想像できませんでした。

おわり

Vitest の assert を使うことでテストコードをシンプルにし、可読性を高めることができるのでおすすめです。

Discussion

Honey32Honey32

assert とどちらが優れているかは分かりませんが、僕は expect.unreachable を使っています!

import { z } from 'zod';
import { test, expect } from 'vitest';

test('parseに失敗すること', () => {
  const result = userSchema.safeParse({
    name: ""
  });

  if (result.success) {
    expect.unreachable("expect: 失敗, actual: 成功");
    // unreachable は never を返すので、静的フロー解析が効く。
  }

  const tree = z.treeifyError(result.error);
  expect(tree.properties?.name?.errors[0]).toBe("Name is required");
});
やなぎやなぎ

expect.unreachable、初耳ですがそっちの方がセマンティックに見えますね
assertはArrange(準備)の段階とかで使うのが良いのかもなーと思いました!