Коли варто мігрувати на новий automation test framework та як це зробити

💡 Усі статті, обговорення, новини про тестування — в одному місці. Приєднуйтесь до QA спільноти!

Привіт, я Інна, Automation QA Lead в Edtech-стартапі, який допомагає будувати кар’єру в ІТ — Mate academy. За більш ніж 13 років досвіду в тестуванні мені доводилося створювати фреймворки для автоматизації тестування як з нуля, так і мігрувати вже наявні.

Створити новий фреймворк є відносно простою задачею, адже ти просто береш і робиш. 🙂 А ось у випадку, коли він вже існує, і треба ухвалити рішення кардинально щось змінити, часто виникають запитання — чи варто? Може просто порефакторити? Як зробити це швидко?

Нещодавно ми в компанії мігрували e2e-тестування з Cypress на Playwright, тож виникла ідея поділитися поглядом на цю тему. У статті я розповідаю про те, які проблеми можуть виникнути з тестами та фреймворком, і коли варто мігрувати на новий сетап. Також опишу критерії та кроки міграції. Матеріал може бути корисним Senior Test Automation Engineers та лідам проєктів, які задумуються про міграцію, але також і менш досвідченим інженерам, які хочуть розібратися з цією темою.

Типові проблеми з automation test framework, або чому треба щось змінювати

Зазвичай думка про зміну структури фреймворку та інструментів для автоматизації приходить до інженера, коли код був написаний не оптимально через нестачу часу чи недосвідченість автора, або ж коли інструменти вже не виконують всі потрібні функції.

Якщо ви прийшли на проєкт де автоматизація була створена раніше — варто проаналізувати проблеми, що існують, та подумати про внесення змін. Так само, якщо ви самі писали фреймворк пару років тому, а тепер розумієте, що можна зробити краще, варто подумати про зміни.

Почнемо з переліку типових проблем, на які можна натрапити в тестовому фреймворку.

Повільні тести

Загалом повільні тести — це поширена проблема, і найчастішими причинами цього є:

  • неоптимізоване використання тестової інфраструктури;
  • підготовка тестових даних робиться на UI замість API;
  • підготовка та видалення однакових тестових даних повторюється для кожного тесту;
  • однакові тестові кроки повторюються в багатьох тестах;
  • використаний «повільний» інструмент для автоматизації, наприклад, це актуально для Web UI, або Mobile-автоматизації;
  • тести не можуть бути запущені в паралель через їхню залежність.

Як зрозуміти, чи може бути швидше? Для того, щоб відповісти на це питання, потрібно мати інструменти, які дають можливість проаналізувати таймлайн проходження тестів. Маючи інструментарій, треба проаналізувати, скільки займають в окремому тесті підготовка даних, очистка даних та самі тестові степи. Чи є щось, що сильно вибивається? Якщо так, то це може бути місце для покращення.

Чому важливо, щоб тести були швидкими? Що повільніші тести, то більше часу потрібно на перевірку одного білда. Що більше часу йде на перевірку, то повільніше ви рухаєтесь, як команда. Це особливо критично в проєктах, де автотести є частиною CI/CD-процесу.

Нестабільні тести

Причинами нестабільних тестів найчастіше є:

  • неунікальні тестові дані;
  • прихована залежність тестів викликана послідовністю їхнього виконання або паралельним запуском;
  • неякісно продумані методи очікування для UI-тестів;
  • невраховані особливості імплементації продукту (наприклад, наявність вебсокетів).

Існує переконання, що нестабільні тести (flaky) — це обов’язкове правило для автоматизації UI. Але сучасні інструменти дають багато можливостей зробити навіть ці тести стабільними, тому точно не потрібно здаватися, але навпаки — вдатися до аналізу причин. Також можуть бути нестабільними й інші види тестів, як-от, наприклад, API.

З якими б тестами ви не працювали, їхня стабільність впливає на кількість реранів, які потрібно виконувати для перевірки. Відповідно flaky-тести збільшують час виконання і витрачені кошти. Також вони викликають у інженерів «упередження», що тестам не можна вірити і, відповідно, велику спокусу замерджити код, не дочекавшись «зеленого світла». Тому завжди варто вкладатися в пошук причин та стабілізацію ваших тестів.

Використані застарілі інструменти, або вони мають обмеження

Може статися, що інструмент, який колись був мейнстримовим чи обнадійливим, з плином часу застарів, перестав якісно виконувати свою функцію, або його перестали підтримувати. Так, наприклад, сталося з Protractor — фреймворком, створеним для автоматизації UI для Angular-застосунків.

Protractor був дуже популярним, але коли його перестали підтримувати, використання поступово стало неможливим, адже не було забезпечено підтримку нових версій браузерів, інтеграцію з новими бібліотеками та баг-фіксинг. Також, якщо нові версії інструменту не випускаються роками, з’являється дуже велика ймовірність, що на ринку вже є щось, що набагато краще справляється з поставленою задачею.

Відсутні ключові складові для легкої підримуваності чи розширення фреймворку

Існує типовий набір інструментів, які мають бути в хорошому тестовому фреймворку (пишу про них детальніше в наступних розділах), і якщо у вас щось з цього відсутнє, це може впливати на легкість та швидкість роботи з новими та тестами, що існують.

Наприклад, якщо у вас немає зручних репортів та аналітики, де можна не тільки зрозуміти, чому впав той чи інший тест, але також проаналізувати таймлайн виконання тестів, середню швидкість проходження, побачити розбивку покриття фічами, список flaky-тестів тощо, то варто задуматися, як це виправити та як покращити репортинг.

Також може бути, що у вас недостатньо логування того, що відбувається в тестах, а це впливає на легкість аналізу, чому впав той чи інший тест.

Важко підтримувати та масштабувати фреймворк через погану структуру та дублікацію коду

Причиною погано структурованого коду зазвичай є недосвідченість інженерів, що його пишуть. Також до автотестів часто є трохи поверхове ставлення, часто здається: ну для чого тут заморочуватися? Працює — не чіпай.

Але насправді якість коду для автомейшен-фреймворку є не менш важливою, ніж якість продуктового коду. Вона, зокрема, прямо впливає на швидкість написання нових тестів та на можливість додавання тестів для різного функціоналу (scalability), адже за правильної організації коду ви можете легко перевикористати його, і тоді додавання нових сценаріїв — це тільки питання додавання низки нових методів.

Також у такому фреймворку набагато легше підтримувати тести, що існують, та фіксити проблеми.

Непослідовний підхід до написання тестів та неоптимальний тест-кавередж

Може бути, що тести є написаними різними інженерами чи командами без погодження та оптимізації спільного підходу. Наприклад, хтось пише коротенькі сценарії на одну перевірку, хтось навпаки — дуже довгі та складні сценарії, у яких важко розібратися. Тести можуть мати різні назви: хтось в одному spec-файлі пише багато тестів, хтось окремо створює файл для кожного тестового сценарію.

Коли такого багато, стає дуже важко зрозуміти, що покрито, а що ні, куди додавати новий сценарій, як краще його написати. Також може бути, що e2e-тести, що існують, покривають прості сценарії, які можна було б покрити на нижчому рівні.

Коли можна обійтися рефакторингом

Кардинальних змін можна уникнути, коли з перелічених проблем наявна тільки одна-дві.

Наприклад: у вас є продумана структура фреймворку, інструменти виконують потрібні задачі та мають хорошу підтримку. Тестове покриття якісне і не потребує оптимізації. Єдина проблема — тести повільні. У такому разі потрібно проаналізувати, що ж робить їх повільними, оптимізувати підготовку даних, використати потужніші сервери, розпаралелити тощо.

Те саме з flaky-тестами: якщо все інше добре, можна локалізувати проблеми та поступово фіксити, не змінюючи нічого кардинально. Коли фреймворк загалом організований правильно, то робити рефакторинг його частини має бути не так складно, і це логічний і потрібний крок.

Складніше буде, якщо ви зіткнулися з обмеженнями використаних інструментів. У такому разі навіть якщо все інше добре, скоріше за все, доведеться розглянути їхню зміну, хоча альтернативно ви можете пошукати обхідні шляхи чи зробити потрібний функціонал.

Але також потрібно пам’ятати, що якщо інструмент перестають офіційно підтримувати, рано чи пізно можна зіткнутися з проблемами (наприклад, він перестане підтримувати нові версії браузера). Якщо ви хочете, щоб ваша автоматизація жила та приносла користь багато років, вона має базуватися на підходах та інструментах, які теж будуть «жити».

Якщо ж з перелічених проблем у вас наявні 3-4 чи всі, точно варто почати все спочатку.

Які переваги у міграції на новий фреймворк

На перший погляд задача «все переробити» здається складною та ресурсно-затратною, але на довгому проміжку ви точно будете у виграші. Якщо висловитися одним реченням, то головна перевага — це можливість врахувати всі попередні помилки та комплексно втілити в життя всі потреби вашого проєкту на основі досвіду.

Вирішивши все переписати, ви також можете зіткнутися з небажанням керівництва чи замовника погоджуватися на такий радикальний крок. Але не спішіть відкидати цю ідею, адже в більшості випадків потрібно просто детально пояснити всім зацікавленим сторонам, які саме переваги ви будете мати від такої міграції.

То які ж переваги у міграції на новий фреймворк? Фактично, це вирішення всіх перелічених вище проблем:

Можливість використати нові технології

Як я вже писала вище, з моменту, коли створювався ваш тестовий фреймворк, могло минути кілька років, могли з’явитися нові інструменти, або ж були сильно розвинені ті, які ви раніше б не розглядали через відсутність серйозних переваг. Міграція дає можливість зробити аналіз заново й вибрати найкращий варіант на цей момент.

Наприклад, у нашому випадку раніше був вибраний Cypress. Це хороший інструмент, ним користується велика спільнота інженерів, і загалом вибір був на той час об’єктивним. Але прийшовши до питання переписування та проаналізувавши варіанти, що існують наразі, для Mate academy ми вибрали Playwright через кілька його переваг саме для нашого продукту — LMS-платформи (Детальніше про критерії вибору інструментів напишу нижче).

Пришвидшити СІ/CD, покращити стабільність тестів та заощадити ресурси

Якщо у вас були проблеми з повільними або flaky-тестами, переписуючи тести в новому фреймворку, ви можете врахувати всі помилки описані вище і закласти правильну архітектуру. Часто набагато легше це зробити, пишучи з нуля, ніж коли вам потрібно рефакторити код, у який паралельно продовжують додавати нові тести.

Переписавши фреймворк та тести, нам вдалося скоротити середній час проходження одного тесту з 30 до шести секунд, що суттєво вплинуло на швидкість всього пайплайну та на кількість ресурсів, потрібних для їхнього запуску. Досягти такого результату вдалося через сукупність чинників, але основні з них — це вибраний швидший інструмент та оптимізовану підготовку тестових даних.

Наші тести раняться в ізольованому середовищі, яке створюється спеціально для їхнього проходження і видаляється після їхнього закінчення. Це означає, що для кожного тесту потрібно підготувати тестові дані в базі. Один з варіантів в такому сетапі, який був і у нас, — це створення даних для кожного тесту та повна очистка бази після закінчення кожного тесту, що дає стабільність та прибирає потребу в продумуванні, що залишати, а що видаляти.

Але це суттєво впливає на час виконання кожного тесту. Мігруючи фреймворк, ми зробили ставку на розділення глобального сетапу, який робиться один раз для всіх тестів, та підготовку специфічних тестових даних для кожного тесту.

Можливість переглянути тестове покриття та оптимізувати його

Оскільки ви вже беретеся за переписування, то окрім застосування нової організації коду та нових інструментів ви маєте шанс переглянути тестове покриття з метою його покращення. Ви можете об’єднати, чи навпаки розділити тест-кейси, можете, наприклад, зробити їх виконання більш рівномірним за часом для оптимізації паралельного запуску.

Також ви можете вирішити, що якісь тести насправді було б краще перенести «вниз» пірамідою тестування. Ось, наприклад, під час міграції ми використали цю можливість та перенесли тести на рівень нижче — частина тестів з рівня e2e перейшили на рівень UI component tests. Це також вплинуло на час виконання СI/CD-пайплайна.

Можливість запровадити organization-wide style-guide для автотестів

Часто організації мають style-guide-доки для продуктового коду, але можуть не мати його для авто-тестів. Але цей документ є дуже корисним, особливо якщо у вас один автомейшен фреймворк, який використовується всіма командами. Кожен тестувальник або девелопер мають свої переконання про те, як писати код та тести. Якщо це не організувати, то тести на різний функціонал можуть бути написані по-різному. Хоч це і не трагедія, але на довгому проміжку набагато легше та зручніше працювати з фреймворком, де все передбачувано і написано в одному стилі. Створення style-guide-документу дає вам можливість також посилатися на нього під час код-ревʼю та економити свій час, не пояснюючи джуніорам, що і як прийнято писати у вашій організації.

Також, якщо у вас були проблеми з дуплікацією коду та його читабельністю, то міграція на новий тестовий фреймворк дає шанс переписати все більш структуровано та продумано, враховуючи попередні помилки.

Як обрати інструменти для нового тестового фреймворку

Отже ви вирішили переписувати фреймворк і маєте вибрати набір інструментів. Загалом тут план дій такий самий, як і під час вибору інструментів для написання з нуля, але порівнювати треба з тими інструментами, які вже були використані раніше.

Кроки під час вибору інструментів автоматизації:

  1. Зробіть дослідження альтернативних інструментів та виберіть один-два, максимум три потрібних для порівняння. Створіть документ, який порівнює інструменти за критеріями, що важливі для вас. Не спішіть схилятися до рекомендацій в першій же Google-статті. Пам’ятайте — пріоритети у вас та автора статті можуть бути різними.
  2. Базуючись на зібраній інформації, виберіть один-два найбільш потрібні інструменти, щоб зробити POC (proof of concept).
  3. Зробіть POC-проєкт з вибраними для порівняння інструментами. Він дозволить переконатися, що критерії, які важливі для вас, можна втілити в життя. Не лінуйтеся реалізувати в POC всі аспекти, які ви вважаєте важливими, щоб потім не виникло неочікуваних сюрпризів.
  4. Додайте деталей до вашого документу, спираючись на вже зроблений POC.
  5. Поділіться документом та кодом з усіма зацікавленими колегами, обговоріть та ухваліть фінальне рішення.

Критерії для вибору інструментів автоматизації тестування

  1. Мова програмування, що підтримується. Тут у більшості випадків, вас буде цікавити та мова, яка вже використовується у вашому проєкті.
  2. Інструмент, що дозволяє взаємодіяти з інтерфейсом, який ми тестуємо (Web UI, REST API, GraphQL, Mobile app, etc.). Наприклад, якщо це Web UI автоматизація мовою TypeScript, маємо на вибір Cypress, Playwright, або Selenium-based фреймворки.
  3. Інструмент для організації тестів (test cases, suites, before and after hooks, etc.). Такий функціонал може бути як інтегрований з іншим інструментом (наприклад, Playwright), так і додатково доданий.
  4. Інструмент для репортів. Можна так само скористатися або інтегрованими можливостями зі стандартних фреймворків, або ж підключити додатковий репортер. Тут, залежно від задач автоматизації, важливо звернути увагу на можливість легко додавати до репортів тестові степи, скриншоти, відео, трейс логи, тощо. А також можливість бачити аналітику для тест-ранів.
  5. Можливість паралелізації тестів. Можливість автоматичного шардингу.
  6. Можливість автоматичних перезапусків та конфігурація їхньої кількості.
  7. Можливість параметризації тестів.
  8. Більш специфічні можливості інструментів залежно від їхнього призначення. Наприклад, якщо ви розглядаєте інструмент для Web UI-автоматизації, вам може бути важливою підтримка роботи в кількох браузерних вкладках (контекстах), підтримка різних типів браузерів, або ж підтримка тестування Mobile-версії вашого продукту.
  9. Наявність / відсутність спеціального DSL (domain specific language) у того чи іншого інструмента та рівень підготовки інженерів, що будуть писати тести. Наприклад, це актуально для порівняння Cypress vs Playwright. Де Cypress вимагає дотримуватися певного command-chaining синтаксису, а Playwright вимагає використання async / await keywords для синхронного виконання коду.
  10. Швидкість виконання тестів. Особливо важливо для Web UI та Mobile app.
  11. Розмір комʼюніті, що використовує цей інструмент, та якість документації. Це важливо для легшого пошуку рішень проблем, з якими ви можете зіткнутися під час використання.

Варто додати, що більшість інструментів, які наразі існують, будуть реалізовувати переважну частину з перелічених критеріїв, тож ключовими в ухваленні вами рішення стануть лише кілька переваг одного інструменту над іншим.

Наприклад, у нашому випадку під час переходу з Cypress на Playwright стали ключовими такі переваги Playwright:

  1. Швидкість виконання тестів.
  2. Підтримка роботи в кількох браузерних вкладках одночасно — для автоматизації сценаріїв з кількома юзерами. А також можливість працювати з API requests в тому ж браузерному контексті.
  3. Out of the box запуск тестів паралельно на кількох workers & sharding.
  4. Відсутність специфічного для Cypress command-chaining-синтаксису, що дає більшу свободу дій в організації коду та наявність фікстур.

Як організувати процес міграції

Коли набір інструментів та структуру нового фреймворку погоджено з командою, можна переходити до міграції. Для її успіху варто зробити такі кроки:

1. Інтегруйте новий фреймворк та перші кілька тестів у CI/CD-пайплайн

Перед тим, як команда почне мігрувати, усі тести, що існують, потрібно інтегрувати в новий фреймворк до вашого СI/CD-пайплайну, щоб всі нові зміни автоматично тестувалися. Само собою, що на цей момент ви маєте запускати й старі тести в старому фреймворку, і нові в новому. Краще робити окремі джоби для обох фреймворків — так буде легше аналізувати результати та спостерігати за метриками. Окрім того, якщо старі тести не підтримували паралельний запуск, то нові можуть вплинути на їхнє проходження. У нашому випадку нові та старі тести запускалися паралельно в GitHub actions workflow як окремі джоби.

2. Створіть code-style guidelines та засетапте code-linter

Після того, як фреймворк інтегровано до СI/CD та перші кілька тестів написано, варто приділити час та створити документ з описом, як у вашій організації прийнято писати тести. Ідеально, якщо документ містить список правил, кожне з яких має порядковий номер, щоб на нього було легко посилатися, а також code-snippet з прикладом, як варто писати і як ні.

Чому це важливо — детально описано вище у розділі про переваги міграції.

Також дуже рекомендую додати code-linter та автоматизувати всі перевірки для яких це можливо. А ще можна використати prettier, який допоможе автоматично виправити код відповідно до встановлених правил.

3. Створіть дашборд з метриками

Дуже важливо мати дашборд з метриками, на якому ви можете швидко побачити, як відбувається ваша міграція.

У нашому випадку ми мали можливість бачити на дашборді:

  • зміну кількості тестів в старому сетапі;
  • зміну кількості тестів в новому сетапі;
  • середню швидкість проходження одного тесту, щоб порівняти, як було та стало;
  • відсоток успіху проходження тестів у мастер-бранчі, щоб швидко виявляти flaky-тести і одразу фіксити їх.

4. Перенесіть усі старі тести в новий фреймворк

Нарешті ви добралися до перенесення самих тестів, вітаю. 🙂

Тут можна піти одним з двох шляхів:

Перший: розробка нових тестів відбувається у новому фреймворку, а старі раняться у старому і переносяться поступово.

У якості технічного боргу команда поступово переносить старі тести до нового фреймворку, наприклад, беручи 10-20 % від спринт-капасіті під цю задачу. Проблема з цим підходом в тому, що якщо у вас багато тестів, міграція може зайняти тривалий період і вам доведеться паралельно підтримувати два фреймворки, що є затратно по часу та ресурсам.

Щоб зрозуміти скільки часу знадобиться на міграцію, потрібно приблизно виміряти час на розробку одного тесту однією людиною та поділити його на кількість всіх тестів та людей, що можуть займатися цією задачею. Якщо отриманий час вас влаштовує — можна йти цим шляхом.

Другий: усі тести переносяться до нового фреймворку одразу.

Плюси цього варіанту — ви не створюєте технічний борг. Одночасно підтримувати два фреймворки потрібно кілька тижнів, поки відбувається процес міграції. Основна ж складність, що для міграції потрібно буде виділити час усієї команди, або хоча б великої її частини.

Ми з командою пішли другим шляхом. На момент переходу у нас було досить багато e2e-тестів. Порахувавши, що на їх перенесення команда тестувальників має витратити від 3 місяців (а ще ж нові потрібно писати), ми вирішили, що буде набагато вигідніше взятися за справу всіма інженерами.

Раніше у блозі на DOU наш CEO Роман Апостол вже розповідав про інженерну культуру в нашій команді, і в цьому випадку вона дала велику перевагу, бо інженери можуть не тільки писати код, але також і автотести.

Тож кожна команда (девелопери та тестувальники) виділили один спринт роботи на цю задачу. Зрештою всі e2e-тести були перенесені протягом одного місяця. Також перевагою стало те, що всі інженери ознайомилися та звикли до нового фреймворку, і надалі вже змогли швидко додавати нові тести. Із складнощів можна виділити те, що коли велика кількість інженерів контрибутить в один і то й же функціонал (наприклад, кілька інженерів переносять тести для однієї фічі), то рано чи пізно доведеться резолвити конфлікти в коді. Але й із цим ми впоралися. 🙂

Закінчивши міграцію, ми побачили, що після зміни інструменту та переписування фреймворку стадія e2e-тестування в нашому CI/CD-пайплайні стала займати замість 35-40 хвилин — 10-15 хвилин, і це за умови, що кількість використаних ресурсів (віртуальних машин) скоротилася втричі.

Висновок

Якщо ви маєте описані вище проблем з автомейшен-фреймворком, потрібно розглянути це як можливість для суттєвого покращення автоматизації на проєкті. Не варто переписувати все, якщо ви маєте тільки одну-дві проблеми, бо рефакторинг ніхто не скасовував. Але якщо ви бачите, що ваш фреймворк має великий набір проблем, тоді варто вкластися у вибір кращого інструменту та архітектури, і здійснити міграцію.

Описані вище критерії вибору інструментів допоможуть вам розглянути всі потрібні аспекти, а стадії міграції зорієнтують у варіантах міграції та послідовності дій. Вам також може здаватися, що міграція займає дуже багато часу та ресурсів команди, і це правда. Але переваги, які ви отримаєте, будуть працювати на довгому проміжку та обов’язково окупляться.

Дуже сподіваюся, що наш приклад зможе когось надихнути на покращення свого автомейшен фреймворку, а рекомендації зі статті допоможуть втілити зміни в життя.

👍ПодобаєтьсяСподобалось21
До обраногоВ обраному11
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Дякую, стаття дуже сподобалась!

Дуже цікавий матеріал, дякую! Легко читалося та затригерило подумати про деякі речі/процеси.

Дякую за відгук, дуже рада, що спонукало до роздумів!

Цікаво! Дуже дікую

Дякую за матерiал. Кiлька разiв теж робив такi мiграцiï, бувало боляче.

Дякую за відгук! Сподіваюся, що виправдалися зусилля?

Так. Останнього разу переводив компанiю з Python + Robot framework на Java + Selenide + Rest-assured + Junit 5. Старий код писався кiлька рокiв, був повiльний(4 — 6 годин на 1 запуск), тести неможливо було запустити в паралель, структура обiйняти й плакати — якась дика мiшанина чистого пайтона, робота i ще якоïсь матерi. Стек компанiï Java, хз звiдки взявся robot framework. Розробили базовий скелет, я написав документацiю, провiв кiлька презентацiй i вiддав людям. Протягом року я робив код рев’ю 8-9 командам i воював за чистоту коду. Тести почали писати не тiльки QA, але й розробники(навiть фронтендщики, ïм сподобався $ синтаксис Selenide доречi). Тепер замiсть 4 — 6 один тестовий ран був десь годину(!!1!1111!!!1!), iнодi менше, завдяки гнучкiй фiльтрацiï Junit 5. На додачу я розробив плагiн для IntelliJ який експортував степи з page objects як тест-кейс i експортував ïх у TestRail. Кастомний extension для Junit 5 збирав id тестiв i створював тест рани в TestRail, моя менеджерка могла бачити стан релiзного тестування для мануального i автоматизованого тестування в одному мiсцi.

Дуже цікавий приклад міграції) Дякую, що поділилися!

Дякую, дуже якісна стаття!

єдина причина це енд оф лайф тестового фреймворку, все інше скіл ішґю і при переписувані ви швидко будете знову там де були і з минулим фреймворкомfа

Це якщо переписує людина на тому самому скіл левелі, що рідко буває. Зазвияай це може бути або більш скіловий інженер/ка або той самий, але вже з набитими гульками )

Повністю погоджуюсь з Геннадієм :)

Підписатись на коментарі