如何使用 sinon 模拟导入的 class 定义

How do I mock an imported class definition with sinon

我似乎无法在我的规范文件中正确模拟导入,我想知道是否有人能看到我遗漏了什么。

这是为我的数据库连接

导出的 class
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();

这是试图使用数据库文件的操作文件。

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;
    },
  },
};

这是我的测试文件。

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);
    });
  });
});

当我 运行 测试时,导入似乎从未被实现文件中的存根替换,我不明白为什么。

有两个注意事项:

  1. 当您从测试文件和 GraphQL 解析器文件中的 database.ts 文件导入 db 时,它们是不同的实例。因此,即使您将 db 实例的方法存根在测试文件中。解析器仍然使用带有原始方法(未存根)的 db 实例。测试存在潜在风险。

  2. 在 GraphQL 解析器中使用依赖项的最佳实践是根据解析器 context 参数传递依赖项(您的案例的 db 实例)。因为它是某种依赖注入,它使代码更容易测试。

例如

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);
    });
  });
});

我们甚至不需要导入 db 并将其存根。我们可以创建一个存根 db 并将其传递给解析器的上下文。

包含覆盖率报告的单元测试结果:

  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 |
----------------|----------|----------|----------|----------|-------------------|