пятница, 26 августа 2011 г.

Unit tests: DRY vs Clarity

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

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

Прямой, дубовый подход - каждый тест кейс представить в виде отдельной спеки:

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

Это же, но с циклом:

Гораздо короче. Однако этот код мне не нравится: cлишком много закорючек, тяжелее читать. А может я слишком придираюсь?

Не могу решить, что лучше: простая и очевидная простыня копипаста, или код немного посложнее, но компактнее и DRY-ёвее.

Кстати, в этих ваших жавах и сишарпах (NUnit и JUnit) используется подход, который и читаемый, и почти без копипаста:

Вот это мне по нраву. Жалко, в RSpec такого нету.

Кстати, в руби есть такой фреймворк - Cucumber. Он, правда, не для юнит-тестов, а для BDD. Вот в нём тесты выглядят человечнее всего:

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

Весь код - на гитхабе.

вторник, 17 мая 2011 г.

Генерируем шаблон для решений задач с GCJ

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

Решим, например, CandySplitting.

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

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

Итак, приступим. Я исхожу из предположения, что шарп девелоп уже установлен.

1. Скачиваем адд-ин.
2. Запускаем его и устанавливаем.
3. Рестартуем SharpDevelop.
4. Идём в File -> New -> Solution.
5. Выбираем Google Code Jam Solution Project в качестве шаблона и создаём проект CandySplitting.
6. Открываем Unit Tests Pad: View -> Tests -> Unit Tests. Запускаем тесты. Они, естественно, будут красные.
7. Сейчас вместо тестов заглушки. Делаем вместо них нормальные тесты. Открываем CandySplittingTest.cs и чиним:
using System;
using System.IO;
using NUnit.Framework;
using DNNX.GoogleCodeJam.Common;

namespace DNNX.GoogleCodeJam.CandySplitting.Test
{
    [TestFixture]
    public class CandySplittingTest : BaseTest<int[]>
    {
        [Test]
        public void SmokeTest()
        {
            var input = @"2
5
1 2 3 4 5
3
3 5 6";
            
            var expectedOutput = @"
Case #1: NO
Case #2: 11";

            SmokeTest(input, expectedOutput);
        }
        
        [Test]
        public void EdgeCase()
        {
            Assert.AreEqual("NO", CreateSolution().Solve(new[] {1, 2, 2}));
        }
        
        
        protected override Solution<int[]> CreateSolution(TextReader input, TextWriter output)
        {
            return new CandySplitting(input, output);
        }
    }
}
8. Тесты сделать актуальными просто. В SmokeTest просто копипастим тесткейсы прямиком из условия задачи. Этого часто бывает достаточно. Вот как стал выглядеть SmokeTest, например:
        [Test]
        public void SmokeTest()
        {
            var input = @"2
5
1 2 3 4 5
3
3 5 6";
            
            var expectedOutput = @"
Case #1: NO
Case #2: 11";

            SmokeTest(input, expectedOutput);
        }
9. Запускаем тесты опять, они по-прежнему красные. Неудивительно, решения-то ещё не написали. Идём в CandySplitting.cs.
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using DNNX.GoogleCodeJam.Common;

namespace DNNX.GoogleCodeJam.CandySplitting
{
    public class CandySplitting : Solution<int[]>
    {
        public CandySplitting(TextReader input, TextWriter output)
            : base(input, output)
        {
        }
        
        public CandySplitting() {}
        
        public override object Solve(int[] a)
        {
            return null;
        }
        
        public override int[] ReadTestCase()
        {
            return null;
        }
    
        public static void Main(string[] args)
        {
            new CandySplitting().REPL();
            Console.ReadLine();
        }    
    }
}
10. Нужно перегрузить два метода: ReadTestCase() (он читает данные из входного потока и преобразует их в аргументы задачи), и Solve() (он решает то, что ему прочитал ReadTestCase()). Для чтения из в базовом классе Solution есть специальные методы int[] ReadInt, int[] ReadInts() и подобные, чтобы не приходилось руками писать всякие int.Parse(Console.ReadLine()).
        public override object Solve(int[] a)
        {
            var xor = a.Aggregate(0, (acc, x) => acc ^ x);
            if (xor == 0)
                return a.Sum() - a.Min();
            return "NO";
        }
        
        public override int[] ReadTestCase()
        {
            ReadString();
            return ReadInts();
        }
11. Запускаем тесты - они зелёные. Можно ещё добавить несколько (уже только на метод Solve, чтобы не заморачиваться с многострочными литералами).
12. Отсылаем задачу.
...
N. PROFIT!

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

воскресенье, 15 мая 2011 г.

Решаем Google Code Jam правильно

Навеяно постом Кудесника. Вкратце: он предлагает quick&dirty write-only way способ решить задачу с Google Code Jam ("олимпиадный") и ещё один, корпоративный, с дотнетом, интерфейсами и паттернами ("корпоративный").

Я решил предложить третий путь - истинный. Ему я следовал на самом соревновании, когда я решал задачу. Преимущества:
  1. Кода мало.
  2. Этот код быстро пишется
  3. Этот код легко читается.
  4. Этот код - тру оопе.
  5. Там даже тесты есть.



Кто хочет спорить - валяйте.

 

Попробую блоггер

Буду сюда технический шыз писать, пока не надоест.

Немного кода:

class GCJSolution
  def initialize(input = STDIN, output = STDOUT)
    @input = input
    @output = output
  end
  
  def read_strings
    @input.gets.chomp.split(' ')
  end

  def read_ints
    read_strings.map(&:to_i)
  end
  
  def read_int
    @input.gets.to_i
  end
  
  def read_solve_write_all
    number_of_tests = read_int
    for test_no in 1..number_of_tests
      test_case = parse_input()
      ans = solve(test_case)
      @output.puts "Case ##{test_no}: #{ans}"
    end
    number_of_tests
  end
end