前面幾天我們已經用 TDD 的方式完成了 <Editor />
元件,但不要忘了 TDD 中很重要的「重構」部分。在 __tests__/post-editor.js
的兩個測試中有重複的邏輯,這些重複的邏輯對於未來的維護來說會增加不必要的負擔,不易分辨每個測試之間的差別是什麼。現在我們要來重構這些重複的程式碼,讓每個測試之間的關注點更明確。
下面是 __tests__/post-editor.js
目前的兩個測試:
tests/post-editor.js
test('renders a form with title, content, tags, and a submit button', async () => {
mockSavePost.mockResolvedValueOnce()
const fakeUser = userBuilder()
const {getByLabelText, getByText} = render(<Editor user={fakeUser} />)
const fakePost = postBuilder()
getByLabelText(/title/i).value = fakePost.title
getByLabelText(/content/i).value = fakePost.content
getByLabelText(/tags/i).value = fakePost.tags.join(', ')
const submitButton = getByText(/submit/i)
fireEvent.click(submitButton)
expect(submitButton).toBeDisabled()
expect(mockSavePost).toHaveBeenCalledWith({
...fakePost,
authorId: fakeUser.id,
})
expect(mockSavePost).toHaveBeenCalledTimes(1)
await wait(() => expect(MockRedirect).toHaveBeenCalledWith({to: '/'}, {}))
})
test('renders an error message from the server', async () => {
const testError = 'test error'
mockSavePost.mockRejectedValueOnce({data: {error: testError}})
const fakeUser = userBuilder()
const {getByText, findByRole} = render(<Editor user={fakeUser} />)
const submitButton = getByText(/submit/i)
fireEvent.click(submitButton)
const postError = await findByRole('alert')
expect(postError).toHaveTextContent(testError)
expect(submitButton).not.toBeDisabled()
})
可以發現下面這幾行是兩個測試中重複出現的邏輯:
const fakeUser = userBuilder()
const {getByLabelText, getByText} = render(<Editor user={fakeUser} />)
const fakePost = postBuilder()
getByLabelText(/title/i).value = fakePost.title
getByLabelText(/content/i).value = fakePost.content
getByLabelText(/tags/i).value = fakePost.tags.join(', ')
const submitButton = getByText(/submit/i)
新增一個 function renderEditor
,將重複的程式碼抽到這個 funtion。將 render()
method 回傳的結果宣告為 utils
變數。renderEditor
回傳一個 object,裡面有展開後的 utils 屬性( ...utils
)、 submitButton
、 fakeUser
、 fakePost
。
tests/post-editor.js
function renderEditor() {
const fakeUser = userBuilder()
const utils = render(<Editor user={fakeUser} />)
const fakePost = postBuilder()
utils.getByLabelText(/title/i).value = fakePost.title
utils.getByLabelText(/content/i).value = fakePost.content
utils.getByLabelText(/tags/i).value = fakePost.tags.join(', ')
const submitButton = utils.getByText(/submit/i)
return {
...utils,
submitButton,
fakeUser,
fakePost,
}
}
第一個測試原本被抽出的程式碼,現在可以取代為 const {submitButton, fakePost, fakeUser} = renderEditor()
:
test('renders a form with title, content, tags, and a submit button', async () => {
mockSavePost.mockResolvedValueOnce()
const {submitButton, fakePost, fakeUser} = renderEditor()
fireEvent.click(submitButton)
expect(submitButton).toBeDisabled()
expect(mockSavePost).toHaveBeenCalledWith({
...fakePost,
authorId: fakeUser.id,
})
...
}
第二個測試原本被抽出的程式碼,現在可以取代為 const {submitButton, findByRole} = renderEditor()
:
test('renders an error message from the server', async () => {
const testError = 'test error'
mockSavePost.mockRejectedValueOnce({data: {error: testError}})
const {submitButton, findByRole} = renderEditor()
fireEvent.click(submitButton)
const postError = await findByRole('alert')
expect(postError).toHaveTextContent(testError)
expect(submitButton).not.toBeDisabled()
})
重構後,測試亮綠燈 ✅,代表沒問題。