From 13fbf13df8865623372fc1146a11aa5cfcbc2661 Mon Sep 17 00:00:00 2001 From: lgandecki Date: Wed, 19 Jun 2019 22:14:41 +0400 Subject: [PATCH] allow for testing services in isolation --- README.md | 2 + index.js | 115 +++++++++++++++--- package-lock.json | 5 + package.json | 3 + ...ation.test.js => multipleServices.test.js} | 12 +- tests/singleService.test.js | 80 ++++++++++++ 6 files changed, 193 insertions(+), 24 deletions(-) rename tests/{integration.test.js => multipleServices.test.js} (96%) create mode 100644 tests/singleService.test.js diff --git a/README.md b/README.md index f1e2865..61abc5f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # federation-testing-tool Test your Apollo GraphQL Gateway / Federation micro services. With this package you don't have to worry about the whole complexity that comes with joining the GraphQL federated micro services and preparing them for testing. +### NOTE: THIS IS NOT READY YET. I'M OPEN FOR FEEDBACK, BUT PLEASE BE AWARE THAT THE API KEEPS CHANGING EVERYDAY. I DIDN'T EXPECT PEOPLE TO START USING IT! I PLAN TO RELEASE A TUTORIAL THIS COMING WEEKEND AND BY THAT TIME THE PACKAGE WILL BE READY. STAY TUNED. + Install it with ```bash npm install --save-dev federation-testing-tool diff --git a/index.js b/index.js index 4543225..fcb8abe 100644 --- a/index.js +++ b/index.js @@ -6,8 +6,13 @@ const { } = require("@apollo/gateway"); const { addMockFunctionsToSchema } = require("graphql-tools"); const { addResolversToSchema } = require("apollo-graphql"); - -const { buildFederatedSchema, composeServices } = require("@apollo/federation"); +const { print } = require("graphql"); +const { + buildFederatedSchema, + composeAndValidate +} = require("@apollo/federation"); +const clone = require("clone"); +const gql = require("graphql-tag"); function buildLocalService(modules) { const schema = buildFederatedSchema(modules); @@ -24,7 +29,70 @@ function buildRequestContext(variables, context) { }; } -const setupSchema = services => { +function prepareProviderService(service) { + let allTypeNames = []; + + const typeDefsForMockedService = clone(service.typeDefs); + + typeDefsForMockedService.definitions = typeDefsForMockedService.definitions + .filter(d => d.name.value !== "Query" && d.name.value !== "Mutation") + .filter(d => d.kind === "ObjectTypeExtension"); + + typeDefsForMockedService.definitions.forEach(def => { + def.kind = "ObjectTypeDefinition"; + allTypeNames.push(def.name.value); + + def.fields = def.fields.filter(f => + f.directives.find(d => d.name.value === "external") + ); + def.fields.forEach(f => { + f.directives = f.directives.filter(d => d.name.value !== "external"); + }); + }); + + if (allTypeNames.length) { + const typesQueries = allTypeNames.map(n => `_get${n}: ${n}`).join("\n"); + const newTypeDefString = ` + extend type Query { + ${typesQueries} + } + ${print(typeDefsForMockedService)} + `; + + // I'm doing it like this because otherwise IDE screams at me for an incorrect GraphQL string + let newTypeDefs = gql` + ${newTypeDefString} + `; + + return { + __provider: { + typeDefs: newTypeDefs + } + }; + } + return undefined; +} + +const setupSchema = serviceOrServices => { + let services; + if (!serviceOrServices.length) { + services = [ + { + serviceUnderTest: { + resolvers: serviceOrServices.resolvers, + typeDefs: serviceOrServices.typeDefs, + underTest: true + } + } + ]; + const providerService = prepareProviderService(serviceOrServices); + if (providerService) { + services.push(providerService); + } + } else { + services = serviceOrServices; + } + let serviceMap = {}; services.forEach(service => { let serviceName = Object.keys(service)[0]; @@ -39,7 +107,7 @@ const setupSchema = services => { }) ); - let composed = composeServices(mapForComposeServices); + let composed = composeAndValidate(mapForComposeServices); if (composed.errors && composed.errors.length > 0) { throw new Error(JSON.stringify(composed.errors)); @@ -80,19 +148,15 @@ function execute(schema, query, mutation, serviceMap, variables, context) { ); } -const executeGraphql = ({ - query, - mutation, - variables, - context, +function validateArguments( services, - mocks = {}, + service, schema, - serviceMap -}) => { - if (services) { - ({ schema, serviceMap } = setupSchema(services)); - } else { + serviceMap, + query, + mutation +) { + if (!(services || service)) { if (!schema) { throw new Error( "You need to pass either services array to prepare your schema, or the schema itself, generated by the setupSchema function" @@ -104,6 +168,27 @@ const executeGraphql = ({ ); } } + if (!(query || mutation)) { + throw new Error("Make sure you pass a query or a mutation"); + } +} + +const executeGraphql = ({ + query, + mutation, + variables, + context, + services, + mocks = {}, + schema, + serviceMap, + service + }) => { + validateArguments(services, service, schema, serviceMap, query, mutation); + + if (services || service) { + ({ serviceMap, schema } = setupSchema(services || service)); + } setupMocks(serviceMap, mocks); diff --git a/package-lock.json b/package-lock.json index 1d59f36..c2647f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1822,6 +1822,11 @@ } } }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", diff --git a/package.json b/package.json index 90d263d..8a0aec1 100644 --- a/package.json +++ b/package.json @@ -36,5 +36,8 @@ "jest": "^24.8.0", "patch-package": "^6.1.2", "semantic-release": "^15.13.16" + }, + "dependencies": { + "clone": "^2.1.2" } } diff --git a/tests/integration.test.js b/tests/multipleServices.test.js similarity index 96% rename from tests/integration.test.js rename to tests/multipleServices.test.js index 904c9bf..23c54a7 100644 --- a/tests/integration.test.js +++ b/tests/multipleServices.test.js @@ -91,9 +91,7 @@ describe("Based on the mocked data from the external service", () => { upc: "1", name: "Table", weight: 10, - price: 10, - elo: "", - __typename: "Product" + price: 10 }) }; @@ -110,9 +108,7 @@ describe("Based on the mocked data from the external service", () => { upc: "1", name: "Table", weight: 10, - price: 14000, - elo: "", - __typename: "Product" + price: 14000 }) }; @@ -141,9 +137,7 @@ test("should allow for using mutations, going across the services", async () => upc: "3", name: "Hello", weight: 10, - price: 14000, - elo: "", - __typename: "Product" + price: 14000 }) }; diff --git a/tests/singleService.test.js b/tests/singleService.test.js new file mode 100644 index 0000000..988a106 --- /dev/null +++ b/tests/singleService.test.js @@ -0,0 +1,80 @@ +const gql = require("graphql-tag"); +const { executeGraphql } = require("../"); + +const typeDefs = gql` + extend type Product @key(fields: "upc") { + upc: String! @external + weight: Int @external + price: Int @external + inStock: Boolean + shippingEstimate: Int @requires(fields: "price weight") + } +`; + +let inventory = [ + { upc: "1", inStock: true }, + { upc: "2", inStock: false }, + { upc: "3", inStock: true } +]; + +const resolvers = { + Product: { + __resolveReference(object) { + return { + ...object, + ...inventory.find(product => product.upc === object.upc) + }; + }, + shippingEstimate: object => { + if (object.price > 1000) return 0; + return object.weight * 0.5; + } + } +}; + +const service = { + typeDefs, + resolvers +}; + +describe("Based on the data from the external service", () => { + const query = gql` + { + _getProduct { + inStock + shippingEstimate + } + } + `; + + it("should set the shippingEstimate at 0 for an expensive item and retrieve inStock", async () => { + const mocks = { + Product: () => ({ + upc: "1", + weight: 10, + price: 14000, + }) + }; + + const result = await executeGraphql({ query, mocks, service }); + + expect(result.data._getProduct.shippingEstimate).toEqual(0); + expect(result.data._getProduct).toEqual({ + inStock: true, + shippingEstimate: 0 + }); + }); + + it("should calculate the shipping estimate for cheap item", async () => { + const mocks = { + Product: () => ({ + upc: "1", + weight: 10, + price: 10, + }) + }; + + const result = await executeGraphql({ query, mocks, service }); + expect(result.data._getProduct.shippingEstimate).toEqual(5); + }); +});