質問するログイン新規登録
React Native

React Nativeは、ネイティブモバイルアプリ(iOS/Android)を作成できるJavaScriptフレームワークです。Reactと同じ設計のため、宣言的なコンポーネントでリッチなUIを開発することが可能です。

TypeScript

TypeScriptは、マイクロソフトによって開発された フリーでオープンソースのプログラミング言語です。 TypeScriptは、JavaScriptの構文の拡張であるので、既存の JavaScriptのコードにわずかな修正を加えれば動作します。

React.js

Reactは、アプリケーションのインターフェースを構築するためのオープンソースJavaScriptライブラリです。

Q&A

解決済

1回答

1963閲覧

Reactのユニットテスト時に「Cannot destructure property 'ref' of 'register(...)' as it is undefined.」

test111222

総合スコア3

React Native

React Nativeは、ネイティブモバイルアプリ(iOS/Android)を作成できるJavaScriptフレームワークです。Reactと同じ設計のため、宣言的なコンポーネントでリッチなUIを開発することが可能です。

TypeScript

TypeScriptは、マイクロソフトによって開発された フリーでオープンソースのプログラミング言語です。 TypeScriptは、JavaScriptの構文の拡張であるので、既存の JavaScriptのコードにわずかな修正を加えれば動作します。

React.js

Reactは、アプリケーションのインターフェースを構築するためのオープンソースJavaScriptライブラリです。

0グッド

1クリップ

投稿2023/08/01 13:22

0

1

実現したいこと

エラーを理解し、テストの第一歩としてレンダリングを成功させたい。

前提

React学習のためのアプリの制作が終わり、勉強を兼ねてテスト作成に取り組んでいます。
簡単なコンポーネントに対してのテストは書けたものの、少し複雑なPropsを渡す必要のあるコンポーネントに対してテストを行おうとしたところ、エラーが発生。

テスト時は私の認識不足でいつもエラーが発生はするのですが、調べたり試行錯誤することで解消出来ていたのですが、今回、解消が出来なかったため質問させていただきました。

おそらくですが、ReactHookFromのRegisterをmock化する方法が間違えているのだと思うのですが何をテストに対しても知識が浅いため、どのようにmock化すればエラーが解消できるのかがわかりませんでした。

発生している問題・エラーメッセージ

以下、テストの結果を貼り付けます。

FAIL src/__Test__/molecules/OkaimonoDetail.test.tsx OkaimonoDetail ✕ テスト中 (91 ms) ● OkaimonoDetail › テスト中 TypeError: Cannot destructure property 'ref' of 'register(...)' as it is undefined. 90 | {fields.map((field, index) => { 91 | const { > 92 | ref, | ^ 93 | onChange: registerOnChange, 94 | ...rest 95 | } = register(`listForm.${index}.purchaseName`, {

該当のソースコード

■コンポーネントのコード import { SmallAddIcon } from "@chakra-ui/icons"; import { Box, ComponentWithAs, Divider, FormLabel, Heading, HStack, Icon, IconProps, Input, InputGroup, InputRightElement, Text, VStack, } from "@chakra-ui/react"; import { ListFormParams, MergeParams } from "interfaces"; import React, { Dispatch, memo, SetStateAction, VFC } from "react"; import { FieldArrayWithId, FieldErrors, FieldValues, UseFieldArrayRemove, UseFormGetValues, UseFormRegister, UseFormSetValue, UseFormWatch, } from "react-hook-form"; type Props = { fields: FieldArrayWithId<MergeParams, "listForm", "key">[]; // eslint-disable-next-line no-unused-vars insertInputForm: (index: number) => void; SmallCloseIcon: ComponentWithAs<"svg", IconProps>; remove: UseFieldArrayRemove; register: UseFormRegister<MergeParams>; errors: FieldErrors<MergeParams>; validationNumber: RegExp; readOnly?: boolean; getValues?: UseFormGetValues<MergeParams>; deleteIds?: string[]; setDeleteIds?: Dispatch<SetStateAction<string[]>>; watch: UseFormWatch<FieldValues>; expiryDate?: boolean; // eslint-disable-next-line no-unused-vars onListChange?: (event: React.ChangeEvent<HTMLInputElement>, index: number, newValue: string) => void; purchaseNameSuggestions?: ListFormParams[]; setValue?: UseFormSetValue<MergeParams>; setPurchaseNameSuggestions?: React.Dispatch<React.SetStateAction<ListFormParams[]>>; purchaseNameIndex?: number; }; export const OkaimonoDetail: VFC<Props> = memo((props) => { const { fields, insertInputForm, SmallCloseIcon, remove, register, errors, validationNumber, readOnly = false, getValues, setDeleteIds, watch, expiryDate, onListChange, purchaseNameSuggestions, setValue, setPurchaseNameSuggestions, purchaseNameIndex, } = props; const onClickSuggests = ( event: React.MouseEvent<HTMLParagraphElement, MouseEvent>, purchaseName: string, index: number ) => { event.preventDefault(); if (setValue && setPurchaseNameSuggestions && purchaseName) { setValue(`listForm.${index}.purchaseName`, purchaseName); setPurchaseNameSuggestions([]); } }; return ( <Box> <Heading as="h3" size="sm" textAlign="center" pt={1} pb={3}> お買い物リスト </Heading> {fields.map((field, index) => { const { ref, onChange: registerOnChange, ...rest } = register(`listForm.${index}.purchaseName`, { required: { value: true, message: "商品名が入力されていません" }, maxLength: { value: 35, message: "最大文字数は35文字までです。" }, }); const customOnChange = (event: React.ChangeEvent<HTMLInputElement>) => { // 親コンポーネントから渡された onChange ハンドラを実行 if (onListChange) { onListChange(event, index, event.target.value); } // 入力が空の場合、候補リストをクリアする if (setPurchaseNameSuggestions && event.target.value === "") { setPurchaseNameSuggestions([]); } // React Hook Form の onChange ハンドラを実行 if (registerOnChange) { registerOnChange(event); } }; const startDate = watch(`listForm.${index}.expiryDateStart`); return ( <HStack key={field.key} px={2} py={3} w="100%" bg="white" rounded="xl" mb="2"> <VStack spacing={1} w="5%"> <Box display={fields.length < 20 || readOnly ? "block" : "none"}> <SmallAddIcon bg="teal.500" rounded="full" color="white" onClick={(event) => { if (readOnly) { event.preventDefault(); // eslint-disable-next-line no-alert alert("確認画面では使用できません。"); return; } insertInputForm(index); }} /> </Box>

■ユニットテストのコード

import { SmallCloseIcon } from "@chakra-ui/icons"; import { render, RenderResult } from "@testing-library/react"; import { OkaimonoDetail } from "components/molecules/OkaimonoDetail"; describe("OkaimonoDetail", () => { const getValuesMock = jest.fn().mockReturnValue(99); //getValuesを再現するために記述。 const testPurchaseObjs = [ { userId: "test1", shopId: "shopId", id: "1", asc: "0", purchaseName: "testPurchase1", price: "999", shoppingDetailMemo: "testMemo", amount: "amount", shoppingDate: "2023-07-27", expiryDateStart: "2023-07-28", expiryDateEnd: "2023-07-29", listId: "99", isBought: true, isFinish: true, differentDay: 1, isDelete: false, memosCount: 3, totalBudget: 9999, }, { userId: "test2", shopId: "shopId", id: "2", asc: "1", purchaseName: "testPurchase2", price: "999", shoppingDetailMemo: "testMemo", amount: "amount", shoppingDate: "2023-07-27", expiryDateStart: "2023-07-28", expiryDateEnd: "2023-07-29", listId: "99", isBought: true, isFinish: true, differentDay: 1, isDelete: false, memosCount: 3, totalBudget: 9999, }, ]; const FieldMocks = [ { amount: "", asc: "", expiryDateEnd: "", expiryDateStart: "", id: "", key: "43ea4b2c-54a6-465b-9ae8-25e68eb94dc9", price: "", purchaseName: "", shoppingDetailMemo: "" } ]; const mockFunctions = { fields: FieldMocks, insertInputForm: jest.fn(), SmallCloseIcon, remove: jest.fn(), register: jest.fn().mockReturnValue({ ref: jest.fn(), onChange: jest.fn(), name: 'listForm', }), errors: {}, validationNumber: /^[0-9]+$/, readOnly: false, getValues: getValuesMock, setDeleteIds: jest.fn(), watch: jest.fn(), expiryDate: false, onListChange: jest.fn(), purchaseNameSuggestions: testPurchaseObjs, setValue: jest.fn(), setPurchaseNameSuggestions: jest.fn(), purchaseNameIndex: 0, }; let utils: RenderResult; beforeEach(() => { utils = render(<OkaimonoDetail {...mockFunctions} />); }); test("テスト中", () => { console.log(mockFunctions.register.mock.calls); }); });

試したこと

上記のテストコードの通り、console.logを記載し、エラーの特定を行いました。
しかし、以下のような結果が返されてしまいます。

console.error
The above error occurred in one of your React components:

at /Users/apple/Github/myportfolio03/frontend/src/components/molecules/OkaimonoDetail.tsx:55:5 Consider adding an error boundary to your tree to customize error handling behavior. Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.

55行目はコンポーネントのconst内の一番上のfieldsに該当するため、テストコード内のFieldMocksを見直してみたのですが、誤りらしい部分を見つけられませんでした。

どうか、エラーの原因特定にお力をお貸しいただけないでしょうか。

補足情報(FW/ツールのバージョンなど)

"react-hook-form": "^7.43.9",
"react": "^17.0.2",
"@chakra-ui/react": "^1.0.4",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^12.0.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",打ち消し線

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

質問に対する正確な回答でなくすみません。。。

単純に質問者さんの作成したコンポーネントが大きすぎてテストに苦労されている印象を受けました。(テスト等のやり方については調べながらできていたとあるので、テストに対してもそこそこ知識があるとお見受けします。

そのため、コンポーネントを分割してテストが必要な部分だけをテストする(≒よりシンプルにしたコンポーネントに対してテストをする)方法を採用してみてはいかがでしょうか?

分割できる箇所

  • fields.map()field一つずつに対してDOMを返しているところ
    • 別のコンポーネントとして作成したものをimportする形で呼び出します

分割をしたことで、テストする対象が2つになってしまいましたがそれぞれはシンプルなものになったので現状のものに対するテストを書くよりもよりシンプルになると思います。
これ自体はReactのコンポーネントに対する考え方なので覚えておくと良いと思います。

https://react.dev/learn/thinking-in-react

他にもロジックとDOMの部分を別のコンポーネントに分けることで、コンポーネントが適切なpropsを受けた時に期待通りの表示をするかどうかなどもテストできるようになります。

テストの実施方法について

RHFを利用されていて、register()周りでエラーが出ていそうなので、参考程度ですがRHFの公式にあるformのテスト方法のドキュメントを貼らせていただきます。
(この辺りは、具体的に何をテストしたいのかがわからないと回答が難しいため、すみません。。。

https://react-hook-form.com/advanced-usage#TestingForm

お役に立てることを願っています。

投稿2023/08/03 02:54

uky

総合スコア270

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

test111222

2023/08/03 08:19

ご返信いただきありがとうございます! >作成したコンポーネントが大きすぎてテストに苦労されている印象を受けました。 仰るとおり、コンポーネントの規模が小さいテストについては、つまりながらもテストを作成できていたのですが今回のように大きな規模のコンポーネントになった途端、テストに行き詰まってしまいました。 改めてコンポーネント分割をしてみて、頭を整理してからテストをしてみようと思います。 また、テストの実施方法についてもご共有いただきありがとうございました。 一読して、register()周りの知識を固めようと思います。 貴重なアドバイス、本当にありがとうございました。 また、質問させて頂くこともあるかとは思いますが、その際は何卒よろしくお願いいたします。
uky

2023/08/03 09:36

こちらこそ的確な回答でなく申し訳ありませんでした。 また投稿が確認できましたら回答させていただきます!
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.30%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問