В этой истории я хотел бы рассказать об основах модульного тестирования и о том, как его следует выполнять. Выбранная среда тестирования — Jest.

Итак, давайте погрузимся и посмотрим на функцию, которую мы собираемся тестировать:

//UtilityService.ts
import path from "path";
import fs from "fs";
const getAppVersion = () => {
  const relativeFilePath = "../../downloads/output.json";
  const outputFilePath = path.resolve(__dirname, relativeFilePath);
  const outputFile = fs.readFileSync(outputFilePath, "utf8");
  const output = JSON.parse(String(outputFile));
  return output[0].apkData.versionName;
};
const utilityService = { getAppVersion };
export { utilityService };

Прежде всего, нам нужно решить, что следует тестировать. Я думаю, что мы должны проверить следующее:
1. Проверить, что path.resolve вызывается с правильными входными данными
2. Проверить, что path.resolve возвращает окончательный путь к файлу, который должен быть String
3. Проверить, что fs.readFileSync вызывается с правильными входными данными
4. Проверить, что fs.readFileSync возвращает строковый объект JSON
5. Проверить, что getAppVersion возвращает версию правильно

Чтобы протестировать path.resolve, нам нужно использовать функцию spyOn, которая есть в Jest. Это будет выглядеть примерно так: jest.spyOn(path, "resolve") будет отслеживать вызовы реальной функции. То же самое делается и для fs.readFileSync, который мы используем.

Одна вещь, которую вам, возможно, нужно знать здесь, это то, что мы можем смоделировать возвращаемое значение, используя .mockReturnValue()из смоделированной функции. Что это будет делать, так это имитировать возвращаемое значение функции, не заботясь о внутренних процессах. Это полезно, когда мы не можем обеспечить одинаковые условия выполнения тестов. Примером здесь может быть издевательство над возвратом fs.readFileSync, чтобы дать нам ожидаемое возвращаемое значение. Как вы могли заметить, возвращаемое значение функции сильно зависит от пути к файлу, который был указан как часть параметров. В наших тестах этот файл может присутствовать или отсутствовать в каталоге, что приведет к ошибке при вызове фактической функции. Следовательно, мы можем обойти это, используя spyFsReadFileSync = jest.spyOn(fs,"readFileSync").mockReturnValue(mockReadFileSyncResponse)

Вот проблема, с которой я столкнулся при написании тестов.

Что?! 20 раз? Как и куда я звонил path.resolve 20 раз? Это меня очень озадачило, и я начал безумную отладку, пытаясь понять, что может привести к увеличению или уменьшению этого числа. До меня дошло, что проблема заключается в объявлении переменных вне тестового блока. Первоначально это выглядело примерно так:

...
describe(".getAppVersion", () => {
  let relativeFilePath: any;
  let originalFolderPath: any;
  let filePath: any;
  let mockReadFileSyncResponse: any;
  let spyPathResolve: any;
  let spyFsReadFileSync: any;
  let version: any;
relativeFilePath = "../../downloads/output.json";
  originalFolderPath = path.resolve(__dirname, "../");
  filePath = path.resolve(originalFolderPath, relativeFilePath);
mockReadFileSyncResponse =
    // tslint:disable-next-line:max-line-length
    '[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":10300,"versionName":"1.3.0","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]';
spyPathResolve = jest.spyOn(path, "resolve");
  spyFsReadFileSync = jest
    .spyOn(fs, "readFileSync")
    .mockReturnValue(mockReadFileSyncResponse);
version = utilityService.getAppVersion();
it("calls path.resolve with the correct directory and relative path", () => {
...

Так что, просто используя блок beforeAll, я смог обойти эту конкретную проблему.

Все, что было нужно, это переместить назначения переменных и вызовы функций внутрь блока beforeAll.

...
describe(".getAppVersion", () => {
  let relativeFilePath: any;
  let originalFolderPath: any;
  let filePath: any;
  let mockReadFileSyncResponse: any;
  let spyPathResolve: any;
  let spyFsReadFileSync: any;
  let version: any;
beforeAll(() => {
    relativeFilePath = "../../downloads/output.json";
    originalFolderPath = path.resolve(__dirname, "../");
    filePath = path.resolve(originalFolderPath, relativeFilePath);
mockReadFileSyncResponse =
      // tslint:disable-next-line:max-line-length
      '[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":10300,"versionName":"1.3.0","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]';
spyPathResolve = jest.spyOn(path, "resolve");
    spyFsReadFileSync = jest
      .spyOn(fs, "readFileSync")
      .mockReturnValue(mockReadFileSyncResponse);
version = utilityService.getAppVersion();
  });
it("calls path.resolve with the correct directory and relative path", () => {
...

Вот пример тестового файла, который охватывает все сценарии, упомянутые выше:

//__tests__/UtilityService.ts
import path from "path";
import fs from "fs";
import { utilityService } from "../Utility";
describe(".getAppVersion", () => {
  let relativeFilePath: any;
  let originalFolderPath: any;
  let filePath: any;
  let mockReadFileSyncResponse: any;
  let spyPathResolve: any;
  let spyFsReadFileSync: any;
  let version: any;
beforeAll(() => {
    relativeFilePath = "../../downloads/output.json";
    originalFolderPath = path.resolve(__dirname, "../");
    filePath = path.resolve(originalFolderPath, relativeFilePath);
mockReadFileSyncResponse =
      // tslint:disable-next-line:max-line-length
      '[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":10300,"versionName":"1.3.0","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]';
spyPathResolve = jest.spyOn(path, "resolve");
    spyFsReadFileSync = jest
      .spyOn(fs, "readFileSync")
      .mockReturnValue(mockReadFileSyncResponse);
version = utilityService.getAppVersion();
  });
it("calls path.resolve with the correct directory and relative path", () => {
    expect(spyPathResolve).toHaveBeenCalledTimes(1);
    expect(spyPathResolve).toHaveBeenCalledWith(
      originalFolderPath,
      relativeFilePath
    );
  });
it("gets correct output file path", () => {
    expect(spyPathResolve).toHaveReturnedWith(filePath);
    expect(filePath).toEqual(expect.any(String));
  });
it("calls fs with the correct file path and encoding", () => {
    const encoding = "utf8";
    expect(spyFsReadFileSync).toHaveBeenCalledWith(filePath, encoding);
  });
it("fs returns the stringified json object", () => {
    expect(spyFsReadFileSync).toHaveReturnedWith(mockReadFileSyncResponse);
  });
it("returns the version correctly", () => {
    expect(version).toEqual("1.3.0");
  });
afterAll(() => {
    jest.restoreAllMocks();
  });
});

Первоначально опубликовано на https://helloitsyf.firebaseapp.com 9 апреля 2018 г.