Reputation: 348
I can't seem to properly mock an import in my spec file and I am wondering if anyone can see what I am missing.
Here is the exported class for my Database connection
import Knex from 'knex';
import { merge } from 'lodash';
import knexfile from '../knexfile';
class Database {
private knexInstance: Knex;
private config: object;
connect(options = {}): void {
if (this.knexInstance) {
return;
}
this.config = merge({}, knexfile, options);
this.knexInstance = Knex(this.config);
}
get query(): Knex {
if (!this.knexInstance) {
this.connect();
}
return this.knexInstance;
}
close(done): void {
if (!this.knexInstance) {
done();
return;
}
this.knexInstance.destroy(done);
}
}
export default new Database();
Here is the action file that is trying to use the database file.
import db from '../../database';
const tableName = 'attempts';
export const typeDef = `
extend type Query {
attempt(id: String): Attempt!
}
extend type Mutation {
createAttempt(questionId: String!, attemptId: String!, choiceId: String): Attempt
}
type Attempt {
id: String!
correctanswers: Int!
userid: String!
examid: String!
}
`;
export const resolvers = {
Query: {
attempt(_, { id = '' }) {
return db
.query(tableName)
.where({ id })
.first();
},
},
Mutation: {
async createAttempt(root, args) {
const [answer] = await db
.query(tableName)
.insert(args)
.returning('*');
return answer;
},
},
};
And here is my test file.
import { createSandbox } from 'sinon';
import { resolvers } from './answer';
import db from '../../database';
import * as should from 'should';
const sandbox = createSandbox();
describe('Answer', () => {
afterEach(() => sandbox.restore());
describe('Query Answer', () => {
it('should return answer by id', async () => {
const expected = { id: 'xxx' };
const firstSpy = sandbox.fake.resolves(expected);
const whereSpy = sandbox.fake.resolves({
first: firstSpy,
});
// This stub never seems to get called. It doesn't look like the import is ever being replaced with the stub in the implementation file.
const querySpy = sandbox.stub(db, 'query').callsFake(() => {
return Promise.resolve({
where: whereSpy,
});
});
const inputId = '100';
const result = await resolvers.Query.answer(null, { id: inputId });
sandbox.assert.calledOnce(querySpy);
sandbox.assert.calledOnce(whereSpy);
sandbox.assert.calledOnce(firstSpy);
result.should.deepEqual(expected);
});
});
});
When I run the tests it doesn't look like the import is ever being replaced with the stub in the implementation file and I don't see why.
Upvotes: 1
Views: 1323
Reputation: 102467
There are two caveats:
They are different instances when you import the db
from database.ts
file in the test file and the GraphQL resolver file. So, even if you stub the methods of db
instance in the test file. The resolver still uses the db
instance with the original methods(not stubbed). There are potential risks for testing.
The best practice for using dependencies within the GraphQL resolver is to pass the dependencies(the db
instance for your case) according to the resolver context
argument. Because it's some kind of Dependency Injection, it makes the code easier to test.
E.g.
answer.ts
:
const tableName = "attempts";
export const typeDef = `
extend type Query {
attempt(id: String): Attempt!
}
extend type Mutation {
createAttempt(questionId: String!, attemptId: String!, choiceId: String): Attempt
}
type Attempt {
id: String!
correctanswers: Int!
userid: String!
examid: String!
}
`;
export const resolvers = {
Query: {
attempt(_, { id = "" }, { db }) {
return db
.query(tableName)
.where({ id })
.first();
},
},
Mutation: {
async createAttempt(root, args, { db }) {
const [answer] = await db
.query(tableName)
.insert(args)
.returning("*");
return answer;
},
},
};
anwser.test.ts
:
import sinon from "sinon";
import { resolvers } from "./answer";
import { expect } from "chai";
describe("Answer", () => {
describe("Query Answer", () => {
it("should return answer by id", async () => {
const expected = { id: "xxx" };
const inputId = "100";
const knexInstanceStub = {
query: sinon.stub().returnsThis(),
where: sinon.stub().returnsThis(),
first: sinon.stub().resolves(expected),
};
const result = await resolvers.Query.attempt(null, { id: inputId }, { db: knexInstanceStub });
sinon.assert.calledOnce(knexInstanceStub.query);
sinon.assert.calledOnce(knexInstanceStub.where);
sinon.assert.calledOnce(knexInstanceStub.first);
expect(result).to.be.deep.eq(expected);
});
});
});
We don't even need to import db
and stub it. We can create a stub db
and pass it to the context of resolver.
Unit test results with coverage report:
Answer
Query Answer
✓ should return answer by id
1 passing (11ms)
----------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
----------------|----------|----------|----------|----------|-------------------|
All files | 84.62 | 33.33 | 80 | 86.36 | |
answer.test.ts | 100 | 100 | 100 | 100 | |
answer.ts | 60 | 33.33 | 50 | 62.5 | 30,31,36 |
----------------|----------|----------|----------|----------|-------------------|
Upvotes: 1