web-dev-qa-db-fra.com

Comment créer des dépendances avec Jest _per test_?

(Voici la repro minimale complète: https://github.com/magicmark/jest_question )

Étant donné l'application suivante:

src/food.js

const Food = {
  carbs: "rice",
  veg: "green beans",
  type: "dinner"
};

export default Food;

src/food.js

import Food from "./food";

function formatMeal() {
  const { carbs, veg, type } = Food;

  if (type === "dinner") {
    return `Good evening. Dinner is ${veg} and ${carbs}. Yum!`;
  } else if (type === "breakfast") {
    return `Good morning. Breakfast is ${veg} and ${carbs}. Yum!`;
  } else {
    return "No soup for you!";
  }
}

export default function getMeal() {
  const meal = formatMeal();

  return meal;
}

J'ai le test suivant:

__tests __/meal_test.js

import getMeal from "../src/meal";

describe("meal tests", () => {
  beforeEach(() => {
    jest.resetModules();
  });

  it("should print dinner", () => {
    expect(getMeal()).toBe(
      "Good evening. Dinner is green beans and rice. Yum!"
    );
  });

  it("should print breakfast (mocked)", () => {
    jest.doMock("../src/food", () => ({
      type: "breakfast",
      veg: "avocado",
      carbs: "toast"
    }));

    // prints out the newly mocked food!
    console.log(require("../src/food"));

    // ...but we didn't mock it in time, so this fails!
    expect(getMeal()).toBe("Good morning. Breakfast is avocado and toast. Yum!");
  });
});

Comment puis-je simuler correctement Foodpar test? En d'autres termes, je souhaite uniquement appliquer le modèle pour le cas de test "should print breakfast (mocked)"

J'aimerais également ne pas modifier idéalement le code source de l'application (bien que peut-être qu'avoir Food une fonction renvoyant un objet serait acceptable, mais que cela ne fonctionne toujours pas.)

Choses que j'ai déjà essayées:

  • insérez l'objet Food dans getMeal + utilisez l'injection de dépendance dans formatMeal
    • (L'intérêt de cette approche IRL est que nous ne voulons pas enfiler Food dans l'application entière)
  • manual mock + jest.mock() - il est possible que la réponse soit ici quelque part, mais il est difficile de contrôler la valeur ici et de la réinitialiser par test en raison de l'étrangeté du temps d'importation
    • Utiliser jest.mock() en haut le remplacerait pour chaque test élémentaire, et je ne vois pas comment changer ou réinitialiser la valeur de Food par test. 

Merci!

16
Mark

Réponse courte

Utilisez require pour récupérer un nouveau module dans chaque fonction de test après la configuration des simulacres.

it("should print breakfast (mocked)", () => {
    jest.doMock(...);
    const getMeal = require("../src/meal").default;

    ...
});

ou 

Transformez Food en une fonction et appelez jest.mock dans la portée du module.

import getMeal from "../src/meal";
import food from "../src/food";

jest.mock("../src/food");
food.mockReturnValue({ ... });

...

Longue réponse

Il y a un extrait dans Jest manual qui se lit comme suit:

Remarque: pour que la simulation soit correcte, Jest a besoin que jest.mock ('nom_module') se trouve dans la même portée que l'instruction require/import.

Le même manuel indique également:

Si vous utilisez des importations de modules ES, vous serez normalement enclin à placer vos instructions d'importation en haut du fichier de test. Mais souvent, vous devez demander à Jest d'utiliser une maquette avant que les modules ne l'utilisent. Pour cette raison, Jest lève automatiquement les appels jest.mock en haut du module (avant toute importation). 

Les importations ES6 sont résolues dans l'étendue du module avant l'exécution de toute fonction de test. Ainsi, pour que les simulacres soient appliqués, ils doivent être déclarés en dehors des fonctions de test et avant que tous les modules soient importés. Le plugin Babel de Jest "remontera" les instructions jest.mock au début du fichier afin qu'elles soient exécutées avant toute importation. Notez que jest.doMock est délibérément non levé .

On peut étudier le code généré en jetant un coup d'œil dans le répertoire de cache de Jest (lancez jest --showConfig pour connaître l'emplacement).

Le module food de l'exemple est difficile à simuler car il s'agit d'un littéral d'objet et non d'une fonction. Le moyen le plus simple consiste à forcer le rechargement du module chaque fois que la valeur doit être modifiée.

Option 1a: n'utilisez pas de modules ES6 issus de tests

Les instructions d'importation ES6 doivent avoir une étendue de module. Toutefois, le "bon vieux" require n'a pas cette limitation et peut être appelé à partir du domaine d'application d'une méthode de test.

describe("meal tests", () => {
  beforeEach(() => {
    jest.resetModules();
  });

  it("should print dinner", () => {
    const getMeal = require("../src/meal").default;

    expect(getMeal()).toBe(
      "Good evening. Dinner is green beans and rice. Yum!"
    );
  });

  it("should print breakfast (mocked)", () => {
    jest.doMock("../src/food", () => ({
      type: "breakfast",
      veg: "avocado",
      carbs: "toast"
    }));

    const getMeal = require("../src/meal").default;

    // ...this works now
    expect(getMeal()).toBe("Good morning. Breakfast is avocado and toast. Yum!");
  });
});

Option 1b: Recharger le module à chaque appel

On peut aussi envelopper la fonction à tester.

Au lieu de

import getMeal from "../src/meal";

utilisation

const getMeal = () => require("../src/meal").default();

Option 2: enregistrez la maquette et appelez les fonctions réelles par défaut

Si le module food exposait une fonction et non un littéral, il pourrait être simulé. L'instance fictive est modifiable et peut être modifiée d'un test à l'autre.

src/food.js

const Food = {
  carbs: "rice",
  veg: "green beans",
  type: "dinner"
};

export default function() { return Food; }

src/meal.js

import getFood from "./food";

function formatMeal() {
  const { carbs, veg, type } = getFood();

  if (type === "dinner") {
    return `Good evening. Dinner is ${veg} and ${carbs}. Yum!`;
  } else if (type === "breakfast") {
    return `Good morning. Breakfast is ${veg} and ${carbs}. Yum!`;
  } else {
    return "No soup for you!";
  }
}

export default function getMeal() {
  const meal = formatMeal();

  return meal;
}

__tests __/meal_test.js

import getMeal from "../src/meal";
import food from "../src/food";

jest.mock("../src/food");

const realFood = jest.requireActual("../src/food").default;    
food.mockImplementation(realFood);

describe("meal tests", () => {
  beforeEach(() => {
    jest.resetModules();
  });

  it("should print dinner", () => {
    expect(getMeal()).toBe(
      "Good evening. Dinner is green beans and rice. Yum!"
    );
  });

  it("should print breakfast (mocked)", () => {
    food.mockReturnValueOnce({ 
        type: "breakfast",
        veg: "avocado",
        carbs: "toast"
    });

    // ...this works now
    expect(getMeal()).toBe("Good morning. Breakfast is avocado and toast. Yum!");
  });
});

Bien sûr, il existe d'autres options telles que la scission du test en deux modules, l'un configurant un modèle et l'autre utilisant un module réel, ou renvoyant un objet mutable à la place d'une exportation par défaut pour le module food afin de chaque test, puis réinitialiser manuellement dans beforeEach.

8
anttix

La réponse @anttix est préférable, mais voici un autre angle qui pourrait être utile dans d'autres scénarios.

babel-plugin-rewire permet à import Food from "./food"; d'être écrasé par le test.

D'abord, yarn add babel-plugin-rewire

babel.config.js

const presets = [
  [
    "@babel/env",
    {
      targets: {
        node: 'current',
      },
    },
  ],
];

const plugins = [ 
  "babel-plugin-rewire"
];

module.exports = { presets, plugins };

meal_test.js

import getMeal from "../src/meal";
import Food from "../src/food";
import { __RewireAPI__ as RewireAPI } from "../src/meal";

describe("meal tests", () => {
  // beforeEach(() => {
  //   jest.resetModules();
  // });
  afterEach(() => {
    RewireAPI.__Rewire__('Food', Food)
  });

  it("should print dinner", () => {
    expect(getMeal()).toBe(
      "Good evening. Dinner is green beans and rice. Yum!"
    );
  });

  it("should print breakfast (mocked)", () => {
    const mockFood = {
      type: "breakfast",
      veg: "avocado",
      carbs: "toast"
    };
    RewireAPI.__Rewire__('Food', mockFood)

    expect(getMeal()).toBe("Good morning. Breakfast is avocado and toast. Yum!");
  });

  it("should print dinner #2", () => {
    expect(getMeal()).toBe(
      "Good evening. Dinner is green beans and rice. Yum!"
    );
  });
});
0
Richard Matsen