Взгляд на популярную Testing Library с Nightwatch — и многое другое

Nightwatch + Testing Library
Nightwatch + Testing Library

Мы создадим подробный пример проекта React с Vite, а затем воспользуемся Nightwatch и Testing Library для тестирования этих компонентов. Мы используем Complex example, доступный в документах React Testing Library, написанный с помощью Jest.

В этом уроке мы рассмотрим следующее:

  1. Установим новый проект React с помощью Vite, который также используется внутри Nightwatch для компонентного тестирования

  2. Установим и сконфигурируем Nightwatch и Testing Library

  3. Сымитируем API запросы с помощью плагина @nightwatch/api-testingН

  4. Напишем комплексный React-компонентный тест, используя Nightwatch и Testing Library.

Шаг 0. Создание нового проекта

Для начала мы создадим новый проект с Vite:

npm init vite@latest

Выберите React и JavaScript при появлении запроса. Это создаст новый проект с React и JavaScript.

Шаг 1. Установка Nightwatch и Testing Library

Testing Library для React может быть установлена с помощью пакета @testing-library/react:

npm i @testing-library/react --save-dev

Чтобы установить Nightwatch, выполните команду инициализации:

npm init nightwatch@latest

Выберите React и JavaScript при появлении запроса. С помощью этого будет установлен nightwatch и плагин @nightwatch/react. Выберите браузер для установки драйвера. В этом примере мы будем использовать Chrome.

1.1. Установка плагина @nightwatch/testing-library

Начиная с версии 2.6, Nightwatch предоставляет свой плагин для использования запросов Testing Library. Потом он понадобится нам для написания нашего теста, поэтому давайте сейчас его установим:

npm i @nightwatch/testing-library --save-dev

1.2. Установка плагина @nightwatch/apitesting

Этот пример содержит заглушку сервера, которая необходима для тестирования компонента. Мы будем использовать встроенную заглушку сервера, которая поставляется с плагином @nightwatch/apitesting. Установим его с помощью следующей команды:

npm i @nightwatch/apitesting --save-dev

Шаг 2. Создание компонента Login

Мы будем использовать тот же компонент, что и в React Testing Library docs. Создаем новый файл src/Login.jsx и добавляем следующий код:

// login.jsx
import * as React from 'react'

function Login() {
  const [state, setState] = React.useReducer((s, a) => ({...s, ...a}), {
    resolved: false,
    loading: false,
    error: null,
  })

  function handleSubmit(event) {
    event.preventDefault()
    const {usernameInput, passwordInput} = event.target.elements

    setState({loading: true, resolved: false, error: null})

    window
      .fetch('/api/login', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({
          username: usernameInput.value,
          password: passwordInput.value,
        }),
      })
      .then(r => r.json().then(data => (r.ok ? data : Promise.reject(data))))
      .then(
        user => {
          setState({loading: false, resolved: true, error: null})
          window.localStorage.setItem('token', user.token)
        },
        error => {
          setState({loading: false, resolved: false, error: error.message})
        },
      )
  }

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="usernameInput">Username</label>
          <input id="usernameInput" />
        </div>
        <div>
          <label htmlFor="passwordInput">Password</label>
          <input id="passwordInput" type="password" />
        </div>
        <button type="submit">Submit{state.loading ? '...' : null}</button>
      </form>
      {state.error ? <div role="alert">{state.error}</div> : null}
      {state.resolved ? (
        <div role="alert">Congrats! You're signed in!</div>
      ) : null}
    </div>
  )
}

export default Login

Шаг 3. Создание компонента Test

Один из основополагающих принципов Testing Library заключается в том, что тесты должны максимально походить на то, как пользователи взаимодействуют с приложением. При написании компонентных тестов в Nightwatch с использованием JSX нам нужно написать тест как компонентную историю, используя формат Component Story Format, декларативный формат, представленный Storybook.

Это позволяет нам писать тесты, сосредотачиваясь на том, как компонент используется, а не на том, как он реализован, что соответствует философии Testing Library. Подробнее об этом можно прочитать в документации Nightwatch docs.

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

3.1. Тест входа с валидными данными

Создаем новый файл src/Login.spec.jsx и добавляем следующий код, который делает то же самое, что и комплексный пример, написанный с помощью Jest:

Чтобы визуализировать компонент с помощью JSX в Nightwatch, мы создаем некий экспорт для визуализированного компонента, при необходимости с набором реквизитов. Функции play и test используются для взаимодействия с компонентом и для верификации результатов.

  • play используется для взаимодействия с компонентом. Он выполняется в контексте браузера, поэтому мы можем использовать объект screen из Testing Library для запроса DOM и запуска событий;

  • test используется для проверки результатов. Он выполняется в контексте Node.js, поэтому мы можем использовать объект Nightwatch browser для запроса DOM и проверки результатов.

// login.spec.jsx
import {render, fireEvent, screen} from '@testing-library/react'
import Login from '../src/login'

export default {
  title: 'Login',
  component: Login
}

export const LoginWithValidCredentials = () => <Login />;
LoginWithValidCredentials.play = async ({canvasElement}) => {
  //fill out the form
};

LoginWithValidCredentials.test = async (browser) => {
  // verify the results
};

Добавление заглушки сервера

В примере используется заглушка сервера для имитации запроса логина. Мы будем использовать встроенную заглушку сервера, который идет с плагином @nightwatch/apitesting.

Для этого мы будем использовать хуки setup и teardown, которые мы можем написать прямо в тестовом файле. Оба хука выполняются в контексте Node.js.

Нам также необходимо установить конечную точку входа в http://localhost:3000/api/login в компоненте Login, который является URL-адресом заглушки сервера.

Завершаем файл теста

Полный файл теста будет выглядеть так:

// login.spec.jsx
import {render, fireEvent, screen} from '@testing-library/react'
import Login from '../src/Login'

let server;
const token = 'fake_user_token';
let serverResponse = {
  status: 200,
  body: {token}
};

export default {
  title: 'Login',
  component: Login,
  setup: async ({mockserver}) => {
    server = await mockserver.create();
    server.setup((app) => {
      app.post('/api/login', function (req, res) {
        res.status(serverResponse.status).json(serverResponse.body);
      });
    });

    await server.start(mockServerPort);
  },

  teardown: async (browser) => {
    await browser.execute(function() {
      window.localStorage.removeItem('token')  
    });
    
    await server.close();
  }
}

export const LoginWithValidCredentials = () => <Login />;
LoginWithValidCredentials.play = async ({canvasElement}) => {
  //fill out the form
  fireEvent.change(screen.getByLabelText(/username/i), {
    target: {value: 'chuck'},
  });

  fireEvent.change(screen.getByLabelText(/password/i), {
    target: {value: 'norris'},
  });

  fireEvent.click(screen.getByText(/submit/i))
};

LoginWithValidCredentials.test = async (browser) => {
  const alert = await browser.getByRole('alert')
  await expect(alert).text.to.match(/congrats/i)

  const localStorage = await browser.execute(function() {
    return window.localStorage.getItem('token');
  });

  await expect(localStorage).to.equal(fakeUserResponse.token)
};

Дебагинг

Одним из основных преимуществ использования Nightwatch для компонентного тестирования, помимо наличия того же API, доступного для end-to-end тестирования, является то, что мы можем запускать тесты в реальном браузере, а не в виртуальной DOM среде, такой как JSDOM.

Это позволяет нам использовать Chrome Dev Tools для отладки тестов.

Например, давайте пойдем дальше и добавим оператор debugger в функцию LoginWithValidCredentials.play:

LoginWithValidCredentials.play = async ({canvasElement}) => {
  //fill out the form
  fireEvent.change(screen.getByLabelText(/username/i), {
    target: {value: 'chuck'},
  });

  fireEvent.change(screen.getByLabelText(/password/i), {
    target: {value: 'norris'},
  });
  
  debugger;
  
  fireEvent.click(screen.getByText(/submit/i))
};

Теперь запустим тест с флагами --debug и --devtools

npx nightwatch test/login.spec.jsx --debug --devtools

Откроется новое окно Chrome с открытыми инструментами разработчика Dev Tools. Теперь мы можем установить точку останова в Dev Tools и пройтись по коду.

Дебагинг с Chrome DevTools
Дебагинг с Chrome DevTools

3.2. Тест логина с сервер исключением

Исходный пример из документации Testing Library также включает тест для случая, когда сервер выбрасывает исключение.

Попробуем написать то же самое в Nightwatch. На этот раз мы будем использовать только функцию test, так как мы также можем взаимодействовать с компонентом. Как мы упоминали ранее, функция test выполняется в контексте Node.js и получает в качестве аргумента объект Nightwatch browser.

Нам также потребуется обновить ответ заглушки сервера, чтобы он возвращал код состояния 500 и сообщение об ошибке. Этого легко добиться, написав тестовый хук preRender для истории компонента LoginWithServerException.

export const LoginWithServerException = () => <Login />;
LoginWithServerException.preRender = async (browser) => {
  serverResponse = {
    status: 500,
    body: {message: 'Internal server error'}
  };
};

LoginWithServerException.test = async (browser) => {
  const username = await browser.getByLabelText(/username/i);
  await username.sendKeys('chuck');

  const password = await browser.getByLabelText(/password/i);
  await password.sendKeys('norris');

  const submit = await browser.getByText(/submit/i);
  await submit.click();

  const alert = await browser.getByRole('alert');
  await expect(alert).text.to.match(/internal server error/i);

  const localStorage = await browser.execute(function() {
    return window.localStorage.getItem('token');
  });

  await expect(localStorage).to.equal(token)
};

Шаг 4. Запуск теста

Наконец, давайте запустим наш тест. Будет запущен LoginWithValidCredentials и LoginWithServerException компонент историй в Chrome.

npx nightwatch test/login.spec.jsx

Чтобы запустить тест, не открывая браузер, мы можем передать флаг --headless. Если все пойдет хорошо, вы должны увидеть следующий аутпут:

[Login] Test Suite
────────────────────────────────────
ℹ Connected to ChromeDriver on port 9515 (1134ms).
  Using: chrome (108.0.5359.124) on MAC OS X.

Mock server listening on port 3000

  Running <LoginWithValidCredentials> component:
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
[browser] [vite] connecting...
[browser] [vite] connected.
  ✔ Expected element <LoginWithValidCredentials> to be visible (15ms)
  ✔ Expected element <DIV[id='app'] > DIV > DIV> text to match: "/congrats/i" (14ms)
  ✔ Expected 'fake_user_token'  to equal('fake_user_token'): 

  ✨ PASSED. 3 assertions. (1.495s)

  Running <LoginWithServerException> component:
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
[browser] [vite] connecting...
[browser] [vite] connected.
  ✔ Expected element <LoginWithServerException> to be visible (8ms)
  ✔ Expected element <DIV[id='app'] > DIV > DIV> text to match: "/internal server error/i" (8ms)
  ✔ Expected 'fake_user_token'  to equal('fake_user_token'): 

  ✨ PASSED. 3 assertions. (1.267s)

  ✨ PASSED. 6 total assertions (4.673s)

Заключение

Вот и все! Вы можете найти полный код этого примера в репозитории GitHub.Не стесняйтесь заходить в Nightwatch Discord, если у вас есть какие-либо вопросы или отзывы.

Также хочу порекомендовать вам бесплатный урок от OTUS, в рамках которого мы узнаем что из себя представляет PlayWright. Почему автотестеры все чаще стали выбирать именно его? Ознакомимся с его документацией, подключим к тестовому проекту и попробуем его особенности на практике.

Комментарии (2)


  1. XeL077
    00.00.0000 00:00
    +1

    Что думаете о переходе с Nightwatch на Cypress или Playwright? Nightwatch кажется уже устаревшим.


    1. SergeiShaikin Автор
      00.00.0000 00:00

      Не плохая идея.