apollo-server-testing: how to type createTestClient() return? (eslint unbound-method error) - node.js

I am trying to setup testing for a node.js / typescript / apollo-server / typegrapql / typeorm project using jest, ts-jest and apollo-server-testing. My test server and client are setup like this:
import 'dotenv/config';
import { ApolloServer } from 'apollo-server-express';
import connectToDatabase from '../database/createConnection';
import buildApolloSchema from './buildApolloSchema';
import { createTestClient } from 'apollo-server-testing';
let server: ApolloServer;
let query: any; // <- Type here??
let mutate: any; // <- Type here??
export { server, query, mutate };
const setupTestEnvironment = async () => {
await connectToDatabase();
const schema = await buildApolloSchema();
if (schema) {
const server = new ApolloServer({
schema,
context: ({ req, res }) => ({
req,
res,
}),
});
const testClient = createTestClient(server);
query = testClient.query; // <- Error here: Avoid referencing unbound methods...
mutate = testClient.mutate; // <- Error here: Avoid referencing unbound methods...
}
};
Any idea of how can I type variables query and mutate to avoid implicit any errors? There seems to be some type inference but I don't seem to be able to import types from the apollo-server-testing package and so far I have left them as any (quickfix suggests never).
Right now the assignments at the end are also throwing the error Avoid referencing unbound methods which may cause unintentional scoping of this.
I have tried all sorts of destructuring assignments (including const {query, mutate} = createTestClient(server) as it appears in the docs) and I get the same error.
Any clues?

You should be able to type query and mutate with the types from apollo-server-testing like this:
import {
ApolloServerTestClient,
createTestClient,
} from "apollo-server-testing";
let server: ApolloServer;
let query: ApolloServerTestClient["query"];
let mutate: ApolloServerTestClient["mutate"];
The Avoid referencing unbound methods which may cause unintentional scoping of this. error is related to the way the types of createTestClient are defined. Since that's apollo-server-testing code, that's hard for you to resolve. Consider disabling the rule for these lines:
const testClient = createTestClient(server);
// eslint-disable-next-line #typescript-eslint/unbound-method
query = testClient.query;
// eslint-disable-next-line #typescript-eslint/unbound-method
mutate = testClient.mutate;
The root of the issue seems to be this definition in apollo-server-testing:
export interface ApolloServerTestClient {
query<TData = any, TVariables = Record<string, any>>(query: Query<TVariables>): Promise<GraphQLResponse<TData>>;
mutate<TData = any, TVariables = Record<string, any>>(mutation: Mutation<TVariables>): Promise<GraphQLResponse<TData>>;
}
If the definition would be like this, the rule shouldn't trigger:
export interface ApolloServerTestClient {
query: <TData = any, TVariables = Record<string, any>>(query: Query<TVariables>) => Promise<GraphQLResponse<TData>>;
mutate: <TData = any, TVariables = Record<string, any>>(mutation: Mutation<TVariables>) => Promise<GraphQLResponse<TData>>;
}
Edit: Related to issue 4724 in apollo-server-testing

Related

I can't understand how do 'global`s work in TypeScript/NodeJS and what is their difference?

I am reading a code like below:
import { MongoMemoryServer } from "mongodb-memory-server";
import mongoose from "mongoose";
import request from "supertest";
import { app } from "../app";
declare global {
function signin(): Promise<string[]>;
}
let mongo: any;
beforeAll(async () => {
process.env.JWT_KEY = "asdfasdf";
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
const mongo = await MongoMemoryServer.create();
const mongoUri = mongo.getUri();
await mongoose.connect(mongoUri, {});
});
beforeEach(async () => {
const collections = await mongoose.connection.db.collections();
for (let collection of collections) {
await collection.deleteMany({});
}
});
afterAll(async () => {
if (mongo) {
await mongo.stop();
}
await mongoose.connection.close();
});
global.signin = async () => {
const email = "test#test.com";
const password = "password";
const response = await request(app)
.post("/api/users/signup")
.send({
email,
password,
})
.expect(201);
const cookie = response.get("Set-Cookie");
return cookie;
};
I can't understand the purpose of global.signin function and how does it work? I guess it has something to do with Jest but as long as I know the Jest codes should be inside the __test__ folder with the same file name and .test.ts extension. But the above function is defined and used inside the setup.ts file in the root of the application.
I also see some codes like following:
declare global {
namespace Express {
interface Request {
currentUser?: UserPayload;
}
}
}
In some .ts files of the project as well that I am not sure are these global variables the same as the other globals I mentioned above or these are different things? I am interested to know how this global variables work as well?
The piece of code you shared is making use of global augmentation https://www.typescriptlang.org/docs/handbook/declaration-merging.html#global-augmentation
// Hint typescript that your global object will have a custom signin function
declare global {
function signin(): Promise<string[]>;
}
// Assign value to global.signin
global.signin = async () => { /* implementation */ };
Likely one or multiple modules ("mongoose", "supertest", "../app") imported by the test file is using global.signin (or window.signin) at some point (or maybe one of their nested imports is => look for "signin(" in the project). Thus for testing purposes, global.signin needed to be mocked. However just adding global.signin = something would raise a typescript error, because signin is not a standard global variable. This is where declare global comes into play. It hints typescript that in your particular context, a signin function is expected to exist in global scope.
JavaScript/TypeScript running in node will try to resolve anything it can't find in the current local scope in global (the same way a browser would look in window). Any function or variable you can access globally (e.g. setTimeout()), can also be accessed with global. as prefix. It just makes it explicit.
What happens in your code are two things:
declare global {
function signin(): Promise<string[]>;
}
Here it tells typescript's type system that the global object also has a function called signin. This part is not required but it makes sense required for typescript to allow you to access / define that function, in JavaScript you simply define it.
https://www.typescriptlang.org/docs/handbook/declaration-merging.html has some details how declare works.
global.signin = async () => {
// code ...
};
And here it is actually added to the global object.
In JavaScript non strict mode you could even write (notice the lack of var/let/const/global.)
signin = async () => {
// code ...
};
I don't see signin getting used anywhere in that code so the reason for it is unclear to me. As long as the file that defines it gets loaded you can call the function simply by referring to it as signin(). The global. is added implicitly.
The purpose of
declare global {
namespace Express {
interface Request {
currentUser?: UserPayload;
}
}
}
is more practical, in express you may want to add properties to your requests that get added by middleware. By declaring that the Express Request has a property called currentUser you get to do
app.get((req, res) => {
const user: UserPayload = req.currentUser
...
})
without typescript complaining about an unknown property.
More on that for example https://blog.logrocket.com/extend-express-request-object-typescript/

How to separately type Fastify decorators in different plugins?

I'm having trouble typing decorators in two plugins (scopes):
import Fastify, { FastifyInstance } from 'fastify'
const fastify = Fastify()
// scope A
fastify.register((instance) => {
instance.decorate('utilA', 'AAA')
instance.get('/', (req, reply) => {
const data = instance.utilA // Property 'utilA' does not exist on type 'FastifyInstance<...>'
reply.send(data)
})
}, { prefix: '/A/' })
// scope B
fastify.register((instance) => {
instance.decorate('utilB', () => 'BBB')
instance.get('/', (req, reply) => {
const data = instance.utilB() // Property 'utilB' does not exist on type 'FastifyInstance<...>'
reply.send(data)
})
}, { prefix: '/B/' })
I can define types globally:
declare module 'fastify' {
interface FastifyInstance {
utilA: string
utilB: () => string
}
}
And it solves the compiler errors. But since utilA and utilB are defined globally compiler allows me to use utilB in the scope A and vice versa.
Is there a way to do the same but independently per scope?
No I do not think this is possible with the declare module '' syntax due to the way that TypeScript works (although someone smarter than me might be able to figure out how).
One thing you can do is create a sub type of the FastifyInstance with your new properties (they will need to be optional or TypeScript might get mad at you depending on your tsconfig settings) and type your plugins fastify instance with that type. For example:
// Scope A
import { FastifyInstance } from 'fastify';
type MyPluginsFastifyInstance = FastifyInstance & { myThing?: MyType; }
export default async (fastify: MyPluginFastifyInstance) => {
... // Implementation.
fastify.myThing // TypeScript is happy :)
}
// Scope B
export default async (fastify) => {
fastify.myThing // TypeScript is mad at you >:(
}

node ts-jest spyOn method does not match overload

I'm trying to use Jest in conjunction with ts-jest to write unit tests for a nodeJS server. I have something set up very similar to below:
impl.ts
export const dependency = () => {}
index.ts
import { dependency } from './impl.ts';
export { dependency };
consumer.ts
import { dependency } from '../impl' <- importing from index.ts
export const consumer = () => {
try {
dependecy();
return true;
} catch (error) {
return false;
}
}
consumer.test.ts
import * as dependencies from '../impl'
import { consumer } from './consumer'
const mockDependency = jest.spyOn(dependencies, 'depenedncy');
describe('function consumer', function () {
beforeEach(function () {
mockDependency.mockReturnValueOnce(false);
});
test('should return true', () => {});
})
This is just toy code, but the actual export / import / test files follow a similar structure. I'm getting typescript errors along these lines:
TS2769: No overload matches this call.
Specifically, that the method being spied on is not part of the overload of the import for dependencies, so I can't stub it out. I am doing literally the same thing in a different test file and it has no issues. Anyone know how to resolve the typing issue?
The issue turned out to be in the typing of the dependency function itself. The return value typing was incorrect and that was what was resulting in the Typescript error. Essentially I had this:
export const dependency: Handler = () => {
return () => {} <- is of type Handler
}
rather than this
export const dependency = (): Handler => {
return () => {} <- is of type Handler
}
Stupid mistake, hope it helps someone else in the future. My take away is that if you have a type error that doesn't make sense make sure you check the typing of all variables involved.

Is there a way to change the mocked value of a required dependency?

I'm facing a problem I'm not able to resolve on my own, maybe some of you faced the same problem.
Let me show you what I'm trying to do, here is the mock:
let mockConfig = {name: 'dude'};
jest.mock('../../../configManager', () => mockConfig);
configManager is a dependency of the function I'm trying to test.
It works well but I want to change the returning object of configManager in another test so the tested function behaves differently.
Let me show you, here is the function I'm testing:
const config = require('../../../configManager');
module.exports = () => {
if (config.name === 'dude') {
do stuff;
}
if (config.name === 'dudette') {
do something else;
}
So, typically, I want to change the config.name to 'dudette' to be able to test the second part of my function.
Naturally, when I want to do this with an imported function, I just do:
let mockJsonQueryResult = { value: 'stuff' };
jest.mock('json-query', () => jest.fn(() => mockJsonQueryResult));
and then in the test, I directly set another value to mockJsonQueryResult:
mockJsonQueryResult = { value: 'hotterStuff' };
But I don't find any way of doing this with a dependency that returns an object, with a dependency returning a function, no problem.
Is there even any way of doing this?
Thanks in advance!
Edit: this is not the same as how to change jest mock function return value in each test? as #Dor Shinar suggested because his problem is to mock a function, even if it is inside a returning object it is still a function, I just want to change a value inside the returned object.
So, I found a solution I'm not completely satisfied with but it works:
I simply set the original full object and then for my tests, change the value of specific properties by setting them directly before calling the function I want to test.
example:
let mockConfig = { person: { name: 'dude', origin: {country: 'France'} } };
jest.mock('../../../configManager', () => mockConfig);
mockConfig.person = {};
mockConfig.person.name = 'dudette';
You don't need to mock the module at all.
If your module export is just an object with property values then just change the properties as needed.
Here is a simple working example to demonstrate:
configManager.js
module.exports = {
name: 'original'
}
code.js
const config = require('./configManager');
module.exports = () => `name: ${config.name}`;
code.test.js
const config = require('./configManager');
const func = require('./code');
test('func', () => {
expect(func()).toBe('name: original'); // Success!
config.name = 'dude';
expect(func()).toBe('name: dude'); // Success!
config.name = 'dudette';
expect(func()).toBe('name: dudette'); // Success!
})
Details
A module binding can't be directly changed to something else:
const config = require('./configManager');
config = { name: 'mock' }; // <= this doesn't work
...but you can change the properties of an object representing a module binding:
const config = require('./configManager');
config.name = 'mock'; // <= this works!
...and any code using the module will automatically see the changes.

Typescript - Add types for 'promisifed' methods

I have a very simple module that wraps the "fs" module.
This module simply "promisifies" all of "fs" methods and exports the new object:
fsWrapper.ts
import Promise from "bluebird";
import * as fs from "fs";
const fsWrapper = Promise.promisifyAll(fs);
export = fsWrapper;
Now I can use this wrapper instead of "promisifiying" the "fs" module inside every caller module, like so:
main.ts
import fsWrapper from "./fsWrapper";
function test(): void {
fsWrapper.readFileAsync("tst.txt", "utf8")
.then((data: Buffer) => {
console.log("data:", data.toString());
})
}
This of course doesn't work with typescript since "fs" doesn't hold the readFileAsync method and I receive a compiler error.
While searching of ways of properly typings this wrapper, I found the following typescript issue.
Using its approach, I can create my own fsWrapper.d.ts, in which I need to manually add the *Async methods, similar to the following:
fsWrapper.d.ts
import Promise from "bluebird";
declare module "fs" {
export function readFileAsync(path: string, options: { encoding?: string | null; flag?: string; } | string | undefined | null) : Promise<Buffer>;
...
}
The problem here is:
Manually adding all the needed methods is tedious and error prone.
If these methods will change in future versions, I will have no idea as my code will keep compiling and I will suffer runtime exceptions.
I know that util.promisify is properly typed, and therefore thought of somehow extending "fs" with these new methods, similar to the following:
fsWrapperTest.ts
import * as fs from "fs";
import { promisify } from "util";
let fsWrapper = fs;
fsWrapper.readFileAsync = promisify(fs.readFile);
export = fsWrapper;
But this outputs an error that indicates that I can't extend existing modules:
error TS2551: Property 'readFileAsync' does not exist on type 'typeof "fs"'. Did you mean 'readFileSync'?
Is there any way for me to properly type this wrapper while keeping "up-to-date" typings and working with Intellisense?
Edit:
Just to clarify, I understand how to create an object that will have all "fs" methods promisified (that was the case with the original fsWrapper.ts above).
What i'm struggling is typing it properly for Intellisense usage.
For example, running the following:
import * as fs from "fs";
import { promisify } from "util";
let fsWrapper = Object.keys(fs).reduce((p: typeof fs, v: string) => { p[v] = promisify(fs[v]); return p }, {})
Will give me a fsWrapper: {} typing for the object.
I would like to have all of "fs" methods + the new 'promisifed' methods as its type.
EDIT - selected solution
I ended up using the declare module 'fs' approach by extending the 'fs' module with all the *Async methods that I was using in my code.
Not ideal but it seems that there is no better alternative..
If you are using TypeScript 3.0 or newer, the improvements to mapped types and parameter tuples should make this possible. For example:
type UnpackedPromise<T> = T extends Promise<infer U> ? U : T
type GenericFunction<TS extends any[], R> = (...args: TS) => R
type Promisify<T> = {
[K in keyof T]: T[K] extends GenericFunction<infer TS, infer R>
? (...args: TS) => Promise<UnpackedPromise<R>>
: never
}
That creates a type for unwrapping a promise, a type for a generic function, and then an inferred mapped type for the promisified version that maps each key to a new value that is the promisified version of the previous function. To use that type (playground link):
const original = {
one: () => 1,
two: () => 2,
add: (a: number, b: number) => a + b
}
const promisified = original as unknown as Promisify<typeof original>
promisified.one()
promisified.add(1, 2)
And for your example, here's how that would be used:
import * as fs from "fs";
import { promisify } from "util";
let fsWrapper = Object
.keys(fs)
.reduce((p: typeof fs, v: string) => { p[v] = promisify(fs[v]); return p }, {}) as unknown as Promisify<typeof fs>
Notice that you are casting to unknown and then to Promisify<*> - this is because by default it will consider the mapped type to be possibly a mistake.
Node.js >= v10.x started to provide already promisified functions.
You can use them via require("fs").promises or require("fs/promises") (>= v14.x)
https://nodejs.org/docs/latest-v10.x/api/fs.html#fs_fs_promises_api
https://nodejs.org/docs/latest-v14.x/api/fs.html#fs_fs_promises_api
I use
const fs = { ...require("fs"), ...require("fs/promises") };
There is an existing thread on github exactly for this, see https://github.com/Microsoft/TypeScript/issues/8685
So, you can do something like:
import { Promise } from 'bluebird';
import * as fs from "fs";
declare module "fs" {
interface fs {
[method: string]: any;
}
}
const fsWrapper = Promise.promisifyAll(fs);
export = fsWrapper;
This is already very similar to what you were doing to get past the issue of calling the methods.
Please note that as far as I know the only way to allow full Intellisense is to do module augmentation as described in the typescript documentation https://www.typescriptlang.org/docs/handbook/declaration-merging.html
But that implies you manually writing the interface for the new methods generated by the promisified inside that module structure above. i.e.
declare module "fs" {
interface fs {
readFileAsync(... ),
writeFileAsyng(... ),
etc ...
}
}

Resources