Jest mock方法首次调用返回预期值,二次调用返回空数组的问题求助
我正在开发一个用Jest测试的React应用,目前整体进展顺利。我有一个依赖洗牌服务的组件,组件靠这个服务来渲染,所以我在beforeEach里mock了这个服务,在afterEach里重置mock。到目前为止,所有测试都运行正常。
import '@testing-library/jest-dom'; import { render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { Card, Suit } from '../cardContent/cardContent'; import { BuildDeckService } from '../services/buildDeck/buildDeck.service'; import GameLogic from './gameLogic'; describe('GameLogic', () => { beforeEach(() => { const expectedDeck: Card[] = [ ... ]; jest.spyOn(BuildDeckService, 'buildDeck').mockReturnValue(expectedDeck); }); afterEach(() => { jest.resetAllMocks(); }); // 我的所有测试 });
不过最近我写了几个测试,需要buildDeck函数被调用两次——一次是组件首次渲染时,另一次是开始新游戏时。
it('reshuffles and deals the deck', async () => { const user = userEvent.setup(); render(<GameLogic></GameLogic>); // 第一次调用buildDeck const tableauSection = screen.getByTestId('tableau'); const tableauPiles = within(tableauSection).getAllByRole('button'); const foundationSection = screen.getByTestId('foundations'); const foundations = within(foundationSection).getAllByRole('button'); await user.click(tableauPiles[12]); await user.click(foundations[0]); const newGameBtn = screen.getByRole('button', { name: 'New Game' }); await user.click(newGameBtn); // 第二次调用buildDeck expect(within(tableauPiles[12]).getByText('A')).toBeVisible(); expect(within(tableauPiles[12]).getByAltText('of Spades')).toBeVisible(); });
供参考,await user.click(newGameBtn);只是调用了一个叫startNewGame()的函数:
const startNewGame = () => { setFoundations([[], [], [], [], [], [], [], []]); setTableau(dealNewGame()); }; const dealNewGame = (): Card[][] => { const deck: Card[] = BuildDeckService.buildDeck(); // 我可以在这里console.log(deck),看到第一次mock调用时牌组是完整的,第二次是空数组 const newTableau: Card[][] = [[], [], [], [], [], [], [], [], [], [], [], [], [], []]; // 大量将牌组发牌到newTableau的逻辑 return newTableau; };
这些测试失败了,因为第二次调用buildDeck时,它返回的是空数组[]而不是expectedDeck,导致组件后续逻辑混乱。这和我对mockReturnValue()工作方式的理解不符,也让我没法完成新功能:(
为了解决这个问题,我试了能想到的最hack的办法:在测试里,就在用户点击新游戏按钮之前,重置所有mock并重新定义返回值。后来我简化成只重新定义mock,测试变成了这样:
it('reshuffles and deals the deck', async () => { const user = userEvent.setup(); render(<GameLogic></GameLogic>); const tableauSection = screen.getByTestId('tableau'); const tableauPiles = within(tableauSection).getAllByRole('button'); const foundationSection = screen.getByTestId('foundations'); const foundations = within(foundationSection).getAllByRole('button'); await user.click(tableauPiles[12]); await user.click(foundations[0]); const newGameBtn = screen.getByRole('button', { name: 'New Game' }); jest.spyOn(BuildDeckService, 'buildDeck').mockReturnValue(expectedDeck); await user.click(newGameBtn); expect(within(tableauPiles[12]).getByText('A')).toBeVisible(); expect(within(tableauPiles[12]).getByAltText('of Spades')).toBeVisible(); });
这确实让测试通过了,证明主代码是没问题的,测试写法也正确。但这需要在beforeEach块内外都定义expectedDeck,不然会破坏原来的测试。
而且只有单独运行这个测试时才有效。目前有两个测试需要调用两次mock服务,不管顺序如何,第二个测试总是失败,即使我在afterEach里调用了jest.resetAllMocks()。
从那以后,我尝试了各种定义mock的方式,包括BuildDeckService.buildDeck = jest.fn().mockReturnValue(expectedDeck)、jest.spyOn(BuildDeckService, 'buildDeck').mockImplementation(() => expectedDeck);、(BuildDeckService.buildDeck as jest.Mock).mockReturnValue(expectedDeck),甚至在文件顶部用jest.mock('.../filePathToBuildDeckService'),但都没效果。
我还在afterEach块里试了所有版本的jest.resetAllMocks()、jest.clearAllMocks()、jest.restoreAllMocks、(BuildDeckService.buildDeck as jest.Mock).mockReset()等等——都没用。
我试过把mock定义和重置放在beforeAll()和afterAll()块里——没变化。
我试过链式调用两次.mockReturnValueOnce(expectedDeck)——没变化。
我问过Copilot(它给我展示了一堆花里胡哨但没用的mock方式)、各种Discord服务器上的人,也查了文档,但都没进展。
感觉这肯定是某个简单又基础的点,容易被忽略。而且看起来mockReturnValue()的工作方式和我想的不一样,我以为只要在最外层的beforeEach()里设置一次,不管调用多少次mock服务,都会返回同样的结果(假设测试之间重置了mock,我觉得我已经这么做了)。
也不是说第二次调用时mock失效了——如果是那样的话,我应该得到随机洗牌的牌组,而不是空数组。问题很明确:第二次调用mock时,它返回的是空数组,而不是我指定的内容。我甚至都没尝试改变第二次调用的返回值!
到底是怎么回事?为什么调用同一个mock服务两次时测试会失败?
备注:内容来源于stack exchange,提问作者Surfdaweb

