Чи не повинні одиничні тести використовувати мої власні методи?


83

Сьогодні я переглядав відео "Основи JUnit ", і автор сказав, що під час тестування заданого методу у вашій програмі ви не повинні використовувати інші власні методи у процесі.

Якщо конкретніше, він говорив про тестування деякого методу створення записів, який брав ім'я та прізвище для аргументів, і він використовував їх для створення записів у заданій таблиці. Але він стверджував, що в процесі тестування цього методу він не повинен використовувати свої інші методи DAO для запиту в базу даних, щоб перевірити кінцевий результат (щоб перевірити, чи справді створено запис із потрібними даними). Він стверджував, що для цього він повинен написати додатковий код JDBC для запиту в базу даних та перевірки результату.

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

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

У мене є сильне почуття з цього приводу декількох останніх програм Excel - VBA, про які я написав (належним чином перевірений пристрій завдяки Rubberduck для VBA ), де застосування цієї рекомендації означало б багато додаткової додаткової роботи, без жодної сприятливої ​​користі.

Чи можете ви поділитися своїми думками з цього приводу?


79
Трохи дивно бачити тест одиниці, який взагалі включає базу даних
Річард Тінгл

4
IMO добре називати інші класи IFF, вони досить швидкі. Знущайтеся над усім, що має перейти на диск або через мережу. Немає сенсу глузувати з простого ІМО класу.
RubberDuck

2
Маєте посилання на це відео?
candied_orange

17
" належним чином перевірений блок завдяки Rubberduck для VBA " - ви, пане, щойно зробили мій день. Я б виправив друкарську помилку і відредагував її з "RubberDuck" на "Rubberduck", але я відчуваю, що це робив якийсь спамер і додав посилання на rubberduckvba.com (у мене є доменне ім'я та співвласник проекту з @RubberDuck) - тому я просто замість цього прокоментую тут. У будь-якому випадку це дивовижно бачити людей, які насправді використовують той інструмент, який відповідав за більшість моїх безсонних ночей протягом більшої частини останніх двох років! =)
Матьє Гіндон

4
@ Mat'sMug і RubberDuck Мені подобається те, що ти робиш з Rubberduck. Будь ласка, продовжуйте це. Звичайно, через це моє життя простіше (мені трапляється робити багато невеликих програм і прототипів в Excel VBA). До речі, згадка про Rubberduck в моєму дописі я просто намагалася бути приємною до себе RubberDuck, що, в свою чергу, було приємно мені тут, в PE. :)
carlossierra

Відповіді:


186

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

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

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


117
Ураг за прагматизм.
Роберт Харві

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

4
"... одиничне тестування - це інструмент, і він покликаний служити вашим цілям. Це не вівтар, про який слід молитися". - Це!
Уейн Конрад

15
"не вівтар, про який слід молитися", - гніваються тренери TDD, що надходять!
День

5
@ jpmc26: ще гірше, що ви не хочете, щоб усі ваші тестові одиниці проходили, оскільки вони правильно обробляли роботу веб-служби, що є правильним і правильним поведінкою, але ніколи насправді не використовуйте код, коли він працює! Після заглушки ви можете вибрати, чи "вгору" чи "вниз", і тестувати обидва.
Стів Джессоп

36

Я думаю, що це зводиться до термінології. Для багатьох "тест одиниці" - це дуже специфічна річ, і за визначенням не може бути умова пропуску / відмови, що залежить від будь-якого коду поза блоком (методом, функцією тощо), який тестується. Це включало б взаємодію з базою даних.

Для інших термін "одиничний тест" значно слабкіший і охоплює будь-який тип автоматизованого тестування, включаючи код тесту, який тестує інтегровані частини програми.

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

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

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

Чи варто вам залежати від інтеграційних тестів чи одиничних тестів чи обох - це набагато більша тема обговорення.


Ви маєте рацію у своїх підозрах. Я зловживаю термінами, і тепер я бачу, як з кожним типом тесту можна обробляти правильніше. Дякую!
carlossierra

11
"Для інших термін" одиничне тестування "набагато втрачає" - окрім всього іншого, так звані "одиничні тестові рамки" - це дійсно зручний спосіб організації та проведення тестів вищого рівня ;-)
Стів Джессоп

1
Замість розрізнення "чистий" проти "вільного", яке завантажено конотаціями, я б припустив, що це різниця між тими, хто переглядає "одиниці", "залежності" тощо з точки зору домену (наприклад, "автентифікація" буде зарахована як одиниця, "база даних" вважатиметься залежністю тощо) порівняно з тими, хто розглядає їх з точки зору мови програмування (наприклад, методи - це одиниці, класи - залежності). Я вважаю, що визначення того, що мається на увазі під фразами на кшталт "тестова одиниця", може перетворити аргументи ("Ви помиляєтесь!") В дискусії ("Це правильний рівень деталізації?").
Варбо

1
@EricKing Звичайно, для великої більшості тих, хто є у вашій першій категорії, ідея взагалі включати базу даних в одиничні тести - це анатема, чи ви використовуєте свої DAO, чи безпосередньо потрапляєте в базу даних.
Periata Breatta

1
@AmaniKilumanga Питання було в основному "хтось каже, що я не повинен робити це в одиничних тестах, але я це роблю в одиничних тестах, і я думаю, це нормально. Це нормально?" І моя відповідь була "Так, але більшість людей називають тест інтеграції замість одиничного тесту". Що стосується вашого запитання, я не знаю, що ви маєте на увазі під "тестами інтеграції, використовуючи методи виробничого коду?".
Ерік Кінг

7

Відповідь - так і ні ...

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

Ці тести між блоками добре підходять для висвітлення коду та під час тестування функціональності від кінця до кінця, але вони мають деякі недоліки, про які слід пам’ятати:

  • Без ізольованого тесту для підтвердження того, що зламається, невдалий тест "перехресного блоку" потребує додаткового усунення несправностей, щоб визначити, що не так з вашим кодом
  • Занадто багато покладаючись на тести між собою, можуть позбавити вас від контрактного мислення, до якого ви завжди повинні бути при написанні SOLID-об'єктно-орієнтованого коду. Ізольовані тести зазвичай мають сенс, оскільки ваші підрозділи повинні виконувати лише одну основну дію.
  • Тестування в кінці до кінця бажано, але може бути небезпечним, якщо для тестування вам потрібно буде записатись у базу даних або виконати певні дії, які ви не хотіли б проводити у виробничих умовах. Це одна з багатьох причин, через які глузуючі рамки, такі як Mockito , настільки популярні, тому що вони дозволяють підробити об'єкт і імітувати тест в кінці, не змінюючи насправді те, чого ви не повинні.

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

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


4

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

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


1
Ключ тут в одиниці. Якщо вам потрібно телефонувати в інші процедури та процеси, для мене це не буде підрозділом. Якщо робиться кілька дзвінків і потрібно перевірити кілька кроків, то, якщо це великий фрагмент коду, ніж одиниця, розбийте його на більш дрібні фрагменти, що охоплюють єдиний фрагмент логіки. Зазвичай, виклик БД також буде знущатися, або БД пам'яті буде використовуватися в тесті блоку над доступом до реальної БД та потребує скасування даних після тесту.
dlb

1

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

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

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

Ви можете сперечатися, чи «одиниця» у тесті одиниці позначає «одиницю коду» або «одиницю функціональності», функціональність, можливо, створена багатьма кодовими одиницями у концерті. Я не вважаю це корисним відмінністю - питання, які я маю на увазі: "чи говорить тест щось про те, чи система надає ділову цінність", і "чи є тест крихким, якщо впровадження зміниться?" Тести на зразок описаного корисні під час TDDing системи - ви ще не написали "отримати об'єкт із запису бази даних", тому не можете перевірити повну одиницю функціональності - але є крихкими щодо змін у впровадженні, тому я б видаліть їх, коли повну операцію можна перевірити.


1

Дух правильний.

В ідеалі в одиничному тесті ви випробовуєте одиницю (індивідуальний метод або малий клас).

В ідеалі ви б заглушили всю систему баз даних. Тобто, ви запустили свій метод у підробленому середовищі та просто переконайтесь, що він викликає правильні API DB у правильному порядку. Ви чітко, позитивно не хочете перевіряти БД під час тестування одного з власних методів.

Переваг багато. Найбільше тести стають сліпими швидко, тому що вам не потрібно займатись налаштуванням правильного середовища DB та згортанням його.

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

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


"просто переконайтеся, що він викликає правильні API DB у правильному порядку" - так, обов'язково. Поки ви не виявите, що ви неправильно прочитали документацію, а власне правильне замовлення, яке ви мали використовувати, зовсім інше. Не знущайтеся над системами, що не знаходяться під вашим контролем.
Joker_vD

4
@Joker_vD Для цього призначені тести на інтеграцію. В одиничних тестах над зовнішніми системами абсолютно слід глузувати.
Бен Аронсон

1

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

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

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


1

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

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

@Test
public void canSaveData() {
    writeDataToDatabase();
    // what can you assert - the only expectation you can have here is that an exception was not thrown.
}

@Test
public void canReadData() {
    // how do I even get data in there to read if I cannot call the method which writes it?
}

Ця проблема виникає через перспективу методів тестування. Не використовуйте методи тестування. Тест поведінки. Яка поведінка класу WidgetDao? Він зберігає віджети. Гаразд, як ви переконаєтесь, що віджети зберігаються? Ну, яке визначення наполегливості? Це означає, що коли ви пишете його, ви можете прочитати його ще раз. Тож читати + писати разом стають тестом, і, на мою думку, більш змістовним тестом.

@Test
public void widgetsCanBeStored() {
    Widget widget = new Widget();
    widget.setXXX.....
    // blah

    widgetDao.storeWidget(widget);
    Widget stored = widgetDao.getWidget(widget.getWidgetId());
    assertEquals(widget, stored);
}

Це логічний, згуртований, надійний, на мій погляд, змістовний тест.

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

Щоб перевірити, чи можна щось зберігати, потрібно зберегти його, а потім прочитати назад. Ви не можете перевірити, чи зберігається щось, якщо ви не можете його читати. Не перевіряйте збереження даних окремо від отримання даних. Ви закінчите тести, які нічого вам не скажуть.


Дуже проникливий підхід, і, як ви кажете, я думаю, що ви справді потрапили в один із основних сумнівів, які я мав. Дякую!
carlossierra

0

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

В якості альтернативи (і з увагою на порушення однієї відповідальності), припустимо, потрібно зберігати версію рядка з кодуванням UTF-8 у поле, орієнтоване на байт, але насправді зберігається Shift JIS. Деякі інші компоненти збираються прочитати базу даних і очікують, що вони побачать UTF-8, звідси і вимога. Тоді зворотний проїзд через цей об’єкт повідомить правильне ім’я та адресу, оскільки він перетворить його назад із Shift JIS, але помилка не виявлена ​​вашим тестом. Сподіваємось, це буде виявлено деяким пізнішим інтеграційним тестом, але вся суть тестових підрозділів полягає в тому, щоб рано наздогнати проблеми і точно знати, який компонент несе відповідальність.

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

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

Розуміння, що тут є проблема, полягає в тому, що два тести, які нібито тестують різні одиниці, насправді роблять те саме . Вони обидва викликають сеттера, за яким йде геттер, а потім перевіряють результат - початкове значення. Але ви хотіли перевірити, що сеттер зберігає дані, а не те, що пара setter / getter працює разом. Тож ви знаєте, що щось не так, просто потрібно розібратися, що і виправити тести.

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

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

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

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

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


0

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

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

Скажімо, у вас є два тести: A - читання бази даних B - вставка в базу даних (залежить від A)

Якщо пропуск, то ви впевнені, що результат B буде залежати від перевіреної в B частини, а не від залежностей. Якщо A failes, ви можете мати помилковий негатив для B. Помилка може бути в B або A. Ви не можете точно знати, поки A не поверне успіх.

Я б не намагався писати деякі запити в тестуванні одиниць, оскільки ви не повинні мати можливість знати структуру БД, яка стоїть за програмою (або вона може змінитися). Значить, ваш тестовий випадок не вдався через ваш код. Якщо ви не пишете тест для тесту, але хто буде перевіряти тест для тесту ...


-1

Зрештою, це зводиться до того, що ви хочете перевірити.

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

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

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

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


@downvoters: ви можете сказати мені свої причини, щоб я міг покращити аспекти, на які, на вашу думку, бракує моєї відповіді?
hoffmale
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.