Мыслим в стиле gotest

Не стану умничать о важности TDD. Воспринимайте как факт — в Go есть встроенные средства для автоматизации тестирования, и сегодня мы их рассмотрим.

Большинство фреймворков для тестирования мне откровенно говоря не по душе. Особенно те которые в стиле JUnit. Да, для java это вполне природная реализация, но натягивая его на, скажем, C, получаем большую некрасивую глупость. Я вообще долгое время довольствовался minUnit размером в 3 строчки, а когда понадобилось тестировать микроконтроллеры написал свой велосипед.

Go, test!

Разработчики Go как всегда поступили максимально просто. Все тесты хранятся в файлах с именами вида xxx_test.go. Для запуска тестов используется утилита gotest.

Каждый тест представляет собой функцию наподобие:

func TestExample(t *testing.T) {
  ...
  if myTestFailed {
     t.Fail()
  }
}

Все. Надо, чтобы имя функции начиналось со слова «Test», и для «провала» теста надо вызывать функцию t.Fail или t.FailNow. Есть еще ряд функций у объекта testing.T, подробнее смотрите здесь.

Живой пример

Без книжных гипотенуз и факториалов и тестов для них. Мне нужно было написать TFTP-сервер. Не вдаваясь в детали протокола TFTP, скажу, что содержимое файла в нем идет блоками определенного размера. Каждый блок обладает номером. Кроме того, все блоки имеют определенный размер (как правило 512 байт), а блок с размером менее 512 байт обозначает конец файла.

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

var CHUNK_SIZE int = 512

// Single TFTP file data chunk
type TftpChunk struct {
	id   int
	data []byte
	ack  bool
}

// wrap data into chunk
func NewTftpChunk(id int, data []byte) (c *TftpChunk) {
	c = new(TftpChunk)
	c.id = id
	c.data = data
	return 
}

// get chunk length
func (c *TftpChunk) Len() int {
	return len(c.data)
}

// convert data chunk to TFTP data packet
func (c *TftpChunk) Packet() (p []byte) {
	p = make([]byte, len(c.data)+4)
	p[0] = 0
	p[1] = DATA
	p[2] = uint8(c.id / 256)
	p[3] = uint8(c.id % 256)
	copy(p[4:], c.data[:])
	return
}

Ошибиться тут сложно, но напишем тесты на всякий случай:

func makeUint16(hi, lo byte) uint16{
	return uint16(lo) + (uint16(hi) * 256) // сдвиг на восемь превращается в смайлик
}

func TestChunk(t *testing.T) {
	c := new(TftpChunk)
	c.id = 31
	c.data = []byte{1, 2, 3, 4, 5}

	// test packet length
	if c.Len() != 5 {
		t.Fail()
	}

	p := c.Packet()
	// test opcode
	if makeUint16(p[0], p[1]) != DATA {
		t.Fail()
	}
	// test id
	if makeUint16(p[2], p[3]) != 31 {
		t.Fail()
	}
	// test data
	if bytes.Compare(p[4:], c.data) != 0 {
		t.Fail()
	}
}

Да, это плохо писать тесты после кода. Больше так не буду, честно. Проверяем, запускаем gotest и он пишет «PASS». Это хорошо.

Теперь приступаем к TFTP файлу. Сперва я хотел чтобы конструктор принимал имя файла. Но задумавшись о том, как это тестировать, решил, что лучше обойтись интерфейсом io.Reader — за ним может скрываться как файл, так и набор байт (bytes.Buffer), что удобно для тестирования (наверное это называется mocking?).

Итак, функция для тестирования:

func TestFile(t *testing.T) {
	data := bytes.NewBuffer([]byte{1, 2, 3})
	f, err := NewTftpFile(data, CHUNK_SIZE)
	if err != nil {
		t.Fail()
	}
	if len(f.chunks) != 1 {
		t.Fail()
	}
	if f.chunks[0].Len() != 3 {
		t.Fail()
	}
}

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

// TFTP file represented as array of chunks
type TftpFile struct {
	chunks []TftpChunk
}

// Create new TFTP file object from local file
func NewTftpFile(r io.Reader, chunkSize int) (f *TftpFile, err os.Error) {
	f = new(TftpFile)
	f.chunks = make([]TftpChunk, 0)
	for {
		var n int
		buf := make([]byte, chunkSize)
		n, err = r.Read(buf)

		id := len(f.chunks)
		c := NewTftpChunk(id, buf)
		f.chunks = append(f.chunks, *c)

		if n != len(buf) {
			break
		}
	}
	return
}

Запускаем — видим только слово FAIL. И как тут понять что пошло не так? Я решил заменить t.Fail на t.Error(…) и указал причину провала для каждого вызова.

Запускаем:

--- FAIL: main.TestFile (0.00 seconds)
	wrong chunk length
FAIL
gotest: "./6.out" failed: exit status 1

Все понятно. Ошибка с размером. И действительно, мы вычитали 3 байта, а создаем блок на 512 байт. Меняем…

c := NewTftpChunk(id, buf)

…на:

c := NewTftpChunk(id, buf[:n])

…и радуемся тому, как проходят все тесты.

Для профилактики я еще создал тест с данными размером ровно 4 килобайта и убедился, что файл содержит не 8, а 9 блоков (чтобы последний блок был меньше 512 байт, он создается с нулевым размером — это тоже проверил). Очередной FAIL напомнил, что я не проверяю значение err при чтении из файла (ошибка оказывалась равной os.EOF). Пришлось исправить. Теперь спокойнее себя чувствую.

Выводы

Нравится testing+gotest? Да, свои недостатки есть, но реализовано все просто, работает гибко и быстро. Приятно, когда о разработке через тестирование задумываются еще при создании языка.

Тестируйте, и побольше вам «PASS» и поменьше «FAIL»!

Реклама

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s