Который раз сталкиваюсь с одной и той же проблемой, когда пишу юнит-тесты: накопипастить кучу (почти) одинаковых тестов, или "сгенерировать" их, сведя количество копипаста к нулю.
Представим ситуацию, когда тесты различаются только входными данными. Классический пример: тупой калькулятор, который умеет складывать числа.
Прямой, дубовый подход - каждый тест кейс представить в виде отдельной спеки:
Всё просто и понятно. Впрочем есть один недостаток: этот код содержит тонну копипаста, что может послужить источником ошибок и вообще оскорбляет чувство прекрасного.
Это же, но с циклом:
Гораздо короче. Однако этот код мне не нравится: cлишком много закорючек, тяжелее читать. А может я слишком придираюсь?
Не могу решить, что лучше: простая и очевидная простыня копипаста, или код немного посложнее, но компактнее и DRY-ёвее.
Кстати, в этих ваших жавах и сишарпах (NUnit и JUnit) используется подход, который и читаемый, и почти без копипаста:
Вот это мне по нраву. Жалко, в RSpec такого нету.
Кстати, в руби есть такой фреймворк - Cucumber. Он, правда, не для юнит-тестов, а для BDD. Вот в нём тесты выглядят человечнее всего:
Есть один минус и в кукумбере, впрочем. Тесты на нём можно сравнить с идеально чистой комнатой, где срач заметён под ковёр и закинут в шкаф, чтоб видно не было (см. "бэкграунд" этого теста здесь). Там регекспы, блоки, доллары и прочий шрот. Не так много, правда, чтоб его бояться.
Весь код - на гитхабе.
Во имя добра и света
пятница, 26 августа 2011 г.
вторник, 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 и чиним:
12. Отсылаем задачу.
...
N. PROFIT!
Технические детали реализации адд-ина и базового класса для солюшена и тестов не расписываю намеренно. И так длинно получилось. Втыкайте в гитхаб, если интересно.
Решим, например, 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 ("олимпиадный") и ещё один, корпоративный, с дотнетом, интерфейсами и паттернами ("корпоративный").
Я решил предложить третий путь - истинный. Ему я следовал на самом соревновании, когда я решал задачу. Преимущества:
Кто хочет спорить - валяйте.
Я решил предложить третий путь - истинный. Ему я следовал на самом соревновании, когда я решал задачу. Преимущества:
- Кода мало.
- Этот код быстро пишется
- Этот код легко читается.
- Этот код - тру оопе.
- Там даже тесты есть.
Кто хочет спорить - валяйте.
Попробую блоггер
Буду сюда технический шыз писать, пока не надоест.
Немного кода:
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
Подписаться на:
Сообщения (Atom)