Skip to content

Commit

Permalink
Merge pull request #4 from TheBrainFamily/testingServicesInIsolation
Browse files Browse the repository at this point in the history
feat: Testing services in isolation
  • Loading branch information
lgandecki authored Jun 19, 2019
2 parents 3d6b78d + 13fbf13 commit 318bead
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 24 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
115 changes: 100 additions & 15 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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];
Expand All @@ -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));
Expand Down Expand Up @@ -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"
Expand All @@ -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);

Expand Down
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,8 @@
"jest": "^24.8.0",
"patch-package": "^6.1.2",
"semantic-release": "^15.13.16"
},
"dependencies": {
"clone": "^2.1.2"
}
}
12 changes: 3 additions & 9 deletions tests/integration.test.js → tests/multipleServices.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
};

Expand All @@ -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
})
};

Expand Down Expand Up @@ -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
})
};

Expand Down
80 changes: 80 additions & 0 deletions tests/singleService.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});

0 comments on commit 318bead

Please sign in to comment.