(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 Food
par 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:
Food
dans getMeal
+ utilisez l'injection de dépendance dans formatMeal
Food
dans l'application entière)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 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!
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({ ... });
...
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.
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!");
});
});
On peut aussi envelopper la fonction à tester.
Au lieu de
import getMeal from "../src/meal";
utilisation
const getMeal = () => require("../src/meal").default();
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
.
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!"
);
});
});