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

В прошлый раз мы начали разработку собственного API. Он работает на нашем сервере Ruby on Rails, и именно к нему мы подключим наш пользовательский интерфейс, который скоро будет создан. Мы написали некоторый предварительный код, позволяющий API получать ноль или более идентификаторов актов, а затем возвращать только те площадки, на которых проводятся концерты, выполненные этими актами.

Сегодня мы расширим наш код API для обработки временных меток.

Временные метки в целом

Кстати, для тех из вас, кто не знает: исторически отметка времени была любым текстовым представлением даты и времени… но в наши дни, особенно в контексте программирования, это способ описания момента даты и времени с помощью всего одного числа, более известного как время Unix.

Даты и время могут иметь всевозможные дурацкие форматы. Представьте себе, скажем, полдень 9 января 2004 года. Все эти способы записи допустимы: 12 часов дня, 1 сентября 2004 года; 12:00 09.01.2004, 12:00 01–09–2004. И это даже без учета часовых поясов (тьфу).

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

Тонны способов. Один из наиболее распространенных способов — временные метки, и убедитесь, что все введенные даты указаны в этом формате. Отметка времени — это просто количество секунд (или миллисекунд) с полуночи 1 января 1970 года по всемирному координированному времени:

Timestamp values for:
01/01/1970:          0
02/01/1970:          86,400 (seconds in a day)
03/01/1970:          172,800
01/02/1970:          2,678,400
01/01/1971:          31,536,000
01/01/1980:          315,532,800
01/01/2000:          946,684,800
25/02/2020, 11:00AM: 1,582,581,600

Это просто определенное количество секунд. Это все.

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

Сегодня мы расширим наш API, чтобы (опционально) получать временные метки начала и окончания, чтобы фильтровать наши ответы JSON Gigs, существующие за их пределами.

Временные метки концертов

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

Как вам хорошая тестовая подмостья?

# spec/requests/search_spec.rb
...
describe "Filtering by start/end timestamps" do
  
  describe "Both timestamps absent" do
    it "returns every venue" do
    end
    it "attaches all gigs to every venue" do
    end
  end
  describe "Start present, end absent" do
    it "returns all venues with at least one gig after start" do
    end
    context "Start malformed" do
      it "complains" do
      end
    end
  end
  describe "Start absent, end present" do
    it "returns all venues with at least one gig before end" do
    end
    context "End malformed" do
        
      it "complains" do
      end
    end
  end
  describe "Both present" do
    context "Start before end" do
      it "returns all venues with at least one gig between start and end" do
      end
    end
    context "End before start" do
      it "complains" do
      end
    end
  end
end

Сладкий. Давай потрепаемся…

…Погоди, только что понял, мы на самом деле не добавили столбец меток времени в таблицу Gigs! Добавим это сейчас.

Но! Должен ли это быть только один столбец… или два? Документы API Ticketmaster сообщают нам, что каждое мероприятие, созданное в стиле Ticketmaster, имеет дату и время начала и окончания: https://developer.ticketmaster.com/products-and-docs/apis/discovery-api /v2/#event-details-v2 Должны ли мы также создавать и начало, и конец?

Возможно нет. Помните весь тот анализ ответов Ticketmaster-JSON, который мы делали еще в статье 9? Я прыгнул обратно и проверил образец ответа только на одно событие:

...
"dates"=>{
  "start"=>{
    "localDate"=>"2020-04-11",
    "localTime"=>"19:30:00",
    "dateTime"=>"2020-04-11T23:30:00Z",
    "dateTBD"=>false,
    "dateTBA"=>false,
    "timeTBA"=>false,
    "noSpecificTime"=>false
  },
  "timezone"=>"America/New_York",
  "status"=>{
    "code"=>"onsale"
  },
  "spanMultipleDays"=>false
},
...

У нас есть объект "start", но нет объекта "end". Похоже, что "start" требуется, но "end" не является обязательным. Итак, давайте просто создадим один столбец. Возможно, если возникнет необходимость, мы могли бы реорганизовать его так, чтобы он был и start_at, и end_at.

$ rails generate migration AddAtToGigs
Running via Spring preloader in process 38411
      invoke active_record
      create   db/migrate/20200109010601_add_at_to_gigs.rb

Ничего особенного, подойдет простое имя at.

Миграция:

# db/migrate/20200109010601_add_at_to_gigs.rb
class AddAtToGigs < ActiveRecord::Migration[5.2]
  def change
    add_column :gigs, :at, :datetime
  end
end

Исполнение:

$ rake db:migrate
== 20200109010601 AddAtToGigs: migrating ===================
-- add_column(:gigs, :at, :datetime)
   -> 0.0381s
== 20200109010601 AddAtToGigs: migrated (0.0383s)===========

Сладкий. Коммит, дифф.

Тем не мение! У нас параллельная проблема. Я не собираюсь исправлять это сейчас, просто запишите это и двигайтесь дальше. Существующий код службы Ticketmaster не заполняет at, не так ли. Я уже запускал свой сервис тонну раз. Прямо сейчас в моей базе данных разработки таблица Gigs имеет 2098 строк, а это означает 2098 значений nil при добавлении нашего столбца at. Плохой.

Таким образом, нам нужно (1) написать еще одну задачу Rake для перебора наших существующих данных Gigs, передать каждое значение ticketmaster_id в API Ticketmaster, а затем получить и заполнить его значение at; затем (2) измените наш существующий код update_from_api, чтобы сохранить значения ticketmaster_id и at.

Но это на потом. Во-первых, тесты.

Нам нужно будет изменить наши существующие тестовые данные, чтобы обрабатывать время:

# spec/requests/search_spec.rb
...
let!(:gig1) { create :gig, act: act1, venue: venue2, at: Time.now + 3.days }
let!(:gig2) { create :gig, act: act1, venue: venue2, at: Time.now + 4.days }
let!(:gig3) { create :gig, act: act1, venue: venue3, at: Time.now + 5.days }
let!(:gig4) { create :gig, act: act3, venue: venue3, at: Time.now + 6.days }
let!(:gig5) { create :gig, act: act3, venue: venue3, at: Time.now + 7.days }
let!(:gig6) { create :gig, act: act4, venue: venue3, at: Time.now + 8.days }
let!(:gig7) { create :gig, act: act4, venue: venue3, at: Time.now + 9.days }
let!(:gig8) { create :gig, act: act4, venue: venue3, at: Time.now + 10.days }
let!(:gig9) { create :gig, act: act4, venue: venue3, at: Time.now + 11.days }
let!(:gig10) { create :gig, act: act4, venue: venue4, at: Time.now + 12.days }
let!(:gig11) { create :gig, act: act5, venue: venue4, at: Time.now + 13.days }
...

Значения at для наших одиннадцати гигов сейчас от 3 до 13 дней в будущем. Хороший удобный разворот для тестовых данных.

Ковпаты на пути к прогрессу

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

Что?! ты плюешься. Но… но вы же автор! Это самопровозглашенный старший разработчик! Учения сыплются из вас, как английские кексы из пухлой задницы Иисуса Христа! Что дает?

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

Теперь, я думаю, самое подходящее время, чтобы упомянуть Пока. Байбаг - это фантастика. Это инструмент отладки. Если вы чем-то похожи на меня, то, скорее всего, первые пять лет своей карьеры программиста вы провели, крича на очевидно идеальный программный код, который по необъяснимым причинам отказывается работать. Скорее всего, вы засорили свой глючный код кучей операторов PRINT и LOGGER, отчаянно пытаясь выманить текущие значения того, что, черт возьми, вы запускали. Получить некоторое представление.

Byebug позволяет кричать на код на одном дыхании. Что он делает, так это то, что вы просто добавляете byebug к той части вашего неработающего кода, которая больше всего покрыта грязью и грязью, а затем запускаете его. Затем в вашем терминале появится небольшая консоль. Состояние приложения приостановлено; переменные в области видимости вашего местоположения byebugged заполнены; можно ковыряться! Это фантастика!

Итак, возвращаясь к сути. Я сделал это:

# spec/requests/search_spec.rb
...
before {
  get '/api/v1/search', params: params
}
describe "Filtering by Act" do
  context "Not supplying any act IDs" do
    let(:params) {}
    describe "returning every single venue" do
      subject { response_json }
      it { byebug; is_expected.to include(
        *[venue1, venue2, venue3, venue4, venue5].map { |venue| 
          JSON.parse venue.to_json(include: :gigs)
        }
      )}
    end
  end
end
...

Глупый старый я просто предположил, что мы создали пять объектов ActiveRecord Venue, поэтому, если мы запрашиваем наш API, вообще не предоставляя никаких фильтров, он должен вернуть все пять, верно? response_json.length должно быть 5, верно?

Подтвердим, что:

$ rspec spec/requests/search_spec.rb:38
Run options: include {:locations= {"./spec/requests/search_spec.rb"=>[38]}}
(byebug) response_json.length
13

Неправильный!

13? Какого черта? Не вызывает ли какой-то фантомный процесс дополнительные места проведения из какой-то параллельной вселенной?

Давайте исследовать. Сначала проверьте идентификатор и имя каждого:

(byebug) response_json.map{|v| [v['id'], v['name']] }
[[2, "Sweet King"], [2, "Sweet King"], [3, "Silver House"], [3, "Silver House"], [3, "Silver House"], [3, "Silver House"], [3, "Silver House"], [3, "Silver House"], [3, "Silver House"], [4, "Golden Eatery"], [4, "Golden Eatery"], [5, "TEJ Curry"], [1, "7090 Spoon"]]

Тогда ладно. Он показывает только пять площадок… но наш API их дублирует.

Почему? Ищи меня. Нам лучше докопаться до сути.

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

(byebug) Venue.count
5
(byebug) Venue.joins(:gigs).count
11
(byebug) Venue.joins(gigs: :act).count
11
(byebug) Venue.left_outer_joins(:gigs).count
13
(byebug) Venue.left_outer_joins(gigs: :act).count
13

Хм-хм! Разве это не любопытно! Хорошо, тогда может показаться, что наши соединения SQL являются (а?) Виновником (могут быть и другие!). Итак, давайте рассмотрим, что такое соединение и почему я счел необходимым добавить в наш запрос какое-либо *_joins вложение.

Итак, давайте рассмотрим необработанные SQL-запросы. Если вы еще не имели удовольствия, Rails предоставляет собственную консоль. Просто введите в свой терминал rails console или сокращенно rails c, и ваше приложение загрузится в этот терминал.

$ rails c
Running via Spring preloader in process 63930
Loading development environment (Rails 5.2.3)
irb(main):001:0> 1+1
=> 2
irb(main):002:0> Rails.env
=> "development"

Здорово. И! Rails предлагает вторую консоль: rails dbconsole, которая открывает терминал непосредственно в любой базе данных, которую вы настроили. В нашем случае это аккуратное маленькое приложение командной строки под названием Psql, которое поставляется вместе с базой данных PostgreSQL.

$ rails dbconsole
psql (11.5)
Type "help" for help.
gigs_development=#

Одна из замечательных вещей, которые делает консоль Rails (но, к сожалению, Byebug не делает) — это распечатка фактического текста SQL, который ActiveRecord генерирует для каждого запроса. Итак, давайте воспользуемся этим, чтобы получить некоторое представление о нашей ошибке. Давайте запустим наши тесты, а затем приостановим их с помощью Byebug в середине теста (чтобы все наши фабричные объекты были добавлены в тестовую базу данных). Затем откройте другую вкладку терминала, запустите dbconsole (для нашей тестовой базы данных), введите строки SQL, изучите возвращенные необработанные данные и посмотрите, какие идеи мы можем получить.

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

Заходим в базу данных разработки! Я буду добавлять .count к каждому запросу, чтобы не тратить тысячи записей Dev-DB, накопленных нашим кодом Scheduler/Cron, который мы создали еще в Статье 8.

$ rails console
Running via Spring preloader in process 65633
Loading development environment (Rails 5.2.3)
irb(main):001:0> Venue.count
(3.8ms)  SELECT COUNT(*) FROM "venues"
=> 561
irb(main):002:0> Venue.joins(:gigs).count
(27.4ms)  SELECT COUNT(*) FROM "venues" INNER JOIN "gigs" ON "gigs"."venue_id" = "venues"."id"
=> 2098
irb(main):003:0> Venue.joins(gigs: :act).count
(16.2ms)  SELECT COUNT(*) FROM "venues" INNER JOIN "gigs" ON "gigs"."venue_id" = "venues"."id" INNER JOIN "acts" ON "acts"."id" = "gigs"."act_id"
=> 2098
irb(main):004:0> Venue.left_outer_joins(:gigs).count
(2.6ms)  SELECT COUNT(*) FROM "venues" LEFT OUTER JOIN "gigs" ON "gigs"."venue_id" = "venues"."id"
=> 2098
irb(main):005:0> Venue.left_outer_joins(gigs: :act).count
(2.6ms)  SELECT COUNT(*) FROM "venues" LEFT OUTER JOIN "gigs" ON "gigs"."venue_id" = "venues"."id" LEFT OUTER JOIN "acts" ON "acts"."id" = "gigs"."act_id"
=> 2098

Сладкий. Теперь мы запускаем нашу спецификацию byebug’d:

$ rspec spec/requests/search_spec.rb:38

Все приостанавливается в середине теста на нашей консоли Byebug. Таким образом, тестовая база данных должна быть заполнена нашими записями FactoryBot. Теперь откройте новую вкладку терминала, откройте консоль Rails DB (и мы заставим ее открываться в тестовом режиме, добавив test к rails dbconsole: rails dbconsole test) — затем вставьте в нее наши строки SQL.

Вот так:

$ rails dbconsole test
gigs_test=# select * from venues;
 id | name | ticketmaster_id
----+------+-----------------
(0 rows)

Что?

Нуль? Почему? Я выполнил этот промежуточный тест, верно? Наши концерты, площадки и акты уже созданы, не так ли?

Сюжет закручивается! (Должны ли толстые люди быть похоронены вместе, как гласит старая шутка).

Поэтому я сильно почесал затылок. Я проверил и перепроверил свои условия тестирования: да, я открыл тестовую консоль базы данных в середине теста; да, запрос Byebug с Venue.count возвращает 5.

Но запрос консоли с select count(*) from venues возвращает 0. Какого черта, чувак?

Немного погуглил меня прямо. Ага. Транзакции.

Транзакция — это вещь в мире SQL. Это способ изолировать потенциально опасные SQL-запросы от остальной части вашего приложения. Идея в том, что представьте, что у вас есть набор запросов, которые вы хотите выполнить. Они могут сломать вещи. У них могут быть побочные эффекты. Вы хотели бы иметь способ изолировать их плохие стороны. Если произойдет что-то ужасное, вам нужен способ безопасно отменить неудачный пакет запросов.

Сделки в помощь! ActiveRecord разработан с нуля для выполнения всех своих запросов внутри транзакций. Просто для безопасности. Byebug работает внутри наших тестовых транзакций. PSql нет. Вот почему мы можем видеть наши записи FactoryBot в Byebug, но не в PSql. Тайна разгадана.

Это здорово и все такое… но что же нам тогда здесь делать? Нельзя ли их как-то отключить? Или просто сдаться?

Конечно, мы можем их отключить, юный кузнечик. Этот ответ StackOverflow ставит нас прямо. Запрыгивайте в spec/rails_helper.rb:40:

# spec/rails_helper.rb
...
RSpec.configure do |config|
  ...
  # If you're not using ActiveRecord, or you'd prefer not
  # to run each of your examples within a transaction, 
  # remove the following line or assign false instead of true.
  config.use_transactional_fixtures = false
  ...
end

Это я сделал. Но значения count оставались ужасно неправильными.

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

Поэтому я также сделал это изменение:

# spec/support/database_cleaner.rb
RSpec.configure do |config|
  
  ...
  config.before :each do
    # Formerly :transaction    
    DatabaseCleaner.strategy = :truncation 
  end
  ...
end

Снова тестирование…

gigs_test=# SELECT * FROM venues;
 id |         name         | ticketmaster_id
----+----------------------+-----------------
  1 | Smokestack Brasserie | egyrrelhip
  2 | Golden Dragon        | olvwokqdge
  3 | 721 Spoon            | qbuffdkfns
  4 | Silver Deli          | xkangcitpl
  5 | Silver Sushi         | execwfxdxk
(5 rows)

Успех!

Отлично, проблема решена. Это очень важно, так что давайте commit и diff. Потрясающий.

Идем дальше. Давайте разберемся с этой ошибкой 5-vs-11-vs-13, давайте выполним и проанализируем наши другие четыре запроса на основе *_joins. Я должен отметить, что я не собираюсь делать select * для всех из них, выбор каждого отдельного столбца сделает результат запроса слишком широким и не поместится на этих страницах Medium. Я выберу ровно столько столбцов, чтобы понять идею.

Во-первых, внутреннее присоединение площадок к концертам.

gigs_test=# SELECT
    venues.id AS venue_id, 
    venues_name AS venue_name, 
    gigs.id AS gig_id 
  FROM venues 
  INNER JOIN gigs ON gigs.venue_id = venues.id;
 venue_id |  venue_name   | gig_id
----------+---------------+--------
        2 | Golden Dragon |      1
        2 | Golden Dragon |      2
        3 | 721 Spoon     |      3
        3 | 721 Spoon     |      4
        3 | 721 Spoon     |      5
        3 | 721 Spoon     |      6
        3 | 721 Spoon     |      7
        3 | 721 Spoon     |      8
        3 | 721 Spoon     |      9 
        4 | Silver Deli   |     10
        4 | Silver Deli   |     11
(11 rows)

Ах. Каждый отдельный SQL-запрос возвращает результаты в виде двумерной таблицы. Таким образом, для того, чтобы это внутреннее объединение перечисляло все 11 концертов, оно должно дублировать некоторые из концертных площадок. Справедливо.

Давайте попробуем присоединиться к площадкам, концертам и выступлениям:

gigs_test=# SELECT 
    venues.id AS venue_id, 
    venues.name AS venue_name, 
    gigs.id AS gig_id, 
    acts.id AS act_id, 
    acts_name AS act_name 
  FROM venues 
  INNER JOIN gigs ON gigs.venue_id = venues.id 
  INNER JOIN acts ON acts.id = gigs.act_id;
 venue_id |  venue_name   | gig_id | act_id | act_name
----------+---------------+--------+--------+----------
        2 | Golden Dragon |      1 |      1 | Da Vinci
        2 | Golden Dragon |      2 |      1 | Da Vinci
        3 | 721 Spoon     |      3 |      1 | Da Vinci
        3 | 721 Spoon     |      4 |      3 | Cassatt
        3 | 721 Spoon     |      5 |      3 | Cassatt
        3 | 721 Spoon     |      6 |      4 | Pollock
        3 | 721 Spoon     |      7 |      4 | Pollock
        3 | 721 Spoon     |      8 |      4 | Pollock
        3 | 721 Spoon     |      9 |      4 | Pollock
        4 | Silver Deli   |     10 |      4 | Pollock
        4 | Silver Deli   |     11 |      5 | Raphael
(11 rows)

Та же сделка. Из Venues, Gigs и Acts именно Gigs имеют наибольшее количество строк в таблице, поэтому, чтобы объединить все три в одной таблице, PostgreSQL должен дублировать различные строки Venue и Act. Справедливо.

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

gigs_test=# SELECT 
    venues.id AS venue_id, 
    venues.name AS venue_name, 
    gigs.id AS gig_id 
  FROM venues 
  LEFT OUTER JOIN gigs ON gigs.venue_id = venues.id;
 venue_id |      venue_name      | gig_id
----------+----------------------+--------
        2 | Golden Dragon        |      1
        2 | Golden Dragon        |      2
        3 | 721 Spoon            |      3
        3 | 721 Spoon            |      4
        3 | 721 Spoon            |      5
        3 | 721 Spoon            |      6
        3 | 721 Spoon            |      7
        3 | 721 Spoon            |      8
        3 | 721 Spoon            |      9
        4 | Silver Deli          |     10
        4 | Silver Deli          |     11
        5 | Silver Sushi         |
        1 | Smokestack Brasserie |
(13 rows)

И вот наш ответ! В нашем запросе нет пункта WHERE.

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

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

Хорошо. Но… что нам остается? Мы не хотим, чтобы наш API возвращал эти 13 строк, не так ли? Нет, сэр. Нам нужны пять площадок плюс список концертов, прикрепленный к каждой. response_json.length==13 это ошибка. Мое решение о левом внешнем соединении было плохим решением.

Проблема заключается в попытке сгенерировать JSON-структуру Venues+Gigs, необходимую для нашего ответа API, с помощью простого SQL-запроса. Неправильный подход. SQL-запросы всегда возвращают только двумерную таблицу данных. Для работы только с площадками потребуется двумерный массив.

В любом случае. Я немного повозился с консолью Rails и записал, какой код ActiveRecord произвел какие SQL-запросы… и оказалось, что для возврата ровно пяти мест с наименьшим количеством SQL-запросов мы просто вообще отказываемся от joins. Вместо этого вы используете это: Venue.includes(gigs: :act).

Что это за includes? Это решение так называемой проблемы N+1. Полное объяснение проблемы N+1 — это еще одна Вещь, выходящая за рамки, так что просто прочитайте это, если вас это интересует. А чтобы ознакомиться с официальной документацией по Rails, читайте это. Сладкий.

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

Должны ли мы сейчас? Вместо тестирования:

it { is_expected.to include(
  *JSON.parse([venue1, ..., venue5].to_json(include: :gigs))
)}

Почему бы вместо этого не проверить это?

it { is_expected.to eq (
  JSON.parse([venue1, ..., venue5].to_json(include: :gigs))
)}

Ожидайте ровно пять площадок, ни больше, ни меньше — и именно в таком порядке.

Кажется, на сегодня достаточно. В следующий раз мы займемся рефакторингом наших тестов.