#18 golang go test Автоматическое тестирование - пример программы

Напишем простую программу для "уникализации" строк, которые подаются в стандартный ввод:

package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	in := bufio.NewScanner(os.Stdin)
	var prev string
	// потенциально бесконечный цикл:
	for in.Scan() {
		txt := in.Text()
		if txt == prev {
			continue
		}
		if txt < prev {
			panic("Строки не отсортированы!")
		}
		prev = txt
		fmt.Println(txt)
	}
}

(при запуске в vscode возможна проблема, но есть решение)

Нам потребуется несколько стандартных пакетов:

  • «os» для получения доступа к стандартному вводу,
  • «fmt» для форматированного вывода, буферизированный ввод-вывод из пакета io.

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

Каждую строку, которую мы считали, будем выводить на экран, если она прежде не встречалась (по факту - просто не совпадается с предыдущей)

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

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

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

Порефакторим перед тестированием

Для того чтобы написать тесты, нам нужно программу немножко «отрефакторить». Важно, чтобы тестируемый код находился в функциях, отличных от main.


Вот так изменится программа после рефакторинга:

package main

import (
	"bufio"
	"fmt"
	"io"
	"os"
)

func uniq(input io.Reader, output io.Writer) error {
	in := bufio.NewScanner(input)
	var prev string
	// потенциально бесконечный цикл:
	for in.Scan() {
		txt := in.Text()
		if txt == prev {
			continue
		}
		// отказываемся от паники в пользу возврата
		// признака ошибки
		if txt < prev {
			return fmt.Errorf("строки не отсортированы")
		}
		prev = txt
		fmt.Fprintln(output, txt)
	}
	return nil // пустая ошибка, если всё окей
}

func main() {
	err := uniq(os.Stdin, os.Stdout)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

-- что мы тут изменили:

  • Вынесем весь наш код в отдельную функцию, которая будет просто принимать на вход поток, из которого мы будем читать, и поток, куда мы будем писать результат. Это всё реализуется через интерфейс Reader и Writer.
  • Вместо стандартного вывода будем использовать функцию Fprintln, которая первым параметром принимает интерфейс Writer.
  • В случае ошибки больше не будем паниковать, а будем возвращать ошибку и обрабатывать ее уже в main или тестах.
  • Не забудем вернуть пустую ошибку при корректном завершении функции.

Пишем тесты

Теперь давайте приступим к написанию непосредственно тестов. Тесты в Go должны лежать в файле, который имеет суффикс test.

В нашем случае файл будет называеться main_test.go - создадим его рядом с файлом программы.

Все тесты в Go:

  1. начинаются с префикса Test
  2. и принимают на вход единственный параметр тестирующего модуля *testing.T


Заполним main_test.go следующим содержимым:

package main

import (
	"bufio"
	"bytes"
	"strings"
	"testing"
)

var testOkInput = `1 2
3
3
4
5`

// контрольные данные
var testOkResult = `1 2
3
4
5
` // последний перенос строки важен!

// тестируем корректных случай
func TestOk(t *testing.T) {
	in := bufio.NewReader(strings.NewReader(testOkInput))
	out := new(bytes.Buffer)
	err := uniq(in, out)
	if err != nil {
		t.Errorf("Функция вернула ошибку")
	}
	result := out.String()
	if result != testOkResult {
		t.Errorf("Тестовые и контрольные данные не совпадают\n %v %v",
			result, testOkResult)
	}
}

// Тут вторая строка короче первой
// --  программа должна бросить ошибку
var testFailInput = `1 2
1`

// тестируем некорректный случай
func TestForError(t *testing.T) {
	in := bufio.NewReader(strings.NewReader(testFailInput))
	out := new(bytes.Buffer)
	err := uniq(in, out)
	if err == nil {
		t.Errorf("Мы ожидали непустую ошибку! - а получили: %v", err)
	}
}

-- теперь мы можем открыть в терминале папку с этим файлом и запустить тесты командой:

 go test -v

-- где флаг -v указывает на то, что мы хотим видеть подробности запуска.

В примере выше мы описали два теста:

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

Явное указание проблемы в тесте - вызов t.Error()

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

Key Words for FKN + antitotal forum (CS VSU):

vedro-compota's picture

отладка:

package main

import (
	"bufio"
	"bytes"
	"fmt"
	"strings"
	"testing"
)

var testOk = `1 2
3
3
4
5`

var testOkResult = `1 2
3
4
5`

func TestOk(t *testing.T) {
	in := bufio.NewReader(strings.NewReader(testOk))
	out := new(bytes.Buffer)
	err := uniq(in, out)
	if err != nil {
		t.Errorf("Функция вернула ошибку")
	}
	result := out.String()
	fmt.Println(len(result))
	fmt.Println(len(testOkResult))
	fmt.Println(testOkResult == result)
	if result != testOkResult {
		t.Errorf("Тестовые и контрольные данные не совпадают\n %v %v",
			result, testOkResult)
	}
}

_____________
матфак вгу и остальная классика =)