12
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

テストで毎回全部の props 書いてる?不要な props を省いてテストを書くための関数を作った話

Last updated at Posted at 2025-03-30

React コンポーネントのテストを書いていると、 テストの文脈に関係のない props を毎回渡すのが面倒… と思ったことはありませんか?

今回は、そうした課題を解決するために段階的に進化させていったテスト用の関数と、最終的にたどり着いた renderWithParams という関数を紹介します。

1. 最初のヘルパー関数

例として、次のようなコンポーネントを想定します:

function ExampleComponent(props: {
  prop1: string
  prop2: string
  prop3: number
  prop4: (_: string) => void
}) {
  return <></>
}

テストごとに関係のない props を書きたくない

たとえば prop1 に関するテストだけを書きたいときでも、他の prop を省略できず、すべて書かなければいけません。

render(
  <ExampleComponent
    prop1="テストに関係のある値"
    prop2=""
    prop3={0}
    prop4={() => {}}
  />
)

そこで最初に作ったのが、このようなヘルパー関数です:

function renderExampleComponent(params: Partial<ComponentProps<typeof ExampleComponent>> = {}) {
	render(
		<ExampleComponent 
			prop1={params?.prop1 ?? ''} 
			prop2={params?.prop2 ?? ''} 
			prop3={params?.prop3 ?? 0} 
			prop4={params?.prop4 ?? (() => {})} 
		/>
	)
}

これにより、必要な props だけを渡すことでテストが書けるようになり、コードがスッキリしました。

2. rerender したくなった時の問題再発

ある時、rerender を使いたくなり、次のように関数を拡張しました:

function renderExampleComponent(params: Partial<ComponentProps<typeof ExampleComponent>> = {}): RenderResult {
	return render(
		<ExampleComponent 
			prop1={params?.prop1 ?? ''} 
			prop2={params?.prop2 ?? ''} 
			prop3={params?.prop3 ?? 0} 
			prop4={params?.prop4 ?? (() => {})} 
		/>
	)
}

しかし、この場合 rerender のたびにまた全ての props を書かないといけなくなり、本来の目的と逆行してしまいます。

const { rerender } = renderExampleComponent({ prop1: 'value1' })

rerender(<ExampleComponent prop1="updated" prop2="" prop3={0} prop4={() => {}} />)

3. rerender にも対応したヘルパー関数に進化

そこで rerender のラッパーも返すように関数を再度アップデートしました:

function renderExampleComponent(params: Partial<ComponentProps<typeof ExampleComponent>> = {}) {
	const { rerender } = render(
		<ExampleComponent
			prop1={params?.prop1 ?? ''}
			prop2={params?.prop2 ?? ''}
			prop3={params?.prop3 ?? 0}
			prop4={params?.prop4 ?? (() => {})}
		/>
	)
	return { 
		rerender: (params: Partial<ComponentProps<typeof ExampleComponent>> = {}) => {
			rerender(
				<ExampleComponent
					prop1={params?.prop1 ?? ''}
					prop2={params?.prop2 ?? ''}
					prop3={params?.prop3 ?? 0}
					prop4={params?.prop4 ?? (() => {})}
				/>
			)
		} 
	}
}

これにより、以下のように rerender でも関係ある props だけ渡せるようになりました:

const { rerender } = renderExampleComponent({ prop1: 'value1' })

rerender({ prop1: 'value1 on 2nd rendering' })

4. ただし、今度はこのロジック自体が重複しはじめた

複数のコンポーネントで同様の構造の renderXxx を作るうちに、「この初期 props を使って rerender を再構成するパターン、また書いてるな…」と気づきます。

ここで、ようやく抽象化のフェーズに突入します。

5. 汎用的に使えるように renderWithParams を導入

そうして生まれたのが、次の renderWithParams 関数です。

export const renderWithParams = <T extends object>(
  buildComponent: (params?: T) => ReactNode,
  paramsOf1stRendering?: T,
) => {
  const { rerender, unmount } = render(buildComponent(paramsOf1stRendering))
  return {
    rerender: (params?: T) => {
      rerender(buildComponent(params))
    },
    unmount,
  }
}

使用例

const buildComponent = (params?: Partial<ComponentProps<typeof ExampleComponent>>) => (
  <ExampleComponent
    prop1={params?.prop1 ?? ''}
    prop2={params?.prop2 ?? ''}
    prop3={params?.prop3 ?? 0}
    prop4={params?.prop4 ?? (() => {})}
  />
)

const { rerender } = renderWithParams(buildComponent, { prop1: 'initial' })

rerender({ prop1: 'updated' })

6. 導入してよかったこと

  • props の差分だけ指定して再レンダリングできる
  • 関係ない props を記述しなくて済む
  • rerender のラップ処理を共通化できる
  • テストが読みやすく、意図が明確に
12
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?