Статьи

Тестирование RSpec для начинающих, часть 2

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

  • Matchers
  • Позволять
  • Предметы
  • Callbacks
  • Генераторы
  • Теги

В первой статье мы потратили немало времени, пытаясь ответить на вопрос «почему?» Тестирования. Я предлагаю, чтобы мы вернулись к «как?» И избавились от дальнейшего контекста. Мы уже подробно рассмотрели эту часть. Давайте посмотрим, что еще может предложить RSpec, с которым вы как новичок можете справиться сразу.

Так что это приблизится к сути вещей. RSpec предоставляет вам массу так называемых совпадений. Это ваш хлеб с маслом, когда вы пишете свои ожидания. До сих пор вы видели .to eq и .not_to eq . Но есть гораздо больший арсенал, чтобы написать свои спецификации. Вы можете проверить, чтобы вызвать ошибки, для истинных и ложных значений или даже для определенных классов. Давайте запустим несколько вариантов, чтобы вы начали:

  • .to eq
  • .not_to eq

Это тесты на эквивалентность.

1
2
3
4
5
6
7
8
 
it ‘some clever description’ do
  expect(agent.enemy).to eq ‘Ernst Stavro Blofeld’
  expect(agent.enemy).not_to eq ‘Winnie Pooh’
end
 

Короче говоря, я упаковал два ожидаемых оператора в один блок it . Тем не менее, рекомендуется проверять только одну вещь за тест. Это делает вещи более сфокусированными, и ваши тесты станут менее хрупкими, когда вы что-то измените.

  • .to be_truthy
  • .to be true
1
2
3
4
5
6
7
8
 
it ‘some clever description’ do
  expect(agent.hero?).to be_truthy
  expect(enemy.megalomaniac?).to be true
end
 

Разница в том, что be_truthy имеет значение true, когда оно не равно nil или false . Таким образом, это пройдет, если результат не является ни одним из этих двух — своего рода «истинно похожим». с другой стороны, чтобы .to be true принимает только значение, которое является true и ничего больше.

  • .to be_falsy
  • .to be false
1
2
3
4
5
6
7
8
 
it ‘some clever description’ do
  expect(agent.coward?).to be_falsy
  expect(enemy.megalomaniac?).to be false
end
 

Как и в двух приведенных выше примерах, .to be_falsy ожидает либо значение false либо значение nil , а .to be false будет выполнять только прямое сравнение с false .

  • .to be_nil
  • .to_not be_nil

И последнее по порядку, но не по значению, это тестирование именно на nil . Я избавляю вас от примера.

  • .to match()

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

1
2
3
4
5
6
7
 
it ‘some clever description’ do
  expect(agent.number.to_i).to match(/\d{3}/)
end
 

Предположим, мы имеем дело с такими агентами, как Джеймс Бонд, 007, которым назначены трехзначные номера. Тогда мы могли бы проверить это таким образом — примитивно здесь, конечно.

  • >
  • <
  • <=
  • >=

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

01
02
03
04
05
06
07
08
09
10
11
12
13
 
it ‘some clever description’ do
 
  …
 
  expect(agent.number).to be < quartermaster.number
  expect(agent.number).to be > m.number
  expect(agent.kill_count).to be >= 25
  expect(quartermaster.number_of_gadgets).to be <= 5
end
 

Теперь мы становимся куда менее скучными. Вы также можете проверить для классов и типов:

  • .to be_an_instance_of
  • .to be_a
  • .to be_an
01
02
03
04
05
06
07
08
09
10
11
12
 
it ‘some clever description’ do
  mission = Mission.create(name: ‘Moonraker’)
  agent = Agent.create(name: ‘James Bond’)
  mission.agents << agent
 
  expect(@mission.agents).not_to be_an_instance_of(Agent)
  expect(@mission.agents).to be_a(ActiveRecord::Associations::CollectionProxy)
end
 

В приведенном выше фиктивном примере вы можете видеть, что список агентов, связанных с миссией, относится не к классу Agent а к ActiveRecord::Associations::CollectionProxy . От этого следует отказаться, так как мы можем легко проверить сами классы, оставаясь при этом очень выразительными. .to be_a и .to be_an делают одно и то же. У вас есть оба варианта для удобства чтения.

Тестирование на ошибки также чрезвычайно удобно в RSpec. Если вы новичок в Rails и еще не уверены, какие ошибки может вызвать у вас фреймворк, вы можете не чувствовать необходимости их использовать — конечно, это имеет смысл. Однако на более позднем этапе развития вы найдете их очень удобными. У вас есть четыре способа борьбы с ними:

  • .to raise_error

Это самый общий способ. Какая бы ошибка ни возникала, она будет брошена в вашу сеть.

  • .to raise_error(ErrorClass)

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

  • .to raise_error(ErrorClass, "Some error message")

Это еще более детально, поскольку вы упоминаете не только класс ошибки, но и конкретное сообщение, которое должно быть выдано с ошибкой.

  • .to raise_error("Some error message)

Или вы просто упоминаете само сообщение об ошибке без класса ошибки. Ожидаемая часть должна быть написана немного по-другому — нам нужно обернуть часть под текстом в самом блоке кода:

01
02
03
04
05
06
07
08
09
10
11
12
 
it ‘some clever description’ do
  agent = Agent.create(name: ‘James Bond’)
   
  expect{agent.lady_killer?}.to raise_error(NoMethodError)
  expect{double_agent.name}.to raise_error(NameError)
  expect{double_agent.name}.to raise_error(«Error: No double agents around»)
  expect{double_agent.name}.to raise_error(NameError, «Error: No double agents around»)
end
 
  • .to start_with
  • .to end_with

Поскольку мы часто имеем дело с коллекциями при создании веб-приложений, было бы неплохо иметь в них инструмент для просмотра. Здесь мы добавили двух агентов, Q и Джеймса Бонда, и просто хотели узнать, кто придет первым и последним в наборе агентов для конкретной миссии — здесь Moonraker.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
 
it ‘some clever description’ do
  moonraker = Mission.create(name: ‘Moonraker’)
  bond = Agent.create(name: ‘James Bond’)
  q = Agent.create(name: ‘Q’)
  moonraker.agents << bond
  moonraker.agents << q
 
  expect(moonraker.agents).to start_with(bond)
  expect(moonraker.agents).to end_with(q)
end
 
..
  • .to include

Этот также полезен для проверки содержимого коллекций.

01
02
03
04
05
06
07
08
09
10
11
 
it ‘some clever description’ do
  mission = Mission.create(name: ‘Moonraker’)
  bond = Agent.create(name: ‘James Bond’)
  mission.agents << bond
 
  expect(mission.agents).to include(bond)
end
 
  • предикаты

Эти сопоставители предикатов являются функцией RSpec для динамического создания сопоставлений для вас. Если у вас есть методы предикатов в ваших моделях, например (заканчивающиеся знаком вопроса), тогда RSpec знает, что он должен создавать для вас соответствия, которые вы можете использовать в своих тестах. В приведенном ниже примере мы хотим проверить, является ли агент Джеймсом Бондом:

1
2
3
4
5
6
7
8
9
class Agent < ActiveRecord::Base
 
  def bond?
    name == ‘James Bond’ && number == ‘007’ && gambler == true
  end
 
  …
 
end

Теперь мы можем использовать это в наших спецификациях следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
 
it ‘some clever description’ do
  agent = Agent.create(name: ‘James Bond’, number: ‘007’, gambler: true)
 
  expect(agent).to be_bond
end
 
it ‘some clever description’ do
  agent = Agent.create(name: ‘James Bond’)
 
  expect(agent).not_to be_bond
end
 

RSpec позволяет нам использовать имя метода без знака вопроса — для формирования лучшего синтаксиса, я полагаю. Круто, не правда ли?

let и let! на первый взгляд может выглядеть как переменная, но на самом деле это вспомогательные методы. Первый лениво оценивается, что означает, что он запускается и оценивается только тогда, когда спецификация фактически использует его, а второй выполняется с ударом (!) Независимо от того, используется он спецификацией или нет. Обе версии запоминаются, и их значения будут кэшироваться в одной и той же области видимости.

1
2
3
4
5
6
7
8
9
describe Mission, ‘#prepare’, :let do
  let(:mission) { Mission.create(name: ‘Moonraker’) }
  let!(:bond) { Agent.create(name: ‘James Bond’) }
 
  it ‘adds agents to a mission’ do
    mission.prepare(bond)
    expect(mission.agents).to include bond
  end
end

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

Вы должны знать эту особенность RSpec, так как let широко известен и используется. При этом, следующая статья покажет вам некоторые проблемы, о которых вы должны знать. Используйте эти вспомогательные методы с осторожностью, или, по крайней мере, в небольших дозах на данный момент.

RSpec предлагает вам возможность объявить объект тестирования очень явно. Есть лучшие решения для этого, и мы обсудим недостатки этого подхода в следующей статье, когда я покажу несколько вещей, которых вы обычно хотите избежать. Но сейчас давайте посмотрим, что может сделать для вас предмет:

1
2
3
4
5
6
7
describe Agent, ‘#status’ do
  subject { Agent.create(name: ‘Bond’) }
 
  it ‘returns the agents status’ do
    expect(subject.status).not_to be ‘MIA’
  end
end

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

Если вы еще не знаете об обратных вызовах, позвольте мне кратко рассказать вам. Обратные вызовы выполняются в определенных точках жизненного цикла кода. В терминах Rails это будет означать, что у вас есть код, который запускается до того, как объекты будут созданы, обновлены, уничтожены и т. Д.

В контексте RSpec это жизненный цикл запускаемых тестов. Это просто означает, что вы можете указать хуки, которые должны запускаться до или после каждого теста, например, в файле спецификации, или просто вокруг каждого теста. Есть несколько более детализированных вариантов, но я рекомендую, чтобы мы пока не терялись в деталях. Перво-наперво:

  • before(:each)

Этот обратный вызов запускается перед каждым тестовым примером.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
describe Agent, ‘#favorite_gadget’ do
 
  before(:each) do
    @gagdet = Gadget.create(name: ‘Walther PPK’)
  end
 
  it ‘returns one item, the favorite gadget of the agent ‘ do
    agent = Agent.create(name: ‘James Bond’)
    agent.favorite_gadgets << @gadget
 
    expect(agent.favorite_gadget).to eq ‘Walther PPK’
  end
 
  …
 
end

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

Не обманывайтесь удобством в этом примере. То, что вы можете делать такие вещи, не означает, что вы должны это делать. Я хочу не заходить на территорию AntiPattern и не выводить вас из себя, но, с другой стороны, я хочу немного объяснить и недостатки этих простых манекенов.

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

  • before(:all)

Этот блок before выполняется только один раз перед всеми остальными примерами в файле спецификации.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
describe Agent, ‘#enemy’ do
 
  before(:all) do
    @main_villain = Villain.create(name: ‘Ernst Stavro Blofeld’)
    @mission = Mission.create(name: ‘Moonraker’)
    @mission.villains << @main_villain
  end
 
  it ‘returns the main enemy Bond has to face in his mission’ do
    agent = Agent.create(name: ‘James Bond’)
    @mission.agents << agent
 
    expect(agent.enemy).to eq ‘Ernst Stavro Blofeld’
  end
 
  …
 
end

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

after(:each) и after(:all) ведут себя одинаково, но просто запускаются после выполнения ваших тестов. after часто используется для очистки ваших файлов. Но я думаю, что пока рано говорить об этом. Так что запишите это в память, знайте, что оно есть, если оно вам понадобится, и давайте перейдем к изучению других, более простых вещей.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
describe Agent do
 
  before(:each) do
    @mission = Mission.create(name: ‘Moonraker’)
    @bond = Agent.create(name: ‘James Bond’, number: ‘007’)
  end
 
  describe ‘#enemy’ do
    before(:each) do
      @main_villain = Villain.create(name: ‘Ernst Stavro Blofeld’)
      @mission.villains << @main_villain
    end
 
    describe ‘Double 0 Agent with associated mission’ do
      it ‘returns the main enemy the agent has to face in his mission’ do
        @mission.agents << @bond
 
        expect(@bond.enemy).to eq ‘Ernst Stavro Blofeld’
      end
    end
 
    describe ‘Low-level agent with associated mission’ do
      it ‘returns no info about the main villain involved’ do
        some_schmuck = Agent.create(name: ‘Some schmuck’, number: ‘1024’)
        @mission.agents << some_schmuck
 
        expect(some_schmuck.enemy).to eq ‘That’s above your paygrade!’
      end
    end
 
    …
 
    …
 
    …
 
  end
end

Как вы можете заметить, вы можете разместить блоки обратного вызова в любой области по своему вкусу и идти так глубоко, как вам нужно. Код в обратном вызове будет выполнен в рамках любой области описания блока. Но небольшой совет: если вы чувствуете необходимость вложения слишком много, а вещи кажутся немного запутанными и сложными, пересмотрите свой подход и подумайте, как можно упростить тесты и их настройку. ПОЦЕЛУЙ! Держать его просто глупо. Кроме того, обратите внимание на то, как хорошо это читается, когда мы заставляем эти тесты провалиться

01
02
03
04
05
06
07
08
09
10
11
12
13
Failures:
 
 1) Agent#enemy Double 0 Agent with associated mission returns the main enemy the agent has to face in his mission
    Failure/Error: expect(@bond.enemy).to eq ‘Ernst Stavro Blofeld’
 
      expected: «Ernst Stavro Blofeld»
           got: «Blofeld»
 
 2) Agent#enemy Low-level agent with associated mission returns no info about the main villain involved
    Failure/Error: expect(some_schmuck.enemy).to eq ‘That’s above your paygrade!’
 
      expected: «That’s above your paygrade!»
           got: «Blofeld»

Давайте также кратко рассмотрим, какие генераторы предоставляет RSpec для вас. Вы уже видели один, когда мы использовали rails generate rspec:install . Этот маленький парень сделал настройку RSpec для нас быстрой и легкой. Что еще у нас есть?

  • rspec:model

Хотите иметь еще одну фиктивную модель?

1
rails generate rspec:model another_dummy_model
1
create spec/models/another_dummy_model_spec.rb

Быстро, не так ли? Или новую спецификацию для проверки контроллера, например:

  • rspec:controller
1
rails generate rspec:controller dummy_controller
1
spec/controllers/dummy_controller_controller_spec.rb
  • rspec:view

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

Функциональные тесты не являются специальностью RSpec как таковые и больше подходят для другой статьи. При этом, если вам любопытно, посмотрите Капибару , которая является отличным инструментом для такого рода вещей. Он позволяет вам тестировать целые потоки, которые объединяют несколько частей вашего приложения — тестирование полных функций при моделировании работы браузера. Например, пользователь, который оплачивает несколько товаров в корзине.

  • rspec:helper

Та же самая стратегия генераторов позволяет нам также разместить помощника без особых хлопот.

1
rails generate rspec:helper dummy_helper
1
create spec/helpers/dummy_helper_helper_spec.rb

Двойная часть helper_helper не была случайностью. Когда мы дадим ему более «значимое» имя, вы увидите, что RSpec просто присоединяет _helper самостоятельно.

1
rails generate rspec:helper important_stuff
1
create spec/helpers/important_stuff_helper_spec.rb

Нет, этот каталог не место для хранения ваших драгоценных вспомогательных методов, которые появляются при рефакторинге ваших тестов. На самом деле они будут идти под spec/support . spec/helpers предназначен для тестов, которые вы должны написать для своих помощников вида — set_date может служить такой помощник, как set_date . Да, полное тестовое покрытие вашего кода должно также включать эти вспомогательные методы. Тот факт, что они часто кажутся небольшими и тривиальными, не означает, что мы должны игнорировать их или игнорировать их потенциальные ошибки, которые мы хотим поймать. Чем сложнее на самом деле получается помощник, тем больше у вас причин для этого писать helper_spec !

На случай, если вы начнете играть с ним сразу, имейте в виду, что вам нужно запускать ваши вспомогательные методы на helper объекте, когда вы пишете свои вспомогательные тесты, чтобы работать. Таким образом, они могут быть выставлены только с помощью этого объекта. Что-то вроде этого:

01
02
03
04
05
06
07
08
09
10
11
describe ‘#set_date’ do
 
 
  helper.set_date
 
 
end
 

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

  • RSpec: почтовик
  • RSpec: функция
  • RSpec: интеграция

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

1
2
3
4
5
require ‘rails_helper’
 
RSpec.describe DummyModel, type: :model do
  pending «add some examples to (or delete) #{__FILE__}»
end
1
2
3
4
require ‘rails_helper’
 
RSpec.describe DummyControllerController, type: :controller do
end
1
2
3
4
5
require ‘rails_helper’
 
RSpec.describe DummyHelperHelper, type: :helper do
  pending «add some examples to (or delete) #{__FILE__}»
end

Не нужно вундеркинду, чтобы понять, что у них всех разные типы. Это :type метаданных RSpec дает вам возможность разрезать и нарезать кубики ваших тестов по файловым структурам. Таким образом, вы можете лучше ориентироваться на эти тесты. Скажем, вы хотите, чтобы какие-то помощники загружались, например, только для спецификаций контроллера. Другим примером может быть то, что вы хотите использовать другую структуру каталогов для спецификаций, которые RSpec не ожидает. Наличие этих метаданных в ваших тестах позволяет продолжать использовать функции поддержки RSpec, а не отключать набор тестов. Таким образом, вы можете использовать любую структуру каталогов, которая вам подходит, если вы добавите метаданные этого :type .

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

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

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

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

Вы даете своим тестам некоторые метаданные в виде символов типа «: wip», «: checkout» или того, что соответствует вашим потребностям. Когда вы запускаете эти целевые группы тестов, вы просто указываете, что RSpec должен игнорировать запуск других тестов на этот раз, предоставляя флаг с именем тегов.

1
2
3
4
5
6
7
describe Agent, :wip do
 
  it ‘is a mess right now’ do
    expect(agent.favorite_gadgets).to eq ‘Unknown’
  end
 
end
1
rspec —tag wip
1
2
3
4
5
6
Failures:
 
  1) Agent is a mess right now
       Failure/Error: expect(agent.favorite_gadgets).to eq ‘Unknown’
 

Вы также можете запускать всевозможные тесты и игнорировать группу групп, помеченных определенным образом. Вы просто предоставляете тильду (~) перед именем тега, и RSpec с радостью игнорирует эти тесты.

1
rspec —tag ~wip

Запуск нескольких тегов синхронно также не является проблемой:

1
2
3
rspec —tag wip —tag checkout
 
rspec —tag ~wip —tag checkout

Как вы можете видеть выше, вы можете смешивать и сочетать их по своему желанию. Синтаксис не идеален — повторение --tag может быть не идеальным — но эй, это тоже не важно! Да, все это немного больше дополнительной работы и лишних умственных затрат при составлении спецификаций, но, с другой стороны, это действительно дает вам мощную возможность нарезать ваш набор тестов по требованию. В более крупных проектах это может сэкономить вам массу времени.

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

Для меня это было немного неприятно в начале, потому что было трудно понять, как что-то тестировать, когда я еще не реализовал это и не до конца понимал, как это будет вести себя.

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

Это не легко, если вы только начинаете со всем этим. Работа с несколькими предметно-ориентированными языками — например, RSpec и Rails — плюс изучение Ruby API может быть адской путаницей. Не расстраивайтесь, если кривая обучения кажется пугающей; это будет легче, если вы будете придерживаться этого. Выключение этой лампочки не произойдет за ночь, но для меня это того стоило.