Problem
I am trying to test the performance of the following query:
query {
classes {
teachers {
user_id
}
}
}
When I start up my server and run the query through the graphQL playground, the dataloader works as expected and batches the queries: teachersForClasses only runs once.
When I run the test to benchmark the query's performance, teachersForClasses runs once for each unique class returned by getClasses.
Why do the behaviours differ? In both cases an HTTP request is being sent to an already running server.
Background
I am using the dataloader library for node.js on a project where I create a graphQL API with Apollo Server.
Code
function getClasses(args, ctx) {
/* returns a list of Class objects */
}
const resolvers = {
Class: {
teachers: (parent: Class, _args, ctx: Context) =>
ctx.loaders.class.teachers.load(parent.class_id)
}
Query: {
class: (_parent, args, ctx): Promise<Class[]> => getClasses(args, ctx)
}
}
The loader is defined like this:
function teachersForClasses(classIds: readonly string[]) {
console.log(classIds) // added for debugging
/* returns an array of User objects for each class ID */
}
export const loader = {
class: {
teachers: new Dataloader<string, User[]>>(teachersForClasses)
}
}
Tests
A server is already running at http://localhost:8080 before this runs.
const url = 'http://localhost:8080'
const request = supertest(url)
async function runQuery(token: string) {
return request
.post('/user')
.set({
ContentType: 'application/json',
Authorization: token
})
.send({
query: `
{
classes {
teachers {
user_id
}
}
}`
})
}
describe('benchmarks', () => {
it('getTeachersForClasses', () => {
/*populates the database with data*/
...
for (let i=0; i < 10; i++) {
console.time('query')
const start = Date.now()
await runQuery(userToken)
const end = Date.now()
console.timeEnd('query')
}
})
})
I found the problem: there was an async directive (permission check) for 'teachers' which meant that each time the dataloader was called with .load(), it was part of a different event loop 'tick' (explained here) & thus the calls weren't batched as expected.
This wasn't an issue in the graphQL playground because I was making the call with an admin user, so the directive resolved immediately.
Related
I have the following arrangement of tests using sinon, mocha and chai:
type ModelObject = {
name: string;
model: typeof Categoria | typeof Articulo | typeof Usuario;
fakeMultiple: () => object[];
fakeOne: (id?: string) => object;
}
const models: ModelObject[] = [
{
name: 'categorias',
model: Categoria,
fakeMultiple: () => fakeMultiple({ creator: oneCategoria }),
fakeOne: oneCategoria
},
{
name: 'articulos',
model: Articulo,
fakeMultiple: () => fakeMultiple({ creator: oneArticulo }),
fakeOne: oneArticulo
},
{
name: 'usuarios',
model: Usuario,
fakeMultiple: () => fakeMultiple({ creator: oneUsuario }),
fakeOne: oneUsuario
}
];
const randomModel = models[Math.floor(Math.random() * models.length)];
describe(`v1/${randomModel.name}`, function () {
this.afterEach(function () {
sinon.restore();
});
context.only("When requesting information from an endpoint, this should take the Model of the requested endpoint and query the database for all the elements of that model", function () {
it.only(`Should return a list of elements of ${randomModel.name} model`, function (done) {
const fakes = randomModel.fakeMultiple();
const findFake = sinon.fake.resolves({ [randomModel.name]: fakes });
sinon.replace(randomModel.model, 'find', findFake);
chai.request(app)
.get(`/api/v1/${randomModel.name}`)
.end(
(err, res) => {
expect(res).to.have.status(200);
expect(res.body.data).to.be.an('object');
expect(res.body.data).to.have.property(randomModel.name);
expect(res.body.data[randomModel.name]).to.have.lengthOf(fakes.length);
expect(findFake.calledOnce).to.be.true;
done();
}
)
});
}}
I use this to test an endpoint that arbitrary returns information about a given model. In my controllers, I'm using a dynamic middleware to determine which model is going to be queried, for example, if the route consumed is "api/v1/categorias", it will query for Categorias model. If the route consumed is "api/v1/articulos", it will query for Articulos model, and so on.
To make the query, i use the following service:
import { Articulo } from '../models/articulo';
import { Usuario } from '../models/usuario';
import { Categoria } from '../models/categoria';
import logger from '../config/logging';
import { Model } from 'mongoose';
const determineModel = (model: string): Model<any> => {
switch (model) {
case 'articulos':
return Articulo;
case 'usuarios':
return Usuario;
case 'categorias':
return Categoria;
default:
throw new Error(`Model ${model} not found`);
}
};
export const getInformation = async (schema: string, page: number, limit: number) => {
try {
const model = determineModel(schema);
const data = await model.find().skip((page - 1) * limit).limit(limit);
const dataLength = await model.find().countDocuments();
return {
data,
total: dataLength,
};
} catch (err) {
logger.error(err);
console.log(err);
throw err;
}
};
The problem here lies when running my tests, it seems that is unable to run the .skip() and .limit() methods for my model.find()
error: model.find(...).skip is not a function
TypeError: model.find(...).skip is not a function
I think that I need to fake those methods, because when running the same test without skip and limit, it works as a charm. My problem lies in the fact that I don't know how to fake those, or to see if my guess is correct.
As a note, I have default params for the variables page and limit (1 and 15 respectively) so I'm not passing empty values to the methods.
I need dynamically assign a new route but it for some reason refuses to work.
When I send a request in the Postman it just keeps waiting for a response
The whole picture of what I am doing is the following:
I've got a controller with a decorator on one of its methods
#Controller()
export class Test {
#RESTful({
endpoint: '/product/test',
method: 'post',
})
async testMe() {
return {
type: 'hi'
}
}
}
export function RESTful({ endpoint, method, version }: { endpoint: string, version?: string, method: HTTPMethodTypes }) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): void {
const originalMethod = descriptor.value
Reflect.defineMetadata(propertyKey, {
endpoint,
method,
propertyKey,
version
}, target)
return originalMethod
}
}
export function Controller() {
return function (constructor: any) {
const methods = Object.getOwnPropertyNames(constructor.prototype)
Container.set(constructor)
for (let action of methods) {
const route: RESTfulRoute = Reflect.getMetadata(action, constructor.prototype)
if (route) {
const version: string = route.version ? `/${route.version}` : '/v1'
Container.get(Express).injectRoute((instance: Application) => {
instance[route.method](`/api${version}${route.endpoint}`, async () => {
return await Reflect.getOwnPropertyDescriptor(constructor, route.propertyKey)
// return await constructor.prototype[route.propertyKey](req, res)
})
})
}
}
}
}
Is it possible to dynamically set the route in the way?
I mainly use GraphQL but sometimes I need RESTful API too. So, I want to solve this by that decorator
In order for the response to finish, there must be a res.end() or res.json(...) or similar. But I cannot see that anywhere in your code.
I wasn't able to make unit testing worked using jest
I'm trying to test a specific function that's calling or expecting result from other function but I'm not sure why it is not working. I'm pretty new to unit testing and really have no idea how could I make it work. currently this is what I've tried
export class OrganizationService {
constructor() {
this.OrganizationRepo = new OrganizationRepository()
}
async getOrganizations(req) {
if (req.permission !== 'internal' && req.isAuthInternal === false) {
throw new Error('Unauthenticated')
}
const opt = { deleted: true }
return this.OrganizationRepo.listAll(opt)
}
}
This is my OrganizationRepository that extends the MongoDbRepo
import { MongoDbRepo } from './mongodb_repository'
export class OrganizationRepository extends MongoDbRepo {
constructor(collection = 'organizations') {
super(collection)
}
}
and this is the MongoDbRepo
const mongoClient = require('../config/mongo_db_connection')
const mongoDb = require('mongodb')
export class MongoDbRepo {
constructor(collectionName) {
this.collection = mongoClient.get().collection(collectionName)
}
listAll(opt) {
return new Promise((resolve, reject) => {
this.collection.find(opt).toArray((err, data) => {
if (err) {
reject(err)
}
resolve(data)
})
})
}
}
and this is the test that I've made
import { OrganizationService } from '../../../src/services/organization_service'
describe('getOrganizations', () => {
it('should return the list of organizations', () => {
// const OrgRepo = new OrganizationRepository()
const orgService = new OrganizationService()
const OrgRepo = jest.fn().mockReturnValue("[{_id: '123', name: 'testname'}, {_id: '456, name: 'testname2'}]")
// orgService.getOrganizations = jest.fn().mockReturnValue('')
const result = orgService.getOrganizations()
expect(result).toBe(OrgRepo)
})
})
I see two issues in the way you are testing:
1.
You are trying to test an asynchronous method, and on your test, you are not waiting for this method to be finished before your expect statement.
A good test structure should be:
it('should test your method', (done) => {
const orgService = new OrganizationService();
const OrgRepo = jest.fn().mockReturnValue("[{_id: '123', name: 'testname'}, {_id: '456, name: 'testname2'}]")
orgService.getOrganizations()
.then((result) => {
expect(result).toEqual(OrgRepo); // I recommend using "toEqual" when comparing arrays
done();
});
})
Don't forget to put done as a parameter for your test!
You can find more about testing asynchronous functions on the Jest official documentation.
2.
In order to test your method properly, you want to isolate it from external dependencies. Here, the actual method OrganizationRepo.listAll() is called. You want to mock this method, for instance with a spy, so that you control its result and only test the getOrganizations method. That would look like this:
it('should test your method', (done) => {
const req = {
// Whatever structure it needs to be sure that the error in your method is not thrown
};
const orgService = new OrganizationService();
const orgRepoMock = spyOn(orgService['OrganizationRepo'], 'listAll')
.and.returnValue(Promise.resolve("[{_id: '123', name: 'testname'}, {_id: '456, name: 'testname2'}]"));
const OrgRepo = jest.fn().mockReturnValue("[{_id: '123', name: 'testname'}, {_id: '456, name: 'testname2'}]");
orgService.getOrganizations(req)
.then((result) => {
expect(result).toEqual(OrgRepo); // I recommend using "toEqual" when comparing arrays
expect(orgRepoMock).toHaveBeenCalled(); // For good measure
done();
});
})
This way, we make sure that your method is isolated and its outcome cannot be altered by external methods.
For this particular method, I also recommend that you test the error throwing depending on the input of your method.
I was able to answer this, first is I mocked the repository using
jest.mock('path/to/repo')
const mockGetOne = jest.fn()
OrganizationRepository.protorype.getOne = mockGetOne
then the rest is the test
I'm creating an Apollo Client like this:
var { ApolloClient } = require("apollo-boost");
var { InMemoryCache } = require('apollo-cache-inmemory');
var { createHttpLink } = require('apollo-link-http');
var { setContext } = require('apollo-link-context');
exports.createClient = (shop, accessToken) => {
const httpLink = createHttpLink({
uri: `https://${shop}/admin/api/2019-07/graphql.json`,
});
const authLink = setContext((_, { headers }) => {
return {
headers: {
"X-Shopify-Access-Token": accessToken,
"User-Agent": `shopify-app-node 1.0.0 | Shopify App CLI`,
}
}
});
return new ApolloClient({
cache: new InMemoryCache(),
link: authLink.concat(httpLink),
});
};
to hit the Shopify GraphQL API and then running a query like that:
return client.query({
query: gql` {
productVariants(first: 250) {
edges {
node {
price
product {
id
}
}
cursor
}
pageInfo {
hasNextPage
}
}
}
`})
but the returned object only contain data and no extensions which is a problem to figure out the real cost of the query.
Any idea why?
Many thanks for your help
There's a bit of a hacky way to do it that we wrote up before:
You'll need to create a custom apollo link (Apollo’s equivalent of middleware) to intercept the response data as it’s returned from the server, but before it’s inserted into the cache and the components re-rendered.
Here's an example were we pull metrics data from the extensions in our API:
import { ApolloClient, InMemoryCache, HttpLink, ApolloLink } from 'apollo-boost'
const link = new HttpLink({
uri: 'https://serve.onegraph.com/dynamic?show_metrics=true&app_id=<app_id>',
})
const metricsWatchers = {}
let id = 0
export function addMetricsWatcher(f) {
const watcherId = (id++).toString(36)
metricsWatchers[watcherId] = f
return () => {
delete metricsWatchers[watcherId]
}
}
function runWatchers(requestMetrics) {
for (const watcherId of Object.keys(metricsWatchers)) {
try {
metricsWatchers[watcherId](requestMetrics)
} catch (e) {
console.error('error running metrics watcher', e)
}
}
}
// We intercept the response, extract our extensions, mutatively store them,
// then forward the response to the next link
const trackMetrics = new ApolloLink((operation, forward) => {
return forward(operation).map(response => {
runWatchers(
response
? response.extensions
? response.extensions.metrics
: null
: null
)
return response
})
})
function create(initialState) {
return new ApolloClient({
link: trackMetrics.concat(link),
cache: new InMemoryCache().restore(initialState || {}),
})
}
const apolloClient = create(initialState);
Then to use the result in our React components:
import { addMetricsWatcher } from '../integration/apolloClient'
const Page = () => {
const [requestMetrics, updateRequestMetrics] = useState(null)
useEffect(() => {
return addMetricsWatcher(requestMetrics =>
updateRequestMetrics(requestMetrics)
)
})
// Metrics from extensions are available now
return null;
}
Then use a bit of mutable state to track each request and its result, and the use that state to render the metrics inside the app.
Depending on how you're looking to use the extensions data, this may or may not work for you. The implementation is non-deterministic, and can have some slight race conditions between the data that’s rendered and the data that you've extracted from the extensions.
In our case, we store performance metrics data in the extensions - very useful, but ancillary - so we felt the tradeoff was acceptable.
There's also an open issue on the Apollo client repo tracking this feature request
I dont have any idea of ApolloClient but i tried to run your query in shopify graphql app. It return results with extensions. Please find screenshot below. Also You can put questions in ApolloClient github.
in my Node.JS project (a backend for an Angular 5 project) I have created a service that deals with the AWS Authentication... I have called this awsAuthenticationService. All works well but I now need to test it. In my awsAuthenticationService.js I have the following method that has some minor logic and then calls a method provided by the "cognitoIdentityServiceProvider". Here is a snippet of my code (I really have reduced this)
constructor() {
this._cognitoIdentityServiceProvider = new AWS.CognitoIdentityServiceProvider(this.cognitoConfig);
}
toggleUserAccess(userName, type) {
const params = {
Username: userName,
UserPoolId: this.cognitoConfig.userPoolId
};
if (type === null) {
return this._cognitoIdentityServiceProvider.adminEnableUser(params).promise();
}
return this._cognitoIdentityServiceProvider.adminDisableUser(params).promise();
}
As you can see from the toggleUserAccess we pass a few parameters, determine what they are then call the appropriate method. I wish to test this by having a unit test that will call the authenticationService.toggleUserAccess, pass some params and spy on the authenticationService._cognitoIdentityServiceProvider methods to see if they were called. I set it up so...
let authenticationService = require('./awsAuthenticationService');
describe('toggleUserAccess', () => {
beforeEach(() => {
authenticationService._cognitoIdentityServiceProvider = {
adminDisableUser(params) {
return {
promise() {
return Promise.resolve(params);
}
};
}
};
authenticationService._cognitoIdentityServiceProvider = {
adminEnableUser(params) {
return {
promise() {
return Promise.resolve(params);
}
};
}
};
});
it('should call adminEnableUser if the type is null', () => {
authenticationService.toggleUserAccess('TheUser', null);
const spyCognito = sinon.spy(authenticationService._cognitoIdentityServiceProvider, 'adminEnableUser');
expect(spyCognito.calledOnce).to.equal(true);
});
it('should call adminDisableUser if the type is null', () => {
authenticationService.toggleUserAccess('TheUser', '0001');
const spyCognito = sinon.spy(authenticationService._cognitoIdentityServiceProvider, 'adminDisableUser');
expect(spyCognito.calledOnce).to.equal(true);
});
});
My tests aren't passing and I think I have set up my sinon.spys incorrectly - can anyone see what I am doing wrong or give advice please
To stub class of AWS.CognitoIdentityServiceProvider, need to stub with its prototype keyword.
// add require statement for your AWS class
const spyCognito = sinon.spy(AWS.CognitoIdentityServiceProvider.prototype, 'adminDisableUser');
expect(spyCognito.calledOnce).to.equal(true);
Hope it helps