Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Peter Jacxsens
Peter Jacxsens

Posted on

     

4. Testing (async) searchParams with Jest in Next 15

This is the fourth part in a series were we look into using and testing the newsearchParams interface inNext 15. In the first part we explained what changed inNext 15 and what the difference is between synchronous and asynchronoussearchParams. In the second part we quickly went over the code for a little example app. In the 3rd part we setupJest and@testing-library/react witheslint plugins.

In this part we're going to start testing. We start of with testing thesearchParams prop in the page route component.

Note: this code is available in agithub repo.

page Component

Let's look at our page component:

// src/app/list/page.tsximportListfrom'@/components/List';importListControlesfrom'@/components/ListControles';importvalidateSortOrderfrom'@/lib/validateSortOrder';typeProps={searchParams:Promise<{[key:string]:string|string[]|undefined}>;};constITEMS=['apple','banana','cherry','lemon'];exportdefaultasyncfunctionListPage({searchParams}:Props){constcurrSearchParams=awaitsearchParams;constsortOrder=validateSortOrder(currSearchParams.sortOrder);return(<><h2className='text-2xl font-bold mb-2'>List</h2><ListControles/><Listitems={ITEMS}sortOrder={sortOrder}/></>);}
Enter fullscreen modeExit fullscreen mode

This is more a container component but it is wheresearchParams is passed. The component receivessearchParams, validates it assortOrder and then passes it to<List />.

We're doing a unit test here. We want to test<ListPage /> in isolation. That means we have to mock thevalidateSortOrder function and the<ListControles /> and<List /> components. Let's start with that.

Mocking

We write a testfile, import everything and then mock said 3 files:

// src/app/list/__tests__/page.test.tsximport{screen,render}from'@testing-library/react';importListPagefrom'../page';importvalidateSortOrderfrom'@/lib/validateSortOrder';importListfrom'@/components/List';importListControlesfrom'@/components/ListControles';jest.mock('@/lib/validateSortOrder');jest.mock('@/components/List');jest.mock('@/components/ListControles');describe('<ListPage />',()=>{});
Enter fullscreen modeExit fullscreen mode

Rendering async components in Jest

Before we continue we need to talk about how you render asynchronous server components inJest.

We have a<Home /> component to test this on. Let's make that async:

// src/app/page.tsxexportdefaultasyncfunctionHome(){return<div>hello world</div>;}
Enter fullscreen modeExit fullscreen mode

We already wrote a test for this component in a previous part when we were setting upJest:

// src/app/__tests__/page.test.tsximport{screen,render}from'@testing-library/react';importHomefrom'@/app/page';describe('<Home />',()=>{test('It renders',()=>{render(<Home/>);expect(screen.getByText(/hello world/i)).toBeInTheDocument();});});
Enter fullscreen modeExit fullscreen mode

It worked before we made<Home /> async, let's run it again. It errors with some confusing messages:

...A suspended resource finished loading inside a test, but the event was not wrapped in act(...).When testing, code that resolves suspended data should be wrapped into act(...):act(() => {  /* finish loading suspended data */});/* assert on the output */...
Enter fullscreen modeExit fullscreen mode

There is more but it doesn't really matter. The thing is that the test doesn't run anymore.

I looked into this, tried and tested different methods likeact,waitFor,await findBy... and more. Nothing worked. It seems thatJest is not equipped to handle async server components.

But we can make it work:

// src/app/__tests__/page.test.tsximport{screen,render}from'@testing-library/react';importHomefrom'@/app/page';describe('<Home />',()=>{test('It renders',async()=>{constcomponent=awaitHome();render(component);expect(screen.getByText(/hello world/i)).toBeInTheDocument();});});
Enter fullscreen modeExit fullscreen mode

We called<Home /> as a function. In case you don't know this, any functional component can simply be called as a function. We also await the function (made the entire test async) and assign it to aconst. Finally, we render ourconst component.

Test passes, everything works. This seems to be the only way to make async server components work inJest so we will use it. Please comment if you know a better way!

async props

Remember,searchParams has to be async. If not we could just do this:

// does not workconstcomponent=awaitListPage({searchParams:{sortOrder:'asc'}});render(component);
Enter fullscreen modeExit fullscreen mode

Immediately, TypeScript will yell at us because the type doesn't match. This is the type we set on searchParams:

typeProps={searchParams:Promise<{[key:string]:string|string[]|undefined}>;};
Enter fullscreen modeExit fullscreen mode

It's a promise that resolves into an object. The value for each property of this object has to be string, an array of strings or undefined. So, we're missing the promise part.

How do we do that then? We could just manually write a promise:

// write promiseconstpromise=newPromise<{[key:string]:string|string[]|undefined;}>((res)=>{res({sortOrder:'asc'});});// pass it to <ListPage />constcomponent=awaitListPage({searchParams:promise});render(component);
Enter fullscreen modeExit fullscreen mode

This works but there is a simpler way. Async functions also return a promise:

asyncfunctiongenerateSearchParams(value:{[key:string]:string|string[]|undefined;}){returnvalue;}
Enter fullscreen modeExit fullscreen mode

We can then call this function like this:

constparams={sortOrder:'asc',};constcomponent=awaitListPage({searchParams:generateSearchParams(params),});render(component);
Enter fullscreen modeExit fullscreen mode

This might be a bit confusing, I know, let me recap. We need to call<ListPage /> with an asyncsearchParams prop. So we created an async function that just returns an object it gets passed as argument. Since it's an async function, it returns said object inside a promise. Exactly what we needed.

testing<ListPage />

Now that everything is setup, let's actually test our component.

// src/app/list/page.tsximportListfrom'@/components/List';importListControlesfrom'@/components/ListControles';importvalidateSortOrderfrom'@/lib/validateSortOrder';typeProps={searchParams:Promise<{[key:string]:string|string[]|undefined}>;};constITEMS=['apple','banana','cherry','lemon'];exportdefaultasyncfunctionListPage({searchParams}:Props){constcurrSearchParams=awaitsearchParams;constsortOrder=validateSortOrder(currSearchParams.sortOrder);return(<><h2className='text-2xl font-bold mb-2'>List</h2><ListControles/><Listitems={ITEMS}sortOrder={sortOrder}/></>);}
Enter fullscreen modeExit fullscreen mode

We first write anit renders test:

// test passestest('It renders',async()=>{constparams={sortOrder:'asc',};constcomponent=awaitListPage({searchParams:generateSearchParams(params),});render(component);expect(validateSortOrder).toHaveBeenCalled();expect(screen.getByRole('heading',{level:2})).toHaveTextContent(/list/i);expect(ListControles).toHaveBeenCalled();expect(List).toHaveBeenCalled();});
Enter fullscreen modeExit fullscreen mode

We already mockedvalidateSortOrder so that will now returnundefined. But, we can use.toHaveBeenCalledWith on thevalidateSortOrder mock to verify that our searchParams.sortOrder value is correctly passed.

Note that this is coreNextJs behavior and we shouldn't really test this. It's aNext thing and should work. We will just do it here to prove our setup works.

// test passestest('searchParams is correctly passed down',async()=>{constparams={sortOrder:'asc',};constcomponent=awaitListPage({searchParams:generateSearchParams(params),});render(component);expect(validateSortOrder).toHaveBeenCalledWith('asc');});
Enter fullscreen modeExit fullscreen mode

Finally, should we test with different sortOrder params? No. Why not? BecausevalidateSortOrder handles this. The function returns eitherdesc orasc (as default or when sortOrder=asc). But, we mockedvalidateSortOrder. So it doesn't matter. Maybe one last test, what happens when there is no sortOrder param?

// test passestest('It calls validateSortOrder with undefined when searchParams.sortOrder does not exist',async()=>{constparams={};constcomponent=awaitListPage({searchParams:generateSearchParams(params),});render(component);expect(validateSortOrder).toHaveBeenCalledWith(undefined);});
Enter fullscreen modeExit fullscreen mode

And that's all we need to test<ListPage />. Here is our final full test file:

// src/app/list/page.tsx// tests passimport{screen,render}from'@testing-library/react';importListPagefrom'../page';importvalidateSortOrderfrom'@/lib/validateSortOrder';importListfrom'@/components/List';importListControlesfrom'@/components/ListControles';jest.mock('@/lib/validateSortOrder');jest.mock('@/components/List');jest.mock('@/components/ListControles');asyncfunctiongenerateSearchParams(value:{[key:string]:string|string[]|undefined;}){returnvalue;}describe('<ListPage />',()=>{test('It renders',async()=>{constparams={sortOrder:'asc',};constcomponent=awaitListPage({searchParams:generateSearchParams(params),});render(component);expect(validateSortOrder).toHaveBeenCalled();expect(screen.getByRole('heading',{level:2})).toHaveTextContent(/list/i);expect(ListControles).toHaveBeenCalled();expect(List).toHaveBeenCalled();});test('searchParams is correctly passed down',async()=>{constparams={sortOrder:'asc',};constcomponent=awaitListPage({searchParams:generateSearchParams(params),});render(component);expect(validateSortOrder).toHaveBeenCalledWith('asc');});test('It calls validateSortOrder with undefined when searchParams.sortOrder does not exist',async()=>{constparams={};constcomponent=awaitListPage({searchParams:generateSearchParams(params),});render(component);expect(validateSortOrder).toHaveBeenCalledWith(undefined);});});
Enter fullscreen modeExit fullscreen mode

Conclusion

Mainly, we learned how to render an async component inJest and how to pass an async prop to an async component. All and all it's not very hard, just a bit cumbersome.

Our little app has some more components and functions but only<ListControles /> matters for this series because it usesuseSearchParams,usePathname anduseRouter. In the next part I will show how to mock and test this component.

I wrote some tests for the other components too but I won't go into to those. You can see the tests in therepo.

If you want to support my writing, you candonate with paypal.

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Front-end developer
  • Location
    Belgium
  • Education
    self taught
  • Joined

More fromPeter Jacxsens

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp