Создание компонента Vue с использованием TDD: краткое введение
Статья для новичков в TDD с Vue.js. В ней описываются первые шаги разработки через тестирование.
Оригинальная статья: Andrea Stagi — Writing a Vue component using TDD: a gentle introduction
В этом руководстве я хочу рассмотреть основные концепции разработки через тестирование (Test Driven Development — TDD). Для этого мы создадим простой компонент Vue с TypeScript, протестируем его с использованием Jest, и настроим coverage и Continuous Integration.
Введение
Разработка через тестирование (TDD) — это процесс разработки, при котором вы пишете тесты перед написанием кода. Сначала вы пишете тест, описывающий ожидаемое поведение, и запускаете его, гарантируя, что он потерпит неудачу, затем вы пишете минимальный код для его прохождения. После этого, если вам нужно, вы можете изменить код, чтобы сделать его правильным (делаете рефакторинг кода). Вы повторяете все эти шаги для каждой функции, которую хотите реализовать, пока не завершите свой проект. Этот процесс сразу заставляет разработчиков писать модульные тесты перед написанием кода, создавая надежный и модульный код.
Итак давай те напишем немного кода для создания компонента image placeholder, который будет извлекает изображения из LoremFlickr — простого сервиса для получения случайных изображений, использующий такие параметры, как ширина, высота, категории (значения, разделенные запятыми) и фильтры … внутри URL. Например что бы получить изображение 320×240 из Бразилии или Рио, нужно использовать следующую ссылку https://loremflickr.com/320/240/brazil,rio
Несмотря на то, что в LoremFlickr есть много опций, в этом руководстве мы сосредоточимся на разработке простого компонента для получения изображения из LoremFlickr только с использованием width и height и фильтрации по categories.
https://loremflickr.com/<width>/<height>/<categories>
Создание проекта
Используя Vue CLI создайте прокт vue-image-placeholder
vue create vue-image-placeholder
Виберите Manually select features
и опции TypeScript
и Unit testing
? Check the features needed for your project: ◉ Babel ◉ TypeScript ◯ Progressive Web App (PWA) Support ◯ Router ◯ Vuex ◯ CSS Pre-processors ◉ Linter / Formatter ◉ Unit Testing ◯ E2E Testing
Используя настройки по умолчанию выберите Jest как тестовый фреймворк.
🧹 Очистите папки assets
, components
и содержимое файла App.vue
внутри src
.
Написание первого теста
В папке tests/unit
переименуйте файл example.spec.ts
в imageplaceholder.spec.ts
.
Мы ожидаем что наш компонент ImagePlaceholder будет отображать тег <img> с src со свойствами width, height и категории images.
<ImagePlaceholder width=500 height=250 images="dog" />
Компонент должен будет генерировать следующий HTML
<img src="https://loremflickr.com/500/250/dog">
Давайте напишем наш первый тест, чтобы проверить, отображает ли компонент ImagePlaceholder со свойствами width: 500, height: 200, images: ‘newyork’ img с src = https: //loremflickr.com/500/200/newyork.
import { shallowMount } from '@vue/test-utils' import ImagePlaceholder from '@/ImagePlaceholder.vue' describe('ImagePlaceholder.vue', () => { it('renders the correct url for New York images', () => { const wrapper = shallowMount(ImagePlaceholder, { propsData: { width: 500, height:200, images: 'newyork' } }) expect( wrapper.findAll('img').at(0).attributes().src ).toEqual('https://loremflickr.com/500/200/newyork') }) })
Если мы попытаемся запустить тесты командой:
yarn test:unit
❌ Все тесты терпят неудачу, как и ожидалось, потому что компонент ImagePlaceholder пока еще не существует.
Чтобы тесты прошли успешно, нам нужно создать компонент ImagePlaceholder.vue
<template> <img :src="url"> </template> <script lang="ts"> import { Component, Prop, Vue } from 'vue-property-decorator'; @Component export default class ImagePlaceholder extends Vue { @Prop({required: true}) readonly width!: number @Prop({required: true}) readonly height!: number @Prop({required: true}) readonly images!: string get url() { return `https://loremflickr.com/${this.width}/${this.height}/${this.images}`; } } </script>
Теперь снова запустим тест yarn test:unit.
yarn run v1.19.2 $ vue-cli-service test:unit PASS tests/unit/imageplaceholder.spec.ts ImagePlaceholder.vue ✓ renders the correct url for New York images (46ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 1.428s Ran all test suites. ✨ Done in 2.40s.
✅ Ура! Теперь тесты запускаются без ошибок!
Мы только что создали минимальный компонент ImagePlaceholder с использованием TDD!
Используем его в действии: скопируйте и вставьте следующий код в main.ts
import Vue from 'vue' import ImagePlaceholder from './ImagePlaceholder.vue' Vue.config.productionTip = false new Vue({ render: h => h( ImagePlaceholder, { props : { width: 500, height:200, images: 'newyork' } }), }).$mount('#app')
и запустим yarn serve
!
Улучшение компонента с помощью TDD
Предположим, что теперь мы хотим добавить новую функцию в компонент ImagePlaceholder: используем «случайную» категорию, если категория изображения не была указана.
Использовав таким образом компонент
<ImagePlaceholder width=500 height=200 />
должно получиться
<img src="https://loremflickr.com/500/200/random">
Первым делом изменим тест.
it('renders the correct url for Random images if not specified', () => { const wrapper = shallowMount(ImagePlaceholder, { propsData: { width: 500, height:200 } }) expect( wrapper.findAll('img').at(0).attributes().src ).toEqual('https://loremflickr.com/500/200/random') })
❌ После запуска теста yarn test:unit: мы получим ошибку
● ImagePlaceholder.vue › renders the correct url for Random images if not specified expect(received).toEqual(expected) // deep equality Expected: "https://loremflickr.com/500/200/random" Received: "https://loremflickr.com/500/200/undefined"
Теперь изменим компонент: теперь prop images больше не требуется, и значение по умолчанию должно быть «random».
//... @Prop({required: false, default: 'random'}) readonly images!: string //...
✅ Запустим тесты снова, и они пройдут, как ожидалось!
Как насчет поддержки квадратных изображений. Сделаем height равным width, если не указано иное?
it('renders a square image if height is not specified', () => { const wrapper = shallowMount(ImagePlaceholder, { propsData: { width: 500 } }) expect( wrapper.findAll('img').at(0).attributes().src ).toEqual('https://loremflickr.com/500/500/random') })
И напишем минимальный код, чтобы он прошел.
@Component export default class ImagePlaceholder extends Vue { @Prop({required: true}) readonly width!: number @Prop({required: false}) readonly height!: number @Prop({required: false, default: 'random'}) readonly images!: string get url(): string { let height = this.height; if (!this.height) { height = this.width; } return `https://loremflickr.com/${this.width}/${height}/${this.images}` } }
✅ Тесты проходят!
Сейчас у нас есть тест для этой новой функции и минимальный код для ее прохождения. Теперь мы можем заняться рефакторингом! 👨🏻💻
export default class ImagePlaceholder extends Vue { @Prop({required: true}) readonly width!: number @Prop({required: false}) readonly height!: number @Prop({required: false, default: 'random'}) readonly images!: string get url(): string { return `https://loremflickr.com/${this.width}/${this.height || this.width}/${this.images}`; } }
✅ Тесты снова пройдены! Мы успешно провели рефакторинг кода, не влияя на результат!
Если у вас есть еще какие-либо идея по развитию компонента,
повторите этот процесс, для их реализации! Помните: вначале обдумайте то что вы хотите создать, потом напишите тест проверяющий этот функционал, естественно тест вначале не будет проходит потом напишите минимальный код реализующий вашу идею, и убедитесь в том что тест начал проходит! Затем не забудьте улучшить ваш кода, если есть такая возможность.
Вы можете найти весь код проекта на GitHub
Добавление покрытия кода -coverage
Покрытие кода — это измерение того, сколько строк, ветвей, операторов вашего кода выполнено во время выполнения автоматических тестов. Приложения с высоким процентом покрытого кода имеют меньшую вероятность обнаружения необнаруженных ошибок по сравнению с приложениями с низким охватом тестированием.
Jest может генерировать покрытие кода легко без внешних инструментов. Чтобы включить эту функцию, добавьте несколько строк в файл jest.config.json, указав, какие файлы будут покрыты
module.exports = { preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel', collectCoverage: true, collectCoverageFrom: ["src/**/*.vue", "!**/node_modules/**"] }
Запустите снова yarn test:unit, и вы получите отчет о покрытии до результатов тестирования.
----------------------|----------|----------|----------|----------|-------------------| File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s | ----------------------|----------|----------|----------|----------|-------------------| All files | 100 | 100 | 100 | 100 | | ImagePlaceholder.vue | 100 | 100 | 100 | 100 | | ----------------------|----------|----------|----------|----------|-------------------| Test Suites: 1 passed, 1 total Tests: 3 passed, 3 total Snapshots: 0 total Time: 5.688s Ran all test suites. ✨ Done in 8.70s.
⚠️ На забудьте добавит папку /coverage
в .gitignore
.
Непрерывная интеграция
Непрерывная интеграция (CI) — это практика разработки, при которой разработчики часто интегрируют код в общий репозиторий, предпочтительно несколько раз в день. Каждая интеграция может быть проверена с помощью автоматической сборки и автоматических тестов. Целью является создание более стабильного программного обеспечения путем разработки и тестирования с меньшими фазами исправления багов. Вот где появляется платформа непрерывной интеграции, такая как TravisCI.
Нам также нужен еще один полезный сервис, Codecov, чтобы отслеживать процент покрытия кода.
TravisCI и Codecov интегрированы с Github, вам просто нужно зарегистрироваться и добавить проект в сервисы. Внутри вашего проекта вам нужно будет создать специальный файл .travis.yml, чтобы активировать CI и сказать TravisCI, как выполнять сборки:
language: node_js node_js: - 10 before_script: - yarn add codecov script: - yarn test:unit after_script: codecov
Кратное описание файла:
- настройка окружения (
node_js 10
) - установка зависимостей (секция
before_script
) - выполнение тестов с покрытием (секция
script
) - отправка отчета по покрытию в Codecov(секция
after_script
)
Установка
Теперь, когда у нас есть готовый компонент, нам нужно настроить процесс сборки. В вашем файле package.json измените секцию build
и удалите строку с serve.
"scripts": { "build": "vue-cli-service build --target lib --name vue-image-placeholder src/main.ts", "test:unit": "vue-cli-service test:unit", "lint": "vue-cli-service lint" },
С —target lib файл main.ts должен быть соответствующим образом изменен для экспорта вашего компонента
import ImagePlaceholder from './ImagePlaceholder.vue' export default ImagePlaceholder
Добавим папку types с файлом index.d.ts, содержащим
declare module 'vue-image-placeholder' { const placeholder: any; export default placeholder; }
Добавим ссылки main и typings в package.json
"main": "./dist/vue-image-placeholder.common.js", "typings": "types/index.d.ts",
Нам так же нужно запретить автоматический внедрение polyfill в babel.config.js
module.exports = { presets: [ ['@vue/app', { useBuiltIns: false }] ] }
И удалим test файлы из секции «include» файла tsconfig.json.
Далее осталось запустить команду сборки:
yarn build
⠦ Building for production as library (commonjs,umd,umd-min)... DONE Compiled successfully in 20857ms 11:37:47 PM File Size Gzipped dist/vue-image-placeholder.umd.min.js 8.50 KiB 3.16 KiB dist/vue-image-placeholder.umd.js 42.33 KiB 11.76 KiB dist/vue-image-placeholder.common.js 41.84 KiB 11.60 KiB
📦 Сборка готова!
Чтобы поиграть с ней, установите проект vue-image-placeholder в какое-нибудь другое локально приложение, используя команду
yarn add ../vue-image-placeholder
и используйте компонент следующим образом
<template> <div id="app"> <h1>Welcome to the Vue Image Placeholder demo!</h1> <ImagePlaceholder width=500 /> </div> </template> <script> import ImagePlaceholder from 'vue-image-placeholder'; export default { name: 'App', components: { ImagePlaceholder } } </script>
✨ Здесь вы найдете официальный репозитарий vue-image-placeholder.