Запустите свои тесты и прочитайте составленное описание для вывода

  • Можете ли вы легко прочитать и понять это?
  • Можете ли вы использовать его в качестве объяснения другому человеку, что делает компонент?
  • Можете ли вы использовать его в качестве спецификации для этого компонента/модуля?

Если нет, внесите изменения.

Когда я пишу тесты, я стараюсь, чтобы результат был как можно ближе к реальным спецификациям компонентов. И выражено на доменном языке. Чтобы любой, кто читал это, мог понять задачу, связанную с предметной областью, которую решает компонент.

Он должен напечатать что-то вроде этого (пример вывода 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 для изменения поведения компонента:

  • Мы сделали его читабельным
  • Мы можем повторно использовать его всякий раз, когда нам это нужно.

Вот и все!