Как написание тестов для вашего будущего я улучшит ваши тесты

kak napisanie testov dlya vashego budushhego ya uluchshit vashi testy?v=1656576490

Практикуя разработку, управляемую тестированием (TDD), мы иногда склонны сосредоточиться на тестировании все. Этот менталитет 100% охват иногда может привести к тому, что мы слишком усложняем вещи.

Раньше я был тем, кто управлял тестированием DRY-er, потому что я ненавидел видеть повторяющийся код. Тогда я был новичком в метапрограммировании на Ruby, и я всегда хотел сделать вещи попроще, смешав повторяющийся код и придумав монстра. Показательный случай:

describe 'when receiving the hero details' do
  it 'should have the top level keys as methods' do
    top_level_keys = %w{id name gender level paragonLevel hardcore skills items followers stats kills progress dead last-updated}

    top_level_keys.each do |tl_key|
      @my_hero.send(tl_key).must_equal @my_hero.response[tl_key.camelize(:lower)]
    end
  end

Ладно, это было еще в 2012 году. Как фон, это был драгоценный камень Ruby (похож на пакет npm) для API Diablo 3 от Blizzard. Так что я здесь тестировал? Прочитав код, он кажется достаточно обычным: он гласит, что ключи верхнего уровня могут быть способами. Итак, если API возвращал что-то вроде:

{
  paragonLevel: 10,
  hardcore: true,
  kills: 1234
}

Тогда учитывая экземпляр героя, я могу просто вызвать их как методы, и он должен возвращать их так:

> hero = Covetous::Profile::Hero.new 'user#1234', '1234'
> hero.paragon_level # 10
> hero.kills # 1234

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

Проблема

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

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

Тестовый код является непроверенным кодом.

ТЕСТОВЫЙ КОД НЕ ПРОВЕРЕН КОД.

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

Итак, вернемся к моему тесту. Если я правильно помню, сначала я делал методы один за другим. Затем я увидел шаблон, который заставил меня подумать, что это будет одинаковый шаблон для всех методов, по крайней мере, почему бы не сделать код DRY-er?

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

@my_hero.send(tl_key).must_equal @my_hero.response[tl_key.camelize(:lower)]

в Ruby, send вызывает передаваемую строку как метод. Так что если tl_key‘s значение было paragonLevel (из массива), эта строка в основном говорит:

@my_hero.paragonLevel.must_equal @my_hero.response['paragonLevel']

Вот здесь я снова сомневаюсь в себе. Мой README говорит, что так должно быть @my_hero.paragon_levelно, глядя на тест, это не так. Кому теперь мне доверять? Мои тесты, которые проходят, или мои README? Это точная причина, почему метапрограммирование в тестах опасно — вы никогда не знаете, проходят ли ваши тесты, то ли из-за того, что они правильные, или из-за того, что вы как-то неправильно его настроили. Это почти то же, что НЕ писать тесты!

Делаем это лучше

Как бы я это переписал? С тех пор я узнал, что написание тестов для моя десятилетняя сама было бы достаточно. То есть, я десять лет назад. Я всегда спрашиваю себя: «Смогу ли я понять это через десять лет без контекста?» Если нет, то это значит, что мне или нужно написать примечание в комментариях или мой тест слишком сложен.

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

# Given I queried my hero against the API:
let(:my_hero) { Covetous::Profile::Hero.new 'corroded-6950', '12345678' }
it 'should have the top level keys as methods' do
  expect(my_hero.id).to eq 12345
  expect(my_hero.name).to eq 'corrodeath'
  expect(my_hero.gender).to eq 'female'
  expect(my_hero.level).to eq 70
  ...
end

Видите, как это откровенно? Это, конечно, повторяется, но через 10 лет я уверен, что все равно пойму, чего были мои ожидания. Мне не нужно «компилировать и интерпретировать» код в своем мозге. Я только что прочитал характеристики!

Кроме того, с этим мне даже не пришлось вспоминать что camelize(:lower) в самом деле так (признание: мне пришлось искать его, когда я читал свой старый код).

Как насчет другого примера? Итак, мы имеем модель:

class Something < ActiveRecord::Base
  VALID_THINGS = %w(yolo swag)
  OTHER_VALID_THINGS = %w(thing another_thing)
  def valid_things_ids
    where(group: group).pluck(:id)
  end
end

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

subject(:valid_things_ids) { described_class.valid_things_ids(group) }

let(:group) { 'example' }

before do
  described_class::VALID_THINGS.each do |thing|
    FactoryGirl.create(:something, group: 'example', name: thing)
  end
end

described_class::VALID_THINGS.each do |thing|
  it "contains things with the name #{thing}" do
    the_thing = described_class.find_by_group_and_name('example', thing)
    expect(valid_things_ids).to include the_thing.id
  end
end

Хорошо. Во-первых, это правильный тест, в котором дан ряд somethingsмы можем вызвать метод, и он возвращает нам все идентификаторы somethings с этой группой (напр. example).

Однако моя проблема состоит в том, нужно ли нам проверить все действительные вещи? Как насчет OTHER_VALID_THINGS? Если мы хотим проверить все возможные значения VALID_THINGS то мы также должны проверить все возможные значения OTHER_VALID_THINGS. Если мы НЕ хотим проверять все возможные значения, то зачем использовать VALID_THINGS? Почему бы просто не создать случайную выборку и просто доказать, что метод работает?

Как насчет чего-нибудь такого?

subject(:valid_things_ids) { described_class.valid_things_ids(group) }

let(:group) { 'blurb' }

let!(:random_thing) { FactoryGirl.create(:something, group: 'blurb', id: 111) }
let!(:another_thing) { FactoryGirl.create(:something, group: 'blurb', id: 222) }
let!(:not_included) { FactoryGirl.create(:something, group: 'shrug', id: 333) }

it do
  expect(valid_things_ids).to include 111
  expect(valid_things_ids).to include 222
  expect(valid_things_ids).not_to include 333
end

Итак, я создаю 3 somethings и дать им идентификаторы. Я заставляю третью иметь другую группу. Теперь, если я запускаю метод из blurb как аргумент, я могу ожидать, что он включает в себя первые два, а не последний.

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

Также обратите внимание на наличие теста. Я ожидаю, что он будет включать идентификаторы 111 и 222. Обычно люди тестируют это так:

expect(valid_things_ids).to include random_thing.id

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

Подведению

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

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *