I'm using react-apollo to build a client that consumes a GraphQL API, however, I'm very stuck on testing. What I want is to mock the server so I can easily test the application without needing to make network calls.
I've found some pointers on how to mock the server:
https://dev-blog.apollodata.com/mocking-your-server-with-just-one-line-of-code-692feda6e9cd
http://dev.apollodata.com/tools/graphql-tools/mocking.html#addMockFunctionsToSchema
But there isn't really an example on how to use this mocked server in my app tests to avoid hitting the server.
My goal is to setup integration tests to assert that the app is actually working:
describe('Profile feature', () => {
beforeAll(() => {
store = setupStore();
app = mount(
<ApolloProvider store={store} client={apolloClient}>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</ApolloProvider>
);
});
});
The store is using Redux and the client is being created like this:
const networkInterface = createNetworkInterface({
uri: process.env.REACT_APP_API_URL
});
export const apolloClient = new ApolloClient({
networkInterface
});
How can I use a mocked server with graphql-tools here instead of the actual API?
I found 2 different ways of creating mocked data for apollo-client queries:
The first is to use graphql-tools to create a mocked server based on your backend schema, in order to connect this mocked server with your tests it's possible to create a mockNetworkInterface like this:
const { mockServer } = require("graphql-tools");
const { print } = require("graphql/language/printer");
class MockNetworkInterface {
constructor(schema, mocks = {}) {
if (schema === undefined) {
throw new Error('Cannot create Mock Api without specifying a schema');
}
this.mockServer = mockServer(schema, mocks);
}
query(request) {
return this.mockServer.query(print(request.query), request.variables);
}
}
You can pass this network interface to the ApolloClient component and it should work just fine!
Having this setup requires to have your API schema up to date in your client, so I found it a bit of a pain to do.
Another way of doing this is using the mockNetworkInterface provided by apollo-client/test-utils
You can use it this way:
import App from './App';
import { UserMock, PublicationMock } from '../__mocks__/data';
import { mockNetworkInterface } from 'react-apollo/test-utils';
import ApolloClient from 'apollo-client';
import { ApolloProvider } from 'react-apollo';
// We will be using here the exact same Query defined in our components
// We will provide a custom result or a custom error
const GraphQLMocks = [
{
request: {
query: UserProfileQuery,
variables: {}
},
result: {
data: {
current_user: UserMock
}
}
}
];
// To set it up we pass the mocks to the mockNetworkInterface
const setupTests = () => {
const networkInterface = mockNetworkInterface.apply(null, GraphQLMocks);
const client = new ApolloClient({ networkInterface, addTypename: false });
const wrapper = mount(
<ApolloProvider client={client}>
<App />
</ApolloProvider>
);
return {
store,
wrapper
};
};
// Then the tests look like this
describe('Profile feature', () => {
test('Profile view should render User details', async () => {
const { wrapper, store } = setupTests();
const waitFor = createWaitForElement('.profile');
await waitFor(wrapper);
const tag = wrapper.find('.profile-username');
expect(tag.text()).toEqual(`${UserMock.first_name} ${UserMock.last_name}`);
});
});
It is important to pass addTypename: false to the ApolloClient instance, otherwise you will need to add __typename to all your queries manually.
You can inspect the implementation of the mockNetworkInterface here: https://github.com/apollographql/apollo-test-utils/blob/master/src/mocks/mockNetworkInterface.ts
You can also use MockedProvider, which makes it even simpler.
withPersons.js
import { gql, graphql } from 'react-apollo'
export const PERSONS_QUERY = gql`
query personsQuery {
persons {
name
city
}
}
`
export const withPersons = graphql(PERSONS_QUERY)
withPersons.test.js
/* eslint-disable react/prop-types */
import React, { Component } from 'react'
import { MockedProvider } from 'react-apollo/test-utils'
import { withPersons, PERSONS_QUERY } from '../withPersons'
it('withPersons', (done) => {
const mockedData = {
persons: [
{
name: 'John',
city: 'Liverpool',
},
{
name: 'Frank',
city: 'San Diego',
},
],
}
const variables = { cache: false }
class Dummy extends Component {
componentDidMount() {
const { loading, persons } = this.props.data
expect(loading).toBe(true)
expect(persons).toBe(undefined)
}
componentWillReceiveProps(nextProps) {
const { loading, persons } = nextProps.data
expect(loading).toBe(false)
expect(persons).toEqual(mockedData.persons)
done()
}
render() {
return null
}
}
const DummyWithPersons = withPersons(Dummy)
mount(
<MockedProvider
removeTypename
mocks={[
{
request: { query: PERSONS_QUERY, variables },
result: { data: mockedData } },
]}
>
<DummyWithPersons />
</MockedProvider>,
)
})
Note: By using a Dummy component you just test your graphql() Queries and Mutations and the way you have configured them (options, props, skip, variables, etc.) So you don't mount your actual React components. It's better to test those in their 'unconnected' state.
I wrote up a blog post a while that might be helpful: http://blog.dideric.is/2018/03/18/Testing-apollo-containers/
Apollo has something called LinkSchema that makes the first approach Carlos mentioned a lot easier. It still takes some setup, but I think it's worth it. If you're creating responses manually, you have to worry a lot more about keeping your tests up to date/getting false positives when the schema changes and you haven't accounted for it in your code.
Related
I am having a problem i can't seem to understand why is happening since i have the same example working in codesandbox, but in my app it shows a different behavior. In my app i can see the context from the consumer both the bool and the function, but when i run the function it runs the empty function "setUpdate: () => {}" instead of running the "updBool()" in UpdateDataProvider.js file. Anyone know why this behaviour happens.
(component.js is not my actual file just a short example of how im using the context)
UpdateDataProvider.js
export const UpdateDataContext = createContext({
update: false,
setUpdate: () => {},
});
export function UpdateDataContexProvider({ children }) {
function updBool(bool) {
setU({ ...u, update: bool });
}
const [u, setU] = useState({ update: false, setUpdate: updBool });
return (
<UpdateDataContext.Provider value={u}>
{children}
</UpdateDataContext.Provider>
);
}
useUpdateData.js
import { useContext } from 'react';
import { UpdateDataContext } from '../../context/updateDataContext';
export function useUpdateDataContext() {
return useContext(UpdateDataContext);
}
component.js
import { UpdateDataContexProvider } from '../../context/updateDataContext';
import { useUpdateDataContext } from '../../hooks/exports';
useEffect(() => {
// loging the context shows me update bool and setUpdate function
console.log(context)
// Running the function will run the empty function in createContext
// in UpdateDataProvider.
context.setUpdate(true)
}, [])
export default Home = () => {
const context = useUpdateDataContext()
return (
<UpdateDataContexProvider>
<Other />
</UpdateDataContexProvider>
)
}
Don't mind my question, the mistake was that i was trying to run the function in useEffect in the home component but not the childs
I am working on a StencilJS project where I have to use MirageJS to make fake API data.
How to call server before StencilJS application loads.
In react we can call makeServer() in the index.ts file, but in the stencil, we don't have such a file.
How can we call this to start the mirage server, Please can someone suggest the correct way.
Below is my server.ts file
mirage/server.ts
import { createServer, Model } from 'miragejs';
import { auditFactory } from './factories';
import { processCollectionRequest } from './utils';
export const makeServer = async ({ environment = 'development' } = {}) => {
console.log('started server');
return createServer({
environment,
factories: {
people: auditFactory,
},
models: {
people: Model,
},
routes() {
this.namespace = '/api/v1';
this.get('/peoples', function (schema, request) {
let res = processCollectionRequest(schema, request, 'peoples', this);
// remove factory properties not in spec
res.items.forEach(e => ['associatedResourceId', 'associatedResourceName', 'associatedResourceType'].forEach(p => delete e[p]));
return res;
});
},
seeds(server) {
server.createList('audit', 20);
},
});
};
I'm not familiar with MirageJS so I might be off, but can you use globalScript (https://stenciljs.com/docs/config) and then run your Mirage server there?
I am a subscription setup but onNext is not getting triggered I am not sure why since this is my first time implementing subscription and docs was not much help with the issue.
Here are the code implementations:
import {
graphql,
requestSubscription
} from 'react-relay'
import environment from '../network';
const subscription = graphql`
subscription chatCreatedSubscription{
chatCreated{
id
initiate_time
update_time
support_id
category_id
email
name
}
}
`;
function chatCreated(callback) {
const variables = {};
requestSubscription(environment, {
subscription,
variables,
onNext: () => {
console.log("onNext");
callback()
},
updater: () => {
console.log("updater");
}
});
}
module.exports = chatCreated;
and here is my network for the subscription
import { Environment, Network, RecordSource, Store } from "relay-runtime";
import Expo from "expo";
import { SubscriptionClient } from "subscriptions-transport-ws";
import { WebSocketLink } from 'apollo-link-ws';
import { execute } from 'apollo-link';
import accessHelper from "../helper/accessToken";
const networkSubscriptions = async (operation, variables) => {
let token = await accessHelper();
if (token != null || token != undefined) {
const subscriptionClient = new SubscriptionClient("ws://localhost:3000/graphql",
{
reconnect: true,
connectionParams: {
Authorization: token,
},
});
execute(new WebSocketLink(subscriptionClient), {
query: operation.text,
variables,
});
}
}
const network = Network.create(fetchQuery, networkSubscriptions);
const store = new Store(new RecordSource());
const environment = new Environment({
network,
store
});
export default environment;
the subscription is called in a componentDidMount method on a component it executes but the onNext method inside the subscription is never triggered when new information is added to what the subscription is listening to.
so i figured out that my issue was the network js not being setup properly and the version of subscription-transport-ws. i added version 0.8.3 of the package and made the following changes to my network file:
const networkSubscriptions = async (config, variables, cacheConfig, observer) => {
const query = config.text;
let token = await accessHelper();
if (token != null || token != undefined) {
const subscriptionClient = new SubscriptionClient(`ws://${api}/graphql`,
{
reconnect: true,
connectionParams: {
Authorization: token,
},
});
subscriptionClient.subscribe({ query, variables }, (error, result) => {
observer.onNext({ data: result })
})
return {
dispose: subscriptionClient.unsubscribe
};
}
}
i hope this helps you if you get stuck with the same issue as mine.
I'm trying to figure out how to use dynamic import in webpack with mithril. To do that elegantly, I think I'll need to use an async function somewhere along the line. Right now this is how I have used the async function:
import m from 'mithril'
let App = async () => {
let { Component } = await import('./components.js')
return {
view () {
return m(Component)
}
}
}
App().then(app => m.mount(document.body, app))
Ideally, I want to use it like this:
import m from 'mithril'
let App = {
async view () {
let { Component } = await import('./components.js')
return m(Component)
}
}
}
m.mount(document.body, App)
Is there something I've been missing from the documentation to acheive what I'd like to do? I've tried to look at every mention of promise, but it's possible that I've missed this.
Any help would be appreciated.
One way that should work is this:
async function main() {
const myModule = await import('./myModule.js');
const {export1, export2} = await import('./myModule.js');
const [module1, module2, module3] =
await Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
]);
}
main();
(async () => {
const myModule = await import('./myModule.js');
})();
For further information follow the link below.
ES proposal: import() – dynamically importing ES modules
Try the following, which provides a simple component named DynamicComponent which can be used anywhere and with children:
App.js
import m from 'mithril'
import { DynamicComponent } from './DynamicComponent'
const App = {
view() {
return m( DynamicComponent, {
component: 'OtherComponent'
}, 'Hello world' ),
}
}
}
m.mount(document.body, App)
OtherComponent.js
import m from 'mithril'
export function OtherComponent() { return {
view({ children }) { return m( 'div', children )}
}}
DynamicComponent.js
import { hooks } from '/hooks'
export function DynamicComponent() { return {
...hooks,
attrs: null,
component: null,
view({ children }) { return (
// Await module resolution and process attributes.
// Use '&&' as a shortcut to only continue
// once 'this.component' isn't null.
// Pass a clone of attrs to the loaded component.
this.component && m( this.component.default, this.attrs, children )
)}
}}
hooks.js
async function oninit({ attrs }) {
// Preload -> Load immediately, in parallel
// Prefetch -> Load when browser is idle (Can be less responsive)
// See more: https://webpack.js.org/guides/code-splitting/#prefetching-preloading-modules
// Dynamically import component and append '.js' (Don't include '.js' in your imports).
if ( attrs.loadType = 'prefetch' ) {
// Lazy load
this.component = await import( /* webpackPrefetch: true */ `
${ attrs.component }.js`
)
} else {
// Immediate load
this.component = await import( /* webpackPreload: true */ `
${ attrs.component }.js`
)
}
/*
Process and pass along attributes
This clones the attributes to prevent any changes from affecting
the original attributes.
You can save memory if it becomes a problem by directly
assigning `v.attrs` to `newAttrs`, but you lose this immutability.
*/
const newAttrs = { ...v.attrs }
// Remove attributes used in `DynamicComponent`
delete newAttrs.component
delete newAttrs.loadType
// Assign to component
this.attrs = newAttrs
m.redraw()
}
export const hooks = {
oninit,
}
I am experimenting using vue-apollo with nuxt by implementing the #nuxtjs/apollo module. I have a working GraphQL server running on localhost:4000. I wrote the following code :
<template>
<div>
<p v-for = "item in stuff" :key="item.id">item.name</p>
</div>
</template>
<script>
import stuff from '~/apollo/queries/stuff'
export default {
apollo: {
stuff: {
query: stuff,
variables: {
limit: 10
}
}
},
data () {
return {
stuff: []
}
}
}
</script>
stuff.gql :
{
stuff {
id
name
}
}
client-config :
import { ApolloLink } from 'apollo-link'
import { HttpLink } from 'apollo-link-http'
import { InMemoryCache } from 'apollo-cache-inmemory'
export default (ctx) => {
const httpLink = new HttpLink({ uri: 'http://localhost:4000' })
// middleware
const middlewareLink = new ApolloLink((operation, forward) => {
const token = process.server ? ctx.req.session : window.__NUXT__.state.session
operation.setContext({
headers: { authorization: `Bearer ${token}` }
})
return forward(operation)
})
const link = middlewareLink.concat(httpLink)
return {
link,
cache: new InMemoryCache()
}
}
The observant reader will see that I basically copied the example code from the docs. What I expected to happen was that the data object of my vue component would get updated with the first 10 results of stuff from my backend. However, I see everything in an $apolloData object which is not accessible from the component. Also, the data is not limited to the first 10 entries. Could someone point out what I am doing wrong? Because I don't see it.
I also tried :
apollo: {
products: {
query: stuff,
variables () {
return {
limit: 10
}
}
}
}
And with all variations on the prefetch option.
OK, so I installed a fresh version of the nuxt starter template today and migrated only the essentials to get apollo working. It worked immediately. I have no clue what was causing the error and due to the fact that I already had a dozen packages installed we probably will never know.