AlterEGO Labs official blog

About ruby, rails and another amazing stuffs…

Пример TDD разработки библиотеки на Ruby

Лирическое отступление

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

Lets go!

Задача

Сколько людей - столько и мнений. Наверное сейчас очень сложно придумать такую задачу, чтобы она была уникальной. Поэтому появилось устоявшееся выражение - писать велосипед. Вот таким велосипедом я сейчас и займусь (я даже специально не гуглил на эту тему, чтобы заранее себя не расстраивать). А проблема вот какая:

Последнее время очень часто возникает необходимость сливать базу данных с продакшена и накатывать ее на девелопменте, т.к. править различные штуки удобнее локально. Для этого приходится делать достаточно много шагов: зайти по ssh и сделать дамп базы, зайти через Filezilla и скачать этот дамп, затем накатить его на dev базу. И тут я задался вопросом ведь яжпрограммист или кто?!

Поэтому я решил написать на ruby утилиту, которая поможет мне автоматизировать этот процесс.

Я понимаю, что это велосипед, но ведь он свой :-)

Цель

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

Листинг 1 - Идея
1
2
3
4
5
6
DbFetcher.steps do
  step ssh_login: { host: 'some.com', username: 'user', password: 'password' }
  step ssh_run: { command: '...' }
  step sftp: { serverpath: '...', localpath: '...' }
  step local_run: { command: '...' }
end

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

Будем пробовать TDD

Есть многие вещи, которые ты не поймешь зачем они нужны, пока ты сам до этого не дойдешь, даже если какой-нибудь гуру будет тебе говорить, что это классно. Такая история и с TDD. Сама идея писать код для проверки кода звучит дико, не правда ли? Но не нужно смотреть на TDD только через призму, того что нужно писать дополнительный код, ведь программисты и так ленивые. Это целая методология со своими условными ‘правилами’ и ее главная цель уменьшить вашу головную боль. Но не буду заострять внимание на этом. Просто будте готовы писать тесты!

Ну-с начнем

Самый сложный момент. С чего начать, когда есть только идея? Существуют два варианта движения:

  1. Inside-out
  2. Outside-in

Выбор зависит индивидуально от программиста (смотря как у кого работает голова), а также от того, какую информацию содержит ваша первоначальная идея (листинг 1).

Давайте еще раз посмотрим, что у нас есть - это пример АПИ, которое должна предоставлять библиотека или, другими словами, ее outside часть. Для нас очевиден путь (по крайней мере я выбрал этот вариант и возможно к концу написания библиотеки пойму, что он был неудобный) outside-in.

Первые шаги

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

1
2
3
<project_root>
|- lib
|- test

Я думаю тут вопросов не должно возникнуть)

В первую очередь давайте настроем наше test environment. Надеюсь вы заметили, что я ни слова не сказал насчет какого-нибудь фреймворка? Да, вы правильно поняли - это будет проект на чистом ruby. Поэтому любые настройки придется делать вручную. Но это даже к лучшему! Так вот, если взять rails, то там уже из коробки идет настроенный unit testing, но я всегда сразу устанавливал Rspec, и файл с настройками назывался test_helper или spec_helper (для rspec). Давайте и у нас создадим такой файл, в котором будут содержаться общие настройки для всех наших тестов:

Листинг 2 - test/test_helper.rb
1
2
3
4
5
require 'minitest/autorun'

Dir[File.dirname(__FILE__) + '/../lib/**/*.rb'].each do |file|
  require file
end

Что мы здесь сделали?

  1. Подключили фреймворк для unit тестирования - Minitest, который идет в ruby stdlib
  2. Загрузили все файлы из папки lib

И вот теперь, наконец-то, можно приступать к написанию кода! А если точнее - теста. Вот тут начинается еще одна интересная вещь - а что тестировать, если ничего нет?

Небольшое отступление. Сейчас мне в голову пришла мысль и я хочу немного изменить и дополнить нашу первоначальную идею:

Листинг 3 - Идея измененная
1
2
3
4
5
6
7
runner = DbFetcher.define_runner do
  step ssh_login: { host: 'some.com', username: 'user', password: 'password' }
  step ssh_run: { command: '...' }
  step sftp: { serverpath: '...', localpath: '...' }
  step local_run: { command: '...' }
end
runner.run

И теперь стало немного понятнее с чего нам начать. Как я уже говорил выше я выбрал путь outside-in, т.е. мы будем двигаться в сторону уточнения внутренней реализации. Поэтому начнем с теста внешнего АПИ. Да, я не опечатался - начнем с ТЕСТА)

Листинг 4 - Первая проба написания теста test/lib/db_fetcher_test.rb
1
2
3
4
5
6
7
require_relative '../test_helper'

class DbFetcherTest < Minitest::Test
  def test_define_new_runner
    assert_instance_of DbFetcher::Runner, DbFetcher.define_runner {}
  end
end

Сейчас давайте разберем листинг 4:

  1. Мы заинклудили наш test_helper, поэтому в данном контексте доступен Minitest
  2. Написали тест, который проверяет правильно работы метода define_runner (здесь лучше сказать, что мы проверяем не правильность, а то, какой результат ожидаем после вызова данного метода). Все, что мы можем сейчас проверить - метод define_runner возвращает новый экземпляр класса DbFetcher::Runner.

А давайте запустим наш тест и посмотрим результат? (подсказка: для запуска теста набирайте ruby test/lib/db_fetcher_test.rb):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Run options: --seed 60105

# Running:

E

Finished in 0.001982s, 504.5409 runs/s, 0.0000 assertions/s.

  1) Error:
DbFetcherTest#test_define_new_runner:
NameError: uninitialized constant DbFetcherTest::DbFetcher
    test/lib/db_fetcher_test.rb:10:in `test_define_new_runner'

1 runs, 0 assertions, 0 failures, 1 errors, 0 skips

Упс… Но все правильно! Ведь кода у нас еще нет! Сейчас мы должны написать минимум кода для того, чтобы удовлетворить данный тест.

Листинг 5 - lib/db_fetcher.rb
1
2
3
4
5
6
7
module DbFetcher
  extend self

  def define_runner(&block)
    Runner.new
  end
end

Так-с… Попытка следующая:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Run options: --seed 17668

# Running:

E

Finished in 0.001540s, 649.3506 runs/s, 0.0000 assertions/s.

  1) Error:
DbFetcherTest#test_define_new_runner:
NameError: uninitialized constant DbFetcher::Runner
    test/lib/db_fetcher_test.rb:10:in `test_define_new_runner'

1 runs, 0 assertions, 0 failures, 1 errors, 0 skips

Вот черт! Совсем забыл про Runner. Давайте определим и его!

Листинг 6 - lib/db_fetcher/runner.rb
1
2
3
4
module DbFetcher
  class Runner
  end
end

А вот теперь скрестили пальцы и-и-и-и:

1
2
3
4
5
6
7
8
9
Run options: --seed 57197

# Running:

.

Finished in 0.001258s, 794.9126 runs/s, 794.9126 assertions/s.

1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

Отлично! 1 assertions значит, что наш тест прошел! Фууух.

Итерационность

Главное слово в этом процессе - итерационность. После того, как вы добились чтобы тест прошел, можете писать тест дальше. Затем вы опять пишете код, удовлетворяющий новые тесты, но и не сломал предыдущие! И этот процесс повторяется снова и снова. Самой главной величиной этого процесса является размер вот этого шага или количество логики, заложенной в тесте, после написания которого вы приступаете к написанию кода. Здесь нет никаких пожеланий и вступает в силу индивидуальность разработчика. Каждый выбирает этот шаг так, чтобы было комфортно: если напишите много тестов - зациклитесь на написании кода, а также есть вероятность сделать тесты неактуальными, т.к. при их написании не учли каких-либо архитектурных моментов; если напишите мало - то придется слишком часто переключать ваш контекст между кодом и тестами, что тоже не гуд, т.к. опять же можно что-то упустить.

Продолжим?

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

Спасибо за внимание!

UPDATE. Как и обещал, выкладываю ссылку на репозитарий db_fetcher. Библиотека еще будет дорабатываться, но базовые концепции уже реализованы и протестированы.

Comments