Для интерфейса этого проекта я собираюсь использовать VueJS. Недавно я ознакомился с кратким руководством по использованию VueJS, так что это хорошая возможность попробовать попрактиковаться в том, чему я научился. К сожалению, у меня нет опыта использования тестов с Vue, так что посмотрим, что получится.

На моем компьютере уже установлен vue-cli, поэтому я создам новый проект с помощью vue create task-manager-app. Я собирался настроить тестовую среду вручную, но у меня возникли некоторые проблемы с настройкой, поэтому давайте просто воспользуемся встроенными параметрами с vue-cli.

Когда все построено, давайте проверим модульное тестирование. В терминале запустите npm run test:unit

Все идет нормально. Если вы заметили, по умолчанию файлы тестов должны заканчиваться spec.js, а не test.js. Ради интереса давайте изменим пример теста на example.test.js, а в конфигурационном файле jest изменим следующую строку с

testMatch: [
    "**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"
  ],

to

testMatch: [
    "**/tests/unit/**/*.test.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"
  ],

Хорошо, теперь мы можем использовать файлы test.js, поскольку это то, что мы делали в прошлом. Прежде чем начать, давайте просто взглянем на файл App.vue.

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png" />
    <HelloWorld msg="Welcome to Your Vue.js App" />
  </div>
</template>
<script>
import HelloWorld from "./components/HelloWorld.vue";
export default {
  name: "app",
  components: {
    HelloWorld
  }
};
</script>
<style>
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

И затем тестовый файл

import { shallowMount } from "@vue/test-utils";
import HelloWorld from "@/components/HelloWorld.vue";
describe("HelloWorld.vue", () => {
  it("renders props.msg when passed", () => {
    const msg = "new message";
    const wrapper = shallowMount(HelloWorld, {
      propsData: { msg }
    });
    expect(wrapper.text()).toMatch(msg);
  });
});

Здесь мы можем понять, как работает модульное тестирование. Мы импортируем компонент HelloWorld и получаем для него оболочку с помощью мелкого монтирования. HelloWorld ожидает поддержку сообщения msg, поэтому мы должны передать и его. Затем, используя оболочку, мы ожидаем, что она будет соответствовать только что отправленному сообщению. Довольно круто.

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

В папке src оставлю только файл main.js и App.vue, а все остальное удалю. Давайте также начнем со следующего в App.vue

<template>
  <div>
    Hello World
  </div>
</template>
<script>
export default {
  name: 'App'
};
</script>
<style>
</style>

Прежде чем начать, давайте сделаем шаг назад и подумаем, какие тесты мне следует создать. У меня есть внутренний API, который обрабатывает операции CRUD для пользователей и задач, на внешнем интерфейсе я хочу иметь доступ ко всем этим операциям. Мне не нужно проверять, сохраняются ли данные в базе данных или что-то в этом роде, внешний интерфейс не должен заботиться о том, как работает серверная часть, пока он возвращает правильную информацию. Итак, сейчас я сосредоточусь на трех вещах.

  1. Компоненты отображают то, что они должны делать
  2. Vue правильно обрабатывает взаимодействия с пользователем (например, кнопка запускает метод)
  3. Изменения в данных обрабатываются (например, добавление задачи фактически показывает другую задачу)

Давайте начнем с компонента, который позволит мне создавать пользователей. Это должна быть форма с полем имени, электронной почты и пароля с кнопкой отправки. Эта кнопка отправки должна запускать запрос к серверной части и в случае успеха, вероятно, должна отображать какое-то сообщение об успехе (позже у нас будет этот журнал в пользователе).

Так как же нам написать эти тесты? Сначала давайте создадим базовый компонент createUser. Итак, в src/components/CreateUser.vue

<template>
  <div>
    Create User Component
  </div>
</template>
<script>
export default {
  name: "CreateUser"
};
</script>
<style>
</style>

Итак, в файле user.test.js давайте сделаем следующее:

import { shallowMount } from '@vue/test-utils';
import CreateUser from '../../src/components/CreateUser';
describe('CreateUser.vue', () => {
  it('Renders a form with name, email, and passworld fields', () => {
    const wrapper = shallowMount(CreateUser);
    expect(wrapper.find('[data-name]').exists()).toBe(true);
    expect(wrapper.find('[data-email').exists()).toBe(true);
    expect(wrapper.find('[data-password').exists()).toBe(true);
  });
});

По сути, в моем компоненте CreateUser я ожидаю найти элементы с атрибутами data-name, data-email и data-password. Я мог бы быть более конкретным и сделать их полями ввода, но я думаю, что это нормально. Посмотрим, смогу ли я пройти эти тесты. Вернемся к компоненту:

<template>
  <div>
    Create User Component
    <form>
      <input type="text" data-name placeholder="Name" />
      <input type="text" data-email  placeholder="Email" />
      <input type="text" data-password placeholder="password" />
      <button type="submit">Create User</button>
    </form>
  </div>
</template>
...

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

Сейчас это очень уродливо, поэтому давайте сделаем базовую стилизацию, используя семантический пользовательский интерфейс. Для этого мы будем использовать semantic ui vue.

npm i semantic-ui-vue semantic-ui-css

В основном файле js

import SuiVue from 'semantic-ui-vue';
import 'semantic-ui-css/semantic.min.css';
Vue.use(SuiVue);

В App.vue

<template>
  <sui-container>
    <create-user></create-user>
  </sui-container>
</template>

В CreateUser.vue

<template>
  <div>
    <h2>Create User Component</h2>
    <sui-form>
      <sui-form-field>
        <label>Name <input type="text" data-name placeholder="Name"/></label>
      </sui-form-field>
      <sui-form-field>
        <label
          >Email <input type="email" data-email placeholder="Email" />
        </label>
      </sui-form-field>
      <sui-form-field>
        <label>
          Password
          <input type="password" data-password placeholder="password" />
        </label>
      </sui-form-field>
      <sui-button primary type="submit">Create User</sui-button>
    </sui-form>
  </div>
</template>

И теперь наше приложение выглядит так

Однако после использования семантического vue я продолжал получать эти раздражающие предупреждения о незарегистрированных пользовательских компонентах. Чтобы исправить это, мы создаем локальный экземпляр Vue и используем семантический vue. Внесенные изменения выделены жирным шрифтом.

import { shallowMount, createLocalVue } from '@vue/test-utils';
import CreateUser from '../../src/components/CreateUser';
import SuiVue from 'semantic-ui-vue';
const localVue = createLocalVue();
localVue.use(SuiVue);
describe('CreateUser.vue', () => {
  it('Renders a form with name, email, and passworld fields', () => {
    const wrapper = shallowMount(CreateUser, { localVue });
    expect(wrapper.find('[data-name]').exists()).toBe(true);
    expect(wrapper.find('[data-email]').exists()).toBe(true);
    expect(wrapper.find('[data-password]').exists()).toBe(true);
  });
});

Далее давайте получим кнопку отправки, чтобы отобразить обратную связь с пользователем.

it('reveals a notification when submitted', () => {
    const wrapper = shallowMount(CreateUser, { localVue });
wrapper.find('[data-name]').setValue('Peter');
    wrapper.find('[data-email]').setValue('[email protected]');
    wrapper.find('[data-password]').setValue('12345678');
    wrapper.find('form').trigger('submit.prevent');
expect(wrapper.find('.message').text()).toBe('The user Peter was created.');
  });

В CreateUser.vue я добавил элемент сообщения в форме sui, а также добавил метод обработчика отправки.

<sui-form @submit.prevent="handleSubmit">
...
</sui-form>
<sui-message v-if="submitted">
      <sui-message-header>Success</sui-message-header>
      <p success-message>User has been created</p>
</sui-message>

Мой тег script теперь выглядит так

<script>
export default {
  name: 'CreateUser',
  data() {
    return {
      submitted: false
    };
  },
  methods: {
    handleSubmit(e) {
      const name = e.target[0].value;
      const email = e.target[1].value;
      const password = e.target[2].value;
      console.log(name, email, password);
      this.submitted = true;
    }
  }
};
</script>

В моей форме я фактически не моделирую свои данные в компоненте vue. Это не казалось необходимым, но это означает, что у меня действительно нет простого доступа к имени пользователя, чтобы вставить сообщение об успешном завершении. Так что мне нужно немного исправить мой тест.

— ДВА ЧАСА СПУСТЯ —

Поэтому я подумал, что могу просто обновить свой тест, и все заработает, но я играл в течение 2 часов и не смог пройти тест. У меня было две конкретные проблемы: первая: моя форма не запускалась, и вторая: метод handleSubmit не мог извлечь данные из формы. В конечном итоге мне пришлось переключить свою форму на использование v-модели и сохранить данные, хранящиеся в свойстве данных. А с формой оказалось, что не может найти форму, если я использовал "суи-форма" или "форма". Я просто решил добавить к нему атрибут формы, чтобы его было легче выбирать.

Мой тест читает

it('reveals a notification when submitted', () => {
    const wrapper = mount(CreateUser, { localVue });
    wrapper.find('[data-name]').setValue('Peter');
    wrapper.find('[data-email]').setValue('[email protected]');
    wrapper.find('[data-password]').setValue('12345678');
    wrapper.find('[form]').trigger('submit.prevent');
expect(wrapper.find('[message]').text()).toBe('User has been created');
  });

И наконец! Получил это работает. Теперь мне нужно выяснить, как проверить, действительно ли я могу отправить запрос на свой сервер. Давайте попробуем что-то вроде:

it('adds a user', async () => {
    const wrapper = mount(CreateUser, { localVue });
    wrapper.find('[data-name]').setValue('Peter');
    wrapper.find('[data-email]').setValue('[email protected]');
    wrapper.find('[data-password]').setValue('12345678');
    const response = await wrapper.vm.handleSubmit();
    expect(response.body).not.toBeNull();
    expect(response.body.name).toBe('Peter');
  });

И давайте запустим npm i axios и npm i env-cmd — save-dev. В config.env в корень добавим ссылку на бэкенд

TASK_API_ROOT_URL=localhost:3000

и в package.json

"scripts": {
    "serve": "env-cmd -f ./config.env vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "test:unit": "env-cmd -f ./config.env vue-cli-service test:unit"
  },

Теперь мы можем попробовать использовать process.env.TASK_API_ROOT_URL для ссылки на наш серверный проект.

Я собираюсь запустить mongodb и запустить api диспетчера задач в другом терминале.

Сначала давайте изменим мой метод handleSubmit

async handleSubmit() {
      console.log(this.name, this.email, this.password);
      const user = await axios.post('http://127.0.0.1:3000/user', {
        name: this.name,
        email: this.email,
        password: this.password
      });
      console.log(user);
      this.submitted = true;
      return user;
    }

Когда я запускаю свой код сейчас, я получаю ошибку CORS. Вернувшись к моему проекту API диспетчера задач, я собираюсь установить cors, используя npm i cors.

В файле app.js

const cors = require('cors');
app.use(cors());

Теперь для всех моих маршрутов разрешен доступ из любого источника. Это не очень безопасно, но для этого не подойдет. В конечном итоге я разрешу свое приложение диспетчера задач только после того, как оно будет отправлено на героку. И я могу успешно создать пользователя.

Проверяя монго, я вижу, что они тоже есть в базе данных. Теперь я хотел использовать переменные среды, но по какой-то причине он меняет мой URL-адрес, чтобы он соответствовал текущему локальному хосту (localhost:8080), а не 3000. Поэтому вместо этого я буду использовать свойства для передачи URL-адреса из компонента приложения.

Template
...
    <create-user :backend_url="BACKEND_URL"></create-user>
...
export default {
  name: 'App',
  data() {
    return {
      BACKEND_URL: 'http://127.0.0.1:3000'
    };
  },
  components: {
    CreateUser
  }
};

И в CreateUser.vue

  props: ['backend_url'],
  methods: {
    async handleSubmit() {
      const user = await axios.post(this.backend_url + '/user', {
        name: this.name,
        email: this.email,
        password: this.password
      });
      this.submitted = true;
    }
  }

Прямо сейчас мой второй тест терпит неудачу, потому что я сделал handleSubmit асинхронным. Теперь мое утверждение выполняется до того, как все действительно завершится.

it('reveals a notification when submitted', done => {
    const wrapper = mount(CreateUser, { localVue });
    wrapper.find('[data-name]').setValue('Peter');
    wrapper.find('[data-email]').setValue('[email protected]');
    wrapper.find('[data-password]').setValue('12345678');
    wrapper.find('[form]').trigger('submit.prevent');
    wrapper.vm.$nextTick(() => {
      expect(wrapper.find('[message]').text()).toBe('User has been created');
      done();
    });
  });

Давайте исправим это, обратившись к параметру done. Когда мы используем done, это позволяет понять, что это асинхронный тест, и он должен дождаться вызова done(). Используя $nextTick, вы откладываете функцию обратного вызова (выделена жирным шрифтом) до следующего цикла обновления. Моя отправка формы должна установить для отправленного значения значение true, которое затем обновляет DOM. Когда произойдет это обновление, будет запущен обратный вызов в nextTick. Вот как я могу отложить свое утверждение до тех пор, пока не произойдет что-то асинхронное.

Следующая проблема связана с аксиомами. Если я не имитирую вызовы API в тестах, я получаю эти ошибки об отказе в соединении. В верхней части файла я поместил следующее

jest.mock('axios');

Ошибок больше нет, но поскольку я на самом деле не вызываю axios в своем тесте, я не могу быть уверен, что пользователь был создан. Я не совсем уверен, что с этим делать. Во всех документах, касающихся тестирования, говорится, что мы должны имитировать наши вызовы, поэтому, возможно, достаточно просто знать, что был вызван handleSubmit. И я могу сделать вывод, что второй тест показывает сообщение уведомления. Но чтобы быть по-настоящему ясным, давайте удостоверимся, что обертка не содержит сообщения перед отправкой.

it('reveals a notification when submitted', done => {
    const wrapper = mount(CreateUser, { localVue });
    expect(wrapper.contains('[message]')).toBe(false);
    wrapper.find('[data-name]').setValue('Peter');
    wrapper.find('[data-email]').setValue('[email protected]');
    wrapper.find('[data-password]').setValue('12345678');
    wrapper.find('[form]').trigger('submit.prevent');
    wrapper.vm.$nextTick(() => {
      expect(wrapper.find('[message]').text()).toBe('User has been created');
      done();
    });

Это тестирование vue сейчас немного расстраивает. Я запутался в том, что я должен тестировать и как это сделать на самом деле. Я думаю, что немного отвлекусь от своего собственного проекта и попытаюсь еще немного изучить, как лучше проводить тестирование vue.

РАЗРЫВ

Хорошо, после нескольких часов борьбы я начал понимать несколько ошибок, которые я совершал. Во-первых, я использовал мелкое крепление вместо монтирования. smallMount не будет отображать подкомпоненты, вместо этого они будут использовать заполнители. Итак, в некоторых руководствах, которые я видел, они использовали мелкое крепление для получения данных формы, а что нет, проблема в том, что я использую семантику пользовательского интерфейса, и они считаются компонентами, а не элементами. Таким образом, использование ‹sui-form› вместо ‹form› означало, что я не мог активировать prevent.default. Я не осознавал, когда переключился в своем коде на мелкое монтирование, поэтому я попытался смоделировать некоторые асинхронные запросы и получил кучу разных ошибок. Но теперь я немного лучше знаком с тестированием vue, так что давайте вернемся к проекту.

Я внес довольно много изменений, поэтому давайте просто вернемся к моему текущему коду (это обозначено коммитом конец части 5https://github.com/iampeternguyen/task-manager-app).

В CreateUser.vue

methods: {
    async handleSubmit() {
      try {
        await this.createUser({
          name: this.name,
          email: this.email,
          password: this.password
        });
        this.submitted = true;
        this.error = false;
      } catch (error) {
        this.error = error.response.data.message;
        this.submitted = false;
      }
    },
    createUser(user) {
      return axios.post(this.backend_url + '/user', user);
    }

Я выделил вызов axios для другого метода. Это упрощает имитацию вызова и создание фиктивных данных.

В тесте/фикстуры/userfixture.js

export const createUserSuccess = {
  _id: '5cf7a5f56cd00f661a7da466',
  name: 'Peter',
  email: '[email protected]',
  createdAt: '2019-06-05T11:22:29.393Z',
  updatedAt: '2019-06-05T11:22:29.393Z',
  __v: 0
};
export const createUserError = {
    data: {
      errors: {
        email: {
          message: 'Please use valid email',
          name: 'ValidatorError',
          properties: {
            message: 'Please use valid email',
            type: 'user defined',
            path: 'email',
            value: 'peter@ljfkfla',
            reason: {}
          },
          kind: 'user defined',
          path: 'email',
          value: 'peter@ljfkfla',
          reason: {}
        }
      },
      _message: 'User validation failed',
      message: 'User validation failed: email: Please use valid email',
      name: 'ValidationError'
    }
};

Это фактические response.body, которые отправляются обратно из запроса на создание пользовательской публикации. Это будет полезно при имитации данных.

it('reveals error message when create user request fails', async () => {
    const createUserMock = jest.fn(() => {
      let error = new Error('error');
      error['response'] = createUserError;
      throw error;
    });
    wrapper.setMethods({
      createUser: createUserMock
    });

    wrapper.find('[form]').trigger('submit.prevent');
    await flushPromises();
    expect(wrapper.vm.submitted).toBe(false);
    expect(wrapper.vm.error).toBe(createUserError.response.data.message);
  });

В моем тесте я использую flushPromises, чтобы можно было использовать async/await, а не вызовы done.

Проведем еще несколько тестов.

it('Makes api request on form submit', async () => {
    const createUserMock = jest.fn();
    wrapper.setMethods({
      createUser: createUserMock
    });
  wrapper.find('[form]').trigger('submit.prevent');
    await flushPromises();
    expect(createUserMock).toHaveBeenCalled();
  });
it('Makes api request with user data', async () => {
    const createUserMock = jest.fn();
    wrapper.setMethods({
      createUser: createUserMock
    });
    wrapper.find('[data-name]').setValue('Peter');
    wrapper.find('[data-email]').setValue('[email protected]');
    wrapper.find('[data-password]').setValue('12345678');
    wrapper.find('[form]').trigger('submit.prevent');
    await flushPromises();
    expect(createUserMock).toHaveBeenCalledWith(
      expect.objectContaining({
        name: 'Peter',
        email: '[email protected]',
        password: '12345678'
      })
    );
  });

Здесь я удостоверяюсь, что моя отправка формы вызовет метод createUser и что метод createUser будет вызываться с данными формы.

Я думаю, что это хорошее место, чтобы остановиться на данный момент. Спасибо, что следите за нами!