<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          測(cè)試中如何處理 Http 請(qǐng)求?

          共 12516字,需瀏覽 26分鐘

           ·

          2022-08-26 22:10

          前言

          不知道大家平時(shí)寫(xiě)單測(cè)時(shí)是怎么處理 網(wǎng)絡(luò)請(qǐng)求 的,可能有的人會(huì)說(shuō):“把請(qǐng)求函數(shù) Mock ,返回 Mock 結(jié)果就行了呀”。

          但在真實(shí)的測(cè)試場(chǎng)景中往往需要多次改變 Mock 結(jié)果,Mock fetch 或者 axios.get 就不太夠用了。

          帶著上面這個(gè)問(wèn)題我找到了 Kent 的這篇 《Stop mocking fetch》。今天就把這篇文章分享給大家。


          正片開(kāi)始

          我們先來(lái)看下面這段測(cè)試代碼有什么問(wèn)題:

          // __tests__/checkout.js
          import * as React from 'react'
          import {render, screen} from '@testing-library/react'
          import userEvent from '@testing-library/user-event'
          import {client} from '~/utils/api-client'

          jest.mock('~/utils/api-client')

          test('clicking "confirm" submits payment'async () => {
            const shoppingCart = buildShoppingCart()
            render(<Checkout shoppingCart={shoppingCart} />)

            client.mockResolvedValueOnce(() => ({successtrue}))

            userEvent.click(screen.getByRole('button', {name/confirm/i}))

            expect(client).toHaveBeenCalledWith('checkout', {data: shoppingCart})
            expect(client).toHaveBeenCalledTimes(1)
            expect(await screen.findByText(/success/i)).toBeInTheDocument()
          })

          如果不告訴你 <Checkout /> 的功能和 /checkout API 的用法,你可能發(fā)現(xiàn)不了這里的問(wèn)題。

          好吧,我來(lái)公布一下答案:首先第一個(gè)問(wèn)題就是把 client 給 Mock 掉了,問(wèn)問(wèn)自己:你怎么知道 client 是一定會(huì)被正確調(diào)用的呢?當(dāng)然,你可能會(huì)說(shuō):client 可以用別的單測(cè)來(lái)做保障呀。但你又怎么能保證 client 不會(huì)把返回值里的 body 改成 data 呢?哦,你是想說(shuō)你用了 TypeScript 是吧?彳亍!但由于我們把 client Mock 了,所以肯定不會(huì)完全保證 client 的功能正確性。你可能還會(huì)說(shuō):我還有 E2E 測(cè)試!

          但是,如果我們?cè)谶@里能真的調(diào)用一下 client 不是更能提高我們對(duì) client 的信心么?好過(guò)一直猜來(lái)猜去嘛。

          不過(guò),我們肯定也不是想真的調(diào)用 fetch 函數(shù),所以我們會(huì)選擇把 window.fetch 給 Mock 了:

          // __tests__/checkout.js
          import * as React from 'react'
          import {render, screen} from '@testing-library/react'
          import userEvent from '@testing-library/user-event'

          beforeAll(() => jest.spyOn(window'fetch'))

          // Jest 的 rsetMocks 設(shè)置為 true
          // 我們就不用擔(dān)心要 cleanup 了
          // 這里假設(shè)你用了類(lèi)似 `whatwg-fetch` 的庫(kù)來(lái)做 fetch 的 Polyfill

          test('clicking "confirm" submits payment'async () => {
            const shoppingCart = buildShoppingCart()
            render(<Checkout shoppingCart={shoppingCart} />)

            window.fetch.mockResolvedValueOnce({
              oktrue,
              jsonasync () => ({successtrue}),
            })

            userEvent.click(screen.getByRole('button', {name/confirm/i}))

            expect(window.fetch).toHaveBeenCalledWith(
              '/checkout',
              expect.objectContaining({
                method'POST',
                bodyJSON.stringify(shoppingCart),
              }),
            )
            expect(window.fetch).toHaveBeenCalledTimes(1)
            expect(await screen.findByText(/success/i)).toBeInTheDocument()
          })

          上面肯定能給你帶來(lái)不少代碼信心,畢竟你真的能測(cè)請(qǐng)求是否真的發(fā)出去了。但是,這里的缺點(diǎn)在于:它不能測(cè) headers 里是否會(huì)帶有 Content-Type: application/json 沒(méi)有這一步,我們也不能確定服務(wù)器是否真的能處理發(fā)出去的請(qǐng)求。還有一個(gè)問(wèn)題,你怎么能確定用戶(hù)鑒權(quán)的信息是不是真的也被帶上呢?

          看到這,肯定有人會(huì)說(shuō):“在 client 的單測(cè)里已經(jīng)驗(yàn)證了呀,你還想我要做啥?我不想再把那里面的測(cè)試代碼也在這復(fù)制一份”。行行行,我知道。但如果有一種即可以不用復(fù)制 client 的測(cè)試代碼,又能提高代碼自信的方法呢?繼續(xù)往下看。

          我一直不太喜歡 Mock 類(lèi)似 fetch 函數(shù)的東西,因?yàn)樽罱K你會(huì)在所有地方把整個(gè)后端的邏輯都重新實(shí)現(xiàn)一遍。 這通常發(fā)生在多個(gè)測(cè)試之間,非常煩人。特別是在一些測(cè)試中,我們要假定后端要返回的內(nèi)容的時(shí)候,就不得不在所有地方都要 Mock 一次。在這種情況下,就會(huì)給你和要做測(cè)試的東西設(shè)置了很多障礙。

          我們的測(cè)試策略就會(huì)變成這樣:

          1. 我們把 client Mock 了(第一個(gè)例子),然后依賴(lài)一些 E2E 測(cè)試來(lái)保障 client 正確執(zhí)行,以此給予我們心靈上一丟丟信心。但這也導(dǎo)致了一旦遇到后端的東西,我就要在所有地方都要重新實(shí)現(xiàn)一遍后端邏輯
          2. 我們把 window.fetch Mock 了(第二個(gè)例子)。這會(huì)好點(diǎn),但這也會(huì)遇到第 1 點(diǎn)類(lèi)似的問(wèn)題
          3. 把所有東西都放在函數(shù)中,然后拿來(lái)做單測(cè)(這樣還行),這樣就避免在集成測(cè)試中再測(cè)一遍(不太好,譯注:不太好是因?yàn)榧蓽y(cè)試應(yīng)該要對(duì)整個(gè)功能進(jìn)行測(cè)試,這樣分開(kāi)測(cè)就不完整了

          最終,這樣的測(cè)試并沒(méi)有給我們太多的心理安慰,反而帶來(lái)很多重復(fù)的工作。

          很長(zhǎng)一段時(shí)間里我的解決方法是:聲明一個(gè)假的 fetch 函數(shù),把后端要 Mock 的內(nèi)容都放里面。我在 Paypal 的時(shí)候就試過(guò),發(fā)現(xiàn)還挺好用的。這里舉個(gè)例子:

          // 把它放在 Jest 的 setup 文件中,就會(huì)在所有測(cè)試文件前被引入了
          import * as users from './users'

          async function mockFetch(url, config{
            switch (url) {
              case '/login': {
                const user = await users.login(JSON.parse(config.body))
                return {
                  oktrue,
                  status200,
                  jsonasync () => ({user}),
                }
              }
              case '/checkout': {
                const isAuthorized = user.authorize(config.headers.Authorization)
                if (!isAuthorized) {
                  return Promise.reject({
                    okfalse,
                    status401,
                    jsonasync () => ({message'Not authorized'}),
                  })
                }
                const shoppingCart = JSON.parse(config.body)
                // 可以在這里添加購(gòu)物車(chē)的邏輯
                return {
                  oktrue,
                  status200,
                  jsonasync () => ({successtrue}),
                }
              }
              default: {
                throw new Error(`Unhandled request: ${url}`)
              }
            }
          }

          beforeAll(() => jest.spyOn(window'fetch'))
          beforeEach(() => window.fetch.mockImplementation(mockFetch))

          然后,我們的測(cè)試就可以寫(xiě)成這樣了:

          // __tests__/checkout.js
          import * as React from 'react'
          import {render, screen} from '@testing-library/react'
          import userEvent from '@testing-library/user-event'

          test('clicking "confirm" submits payment'async () => {
            const shoppingCart = buildShoppingCart()
            render(<Checkout shoppingCart={shoppingCart} />)

            userEvent.click(screen.getByRole('button', {name/confirm/i}))

            expect(await screen.findByText(/success/i)).toBeInTheDocument()
          })

          這個(gè)測(cè)試方法不需要你做多余的事,就是寫(xiě) case。這里還可以給它再多加一個(gè)失敗的 Case,不過(guò)我已經(jīng)很滿(mǎn)意了。

          這樣做的好處是對(duì)大量測(cè)試用例都不用寫(xiě)特別多的代碼就能提高我對(duì)業(yè)務(wù)邏輯的信心了。

          msw

          msw 全稱(chēng) “Mock Service Worker”。 現(xiàn)在 Service Worker 還只是瀏覽器中的功能,不能在 Node 端使用。但是,msw 可以支持 Node 端所有測(cè)試場(chǎng)景。

          它的工作原理是這樣的:創(chuàng)建一個(gè) Mock Server 來(lái)攔截所有的請(qǐng)求,然后你就可以像是在真的 Server 里去處理請(qǐng)求。

          我的做法是:json 來(lái)初始化數(shù)據(jù)庫(kù),或者用 faker(現(xiàn)在別用了) 和 test-data-bot 來(lái)構(gòu)造數(shù)據(jù)。然后用 Server Handler(類(lèi)似 Express 的寫(xiě)法)和 Mock DB 交互并返回 Mock 數(shù)據(jù)。這就可以更容易和快速地寫(xiě)測(cè)試了(配置好 Handler 后)。

          你可能在之前會(huì)用 nock 之類(lèi)的庫(kù)來(lái)做這些事。但 msw 還有一個(gè)優(yōu)勢(shì):你可以將這些 “Server Handler” 用在前端本地開(kāi)發(fā)上,適用于以下場(chǎng)景:

          • API 還沒(méi)實(shí)現(xiàn)完
          • API 崩了的時(shí)候
          • 網(wǎng)速太慢或者沒(méi)聯(lián)網(wǎng)

          你可能聽(tīng)說(shuō)過(guò)做類(lèi)似事情的 Mirage。但它不是用 Service Worker 在客戶(hù)端實(shí)現(xiàn)的,所以你不能在開(kāi)發(fā)者的 Network Tab 里看到 HTTP 請(qǐng)求,但是 msw 則可以。 兩者對(duì)比可以看這里。

          示例

          有了上面的介紹,現(xiàn)在來(lái)看看 msw 是如何 Mock Server 的:

          // server-handlers.js
          // 放在這里,不僅可以給測(cè)試用也能給前端本地使用
          import {rest} from 'msw' // msw 支持 GraphQL
          import * as users from './users'

          const handlers = [
            rest.get('/login'async (req, res, ctx) => {
              const user = await users.login(JSON.parse(req.body))
              return res(ctx.json({user}))
            }),
            rest.post('/checkout'async (req, res, ctx) => {
              const user = await users.login(JSON.parse(req.body))
              const isAuthorized = user.authorize(req.headers.Authorization)
              if (!isAuthorized) {
                return res(ctx.status(401), ctx.json({message'Not authorized'}))
              }
              const shoppingCart = JSON.parse(req.body)
              // do whatever other things you need to do with this shopping cart
              return res(ctx.json({successtrue}))
            }),
          ]

          export {handlers}
          // test/server.js
          import {rest} from 'msw'
          import {setupServer} from 'msw/node'
          import {handlers} from './server-handlers'

          const server = setupServer(...handlers)
          export {server, rest}
          // test/setup-env.js
          // 加到 Jest 的 setup 文件上,可以在所有測(cè)試前執(zhí)行
          import {server} from './server.js'

          beforeAll(() => server.listen())
          // 如果你要在特定的用例上使用特定的 Handler,這會(huì)在最后把它們重置掉
          // (對(duì)單測(cè)的隔離性很重要)
          afterEach(() => server.resetHandlers())
          afterAll(() => server.close())

          現(xiàn)在我們的測(cè)試就可以改成:

          // __tests__/checkout.js
          import * as React from 'react'
          import {render, screen} from '@testing-library/react'
          import userEvent from '@testing-library/user-event'

          test('clicking "confirm" submits payment'async () => {
            const shoppingCart = buildShoppingCart()
            render(<Checkout shoppingCart={shoppingCart} />)

            userEvent.click(screen.getByRole('button', {name/confirm/i}))

            expect(await screen.findByText(/success/i)).toBeInTheDocument()
          })

          比起 Mock fetch,我更喜歡這種方案的理由是:

          • 不用管 fetch 函數(shù)里的實(shí)現(xiàn)細(xì)節(jié)
          • 當(dāng)調(diào)用 fetch 時(shí)有報(bào)錯(cuò),那么真實(shí)的 Server Handler 不會(huì)被調(diào)用,而且我的測(cè)試也會(huì)失敗,可以避免提交有問(wèn)題的代碼
          • 可以在前端本地開(kāi)發(fā)時(shí)復(fù)用這些 Server Handler

          Colocation 和 error/edge case testing

          唯一值得擔(dān)心的是:你可能會(huì)把所有 Server Handler 放在同一個(gè)地方,而依賴(lài)它們的測(cè)試文件又會(huì)被放在不同地方,這可能會(huì)導(dǎo)致文件放置不集中。

          首先,我想說(shuō)的是,只有那些對(duì)你測(cè)試很重要,很獨(dú)特的東西才應(yīng)該盡可能靠近測(cè)試文件。

          你不需要在所有測(cè)試文件中都要重復(fù) setup 一次,只需要 setup 獨(dú)特的東西就可以了。 所以,最簡(jiǎn)單的方式就是:把常用的部分放在 Jest 的 setup 文件里。 不然你會(huì)有很多的干擾項(xiàng),也很難對(duì)真正要測(cè)的東西進(jìn)行隔離。

          對(duì)于自定義的場(chǎng)景,msw 可以在運(yùn)行時(shí)允許你在測(cè)試用例中添加自定義的 Server Handler,也可以一鍵重置成你原來(lái)的 Handler,以此保留隔離性。 比如:

          // __tests__/checkout.js
          import * as React from 'react'
          import {server, rest} from 'test/server'
          import {render, screen} from '@testing-library/react'
          import userEvent from '@testing-library/user-event'

          // 啥也不需要改
          test('clicking "confirm" submits payment'async () => {
            const shoppingCart = buildShoppingCart()
            render(<Checkout shoppingCart={shoppingCart} />)

            userEvent.click(screen.getByRole('button', {name/confirm/i}))

            expect(await screen.findByText(/success/i)).toBeInTheDocument()
          })

          // 邊界情況、錯(cuò)誤情況,需要添加自定義的 Handler
          // 注意 afterEach(() => server.resetHandlers())
          // 可以確保在最近移除新增特殊的 Handler
          test('shows server error if the request fails'async () => {
            const testErrorMessage = 'THIS IS A TEST FAILURE'
            server.use(
              rest.post('/checkout'async (req, res, ctx) => {
                return res(ctx.status(500), ctx.json({message: testErrorMessage}))
              }),
            )
            const shoppingCart = buildShoppingCart()
            render(<Checkout shoppingCart={shoppingCart} />)

            userEvent.click(screen.getByRole('button', {name/confirm/i}))

            expect(await screen.findByRole('alert')).toHaveTextContent(testErrorMessage)
          })

          這么一來(lái),你不僅可以把相關(guān)邏輯的代碼放在一起,還能實(shí)現(xiàn)場(chǎng)景自定義。

          總結(jié)

          當(dāng)然 msw 還有很多其它玩法,讀者可以自行探索。下面先讓我們來(lái)小結(jié)一下。

          這種測(cè)試策略一大優(yōu)勢(shì)就是:當(dāng)你完全忽略代碼的實(shí)現(xiàn)細(xì)節(jié),你就可以盡情地重構(gòu)代碼,同時(shí)你的測(cè)試會(huì)源源不斷地給你信心,讓你不用擔(dān)心會(huì)破壞用戶(hù)體驗(yàn)。這才是測(cè)試應(yīng)該做的事。


          好了,這篇外文就給大家?guī)У竭@里了。總的來(lái)說(shuō),我還是挺喜歡攔截 Http 請(qǐng)求這種 Mock 方法的。msw 不僅可以在測(cè)試中攔截請(qǐng)求,實(shí)現(xiàn)集成、E2E 測(cè)試,還可以在前端開(kāi)發(fā)時(shí)來(lái) Mock 數(shù)據(jù),確實(shí)是一個(gè)有趣的實(shí)踐。

          最近也給我們項(xiàng)目寫(xiě)不少單測(cè),其實(shí)單測(cè)和集成測(cè)試還是有很多互補(bǔ)的地方的。當(dāng)你發(fā)現(xiàn)要測(cè)試的東西太復(fù)雜,或者太多干擾項(xiàng)時(shí),使用集成測(cè)試會(huì)讓你真正從用戶(hù)的角度來(lái)寫(xiě)測(cè)試。這樣一來(lái),你就不會(huì)過(guò)度關(guān)注那些覆蓋率指標(biāo)了,而是從一個(gè)用戶(hù)的角度來(lái)思考這樣的用例能給我?guī)?lái)多少信心。

          如果你喜歡我的分享,可以來(lái)一波一鍵三連,點(diǎn)贊、在看就是我最大的動(dòng)力,比心 ??


          往期干貨

           26個(gè)經(jīng)典微信小程序+35套微信小程序源碼+微信小程序合集源碼下載(免費(fèi))

           干貨~~~2021最新前端學(xué)習(xí)視頻~~速度領(lǐng)取

           前端書(shū)籍-前端290本高清pdf電子書(shū)打包下載


          點(diǎn)贊和在看就是最大的支持??


          瀏覽 37
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  免费的三级网站在线观看 | 俺去啦俺去也www官网 | 久久久理论 | 青青草视频在线免费看 | 少妇久久精品 |