Как стать автором
Обновить

Комментарии 68

Как Тим Лид настоял, что версия 2.0 одной высоконагруженной распределенной отказоустойчивой системы будем делать в виде TDD. В итоге, сами того не зная, пришли к именованию и грануляции BDD.
Версия 1.0 системы имела всего 300 тестов, исключительно интеграционных.

Теперь же у нас более 3000 юнит тестов и около 50 интеграционных и полная увереность, что система не развалится при малейшем изменении.

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

На самом деле TDD/BDD позволяет быстрее и комфортнее писать любые системы, а не только отказоустойчивые.
На чем основано суждение, что «TDD/BDD позволяет быстрее писать любые системы» (про комфорт выкинул, т.к. совсем уж субъективно)? Сколько читал по теме, кто-то из авторов явно, кто-то неявно, но говорят, что исследования по эффективности не проводились?
На моём личном опыте. Мне посчастливилось работать в компании, где все коллеги понимают и используют TDD неукоснительно каждый день — и у нас всё очень хорошо. Быстро и комфортно.

Да, это субъективно. Но мне кажется, если даже вам дадут ссылку на исследование по эффективности TDD, она вас не заставит поверить и начать использовать. А такое вот субъективное мнению — может.
Здесь вопрос в области применимости. Исследование покажет когда это эффективно.

То, что у вас вся организация построена под TDD не означает, что вы бы не добились такой же скорости и комфорта без TDD/BDD (есть и другие подходы, которые решают те же проблемы; например, использование аналитиков, архитекторов, тестировщиков). А так же не означает, что любой проект любой команде быстрее писать с использованием TDD/BDD (для упрощения, можно считать, что команда делает не первый, а 3ий проект на TDD).
Хочу поспорить по поводу «быстрее» — это абсолютно не так. Когда нужен быстрый time to market следование TDD замедляет в 3 раза. Имеется опыт клиенсткого приложения, который начали делать при помощи TDD и все сильно затянулось, а приложение надо было выпускать очень и очень скоро (специфика рынка) — в итоге пришлось отойти от TDD и делать все по старинке. Потом, конечно, пришлось долго долго дописывать тесты после релиза.
Я бы сказал, что TDD позволяет писать приложения, которые очень просто сопровождать ибо будет 100% увереность, что все работает правильно. Но это нефига не ускоряет разработку.
Это вопрос навыков. Не надо по оному опыту судить.
На эту темы есть очень простой довод: большая часть времени уходит не на написание (кода), а на продумывание. Это время вы тратите по-любому, ведь продумывать свои действия надо, так? Ну а если вы оформляете промежуточные результаты своего мыслительного процесса в виде тест-кейсов, то идти он должен проще и стабильнее. Это всё равно что каждую следующую мысль записать на бумажку.
У меня на одном из проектов, который пробовали делать по TDD, значительная часть времени уходила на изменение тестов. Т.е. фактически старые тесты не поймали ни одной ошибки, а поверх писались уже новые тесты. Никакого особого продумывания для классического mvc-веб-приложения не требуется (время на продумывание относительно времени на написание тестов). Естественно, до этого уже есть некая проектная документация (описание базы, архитектуры, верстка).
Видимо ключевое слово «пробовали». Имхо, ТДД/БДД дает ускорение только когда люди уже привыкли и пишут тесты быстро, а не пробуют.
Не спорю, что при определенных навыках тесты будут писаться быстрее. Но это не снимает необходимость их написания, рефакторинга и обновления под новый функционал. Собственно, интересует когда это оправдано, а когда нет. Ответ «Всегда», который по сути есть в статье представляется несколько оптимистичным.
Многие считают, что универсального критерия тут нет.
Может быть, стоит хорошенько попробовать и самому решить, какой критерий подходит для вашей команды и ваших разработчиков.

Я вот попробовал и пришёл к выводу: всегда. :)
Никакого особого продумывания для классического mvc-веб-приложения не требуется

когда глючит программный код, и архитектура трещит по швам, многие пытаются искать спасения в TDD. проблема в том, что автоматические тесты, это тоже программный код. поэтому его в равной степени можно писать не_правильно, — точно с таким же исходом. :)

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

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

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

— эй, вася, что ты такое пишешь?
— еще не знаю, щаз скомпилирую, узнаю.

самое сложное при внедрении TDD это ломка программистского стереотипа: сначала быстро «накодить», ведь и так все ясно, а потом думать зачем, для чего, и исправлять ошибки. :)

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

Если времени на продумывание КОДА особо не требуется, то уж тем более ещё меньше времени потребуется на написание требований к нему.
Нет, это не вопрос навыков — а вопрос сроков. Банально надо сроки разработки умножать на 1.5 при использовании TDD ибо объем кода в тестах будет сопоставим, если не больше, с объемом кода самого приложения.

Но это увеличение на 1.5 того стоит, если приложение потом будет иметь долгую жизнь.

А продумывание оформлять в виде тестов — не кажется ли вам, что это только все усложняет? Прототипирование и продумывание я вообще делаю в виде спарков — когда делают хомячковый проект и там обкатываю идею вообще без тестов, а потом переношу ее на проект уже следуя TDD
> продумывание оформлять в виде тестов — не кажется ли вам, что это только все усложняет?
Нет, не кажется. Опять же, вопрос навыков :)
Или даже привычки.
Кстати, уверенность часто ложная, т.к. шаблоны тяжело действительно хорошо покрыть тестами. Если их много и они достаточно часто меняются, то без носителя знаний проекта (как же оно должно работать и почему пришли к этому) и тестировщика будет сложно.
Но ускоряет тестирование? У меня неоднократно бывали случаи, когда тест приходилось прогонять раз по 30 пока не добьешься нужного поведения. Может быть, вы пытались добиться избыточного покрытия тестами?
На начальном этапе, когда функциональность невелика, почти все держится в голове TDD действительно может замедлит разработку. Выигрыш будет получен через некоторое время, более-менее долгоживущим системам TDD банально позволяет держаться на плаву, сохраняя устойчивость при рефакторинге и оптимизации самой системы, и, что не менее важно — команды, которая делает систему.
ибо будет 100% увереность

<=99% же.
Система не развалится. Развалятся ваши 50 интеграционных тестов и некоторые из 3000 юнитов. Не потому что плохо запрограммировано, а наоборот, потому что функционал изменился, и тесты нужно пересматривать.
Чем больше тестов в эволюционирующей системе, тем бОльшая часть времени тратится на их поддержку.
Такое случается, если вносить архитектурные изменения где-нибудь через год после начала проекта. В этом случае нужно сходить, сказать ответственному за изменение всё что ты о нём думаешь и садиться рефакторить и код и тесты.
Вот и хорошо, что они разваливаются.

Так же это смотря как писать юнит тесты — у нас почти 99% тестов это изолированные тесты, где тестируется класс, а все зависимости ему передаются в виде моков интерфейсов. И если надо поменять поведение этого конкретного класса — мы правим тесты этого конкретного класса.

Взаимодействие тестируется на уровне интеграционного теста, когда приложение запускается как черный ящик и проверяются результаты работы.
Почитал про Behat и увидел «Behat is testing applications outside in. It means, that Behat works only with your application's input/output. If you want to test your models — use unit testing framework instead, Behat created for behavior testing (but can be used for anything +) ).» И несмотря на уточнение в скобках, похоже BDD не является заменой надмножеством TDD, а дополняет его.

P.S. Так до конца TDD и не осилил. Часто пишу тесты, которые, имхо, бессмыслены, но как бы нужны, например проверяю, что new MyClass возвращает экземпляр MyClass. Или часто приходится писать кучу «обвязок», только чтобы проверить записывается ли объект в БД, хотя по коду в 2 строки видно, что не записаться он может только по внешним причинам.
Мне тоже интересно найти ответ на вопрос почему BDD может быть заменой TDD.
Вся идея BDD это использовать слова вроде should и ensure. Всё. В чем эволюция?
Почему нужно было создавать столько новых BDD фреймворков, а не оформить BDD в некоторый набор рекомендаций по написанию TDD и использовать существующие фреймворки?
И по-моему, пример №3 лучше чем пример с BDD %)
Моя изначальная цель была показать, что пример №3 лучше, чем №1 и №2, так что я своей цели добился, видимо. :)

Да есть и набор рекомендаций, есть и целые книжки.
Можно было бы и про JUnit сказать: зачем было городить эту библиотеку вместо того, чтобы просто написать рекомендации, как правильно тестировать код?

Можно было бы и про C++ сказать: зачем было городить целый язык вместо того, чтобы просто написать рекомендации, как правильно программировать на ассемблере?

Эволюция — она такая и есть. Каждая следующая версия обезьяны тоже несильно отличалась от предыдущей, а вот глядишь, в конце концов человечек получился.
Разговор не о том, что BDD не намного лучше чем классические юниты, а о том, что он в некоторых ситуациях хуже. И стоило бы сказать, что BDD не против TDD, он его расширяет.
А я и сказал в самом начале: «BDD — это дальнейшее развитие идей TDD».
Очень интересно узнать, в каких случаях BDD хуже? Можете привести какие-то примеры?
А еще лучше написать статью — я думаю, это многим было бы интересно.
Потому что автор топика показывает не Behavior, а Unit-тесты с синтаксисом Behavior-тестов.
На самом деле, основная идея BDD именно в обратном подходе к разработке, а не в синтаксисе. TDD является моделью разработки inside-out (от частного к общему), когда мы гарантируем работу отдельно взятых атомарных модулей и предполагаем, что система в общем, при этом, будет работать нормально. Через некоторое время, было замечено, что это не так и что верная работа отдельных модулей вовсе не гарантирует верную работу системы в целом.

Появилась потребность в тестировании системы в общем. Такую модель разработки назвали outside-in (от общего к частному) или BDD. Смысл сводится к тому, что мы также сокрываем реализацию в черный ящик и работаем только с выходами/входами, только не отдельных модулей, а всей системы в целом (веб-приложение — Request/Response, консольное приложение — Arguments/Output). Оказалось, что подобную логику легче описывать на специфических терминах типа should/given/when и т.п. Это язык бизнес-логики, на котором разговаривают наши заказчики и UT-фрэймворки оказались к нему неприспособленными. Это как крепить на запорожец обвесы от BMW и считать, что у тебя M3.

https://github.com/sebastianbergmann/phpunit/commit/6aa9183496f9bb2131d17ba08195a93a07937762#commitcomment-164598. Тут Sebastian говорит про то, что он депрекэйтит BDD функциональность в phpUnit 3.5 из-за ее неспособности решать поставленные задачи оптимально и появления на рынке достойного средства для этого — Behat.

Задачи выполняются какбы одинаковые, но цели разные (тестирование атомарных модулей и тестирование всей системы в целом) — отсюда специфичные требования и новый функционал, который не нужен в TDD, но необходим в BDD.

Так что для сравнения BDD с BDD на UT фрэймворке следует приводить вот этот пример: https://gist.github.com/478885.
Тут ниже дали ссылку на ваш доклад. Посмотрел, несколько вопросов есть по BDD вообще, и Behat в частности:
— использование BDD — это, грубо говоря, переход от разработки основанной на юнит-тестах к разработке, основанной на функциональном/интеграционном тестировании?
— использование юнит-тестов при BDD необходимо, желательно или избыточно?
— а Behat для symfony разработчика — это, грубо говоря, обёртка над функциональными тестами symfony, делающая удобным/стандартизированным то, что и так есть (спеки/user srory можно написать в произвольном виде, и на их основе писать функциональные тесты)?
— использование BDD — это не переход, а добавление к юнит-тестам behavior тестов
— верная работа отдельных модулей не гарантирует верную работу системы в целом, но верная работа системы в целом не гарантирует верную работу отдельных модулей ;-)
— sfBehatPlugin — это набор шагов-оберток поверх sfTestFunctional (https://github.com/everzet/sfBehatPlugin/tree/master/features/steps/). EverzetBehatBundle — это набор шагов, использующих стек HTTP компонентов Symfony2 для эмуляции браузинга и тестирования аутпута (http://dl.dropbox.com/u/282797/BehatBundle_results.html). У EverzetBehatBundle более тесная интеграция с фрэймворком за счет того, что Symfony2 как и Behat используют DIC.
> BDD — это, грубо говоря, переход от разработки основанной на юнит-тестах к разработке, основанной на функциональном/интеграционном тестировании?
Однозначно НЕТ!
Юнит-тестирование и функциональное/интеграционное тестирование — это два разных вида тестирования, они оба необходимы и не могут заменить друг друга.

BDD всего лишь пытается улучшить и то, и другое.
у меня TDD ассоциируется с en.wikipedia.org/wiki/Design_by_contract, только «вывернутым наизнанку». в обоих случаях спецификации описываются в виде пред- и пост- условий, с использованием предикатных выражений:

«assert value == false» или «value should be false», — не суть.

но в TDD система описывается «снаружи» (ака черный ящик), а в DbC — «изнутри».

BDD же отражает другой аспект — императив — т.е. требования к поведению — «в динамике». какие действия и в какой последовательности нужно делать. часто это тоже важно:

Given…

When I go google.com
And I see search field
And I type «wtf BDD»


Then…

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

термины юнит/функциональные/интеграционные тесты отражают ортогональный — системный — аспект. те же юнит тесты можно записывать и в стиле bdd и в стиле tdd — всё зависит от типа требований.
Я не говорю про тестирование как таковое, я говорю о * driven development. При использовании TDD я должен начинать разработку очередного «юнита» с написания тестов, к нему (ну, плюс «каркас» юнита, чтобы не было fatal/compile/… error), специфицирующих, по сути, его поведение. Рассуждая по аналоги, при использовании BDDя должен начинать разработку с написания behavior приложения в целом.
> В BDD нигде не говорится, что он предназначен именно для описания приложения в целом. Ну, и нигде не говорится, что спецификации BDD надо писать ДО приложения. Так что правильного ответа на ваш вопрос не знаю.
В нашем фирме мы пишем функциональные тесты всё-таки ПОСЛЕ того, как приложение написано.
> Через некоторое время, было замечено…
Да что вы, необходимость интеграционного тестирования была очевидна задолго до того, как слово «юнит-тесты» появилось на свет.

> TDD является моделью… предполагаем, что система в общем, при этом, будет работать нормально.
Да что вы, никто не предполагает, что благодаря одному только TDD система будет работать нормально. В любом описании TDD всегда говорится, что одних юнит-тестов недостаточно, и другие виды тестирования тоже необходимы.

> основная идея BDD именно в обратном подходе к разработке, а не в синтаксисе.
Да что вы, откуда вы это взяли?
Вот цитата с википедии:
«BDD is… technique that encourages collaboration between developers, QA and non-technical or business participants in a software project. <...> It extends TDD by writing test cases in a natural language that non-programmers can read.»

То есть: «BDD — это техника… которая улучшает взаимодействие между программистами, тестеровщиками и не-техническими людьми в проекте. <...> BDD расширяет TDD путём написания тест-кейсов на естественном языке, который могут читать и не-программисты.»

en.wikipedia.org/wiki/Behavior_Driven_Development
По-моему не хватает в статье нормального введения. Потому что щас в качестве введения «разницу между TDD и BDD», а какая может быть разница если одно лежит в основе другого?
Изучив историю появления BDD, можно сделать следующие выводы:
1. Многие разработчики не знали как и не умели писать хорошие тесты используя TDD
2. Придумывают заменить слово test на should / ensure that, называют это BDD
3. BDD можно использовать с любым unit test framework-ом
4. Идея развивается, придумываются спецификации, ориентированость на user cases, а не на техническую сторону
5. Появляются BDD framework-и, которые больше подходят для написания функциональных тестов.
5. TDD не исчез и его нужно применять (стараясь использовать стиль BDD)
6. BDD — acceptance tests

Так?
Всё верно!
>> Через некоторое время, было замечено…
>Да что вы, необходимость интеграционного тестирования была очевидна задолго до того, как слово «юнит-тесты» появилось на свет.
Я говорил не про интеграционное тестирование вообщем, а про интеграционное тестирование на основе behavior тестов. И оно уж никак не могло появиться раньше TDD ;-)

> То есть: «BDD — это техника… которая улучшает взаимодействие между программистами, тестеровщиками и не-техническими людьми в проекте. <...> BDD расширяет TDD путём написания тест-кейсов на естественном языке, который могут читать и не-программисты.»

Да, тут Вы правы. Я смешал понятия Outside-In Development с BDD, т.к. BDD сейчас чаще всего применяется именно при OIN разработке и я до сих пор не увидел смысла применять синтаксис BDD для написания юнит-тестов. В TDD практически не возникает коммуникативных проблем, которые существуют в Outside-IN ориентированной разработке, которые и породили необходимость в новом DSL.

Вообщем, извините за укол в Вашу сторону, виноват я.
И спасибо, что поправили. А то укоренился бы еще больше в заблуждении. ;-)
НЛО прилетело и опубликовало эту надпись здесь
Спасибо, добавил.
Спустя какое-то время приходит ещё один разработчик и замечает, что даже этот хороший юнит-тест на является вполне читабельным


на -> не.
спасибо, исправил.
Что-то я так и не понял в чем слоь. TDD — это методология, она управляет разработкой, я не могу добавлять функциональность не придумав как ее можно протестировать т.е. не придумав ограничения (спецификации). Различия в «попытке №3» и примере с BDD только в синтаксисе (который, надо признать, намного удобнее читать) и названии тестов? Ну так все гуру тестирования говорят что у тестов должны быть осмысленные имена, и что тесты — лучшая документация для программы.

Ну а вообще, спасибо за статью, обязательно поковыряюсь в библиотеках для .NET.
вот и я тоже не понял при чем тут TDD равно как и BDD. Возможно пример #2 относится как-то к TDD, но это осталось за пределами заметки. А вообще, из текста возникает (во всяком случае у мена) ложное впечатление, что вся разница между «типичными юнит тестами», TDD и BDD в наименовании методов и степени грануляции тестов. Также, я не смог понять обещанной «покажу разницу между TDD и BDD». Вот если бы не знал то подумал, что там вся соль в именовании тестов и их размере, что на самом деле является просто характеристикой осмысленных и читабельных тестов.
Почему же ложное? Вот вам определение с википедии: en.wikipedia.org/wiki/Behavior_Driven_Development

«It extends TDD by writing test cases in a natural language that non-programmers can read.»

Просто я не весь language показал, ну так это и не было моей целью.
Для России это все-таки означает русский язык, иначе представителю заказчика это вряд ли можно будет показать. Так же ему не интересна реализация тестов. Такое есть в rspec и cucumber для ruby. Пример фичи( github.com/aslakhellesoy/cucumber/blob/master/examples/i18n/ru/features/division.feature ):

# language: ru
Фича: Деление чисел
  Поскольку деление сложный процесс и люди часто допускают ошибки
  Нужно дать им возможность делить на калькуляторе

  Структура сценария: Целочисленное деление
    Допустим я ввожу число <делимое>
    И затем ввожу число <делитель>
    Если я нажимаю "/"
    То результатом должно быть число <частное>

  Значения:
    | делимое | делитель | частное |
    | 100     | 2        | 50      |
    | 28      | 7        | 4       |
    | 0       | 5        | 0       |
А для Питона есть что нибудь похожее?
https://github.com/aslakhellesoy/cucumber/wiki/Python и lettuce.it/
Спасибо, добавил.
Прочитал статью по ссылке. Получается, что это попытка скрестить «пользовательские сценарии» c unit-тестами?

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

Unit-тесты, с другой стороны, пишутся на языке программирования и могут заботиться о таких деталях о которых бизнес-пользователь ни сном, ни духом… Например, транзакции.

PS: А пример приведённый в данном хабратопике кодируется с помощью регулярного выражения. И зачем его так усиленно тестировать, ума не приложу.
Отлично!
Здорово, что вы пришли к такому выводу.
Посмотрев на юнит-тесты, я тоже пришел к такому выводу.
Но класс ReferenceNumber писали ДО юнит-тестов, и он получился гораздо длиннее. В этом-то и соль!
для .NET есть NUnut, и метод Assert.That, позволяющий писать тест, который сам объясняет что он делает. Как пример, можно взять след.:

string[] s = new string[] { «30520», «30530», «C0430», «C0441», «C0440», «C0421», «70511», «70521» };
Assert.That(this.helper.GetNodesAtVoltage(2f, 5f), Is.EquivalentTo(s));

или вот еще один пример

Assert.That(mpsh.List_Nodes, Has.Some.Property(«uz»).EqualTo(«1»));

В первом случае говорится, что все содержимое полученного списка должно быть эквивалентно массиву s
Во втором, что в списке есть один элемент, у которого свойство 'uz' равно '1'
Это совсем не BDD. Оычное, красиво оформленное TDD. Тут тестируется не поведение объекта, а сравнение результатов с эталоном и не более. А вот тестирование поведения выглядит, например, так:

@article.should.receive(:save).and_return(true)
put /article/1

Вот тут, во время выполнения экшена на пут запрос, проверяется вызов метода save и результат этого вызова.
Если хорошенькое вдуматься, то это то же самое, только чуть по-другому названное.
так ведь я как раз об этом и говорю.
Совсем не то же самое. Но это нужно осознать, вдумавшись.
Точек между словами больше… они что-то значат и вызывают? Т.е. можно просто article.should написать? Или это чисто для группировки тестов используется… мол выбрать все тесты с article, и все с receive..?

Странный синтаксис. Напишите статью тогда уж — тема же интересная
Это синтаксис Rspec. Статью писать не буду — их в интернете итак полно )

@article.should.receive(:save).and_return(true)

Я даже не знаю, как объяснить в терминах дотнета. Синтаксис как раз очень хорош. Он примерно значит следующее:

во время выполнения экшена у статьи должен вызываться метод save, который возвращает true.

Это буквальный перевод.

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

article = mock(Article.class);
put(article, 1);
verify(article).save().shouldBeEqual(1);
verify(article).save().shouldBeEqual(1);

В это сточке происходит вызов метода save и его сверка с 1? Или save произошёл раньше, внутри метода put?
save должен был произойти раньше, внутри метода put.
А verify(article).save() убеждается, что он действительно произошёл.

Здесь есть больше примеров:
mockito.org/
habrahabr.ru/blogs/java/72617/

В RSpec просто есть встроенная аналогичная функциональность, которая, похоже, выглядит получше и покороче благодаря тому, что язык Ruby сам по себе лаконичнее.
НЛО прилетело и опубликовало эту надпись здесь
ScalaTest не только для Scala, можно и для проектов на Java юзать.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации