diff --git a/docs/advanced/testing.md b/docs/advanced/testing.md index 82fd102f39..d00e958bd8 100644 --- a/docs/advanced/testing.md +++ b/docs/advanced/testing.md @@ -1,6 +1,226 @@ --- title: Testing -order: 3 +order: 4 --- + # Testing + +When testing your components, you're likely going to want to check arguments and force different states for your components using Urql. + +> **Note:** Examples demonstrate the _React hooks_ version of Urql being used but underlying patterns apply to all implementations. + +## Mocking the client + +For the most part, Urql's hooks are just adapters for talking to the Urql client. + +The way in which they do this is by making calls to the client via context. + +- `useQuery` calls `executeQuery` +- `useMutation` calls `executeMutation` +- `useSubscription` calls `executeSubscription` + +Here's an example client mock being used while testing a component. + +```tsx +import { mount } from 'enzyme'; +import { Provider } from 'urql'; +import { MyComponent } from './MyComponent'; + +const mockClient = { + executeQuery: jest.fn(), + executeMutation: jest.fn(), + executeSubscription: jest.fn(), +}; + +it('renders', () => { + const wrapper = mount( + + + + ); +}); +``` + +## Testing calls to the client + +Once you have your mock setup, calls to the client can be tested. + +```tsx +it('skips the query', () => { + mount( + + + + ); + expect(mockClient.executeQuery).toBeCalledTimes(0); +}); +``` + +Testing mutations and subscriptions also work in a similar fashion. + +```tsx +it('triggers a mutation', () => { + const wrapper = mount( + + + + ); + const variables = { + name: 'Carla', + }; + + wrapper + .find('input') + .simulate('change', { currentTarget: { value: variables.name } }); + wrapper.find('button').simulate('click'); + + expect(mockClient.executeMutation).toBeCalledTimes(1); + expect(mockClient.executeMutation).toBeCalledWith( + expect.objectContaining({ variables }) + ); +}); +``` + +## Forcing states + +For testing render output, or creating fixtures, you may want to force the state of your components. + +### Fetching + +Fetching states can be simulated by returning a stream which never returns. Wonka provides a utility for this, aptly called `never`. + +Here's a fixture which stays in the _fetching_ state. + +```tsx +import { Provider } from 'urql'; +import { never } from 'wonka'; +import { MyComponent } from './MyComponent'; + +const fetchingState = { + executeQuery: () => never, +}; + +export default ( + + + +); +``` + +### Response (success) + +Response states are simulated by providing a stream which contains a network response. For single responses, Wonka's `fromValue` function can do this for us. + +**Example snapshot test of response state** + +```tsx +const responseState = { + executeQuery: () => + fromValue({ + data: { + posts: [ + { id: 1, title: 'Post title', content: 'This is a post' }, + { id: 3, title: 'Final post', content: 'Final post here' }, + ], + }, + }), +}; + +it('matches snapshot', () => { + const wrapper = mount( + + + + ); + expect(wrapper).toMatchSnapshot(); +}); +``` + +### Response (error) + +Error responses are similar to success responses, only the value in the stream is changed. + +```tsx +import { Provider, CombinedError } from 'urql'; + +const errorState = { + executeQuery: () => + fromValue({ + error: new CombinedError({ + networkError: Error('something went wrong!'), + }), + }), +}; +``` + +### Handling multiple hooks + +Returning different values for many `useQuery` calls can be done by introducing conditionals into the mocked client functions. + +```tsx +const mockClient = () => { + executeQuery: ({ query }) => { + if (query === GET_USERS) { + return fromValue(usersResponse); + } + + if (query === GET_POSTS) { + return fromValue(postsResponse); + } + }; +}; +``` + +## Simulating changes + +Simulating multiple responses can be useful, particularly testing `useEffect` calls dependent on changing query responses. + +For this, a _subject_ is the way to go. In short, it's a stream which you can push responses to. The `makeSubject` function from Wonka is what you'll want to use for this purpose. + +Below is an example of simulating subsequent responses (such as a cache update/refetch) in a test. + +```tsx +import { mount } from 'enzyme'; +import { act } from 'react-dom/test-utils'; +import { Provider } from 'urql'; +import { makeSubject } from 'wonka'; +import { MyComponent } from './MyComponent'; + +const [stream, pushResponse] = makeSubject(); + +const mockedClient = { + executeQuery: () => stream, +}; + +it('shows notification on updated data', () => { + const wrapper = mount( + + + + ); + + // First response + act(() => { + pushResponse({ + data: { + posts: [{ id: 1, title: 'Post title', content: 'This is a post' }], + }, + }); + }); + expect(wrapper.find('dialog').exists()).toBe(false); + + // Second response + act(() => { + pushResponse({ + data: { + posts: [ + { id: 1, title: 'Post title', content: 'This is a post' }, + { id: 1, title: 'Post title', content: 'This is a post' }, + ], + }, + }); + }); + expect(wrapper.find('dialog').exists()).toBe(true); +}); +```