» » А пусть тесты сами себя и поддерживают

 

А пусть тесты сами себя и поддерживают

Автор: admin от 24-05-2017, 09:15, посмотрело: 99

Сегодня я хочу рассказать о необычном подходе к написанию тестов, к которому я как-то незаметно пришел в ходе работы над несколькими проектами разной величины, и который я почему-то не встречал в чистом виде у других, хотя он, в общем-то, лежит на поверхности. С недавних пор я начал писать кое-какой код на Go, и как только встал вопрос о написании тестов, я опять вспомнил об этом подходе.

Как обычно выглядят тесты?


Очень схематично, каждый юнит-тест обычно состоит из следующих шагов:

  • инициализации входных данных;

  • выполнения бизнес-логики и получения результата;

  • сравнения результата с эталоном.


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

    Но ведь все это можно унифицировать!


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

    Встречайте agenda-тесты


    Я назвал такой подход agenda-тестированием, потому что я люблю аббревиатуры, и agenda — это, на самом деле, auto-generated-data. В чем его суть?

  • Входные и выходные данные тестов хранятся в файлах (JSON или что-то еще — неважно).

  • Тест может работать в двух режимах:

    • Режим инициализации: тест производит вычисление выходных данных и сохраняет эти данные в файл-эталон

    • Режим тестирования: тест производит вычисление выходных данных, читает ранее сохраненные эталонные данные и сравнивает их; данные отличаются — тест провален.



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


  • И это все?.. И это все! Давайте посмотрим, как это работает на примере Go, для которого я опубликовал небольшую библиотеку, и которую без труда можно портировать на любой другой язык.

    Для начала создадим файл «бизнес-логики»: кода, который мы собираемся тестировать:

    Файл example.go
    package example
    
    import "errors"
    
    type Movie struct {
    	TotalTime   int  `json:"total_time"`
    	CurrentTime int  `json:"current_time"`
    	IsPlaying   bool `json:"is_playing"`
    }
    
    func (m *Movie) Rewind() {
    	m.CurrentTime = 0
    }
    
    func (m *Movie) Play() error {
    	if m.IsPlaying {
    		return errors.New("Movie is already playing")
    	}
    	m.IsPlaying = true
    	return nil
    }


    Теперь создадим тест:

    Файл example_test.go
    package example
    
    import (
    	"encoding/json"
    	"testing"
    
    	"github.com/iafan/agenda"
    )
    
    func TestMovie(t *testing.T) {
    	agenda.Run(t, ".", func(path string, data []byte) ([]byte, error) {
    		type MovieTestResult struct {
    			M   *Movie      `json:"movie"`
    			Err interface{} `json:"play_error"`
    		}
    
    		in := make([]*Movie, 0)
    
    		// в data у нас прочитанный файл с тестовыми данными,
    		// который надо развернуть в структуру
    		if err := json.Unmarshal(data, &in); err != nil {
    			return nil, err
    		}
    
    		out := make([]*MovieTestResult, len(in))
    
    		for i, m := range in {
    			// собственно, "бизнес-логика" теста
    
    			// Функция Rewind() изменяет свойства структуры
    			m.Rewind()
    			// Play() возвращает nil или ошибку
    			err := m.Play()
    
    			// сохраняем выходные "эталонные" данные
    			// 1) мы хотим сравнивать поля структуры Movie
    			// 2) мы хотим сравнивать полученную ошибку или ее отсутствие
    			out[i] = &MovieTestResult{m, agenda.SerializableError(err)}
    		}
    
    		// полученную выходную структуру сериализуем в бинарные данные
    		// и возвращем для сравнения или сохранения в файл
    		return json.MarshalIndent(out, "", "t")
    	})
    }


    Вся магия agenda-теста здесь в строчке:
    agenda.Run(t, ".", func(...){...}}

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

    Теперь создадим файл с тестовыми данными:

    Файл test_data.json
    [
    	{"total_time":100,"current_time":0,"is_playing":false},
    	{"total_time":150,"current_time":35,"is_playing":true},
    	{"total_time":95,"current_time":4,"is_playing":true},
    	{"total_time":125,"current_time":110,"is_playing":false}
    ]

    Можно запускать тест в режиме инициализации:
    $ go test -args init

    При этом рядом с входным файлом будет создан файл с эталонными данными:

    Файл test_data.json.result
    [
    	{
    		"movie": {
    			"total_time": 100,
    			"current_time": 0,
    			"is_playing": true
    		},
    		"play_error": null
    	},
    	{
    		"movie": {
    			"total_time": 150,
    			"current_time": 0,
    			"is_playing": true
    		},
    		"play_error": "Movie is already playing"
    	},
    	{
    		"movie": {
    			"total_time": 95,
    			"current_time": 0,
    			"is_playing": true
    		},
    		"play_error": "Movie is already playing"
    	},
    	{
    		"movie": {
    			"total_time": 125,
    			"current_time": 0,
    			"is_playing": true
    		},
    		"play_error": null
    	}
    ]


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

    Теперь можно запустить тест в обычном режиме:
    $ go test

    Тест, разумеется, должен пройти без ошибок.

    Теперь, когда вы вносите изменения в код по ходу жизни проекта, вы будете использовать два сценария работы с такими тестами:

    • Если ожидается, что изменения в коде не должны привести к изменениям данных: запускаем go test и убеждаемся, что тесты не поломаны.

    • Если ожидается, что изменения в коде должны привести к изменениям данных: запускаем go test -args init, а затем с помощью, например, git diff убеждаемся, что все изменения данных ожидаемы.



    Разделение кода и тестовых данных имеет как достоинства, так и недостатки:

    К недостаткам можно отнести большее количество файлов, которые будут присутствовать в коммитах. Для простых юнит-тестов с несложными данными ограниченного объема больше подойдут табличные тесты.

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

    Источник: Хабрахабр

    Категория: Программирование

    Уважаемый посетитель, Вы зашли на сайт как незарегистрированный пользователь.
    Мы рекомендуем Вам зарегистрироваться либо войти на сайт под своим именем.

    Добавление комментария

    Имя:*
    E-Mail:
    Комментарий:
    Полужирный Наклонный текст Подчеркнутый текст Зачеркнутый текст | Выравнивание по левому краю По центру Выравнивание по правому краю | Вставка смайликов Выбор цвета | Скрытый текст Вставка цитаты Преобразовать выбранный текст из транслитерации в кириллицу Вставка спойлера
    Введите два слова, показанных на изображении: *