Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

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


Информационный Канал Subscribe.Ru

Мир экстремального программирования

Новая статья

Метрика, ведущая к гибкости.
Данная статья, от одного из авторов экстремального программирования Рона Джеффриса, поможет найти ответы на давно уже наболевшие вопросы: "Почему XP лучше?" и "Зачем платить больше?"
Читать всё...

Экстремальное программирование - бой ошибкам

С помощью модульного тестирования

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

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

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

Итак, сначала определимся с интерфейсом класса. Назовём его Vote. Кому не жалко времени, может сделать это в Rose, я обычно предпочитаю карандаш и бумагу, или сразу начисто, в редакторе кода. Настроение, навеянное статьями сайта Microsoft, подталкивает меня сделать пример на C#. Правда лучше C++ пока ещё никто ничего не придумал.

public class Vote
{
 public void Add(int vote)
 {
 }

 public int CalculateRating(int vote)
 {
 }

 public int CalculateAverageRating()
 {
 }

 public int GetRatesCount()
 {
 }
}

Теперь заводим отдельный проект для тестовых случаев, как это делается, я описывал в статье "Внедрение ведомого тестированием метода разработки для языков .Net". Определяем класс VoteTest, для тестовых случаев, и сразу же помозговому атакуем его на предмет возможных тестовых ситуаций.

[TestFixture]
public class VoteTest : Assertion
{
 [Test]
 public void TestHystogram()
 {
  Assert(false);
 }
 [Test]
 public void TestRatesCount()
 {
  Assert(false);
 }
 [Test]
 public void TestAverageRating()
 {
  Assert(false);
 }
}

Да, можно было и не мозговать, а просто добавить слово Test ко всем методам, определённым в Vote. Assert(false) нужно просто чтоб не забыть, что тест ещё не готов, своеобразный "to do list". Начнём с TestRatesCount, т.к. он кажется мне самым простым.

public void TestRatesCount()
{
 Vote vote = new Vote();
 int nVotes = 26;
 for(int i = 0; i < nVotes; i++)
 {
  vote.Add(9);
 }
 AssertEquals(nVotes, vote.GetRatesCount());
}

Компилируем, появились первые ошибки. Исправляем их простым добавлением return 0; где нужно в классе Vote. Теперь всё будто бы успешно компилируется. Запускаем NUnit, кто ещё не установил, может найти последнюю версию на сайте nunit.org. Все тесты светятся красным, что свидетельствует об их работоспособности. Теперь наша задача добавить в класс Vote функциональность, требуемую этим тестом.

Первое, что приходит на ум - это добавить объект ArrayList в наш класс:

protected ArrayList votesList = new ArrayList();

Сразу же добавим к нему property accessor, как этого требуют правила хорошего кода:

public IList VotesList
{
 get
 {
  return this.votesList;
 }
}

Методы Add и GetRatesCount будут содержать всего лишь по одной строке:

public void Add(int vote)
{
 VotesList.Add(vote);
}
public int GetRatesCount()
{
 return VotesList.Count;
}

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

Переходим к следующему тесту, своей простотой меня привлекает тест среднего значения голосов: TestAverageRating.

[Test]
public void TestAverageRating()
{
 Vote vote = new Vote();
 int[] testVotes = {2, 4, 3, 3, 2, 4}; // avg = 3
 foreach(int value in testVotes)
 {
  vote.Add(value);
 }
 AssertEquals(3, vote.CalculateAverageRating());
}

Сразу же просится тест на правильность округления:

[Test]
public void TestAverageRatingRoundingUp()
{
 Vote vote = new Vote();
 int[] testVotes = {2, 3, 3}; // avg = 2.66
 foreach(int value in testVotes)
 {
  vote.Add(value);
 }
 AssertEquals(3, vote.CalculateAverageRating());
}
[Test]
public void TestAverageRatingRoundingDown()
{
 Vote vote = new Vote();
 int[] testVotes = {4, 4, 5}; // avg = 4.33
 foreach(int value in testVotes)
 {
  vote.Add(value);
 }
 AssertEquals(4, vote.CalculateAverageRating());
}

В этих трёх методах явно видно дублирование, паттерн рефакторинга Выделить метод может помочь в этом. В следующем выпуске мы рассмотрим более подробно, как рефакторинг влияет на качество программы.

Займёмся реализацией метода CalculateAverageRating, легко ощутимая цель: выполнение последних трёх тестов. Пытаемся её достичь, определив метод следующим образом:

public int CalculateAverageRating()
{
 int sum = 0;
 foreach(int value in VotesList)
 {
  sum += value;
 }
 return sum / GetRatesCount();
}

Запускаем тесты, чтобы проверить, действительно ли мы добились желаемой цели. Они нас убеждают в обратном: тест округления к большему не пройден. Но настойчивости нам не занимать, попробуем определить, в чём же проблема. Сразу видно, что целочисленное деление нам не подходит, поэтому преобразуем тип переменной sum к double:

double sum = 0;

Результат не изменился, тест всё ещё не проходит. Тогда придётся явно указать процедуру округления:

public int CalculateAverageRating()
{
 double sum = 0;
 foreach(int value in VotesList)
 {
  sum += value;
 }
 double avg = sum / GetRatesCount();
 return (int)Math.Round(avg);
}

Всё, цель достигнута, тесты выполняются.

Направленность на результат в таких мелочах непременно воспитывает дисциплинированность и целеустремлённость в работе и жизни.

Остались ещё один тест, и ещё одна функциональность: данные для построения гистограммы оценок.

От функции CalculateRating мы ожидаем процентное соотношение той или иной оценки к общему числу голосов. Так и запишем:

[Test]
public void TestHystogram()
{
 Vote vote = new Vote();
 int[] testVotes = {2, 4, 3, 3, 2, 4, 5, 2, 2, 2};
 int[] expectedResults = {0, 50, 20, 20, 10, 0, 0, 0, 0};
 foreach(int value in testVotes)
 {
  vote.Add(value);
 }

 int iValue = 1;
 foreach(int result in expectedResults)
 {
  string message = "For " + iValue;
  AssertEquals(message, result, vote.CalculateRating(iValue));
  iValue++;
 }
}

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

public int CalculateRating(int vote)
{
 double nSpecificVotes = 0;
 foreach(int value in VotesList)
 {
  if(vote == value)
  {
   nSpecificVotes++;
  }
 }
 return (int)nSpecificVotes * 100 / GetRatesCount();
}

На округлении можно не заостряться, тест и так работает. А вот на что следует обратить внимание, так это на то, что в этом методе есть операция деления, знаменатель которой может равняться нулю. Отработаем это в самом начале:

if(GetRatesCount() == 0)
{
 return 0;
}

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

Код примера можно скачать здесь.

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

Ну что, кто хочет закрепляющее упражнение? Совсем простое, для ленивых:

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

Обязательно должна была возникнуть масса вопросов. Их можно свободно задавать в дискуссионном листе или на форуме.

До новых встреч,
Ведущий рассылки, Александр Федоренко.


http://subscribe.ru/
http://subscribe.ru/feedback/
Подписан адрес:
Код этой рассылки: comp.soft.prog.xprogramming
Отписаться

В избранное