Как сделать так, чтобы эффективный код появлялся через модульное тестирование? - PullRequest
3 голосов
/ 13 апреля 2010

Я участвую в додзё TDD Coding, где мы пытаемся отработать чистый TDD на простых задачах. Однако мне пришло в голову, что код, полученный из модульных тестов, не самый эффективный. Теперь это нормально в большинстве случаев, но что, если использование кода растет, и эффективность становится проблемой.

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

Вот тривиальный пример в ruby: простая факторизация. Я следовал чистому подходу TDD, заставляя тесты проходить один за другим, подтверждая мой первоначальный приемочный тест (прокомментированный внизу). Какие дальнейшие шаги я мог бы предпринять, если бы я хотел, чтобы появился один из общих алгоритмов простой факторизации ? Чтобы уменьшить проблемную область, скажем, я хочу получить реализацию quadratic sieve ... Теперь в этом точном случае я знаю «оптимальный алгоритм», но в большинстве случаев клиент просто добавляет требование, что функция выполняется менее чем за «х» времени для данной среды.

require 'shoulda'
require 'lib/prime'

class MathTest < Test::Unit::TestCase
  context "The math module" do
    should "have a method to get primes" do 
      assert Math.respond_to? 'primes'
    end
  end
  context "The primes method of Math" do
    should "return [] for 0" do
      assert_equal [], Math.primes(0)
    end
    should "return [1] for 1 " do
      assert_equal [1], Math.primes(1)
    end
    should "return [1,2] for 2" do 
      assert_equal [1,2], Math.primes(2)
    end
    should "return [1,3] for 3" do 
      assert_equal [1,3], Math.primes(3)
    end
    should "return [1,2] for 4" do 
      assert_equal [1,2,2], Math.primes(4)
    end 
    should "return [1,5] for 5" do 
      assert_equal [1,5], Math.primes(5)
    end   
    should "return [1,2,3] for 6" do 
      assert_equal [1,2,3], Math.primes(6)
    end       
    should "return [1,3] for 9" do 
      assert_equal [1,3,3], Math.primes(9)
    end        
    should "return [1,2,5] for 10" do 
      assert_equal [1,2,5], Math.primes(10)
    end                  
  end
#  context "Functionnal Acceptance test 1" do
#    context "the prime factors of 14101980 are 1,2,2,3,5,61,3853"do      
#      should "return  [1,2,3,5,61,3853] for ${14101980*14101980}" do
#        assert_equal [1,2,2,3,5,61,3853], Math.primes(14101980*14101980)
#      end
#    end
#  end
end

и наивный алгоритм, который я создал этим подходом

module Math
  def self.primes(n)
    if n==0
      return []
    else
      primes=[1]  
      for i in 2..n do
        if n%i==0          
          while(n%i==0)
            primes<<i
            n=n/i
          end
        end
      end      
      primes
    end
  end
end

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

edit 2 Я знаю, как проверить время выполнения, но кажется, что переход от тривиального алгоритма к оптимизированному - огромный шаг. Мой вопрос заключается в том, как сделать оптимальный код emerge , другими словами: как вы декомпозируете переход от тривиального кода к оптимальному? Некоторые упоминали, что это подход к проблеме: я предоставил пример проблемы, для которой я не знаю, как продолжить.

Ответы [ 7 ]

4 голосов
/ 18 апреля 2010
  • TDD для правильности и без регрессии и фокусировки на модульном тестировании
  • Профилирование предназначено для производительности , и это функциональное тестирование проблема.

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

При использовании TDD в Dojo мы стараемся следовать приведенным ниже правилам

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

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

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

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

Я считаю, что производительность цель - это, в основном, функциональное тестирование .

3 голосов
/ 18 апреля 2010

TDD не может помочь вам вывести алгоритмы - если это ваш вопрос. Это одна из тех нишевых областей, которая не поддается сильным сторонам TDD (ниша: по сравнению с полчищами, производящими корпоративное программное обеспечение, которое вызывает миллионы фреймворков / библиотек).

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

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

Обновление: подтверждающие доказательства. http://www.markhneedham.com/blog/2009/12/10/tdd-big-leaps-and-small-steps/ Есть еще несколько - однако они похожи на дискуссии на Reddit. самоуверенный, неподдерживаемый, свободный для всех. Не публикуйте их.

3 голосов
/ 13 апреля 2010

Нет, и вы не должны пытаться. Модульные тесты проверяют правильность, а не эффективность - принуждение их к тестированию эффективности является формой преждевременной оптимизации.

2 голосов
/ 13 апреля 2010

В модульном тестировании обычно проверяется частичная корректность функциональности.

Конечно, вы можете добавить временные ограничения в модульный тест, но вам может быть трудно выразить их независимыми от технологии способами (например, O (N ln N).

И то, что вы можете написать модульный тест, который настаивает на том, чтобы результат доставлялся в постоянное время, не означает, что кодировщик устройства может обязательно найти алгоритм, который достигает этого эффекта. (Конечно, он может и не думать о том, как правильно реализовать эту функциональность.

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

1 голос
/ 13 апреля 2010

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

start_time = date_time_now();
Math.primes(1000);
stop_time = date_time_now();
assert stop_time-start_time < target_execution_time;

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

Кроме того, elapsed_time - это всего лишь один пример использования показателя эффективности. Другие показатели для тестирования включают cpu_time, пропускную способность, переданные байты ввода / вывода и т. Д.

0 голосов
/ 18 апреля 2010

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

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

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

TEST: should be faster than O(n^2)
setup: baseline_time_for_10 = time_of( f(10) )
100: assert time_of(f(100)) < baseline_time_for_10 ^ 2    
etc.

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

0 голосов
/ 13 апреля 2010

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...