Концепция написания unit-tests в golang
Тесты. Как много в этом слове. Кажется, что в этом слове вместилось столько боли, связанной с потраченным временем. Но на самом деле тесты — это еще больше спасенного времени, да нервных клеток заодно. Ведь что такое тесты? Тест — это имитация пользователя, который заходит на страницу. И если ошибку найдут тесты, то её уже не увидит пользователь, а это значит, что доверия к сайту будет больше. А доверие к сайту — это довольный работодатель и его хорошее отношение к своим сотрудникам.
Да писать тесты это долго, нудно и порой больно. Как человек, которому на протяжении полугода сбрасывают написание практически всех тестов, могу с некоторой ответственностью заявить, что порой написание тестов едва ли не сложнее, чем написание самого кода. Ведь при написании кода у тебя есть определённая цель, видение своего кода.
А вот при написании тестов ты сталкиваешься с многими подводными камнями: сценарии, данные, проверка того, что пришло на страницу. Подсознательно хочется проверить все и сразу, и не знаю, как другие люди, но порой я от наплыва мыслей и идей терялся, не зная, какой сценарий писать.
Итак, первая концепция, которую я разработал для себя и которой я тщательно следую при написании тестов — это самый детальный подход к планированию работы и тщательнейший тайм-менеджмент.
Итак, планирование работы. Не надо садиться писать тест сразу же после того, как ты добавил новую строчку кода. Порой лучше закончить со своей работой, а потом, еще раз пройдясь по ней взглядом, ты замечаешь то, что упустил прежде, и вносишь изменения в свой код. Тесты следует перерабатывать с учетом новых данных. И поэтому лучше сначала убедиться, что твой код закончен, и только после этого приступать к написанию тестов. Только так ты можешь избежать потерь драгоценного времени, ведь только так ты можешь видеть цельный код, видеть, как должна работать страница, и это помогает продумать сценарии того, что может пойти не так.
Теперь вторая часть концепции — время. Задач всегда много, и порой времени не хватает даже на них, не говоря уже о написании тестов, и чаще всего отсутствие тестов аргументируется нехваткой времени.
Нет, ребят. Так это не работает. Тесты — это важная часть кода. Это твой щит, твоя подушка безопасности. Либо ты отловишь багу, нажав кнопку прогона тестов, и решишь её на месте, либо тебе вернут твой код со словами переделать, и тем самым ты подорвешь к себе доверие. Тщательно планируй свое время, чтобы уделить должное внимание и написанию тестов.
Пускай ты не сможешь протестировать все сразу, но даже начав, ты уже заложишь фундамент. Каждый день добавляя по одному сценарию, можно будет успевать как работать над своим кодом так и покрывать его тестами. Таким образом к моменту, когда ты будешь готов предоставить свою работу на обзор другим, код уже будет покрыт тестами, и чем тщательнее ты проработал сценарии покрытия, тем меньше багов уйдет в готовую работу.
Вторая концепция звучит примерно так: “Больше тестов богу тестов”, или же “Код тестами не испортишь”.
Как говорилось ранее, количество багов обратно пропорционально покрытию тестами. Можно написать пару общих тестов, чтобы проверить успешность исполнения программы, а вот функционал не отработать как следует. Но это плохая идея.
Как известно, дьявол кроется в мелочах. И порой такая мелочь может полностью испортить всю твою работу. У меня было несколько случаев, когда так и произошло. Тогда я и понял, усвоил для себя, что проверять надо все.
В одном случае вся опасность и суть бага заключалась в том, что из-за параллелизма наш код находился в состоянии гонки, когда один поток передавал данные быстрее другого. Это и приводило к поломке.
Второй же случай был более скрыт и только добавление сценария отказа сервиса позволило выявить багу. Поэтому всегда все покрывай тестами. Думаешь что здесь может скрываться баг? Напиши тест. Сомневаешься в том что RPC запрос отработает нормально? Пиши тест. А лучше — пиши несколько тестов, чтобы обложить его со всех сторон и выявить, где же прячется проблема. Сомневаешься в своей функции? Да, ты знаешь ответ. Пиши несколько сценариев, которые будут гонять твою функцию в хвост и гриву.
Не бойся писать тесты. Это может оказаться долгим и муторным процессом. Ведь ты должен писать одно и тоже, делая изменения практически в мелочах. Но именно в этих мелочах, в этих кирпичиках, что ты кладешь на свой фундамент, и кроется одна из главных концепций хорошего кода. Она кроется в том, что твой код протестирован до самых мелких деталей, так что ты просто не оставляешь шансов ошибке где-то спрятаться.
И третья концепция — самая болезненная и съевшая столько моего времени — это выбор формата тестов.
Как известно, код пишется один раз, а читается десять. Поэтому его принято писать как можно четче, как можно более продуманно, чтобы люди, которые его будут читать, могли сразу понять, что именно там написано, причем чтобы им не приходилось залезать в документацию и дергать других людей. Такой код легко поддерживается и изменяется.
То же самое касается и тестов, но тут кроется один подводный камень. Даже если твой код будет чист, как слеза младенца, это не значит, что он будет легко читаем. Ведь до него уже написаны десятки сценариев, и количество строк может превышать тысячу, и из них ты, возможно, написал меньше половины. В таком случае нужно включить в тестирование все прошлые концепции. Продумай, возможно, даже пропиши на бумаге все или как минимум большую часть своих сценариев, чтобы определить, как именно ты будешь тестировать код. Для этого задай себе следующие вопросы:
- ЧТО ты хочешь тестировать?
- КАК ты хочешь это протестировать?
- ЗАЧЕМ ты хочешь это протестировать?
Теперь посмотри на свою работу и продумай свои следующие действия. Готово? Теперь задай себе еще один вопрос: КАК мне сделать мои тесты легкими и понятными?
Ответ будет невероятно прост: Имена. Имя сценария, имя заглушки, имя проверки. Просто посмотрев на эти три вещи, человек уже должен понять что делает конкретно этот тест, что он проверяет, чего от него ожидать. И в случае какой-либо поломки репорт моментально тебе укажет едва ли не строчку кода, где произошла проблема. Это одна из вещей что облегчит как твой код, так и его чтение.
Есть еще один момент. Как я говорил ранее, написание тестов муторно еще и потому, что ты вынужден писать заглушки едва ли не под копирку. Так почему бы не облегчить процесс как себе, так и тому кто будет в будущем читать или, возможно, дополнять твой код?
Абсолютно все заглушки будет объединять одна вещь. У них у всех будет одинаковый запрос и ответ. Он может меняться в мелочах, но суть останется неизменной. Например, у нас есть RPC запросы, которые повторяются в многих обработчиках. Чтобы не писать везде одно и тоже, можно было просто пройтись по этим обработчикам, посмотреть в чем их различия, а потом в общем файле, к которому у всех будет доступ, написать функцию запроса и ответа.
Различия в этих самых запросах и ответах легко решить указанием параметров (возможно, в динамических языках такое не сработает, тогда прощу прощения. У меня есть опыт лишь работы со статическим языком программирования). Эти же параметры решат одновременно и проблему разных сценариев. В зависимости от сценария достаточно будет указать нужный параметр функции, и тогда заглушка отработает так, как мы того ожидаем.
То же самое касается и проверок пришедших данных. При проверке всего кода такие проверки будут в общих чертах схожими, они будут отличаться лишь в том месте, которое мы тестируем.
Так зачем писать одну и туже проверку по десять раз, когда мы можем запросто вынести их в один файл, а потом просто заново переиспользовать в нужных местах? Ведь не просто так программисты вывели две концепции: DRY(Don’t Repeat Yourself) и KISS(Keep It Simple Stupid). Тем самым уменьшается количество кода в тестах, он становится более читаем, и в случае чего очень просто дописать пару-тройку сценариев, внеся минимальные изменения, которые не затронут все остальные тесты.
{name: “page-rendered-sucessfully”,request: genHTTPRequest(“https://example.com/seo?page=1", t),recorder: httptest.NewRecorder(),reqMocker: successMockCalls,validaters: successValidater,},{name: “check-url-redirect”,request: genHTTPRequest(“https://example.com/directory“, t),recorder: httptest.NewRecorder(),reqMocker: redirectMockCalls,validaters: redirectValidater,},func successMockCalls(m *mock.Mocker, r *http.Request) {objects.GetSomePageRequest(m, “example.com”, “seo”, nil, nil).Return(objects.GetSomePageRequest(nil), nil).Times(1)ListProfilesRequest(m, 1).Return(ListProfilesRequest(), nil).Times(1)ServicesRequest(m, 100500201).Return(ServicesRequest(), nil).Times(1)}
Как видно в примере, в двух функциях моков вызывается одна и та-же функция, которая передает разные параметры и разные ответы. Это позволяет увеличить покрытие при меньших объемах тестов. Главное правильно указать параметры запроса и ответа.
Итак, в конце хотелось бы подытожить сказанное. Да, написание тестов — порой процесс не из приятных, особенно когда у тебя не хватает времени на это. Но как бы парадоксально это ни звучало, это же самое написание тестов тебе и экономит то время, которого так катастрофически не хватает.
Ведь потратив время на эти самые тесты, ты моментально выявляешь проблемные места, которые закрываешь, прежде чем отдавать свой код дальше, и тем самым избегая возврата той же задачи с указанием найденных багов. Плюс написание тестов уже стало обязательным атрибутом, от которого никуда не денешься.
Множество раз ко мне обращались коллеги с просьбой написать тесты даже если они внесли изменения размером в пару строк, потому что иначе их код отказывались принимать. А когда тимлид и главный технический директор видят написанные тесты, которые проверяют корректность функционирования добавленного кода, они пропускают его дальше. Так что всегда пишите тесты, чтобы быть уверенными в своем коде и не подводить свою команду.