Запустите свои тесты и прочитайте составленное описание для вывода
- Можете ли вы легко прочитать и понять это?
- Можете ли вы использовать его в качестве объяснения другому человеку, что делает компонент?
- Можете ли вы использовать его в качестве спецификации для этого компонента/модуля?
Если нет, внесите изменения.
Когда я пишу тесты, я стараюсь, чтобы результат был как можно ближе к реальным спецификациям компонентов. И выражено на доменном языке. Чтобы любой, кто читал это, мог понять задачу, связанную с предметной областью, которую решает компонент.
Он должен напечатать что-то вроде этого (пример вывода vitest):
✓ ui/components/PriceInput.test.js (7) ✓ PriceInput: a component for editing price (7) ✓ when focused out (3) ✓ and input is valid (2) ✓ show formatted value: (2) ✓ - round number to 2nd digit ✓ - trim whitespace ✓ and input is empty (1) ✓ - show 0.0 ✓ when focused in (2) ✓ - show unformatted value ✓ - select all ✓ when ESC pressed (1) ✓ - restore previous value ✓ when hovered (1) ✓ - show tooltip with unformatted value
И тестовый файл выглядит так::
import { mount } from "@vue/test-utils"; import { describe, it, expect } from "vitest"; import PriceInput from "./PriceInput.vue"; const setup = async ({ price } = {}) => { const priceInput = mount(PriceInput, { props: { price } }); const getInputEl = () => priceInput.find("input").element; const getInputText = () => getInputEl().value; const focusIn = async () => priceInput.find("input").trigger("focusin"); const focusOut = async () => priceInput.find("input").trigger("focusout"); const type = async (val) => priceInput.find("input").setValue(val); const pressEscape = async () => priceInput.find("input").trigger("keyup.esc"); return { getInputEl, priceInput, getInputText, focusIn, focusOut, type, pressEscape, }; }; describe("PriceInput: a component for editing price", () => { describe("when focused out", () => { describe("and input is valid", () => { describe("show formatted value:", () => { it("- round number to 2nd digit", async () => { const { getInputText } = await setup({ price: "12.345" }); expect(getInputText()).toBe("12.35"); }); it("- trim whitespace", async () => { const { getInputText } = await setup({ price: " 12.345 " }); expect(getInputText()).toBe("12.35"); }); }); }); describe("and input is empty", () => { it("- show 0.0", async () => { const { focusOut, getInputText } = await setup({ price: "" }); await focusOut(); expect(getInputText()).toBe("0.0"); }); }); }); describe("when focused in", () => { it("- show unformatted value", async () => { const { focusIn, getInputText } = await setup({ price: "12.345" }); await focusIn(); expect(getInputText()).toBe("12.345"); }); it("- select all", async () => { const { focusIn, getInputEl } = await setup({ price: "12.345" }); await focusIn(); expect(getInputEl().selectionEnd).toBe(6); // would be much better with: getSelectedText() }); }); describe("when ESC pressed", () => { it("- restore previous value", async () => { const { type, getInputText, pressEscape } = await setup({ price: "12.345", }); await type("234.56"); await pressEscape(); expect(getInputText()).toBe("12.35"); }); }); describe("when hovered", () => { it("- show tooltip with unformatted value", async () => { const { getInputEl } = await setup({ price: "12.345" }); expect(getInputEl().title).toBe("12.345"); // would be much better with: getInputTitle() }); }); });
Как я предпочитаю это делать:
1. Сохраняйте спецификацию компонента в тестовом файле
- Он сообщает назначение компонента в проекте.
- Он сообщает вам желаемое поведение компонента.
- Если вы мигрируете свой компонент (или он сломан) — вы можете использовать эти спецификации, чтобы восстановить его поведение.
2. Не используйте перед каждым()
beforeEach() похожа на использование глобальных переменных в вашем коде — негибкая, часто запутанная и может привести к хрупким и жестким тестам.
2.1 Вместо этого используйте параметризованную фабрику
Назовем его setup(). Или любое имя, которое вы предпочитаете.
2.1.1 Не используйте низкоуровневый API тестовой среды в тестовых примерах
Абстрагироваться от API тестовой/фиктивной инфраструктуры, экспортируя собственный APIдлявзаимодействия/мока/чтения значений из настройка().
Это дает вам:
- более читаемые тесты
меньше строк, меньше шаблонов - единая точка отказа
с точки зрения реализации имитации действий пользователя, конфигурации начального состояния, имитации поведения - тестовые примеры, не зависящие от среды тестирования
все, что вам нужно для миграции на другую платформу, — это переписать setup() и, возможно, изменить библиотеку утверждений - СУХОЙ
Напишите какой-нибудь сложный код для выполнения, например, симуляции раскрывающегося списка только один раз для всех тестовых случаев
2.1.2 Используйте заводские параметры, чтобы получить нужное начальное состояние/поведение
Например:
const setup = async ({ price, isHovered = false, emulateError = false } = {}) => { const priceInput = mount(PriceInput, { props: { price } }); if (isHovered) { await priceInput.trigger('hover'); } if (emulateError) { priceInput.vm.$emit = () => { throw new Error('test error'); }; } const getInputText = () => getInputEl().value; ... return { getInputText, ... }; };
А потом:
describe('when error happens', () => { it('show error message', async () => { const { ... } = await setup({ emulateError: true }); expect(...).toBe('test error'); }); });
Посмотрите, как мы используем параметр emulateError: true для изменения поведения компонента:
- Мы сделали его читабельным
- Мы можем повторно использовать его всякий раз, когда нам это нужно.
Вот и все!