Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Testing infrastructure improvements #453

Merged
merged 47 commits into from Mar 3, 2021
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
08e1b3a
Update Jest and related packages to latest versions
martijnwalraven Jan 28, 2021
1f470ad
Rename `ts-jest` `tsConfig` option to `tsconfig`
martijnwalraven Jan 28, 2021
2e32c05
Remove use of `moduleNameMapper` for local imports
martijnwalraven Jan 28, 2021
806b959
Update TypeScript to 4.1.3
martijnwalraven Jan 28, 2021
7e86ef7
Clean up test projects and reference all tests from root tsconfig.tes…
martijnwalraven Jan 28, 2021
cf7368e
Add a TypeScript watch task to VS Code
martijnwalraven Jan 28, 2021
879ddd0
Re-enable `strictNullChecks` for composition tests and fix using null…
martijnwalraven Jan 28, 2021
22395b6
Rely on `queryPlanSerializer` instead of pretty printing the query pl…
martijnwalraven Feb 2, 2021
68ef9d6
Move snapshot serializers and matchers to federaton-integration-tests…
martijnwalraven Feb 2, 2021
2cb7276
Fix Jest snapshot indentation for `graphql-js` AST
martijnwalraven Feb 4, 2021
417c79e
Add expected newlines in gateway snapshots
martijnwalraven Feb 4, 2021
6d338b9
Update `ts-jest` to latest version
martijnwalraven Feb 4, 2021
6304f18
Update `graphql-js` to v15
martijnwalraven Feb 4, 2021
8d663f1
Fix references for per `__tests__` directory `tsconfig.json`
martijnwalraven Feb 4, 2021
894cfed
Remove unnecessary declaration of `addSnapshotSerializer`
martijnwalraven Feb 4, 2021
12ad3bf
Fix typings for `toHaveFetched` matchers
martijnwalraven Feb 4, 2021
d5c4e71
Clean up `toMatchAST` and add `toMatchQueryPlan`
martijnwalraven Feb 4, 2021
912716d
Simplify Jest configs and ensure `testSetup.js` is included in every …
martijnwalraven Feb 4, 2021
5192e3f
Use `tsconfig.test.json` from `ts-jest` instead of root `__tests__` one
martijnwalraven Feb 4, 2021
9d75cb8
Avoid path mapping for mocks and fix typing issues
martijnwalraven Feb 5, 2021
2b3af8a
Add `@apollo/query-planner` package
martijnwalraven Feb 26, 2021
d9bdbc6
Move `prettyFormatQueryPlan` and related serializers to `@apollo/quer…
martijnwalraven Feb 26, 2021
50a37ac
Update package-lock.json for @apollo/query-planner
martijnwalraven Mar 2, 2021
6ad7fe5
Import matchers from subdirectory instead of root
martijnwalraven Mar 2, 2021
e007c3c
Add more test typing fixes using null coalescing
martijnwalraven Mar 2, 2021
b7ac4d2
Remove unnecessary header iteration from toHaveFetched
martijnwalraven Mar 2, 2021
a359e23
Fix `toHaveFetched` typing to support JSON body
martijnwalraven Mar 2, 2021
7859038
Update __tests__ tsconfig.json to include mocks
martijnwalraven Mar 2, 2021
ff0bfa6
Remove some unused imports from tests
martijnwalraven Mar 2, 2021
2feb7a5
Import `QueryPlan` from `@apollo/query-planner` in test
martijnwalraven Mar 2, 2021
0cb2a3d
Add `@types` packages for `deep-freeze` and `bunyan`
martijnwalraven Mar 2, 2021
9ed379d
Remove more unused imports from tests
martijnwalraven Mar 2, 2021
05dda1a
Temporarily ignore typing errors in tests for incomplete request cont…
martijnwalraven Mar 2, 2021
b8ba02e
Temporarily ignore typing errors in tests for buildService
martijnwalraven Mar 2, 2021
07d848f
Fix type of Promise in test
martijnwalraven Mar 2, 2021
2b774f9
Avoid stringifying variables in test because that breaks typing
martijnwalraven Mar 2, 2021
c0cb248
Add resolver map type to test
martijnwalraven Mar 2, 2021
b76f8c4
Cast to any to allow arbitrary property mutation in test
martijnwalraven Mar 2, 2021
0bd250f
Fix `make-fetch-happen` types to allow `defaults()` chaining
martijnwalraven Mar 2, 2021
646230d
Change unused parameter to _ to avoid type error
martijnwalraven Mar 2, 2021
21dc682
Set `skipLibCheck` to true in `tsconfig` to avoid type errors from de…
martijnwalraven Mar 2, 2021
9d21dd5
Fix typing for Logger mock
martijnwalraven Mar 2, 2021
1b34ce5
Add non-null assertion for known schema types in test
martijnwalraven Mar 2, 2021
68b27f3
Merge branch 'main' into mw/testing-improvements
martijnwalraven Mar 2, 2021
042edda
Add non-null assertion to fix assignment from closure
martijnwalraven Mar 2, 2021
729faad
Update `apollo-server-core` dependency in `@apollo/gateway`
martijnwalraven Mar 2, 2021
58a88ef
Remove `skipLibCheck` and add workaround for typing issue in dependency
martijnwalraven Mar 3, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 25 additions & 0 deletions .vscode/tasks.json
@@ -0,0 +1,25 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "TypeScript watch",
"type": "typescript",
"tsconfig": "tsconfig.json",
"option": "watch",
"problemMatcher": ["$tsc-watch"],
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"reveal": "never",
"revealProblems": "onProblem",
"panel": "shared",
"clear": true
},
"runOptions": {
"runOn": "folderOpen"
}
}
]
}
4 changes: 4 additions & 0 deletions federation-integration-testsuite-js/src/index.ts
@@ -1 +1,5 @@
export * from './matchers';
export * from './snapshotSerializers';
export * from './fixtures';

export { prettyFormatQueryPlan } from './prettyFormatQueryPlan';
4 changes: 4 additions & 0 deletions federation-integration-testsuite-js/src/matchers/index.ts
@@ -0,0 +1,4 @@
import './toCallService';
import './toHaveBeenCalledBefore';
import './toHaveFetched';
import './toMatchAST';
@@ -1,12 +1,11 @@
import { QueryPlan } from '@apollo/gateway';
import { PlanNode } from '../../QueryPlan';
import astSerializer from '../../snapshotSerializers/astSerializer';
import queryPlanSerializer from '../../snapshotSerializers/queryPlanSerializer';
import { QueryPlan, PlanNode } from '@apollo/gateway';
import astSerializer from '../snapshotSerializers/astSerializer';
import queryPlanSerializer from '../snapshotSerializers/queryPlanSerializer';
const prettyFormat = require('pretty-format');

declare global {
namespace jest {
interface Matchers<R, T> {
interface Matchers<R> {
toCallService(service: string): R;
}
}
Expand Down
Expand Up @@ -3,8 +3,8 @@
export {};
declare global {
namespace jest {
interface Matchers<R, T> {
toHaveBeenCalledBefore(spy: SpyInstance): R;
interface Matchers<R> {
toHaveBeenCalledBefore(secondSpy: jest.SpyInstance): R;
}
}
}
Expand Down
Expand Up @@ -5,8 +5,9 @@ import { RequestInit, Headers } from 'apollo-server-env';
export {};
declare global {
namespace jest {
interface Matchers<R, T> {
toHaveFetched(spy: SpyInstance): R;
interface Matchers<R> {
toHaveFetched(requestUrl: string, requestOpts?: RequestInit): R;
toHaveFetchedNth(nthCall: number, requestUrl: string, requestOpts?: RequestInit): R;
}
}
}
Expand All @@ -15,8 +16,8 @@ function prepareHttpOptions(requestUrl: string, requestOpts: RequestInit): Reque
const headers = new Headers();
headers.set('Content-Type', 'application/json');
if (requestOpts.headers) {
for (let name in requestOpts.headers) {
headers.set(name, requestOpts.headers[name]);
for (const [name, value] of new Headers(requestOpts.headers)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just so I'm understanding, this is necessary — rather than just headers.entries() to account for the fact that headers might be a { [name: string]: string }?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code hasn't changed in this PR, but I'm wondering why we can't directly initialize headers as new Headers(requestOpts.headers). What is iterating over the passed in headers and adding them one by one adding?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, the types of toHaveFetched are currently broken because the normal BodyInit doesn't support arbitrary objects, but here it is always assumed an object is passed in and should be serialized as JSON.

headers.set(name, value);
}
}

Expand All @@ -37,7 +38,7 @@ function toHaveFetched(
this: jest.MatcherUtils,
fetch: jest.SpyInstance,
requestUrl: string,
requestOpts: RequestInit
requestOpts: RequestInit = {}
): { message(): string; pass: boolean } {
const httpOptions = prepareHttpOptions(requestUrl, requestOpts);
let pass = false;
Expand All @@ -60,7 +61,7 @@ function toHaveFetchedNth(
fetch: jest.SpyInstance,
nthCall: number,
requestUrl: string,
requestOpts: RequestInit
requestOpts: RequestInit = {}
): { message(): string; pass: boolean } {
const httpOptions = prepareHttpOptions(requestUrl, requestOpts);
let pass = false;
Expand Down
43 changes: 43 additions & 0 deletions federation-integration-testsuite-js/src/matchers/toMatchAST.ts
@@ -0,0 +1,43 @@
import { ASTNode } from 'graphql';
import { MatcherHintOptions } from 'jest-matcher-utils';
import { diffFormatted, indentLines, printExpectedFormatted } from './utils';

declare global {
namespace jest {
interface Matchers<R> {
toMatchAST(expected: ASTNode): R;
}
}
}

expect.extend({
toMatchAST(received: ASTNode, expected: ASTNode) {
const matcherName = 'toMatchAST';
const options: MatcherHintOptions = {
isNot: this.isNot,
promise: this.promise,
};

const pass = this.equals(received, expected);

const message = pass
? () =>
this.utils.matcherHint(matcherName, undefined, undefined, options) +
'\n\n' +
`Expected AST to not equal:\n` +
indentLines(printExpectedFormatted(expected))
: () =>
this.utils.matcherHint(matcherName, undefined, undefined, options) +
'\n\n' +
diffFormatted(expected, received, {
aAnnotation: 'Expected',
bAnnotation: 'Received',
expand: this.expand ?? true,
includeChangeCounts: true,
});
return {
message,
pass,
};
},
});
@@ -0,0 +1,43 @@
import { QueryPlan } from '@apollo/gateway';
import { MatcherHintOptions } from 'jest-matcher-utils';
import { diffFormatted, indentLines, printExpectedFormatted } from './utils';

declare global {
namespace jest {
interface Matchers<R> {
toMatchQueryPlan(expected: QueryPlan): R;
}
}
}

expect.extend({
toMatchQueryPlan(received: QueryPlan, expected: QueryPlan) {
const matcherName = 'toMatchQueryPlan';
const options: MatcherHintOptions = {
isNot: this.isNot,
promise: this.promise,
};

const pass = this.equals(received, expected);

const message = pass
? () =>
this.utils.matcherHint(matcherName, undefined, undefined, options) +
'\n\n' +
`Expected query plan to not equal:\n` +
indentLines(printExpectedFormatted(expected))
: () =>
this.utils.matcherHint(matcherName, undefined, undefined, options) +
'\n\n' +
diffFormatted(expected, received, {
aAnnotation: 'Expected',
bAnnotation: 'Received',
expand: this.expand ?? true,
includeChangeCounts: true,
});
return {
message,
pass,
};
},
});
53 changes: 53 additions & 0 deletions federation-integration-testsuite-js/src/matchers/utils.ts
@@ -0,0 +1,53 @@
import diff, { DiffOptions } from 'jest-diff';
import { EXPECTED_COLOR, RECEIVED_COLOR } from 'jest-matcher-utils';
import prettyFormat from 'pretty-format';
import {
queryPlanSerializer,
astSerializer,
typeSerializer,
} from '../snapshotSerializers';

const defaultFormatOptions: prettyFormat.OptionsReceived = {
plugins: [queryPlanSerializer, astSerializer, typeSerializer],
};

export function diffFormatted(
expected: unknown,
received: unknown,
diffOptions?: DiffOptions,
formatOptions: prettyFormat.OptionsReceived = defaultFormatOptions,
) {
const expectedString = prettyFormat(expected, formatOptions);
const receivedString = prettyFormat(received, formatOptions);

return diff(expectedString, receivedString, diffOptions);
}

export function indentLines(
text: string,
depth: number = 1,
indent: string = ' ',
) {
const indentation = indent.repeat(depth);
return text
.split('\n')
.map((line) => indentation + line)
.join('\n');
}

// The corresponding functions in `jest-matcher-utils` call their own `stringify` function,
// and that doesn't allow passing in custom pretty-format plugins.

export function printReceivedFormatted(
value: unknown,
formatOptions: prettyFormat.OptionsReceived = defaultFormatOptions,
): string {
return RECEIVED_COLOR(prettyFormat(value, formatOptions));
}

export function printExpectedFormatted(
value: unknown,
formatOptions: prettyFormat.OptionsReceived = defaultFormatOptions,
): string {
return EXPECTED_COLOR(prettyFormat(value, formatOptions));
}
@@ -0,0 +1,9 @@
import { QueryPlan } from '@apollo/gateway';
import prettyFormat from 'pretty-format';
import { astSerializer, queryPlanSerializer } from './snapshotSerializers';

export function prettyFormatQueryPlan(queryPlan: QueryPlan) {
return prettyFormat(queryPlan, {
plugins: [queryPlanSerializer, astSerializer],
});
}
@@ -1,27 +1,57 @@
import { ASTNode, print, Kind, visit } from 'graphql';
import { Plugin, Config, Refs } from 'pretty-format';
import { QueryPlanSelectionNode, QueryPlanInlineFragmentNode } from '../QueryPlan';
import { SelectionNode as GraphQLJSSelectionNode } from 'graphql';
import { QueryPlanInlineFragmentNode, QueryPlanSelectionNode } from '@apollo/gateway';
import { ASTNode, Kind, print, SelectionNode as GraphQLJSSelectionNode, visit } from 'graphql';
import { Config, NewPlugin, Refs } from 'pretty-format';

export default {
test(value: any) {
// Note that this isn't a reliable test because other objects may also have a `kind` property
// (like query plans!).
// `graphql-js` does have an unexported `isNode` function, but that currently performs the same check
// and doesn't check whether `kind` represents a valid AST node either:
// https://github.com/graphql/graphql-js/blob/998bea680d6e11e1c055a400a887a9539de08f75/src/language/ast.js#L135-L137
// Perhaps we should attempt to contribute an improved `isNode` function.
return value && typeof value.kind === 'string';
},

serialize(
value: ASTNode,
_config: Config,
config: Config,
indentation: string,
_depth: number,
_refs: Refs,
_printer: any,
): string {
return print(remapInlineFragmentNodes(value))
.trim()
.replace(/\n\n/g, '\n')
.replace(/\n/g, '\n' + indentation);
const lines = print(remapInlineFragmentNodes(value)).trim().split('\n');

// Avoid adding newlines for single line results.
if (lines.length === 0) {
return '';
} else if (lines.length === 1) {
return lines[0];
}

return lines.map(line => {
// We re-indent the lines printed from `graphql-js` to respect the passed in `indentation`
// and`config.indent` values.
// This is important because Jest has started to ignore indentation when diffing snapshots,
// and it does this by invoking snapshot serializers with these values set to 0.
// Without re-indenting, every line printed from `graphql-js` would be shown as changed.
// See https://github.com/facebook/jest/pull/9203
const indentationLength = getIndentationLength(line);
const dedentedLine = line.slice(indentationLength);
// `graphql-js` always indents with 2 spaces
const indentationDepth = indentationLength / 2;

return indentation + config.indent.repeat(indentationDepth) + dedentedLine;
}).join(config.spacingOuter);
},
} as Plugin;
} as NewPlugin;

// From https://github.com/facebook/jest/blob/32aaff83f02c347ccd591727544002490fb4ee9a/packages/jest-snapshot/src/dedentLines.ts#L8
function getIndentationLength(line: string): number {
const result = /^( {2})+/.exec(line);
return result === null ? 0 : result[0].length;
};

/**
* This function converts potential InlineFragmentNodes that WE created
Expand Down Expand Up @@ -79,7 +109,7 @@ export function remapInlineFragmentNodes(node: ASTNode): ASTNode {

function remapSelections(
selections: QueryPlanSelectionNode[],
): ReadonlyArray<GraphQLJSSelectionNode> {
): readonly GraphQLJSSelectionNode[] {
return selections.map((selection) => {
switch (selection.kind) {
case Kind.FIELD:
Expand Down
Expand Up @@ -2,14 +2,4 @@ export { default as astSerializer } from './astSerializer';
export { default as selectionSetSerializer } from './selectionSetSerializer';
export { default as typeSerializer } from './typeSerializer';
export { default as graphqlErrorSerializer } from './graphqlErrorSerializer';

declare global {
namespace jest {
interface Expect {
/**
* Adds a module to format application-specific data structures for serialization.
*/
addSnapshotSerializer(serializer: import('pretty-format').Plugin): void;
}
}
}
export { default as queryPlanSerializer } from './queryPlanSerializer';
@@ -1,5 +1,5 @@
import { Config, Plugin, Refs } from 'pretty-format';
import { PlanNode, QueryPlan } from '../QueryPlan';
import { PlanNode, QueryPlan } from '@apollo/gateway';
import { parse, Kind, visit, DocumentNode } from 'graphql';

export default {
Expand Down Expand Up @@ -48,7 +48,6 @@ function printNode(
`Fetch(service: "${node.serviceName}")` +
' {' +
config.spacingOuter +
indentationNext +
(node.requires
? printer(
// this is an array of selections, so we need to make it a proper
Expand All @@ -61,8 +60,7 @@ function printNode(
printer,
) +
' =>' +
config.spacingOuter +
indentationNext
config.spacingOuter
: '') +
printer(
flattenEntitiesField(parse(node.operation)),
Expand Down
6 changes: 3 additions & 3 deletions federation-integration-testsuite-js/tsconfig.json
Expand Up @@ -3,11 +3,11 @@
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"noImplicitAny": false,
"strictNullChecks": false,
"types": ["node", "jest"]
},
"include": ["src/**/*"],
"exclude": ["**/__tests__"],
"references": []
"references": [
{ "path": "../gateway-js" }
]
}