Статьи

NET для Ruby: обучение написанию тестов, часть II

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

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

Тесты помогли мне выучить Ruby

Как я уже упоминал в своем посте, поскольку Ruby является динамическим языком, я не мог полагаться на то, что компилятор даст мне хоть какое-то указание относительно того, где я что-то напутал. Я мог бы использовать Interactive Ruby Shell (IRB), чтобы попробовать что-то (и я делаю это время от времени), но я бы лучше написал тесты. Почему? Потому что они становятся «исследовательскими» тестами. Они позволяют мне испытывать вещи, пока я не получу тесты. Как только они это сделают, это становится документацией последней вещи, которую я попробовал, которая действительно работала.

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

Выбор тестового фреймворка

Как и в сообществе .NET, в сообществе Ruby также имеется множество тестовых сред. Я решил выбрать тот, который использует большинство моих друзей: RSpec . Я выучил только минимальное, просто чтобы обойтись, не слишком волнуясь, был ли это «путь Руби» или нет. Пока я мог создавать экземпляры классов, вызывать методы и утверждать некоторые ожидания, жизнь была хорошей. Итак, я начал писать такие тесты:

describe Calculator do

  it "should sum two numbers" do
    calculator = Calculator.new
    result = calculator.sum(1, 1)
    result.should == 2
  end

  it "should subtract two numbers" do
    calculator = Calculator.new
    result = calculator.subtract(5, 3)
    result.should == 2
  end
end

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

При выполнении тестов я могу передать переключатель в RSpec и сказать ему отформатировать вывод как документ, и это выглядит так:

 Calculator
  should sum two numbers
  should subtract two numbers

И если какой-либо из моих примеров (кстати, каждый тест называется в RSpec) не удался, я получаю вывод такого типа:

 Calculator
  should sum two numbers (FAILED - 1)
  should subtract two numbers

Как только я набрал обороты, я начал искать способы немного улучшить свой тестовый код. Я надеялся походить на лучшие тесты, которые я писал на C #, с SubSpec , методами расширения «следует» и тому подобное. Немного улучшенные тесты стали выглядеть так:

 describe Calculator do
  context "Given a calculator" do
    before(:each) do
      @calculator = Calculator.new
    end

    describe "when told to add two numbers" do
      it "should return the sum of the two numbers" do
        result = @calculator.sum(1, 1)
        result.should == 2
      end
    end

    describe "when told to subtract two numbers" do
      it "should return the difference of the two numbers" do
        result = @calculator.subtract(5, 3)
        result.should == 2
      end
    end

  end
end

Несколько вещей, чтобы заметить:

  • Использование контекста для предоставления «данной» части моей спецификации
  • Использование before (: each) для настройки моего контекста перед каждым примером (каждым тестом)

С такой структурой мой тестовый вывод выглядит так:

 Calculator
  Given a calculator
    when told to add two numbers
      should return the sum of the two numbers
    when told to subtract two numbers
      should return the difference of the two numbers

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

 describe Calculator do

  context "Given a calculator" do

    before(:each) do
      @calculator = Calculator.new
    end

    describe "when told to add 1 + 1" do

      before(:each) do
        @calculator.sum(1, 1)
      end

      it "should have two input values" do
        @calculator.input_values.length.should == 2
      end

      it "should have last result of 2" do
        @calculator.last_result.should == 2
      end
    end

  end
end

Вывод для этих спецификаций все еще выглядит хорошо:

 Calculator
  Given a calculator
    when told to add 1 + 1
      should have two input values
      should have last result of 2

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

 describe Calculator do

  context "Given a calculator" do

    subject { Calculator.new }

    describe "when told to add 1 + 1" do

      before { subject.sum(1, 1) }

      it { should have(2).input_values }
      its(:last_result) { should == 2 }
    end

  end
end

На что обратить внимание:

  1. Вместо того, чтобы создавать экземпляр Customer и сохранять его в переменной экземпляра в before (: each) , я использую предметный блок, в котором хранится то, что возвращается из него в свойство субъекта в спецификации;
  2. Вместо того, чтобы вызывать тестируемый метод (например, subject.sum (1, 1)) в before (: each) , я просто использую before {} , который делает то же самое, но немного короче;
  3. Вместо того, чтобы предоставлять строки для блоков it , я позволил RSpec создавать строки на основе того, какой код Ruby находится внутри блока (вы скоро увидите результаты);
  4. это по сути относится к субъекту ;
  5. Вместо проверки длины массива input_values я использую * have * matcher. В конце дня RSpec превращает * должен иметь (2) .input_values ​​* в subject.input_values.length.should == 2 , но мне нравится, как первое читается намного лучше;
  6. Чтобы проверить свойство, которое свисает с субъекта , я использую его метод (например, его (: last_result) {should == 2}

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

 Calculator
  Given a calculator
    when told to add 1 + 1
      should have 2 input_values
      last_result
        should == 2

Все это было замечательно, и это было связано с тем, что я делал в .NET (но с гораздо лучшим синтаксисом!). Тем не менее, я все еще фокусировался исключительно на «объектном» поведении. Пользователям не важно, как ведет себя «объект»; они заботятся о том, как ведет себя «приложение».

Так вот что такое BDD …

Я говорил об идее «Учитывая некоторый контекст, когда что-то происходит, тогда некоторое ожидание должно быть удовлетворено» . Важной частью, которая отсутствует в этом, является ответ на следующий вопрос: «Какова ценность бизнеса в том ожидании, которое оправдывается?». Я знал о написании пользовательских историй, таких как:

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

Я действительно не мог сказать, как это могло быть включено в мой рабочий процесс как разработчик. Я упускал из виду весь смысл «перестать думать об объектах и ​​начать думать о реальной стоимости бизнеса за определенной функцией».

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

Следующая остановка: огурец.

Исполняемые приемочные испытания

Через несколько недель после написания тестов в RSpec я научился писать «функциональные» тесты (или «тематические истории») в Cucumber . Огурец построен на основе RSpec, поэтому освоить его было несложно, поскольку я уже кое-что знал. Художественная история выглядит примерно так (пример прямо с сайта Cucumber):

 Feature: Addition
  In order to avoid silly mistakes
  As a math idiot
  I want to be told the sum of two numbers

  Scenario: Add two numbers
    Given I have entered 50 into the calculator
    And I have entered 70 into the calculator
    When I press add
    Then the result should be 120 on the screen

Довольно простой английский. Он содержит часть «Для того, чтобы… как… я хочу…», в ​​которой описывается коммерческая ценность данной функции, и «сценарии», описанные как «дано, когда». Эта часть уравнения охватывает то, что нужно пользователю, людям, для которых вы создаете приложение.

Откуда приходит Ruby или RSpec ? В реализации этих сценариев «шаги»:

 Given /I have entered (.*) into the calculator/ do |n|
  calculator = Calculator.new
  calculator.push(n.to_i)
end

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

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

 class Calculator
  def push(n)
    @args ||= []
    @args << n
  end
end

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

Снаружи

Будут времена, когда мои объекты потребуют более тщательных и детальных испытаний. В этот момент я захожу в RSpec и пишу там свои тесты. Другими словами, с помощью «историй о функциях», созданных с помощью Cucumber, я тестирую поведение «приложения», при этом следя за тем, чтобы у объекта был самый минимальный объектный API для включения этой функции. С другой стороны, с помощью спецификаций, созданных с использованием RSpec, я тестирую поведение своих «объектов», при этом следя за видами сотрудничества и зависимостями, которые нужны моим объектам для выполнения своей работы.

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

Создание функции без запуска приложения

В прошлом я создал несколько приложений, таких как это:

  1. Создать вид
  2. Запустите приложение
  3. Просматривайте многочисленные меню и экраны, пока я не доберусь до представления, созданного на шаге 1
  4. Зайдите в код и отбросьте точку останова
  5. Нажмите на кнопку в приложении
  6. Подождите, пока эта точка останова не будет достигнута
  7. Зайдите в отладчик и посмотрите вокруг
  8. Повторите шаги несколько раз, и в конце концов моя функция выполнена

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

Построение и тестирование API REST

Совсем недавно я закончил работу по созданию REST API в одном из моих приложений на Rails. Этот API обслуживал JSON, используемый мобильными приложениями. Я описал API с историями функций Cucumber, которые работали как документация для моего API и имели приятный «побочный эффект» — автоматические тесты для обеспечения работы API.

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

Вот небольшой пример функции, поддерживаемой этим API:

 Feature: User authentication

  In order to only allow authenticated users to use our applications
    As an API client
    I want to be able to authenticate a user based on his credentials

  Scenario: Authentication Failed

  Given invalid credentials
    When I send a request for user authentication
    Then the response should be "200"
     And I should receive message indicating authorization failure

  Scenario: Authorization Succeeded

  Given valid credentials
    When I send a request for user authentication
    Then the response should be "200"
     And I should receive message indicating authorization success

И вот пример того, как эти шаги реализованы:

 Given /^invalid credentials$/ do
  @login = "INVALID_LOGIN"
  @password = "INVALID_PASSWORD"
end

Given /^valid credentials$/ do
  @user = Factory(:athlete_user)
  @login = @user.email
  @password = @user.password
end

When /^I send a request for user authentication$/ do
  post "api/v3/user_authentication.json?login=#{@login}&password=#{@password}"
end

Then /^I should receive message indicating authorization success$/ do

  success = {:user_authentication =>
               {:status => "success",
                :errorcode => nil,
                :errormessage => nil,
                :login => @login,
                :profile => 'athlete'}}

  last_response.body.should == success.to_json
end

Then /^I should receive message indicating authorization failure$/ do

  error = {:user_authentication =>
               {:status => "failed",
                :errorcode => "UNKNOWN_LOGIN",
                :errormessage => "Invalid credentials.",
                :login => nil,
                :profile => nil}}

  last_response.body.should == error.to_json
end

Время, которое я потратил на написание этого текста, того стоит. Опять же, он работает как документация для моего API во время тестирования API, чтобы убедиться, что он работает.

Это то, что делают все разработчики Ruby?

Нет !! Это не то, что делают все разработчики Ruby. Некоторым разработчикам совсем не нравятся RSpec и / или Cucumber, и вместо этого они используют другие тестовые среды. Люди используют то, что имеет для них смысл. То, что я описываю здесь, — это то, что я считаю лучшими процессами и инструментами для работы, которую я делаю, в проектах, над которыми я сейчас работаю. Это определенно не универсальное решение, и это не значит, что я не постоянно ищу разные вещи, которые люди делают или используют.

Я пишу тесты для всего ?

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

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

Ресурсы

Лучшие ресурсы, которые я использовал, чтобы узнать больше о теме, которую я обсуждал здесь, были The RSpec Boook и The Cucumber Book . Я продолжаю слышать хорошие вещи о спецификации по примерам , так что это в моем списке «для чтения».

Чем ты занимаешься?

Я хотел бы узнать о вашем опыте тестирования, SubSpec, RSpec, Cucumber, TDD, BDD … Все, что мы затронули в этом сообщении в блоге, действительно. Что сработало для вас? Что не сделал? Почему? Какой опыт вы пробовали? Часто я слышу, как люди говорят: «Я попробовал это испытание, но у меня это не сработало». Ну, этого не достаточно. Было бы здорово узнать, как именно человек попробовал это сделать, какой тип приложения, язык, клиент, контекст и т. Д. Я знаю, что несколько попыток не работали, но я бы поговорил со сверстниками о своем опыте. и чаще всего мы не определяем вероятные причины того, почему это не сработало, поэтому я мог внести исправления и повторить попытку. 🙂