Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
at main 305 lines 9.2 kB view raw view rendered
1--- 2title: Testing 3order: 7 4--- 5 6# Testing 7 8Testing with `urql` can be done in a multitude of ways. The most effective and straightforward 9method is to mock the `Client` to force your components into a fixed state during testing. 10 11The following examples demonstrate this method of testing for React and the `urql` package only, 12however the pattern itself can be adapted for any framework-bindings of `urql`. 13 14## Mocking the client 15 16For the most part, urql's hooks are just adapters for talking to the urql client. 17 18The way in which they do this is by making calls to the client via context. 19 20- `useQuery` calls `executeQuery` 21- `useMutation` calls `executeMutation` 22- `useSubscription` calls `executeSubscription` 23 24In the section ["Stream Patterns" on the "Architecture" page](../architecture.md) we've seen, that 25all methods on the client operate with and return streams. These streams are created using 26[the Wonka library](../architecture.md#the-wonka-library), and we're able to create streams 27ourselves to mock the different states of our operations, e.g. fetching, errors, or success with data. 28 29You'll probably use one of these utility functions to create streams: 30 31- `never`: This stream doesn’t emit any values and never completes, which puts our `urql` code in a permanent `fetching: true` state. 32- `fromValue`: This utility function accepts a value and emits it immediately, which we can use to mock a result from the server. 33- `makeSubject`: Allows us to create a source and imperatively push responses, which is useful to test subscription and simulate changes, i.e. multiple states. 34 35Creating a mock `Client` is pretty quick as we'll create an object that contains the `Client`'s methods that the React `urql` hooks use. We'll mock the appropriate `execute` functions that we need to mock a set of hooks. After we've created the mock `Client` we can wrap components with the `Provider` from `urql` and pass it. 36 37Here's an example client mock being used while testing a component. 38 39```tsx 40import { mount } from 'enzyme'; 41import { Provider } from 'urql'; 42import { never } from 'wonka'; 43import { MyComponent } from './MyComponent'; 44 45it('renders', () => { 46 const mockClient = { 47 executeQuery: jest.fn(() => never), 48 executeMutation: jest.fn(() => never), 49 executeSubscription: jest.fn(() => never), 50 }; 51 52 const wrapper = mount( 53 <Provider value={mockClient}> 54 <MyComponent /> 55 </Provider> 56 ); 57}); 58``` 59 60## Testing calls to the client 61 62Once you have your mock setup, calls to the client can be tested. 63 64```tsx 65import { mount } from 'enzyme'; 66import { Provider } from 'urql'; 67import { MyComponent } from './MyComponent'; 68 69it('skips the query', () => { 70 mount( 71 <Provider value={mockClient}> 72 <MyComponent skip={true} /> 73 </Provider> 74 ); 75 expect(mockClient.executeQuery).toBeCalledTimes(0); 76}); 77``` 78 79Testing mutations and subscriptions also work in a similar fashion. 80 81```tsx 82import { mount } from 'enzyme'; 83import { Provider } from 'urql'; 84import { MyComponent } from './MyComponent'; 85 86it('triggers a mutation', () => { 87 const wrapper = mount( 88 <Provider value={mockClient}> 89 <MyComponent /> 90 </Provider> 91 ); 92 93 const variables = { name: 'Carla' }; 94 95 wrapper.find('input').simulate('change', { currentTarget: { value: variables.name } }); 96 wrapper.find('button').simulate('click'); 97 98 expect(mockClient.executeMutation).toBeCalledTimes(1); 99 expect(mockClient.executeMutation).toBeCalledWith(expect.objectContaining({ variables }), {}); 100}); 101``` 102 103## Forcing states 104 105For testing render output, or creating fixtures, you may want to force the state of your components. 106 107### Fetching 108 109Fetching states can be simulated by returning a stream, which never returns. Wonka provides a utility for this, aptly called `never`. 110 111Here's a fixture, which stays in the _fetching_ state. 112 113```tsx 114import { Provider } from 'urql'; 115import { never } from 'wonka'; 116import { MyComponent } from './MyComponent'; 117 118const fetchingState = { 119 executeQuery: () => never, 120}; 121 122export default ( 123 <Provider value={fetchingState}> 124 <MyComponent /> 125 </Provider> 126); 127``` 128 129### Response (success) 130 131Response states are simulated by providing a stream, which contains a network response. For single responses, Wonka's `fromValue` function can do this for us. 132 133**Example snapshot test of response state** 134 135```tsx 136import { mount } from 'enzyme'; 137import { Provider } from 'urql'; 138import { fromValue } from 'wonka'; 139import { MyComponent } from './MyComponent'; 140 141it('matches snapshot', () => { 142 const responseState = { 143 executeQuery: () => 144 fromValue({ 145 data: { 146 posts: [ 147 { id: 1, title: 'Post title', content: 'This is a post' }, 148 { id: 3, title: 'Final post', content: 'Final post here' }, 149 ], 150 }, 151 }), 152 }; 153 154 const wrapper = mount( 155 <Provider value={responseState}> 156 <MyComponent /> 157 </Provider> 158 ); 159 expect(wrapper).toMatchSnapshot(); 160}); 161``` 162 163### Response (error) 164 165Error responses are similar to success responses, only the value in the stream is changed. 166 167```tsx 168import { Provider, CombinedError } from 'urql'; 169import { fromValue } from 'wonka'; 170 171const errorState = { 172 executeQuery: () => 173 fromValue({ 174 error: new CombinedError({ 175 networkError: Error('something went wrong!'), 176 }), 177 }), 178}; 179``` 180 181### Handling multiple hooks 182 183Returning different values for many `useQuery` calls can be done by introducing conditionals into the mocked client functions. 184 185```tsx 186import { fromValue } from 'wonka'; 187 188let mockClient; 189beforeEach(() => { 190 mockClient = () => { 191 executeQuery: ({ query }) => { 192 if (query === GET_USERS) { 193 return fromValue(usersResponse); 194 } 195 196 if (query === GET_POSTS) { 197 return fromValue(postsResponse); 198 } 199 }; 200 }; 201}); 202``` 203 204The above client we've created mocks all three operations — queries, mutations and subscriptions — to always remain in the `fetching: true` state. 205Generally when we're _hoisting_ our mocked client and reuse it across multiple tests we have to be 206mindful not to instantiate the mocks outside of Jest's lifecycle functions (like `it`, `beforeEach`, 207`beforeAll` and such) as it may otherwise reset our mocked functions' return values or 208implementation. 209 210## Subscriptions 211 212Testing subscriptions can be done by simulating the arrival of new data over time. To do this we may use the `interval` utility from Wonka, which emits values on a timer, and for each value we can map over the response that we'd like to mock. 213 214If you prefer to have more control on when the new data is arriving you can use the `makeSubject` utility from Wonka. You can see more details in the next section. 215 216Here's an example of testing a list component, which uses a subscription. 217 218```tsx 219import { OperationContext, makeOperation } from '@urql/core'; 220import { mount } from 'enzyme'; 221import { Provider } from 'urql'; 222import { MyComponent } from './MyComponent'; 223 224it('should update the list', done => { 225 const mockClient = { 226 executeSubscription: jest.fn(query => 227 pipe( 228 interval(200), 229 map((i: number) => ({ 230 // To mock a full result, we need to pass a mock operation back as well 231 operation: makeOperation('subscription', query, {} as OperationContext), 232 data: { posts: { id: i, title: 'Post title', content: 'This is a post' } }, 233 })) 234 ) 235 ), 236 }; 237 238 let index = 0; 239 240 const wrapper = mount( 241 <Provider value={mockClient}> 242 <MyComponent /> 243 </Provider> 244 ); 245 246 setTimeout(() => { 247 expect(wrapper.find('.list').children()).toHaveLength(index + 1); // See how many items are in the list 248 index++; 249 if (index === 2) done(); 250 }, 200); 251}); 252``` 253 254## Simulating changes 255 256Simulating multiple responses can be useful, particularly testing `useEffect` calls dependent on changing query responses. 257 258For this, a _subject_ is the way to go. In short, it's a stream that you can push responses to. The `makeSubject` function from Wonka is what you'll want to use for this purpose. 259 260Below is an example of simulating subsequent responses (such as a cache update/refetch) in a test. 261 262```tsx 263import { mount } from 'enzyme'; 264import { act } from 'react-dom/test-utils'; 265import { Provider } from 'urql'; 266import { makeSubject } from 'wonka'; 267import { MyComponent } from './MyComponent'; 268 269const { source: stream, next: pushResponse } = makeSubject(); 270 271it('shows notification on updated data', () => { 272 const mockedClient = { 273 executeQuery: jest.fn(() => stream), 274 }; 275 276 const wrapper = mount( 277 <Provider value={mockedClient}> 278 <MyComponent /> 279 </Provider> 280 ); 281 282 // First response 283 act(() => { 284 pushResponse({ 285 data: { 286 posts: [{ id: 1, title: 'Post title', content: 'This is a post' }], 287 }, 288 }); 289 }); 290 expect(wrapper.find('dialog').exists()).toBe(false); 291 292 // Second response 293 act(() => { 294 pushResponse({ 295 data: { 296 posts: [ 297 { id: 1, title: 'Post title', content: 'This is a post' }, 298 { id: 1, title: 'Post title', content: 'This is a post' }, 299 ], 300 }, 301 }); 302 }); 303 expect(wrapper.find('dialog').exists()).toBe(true); 304}); 305```