Сегодня я хочу поговорить про тест-дизайн и тест-анализ. Навеяно ситуацией с работы, но и у студентов часто встречается. Применение техник — это здорово и круто! Но, пожалуйста, не надо усложнять.
Когда люди узнают про тест-анализ, про то, что можно сокращать количество тестов, объединяя несколько в один и выкидывая малоприоритетные, в головах устанавливается какой-то флажок «Меньше тестов = круче!». Иначе не знаю, как это объяснить.
Проводила ревью автотестов джуниора (назовем его Вася) на сложную задачу: API-метод, возвращающий результаты поиска с некоторым набором параметров. Параметров много, поэтому и вариаций тестов много. В теории.
Смотрю на результат — получилось 30 тестов, описание которых не понимаю даже я. При том, что я на проекте работаю 6 лет и знаю его очень хорошо. А описание нам надо отправить заказчику.
Почему я не понимаю описание? Слишком много всего сразу проверяется. Первый же тест — комбинация десятка параметров. И результат, соответственно, на 10 строк текста. А ведь хороший тест — это маленькая конкретная проверка. Особенно когда это автотест, то есть время тратит робот, а не человек.
Ну что? Переделали. Тестов стало 150, поделенных на 9 папочек, но зато все простые и понятные. Пока писали простые тесты, нашлось несколько багов, пропущенных ранее... Почему в данном случае 150 лучше чем 30? И почему 30 тестов упустили баги?
Посмотрим на примере попроще, но близком к описываемому случаю. Вот, например, фильтры в интернет-магазине:
Платья / шорты / топики
Платья: вечерние, повседневные..
Цвет
Материал
Размер
...
Можно установить один фильтр, а можно несколько сразу. Вот вам и поиск с разными параметрами! Ведь где-то там, под пользовательским интерфейсом скрывается как раз некий API-метод, который умеет возвращать результат под разные входные условия.
У нас еще есть логика из серии «возвращай разный результат: или только текстовое описание, или текст + картинку, или только картинку товара». Но сути дела это не меняет.
Как мы начинаем тестировать? Какой будет первый тест? Сразу же понеслись «а вот выберу платья, цвет серый, материал плюш, размер 42...»? Нет, это уже комплексное тестирование. Оно тоже важно и нужно, но начинаем с простого.
Берем каждый фильтр и тестируем по отдельности. Вроде кажется, что тестов будет over дофига, в базе магазина ведь целая куча данных! Но тут надо разделять тесты на функционал и тесты на данные.
Тесты на функционал
Вот, допустим, цвет. У нас есть 7 цветов: синий, розовый, красный, зеленый, белый, черный, голубой. Нам надо проверить, как каждый цвет работает в отдельности + комплексные тесты (а что, если я выбрала не один цвет, а два? А что, если вообще все?).
При этом не надо упарываться на то, что в магазине есть 10 000 платьев синего цвета, «неужели мне все их проверять?». Нет, это уже будут тесты на данные. А нас сейчас интересует функционал. Возвращаются ли в принципе синие вещи?
Если мы тестируем свое приложение, у нас есть доступ или к коду, или к разработчикам. Мы можем выяснить сам механизм. И вряд ли разработчик каждую вещь добавляет в код через IF id = 101 THEN... Нет, скорее он делает набор меток типа color (цвет). Вот по значению метки и возвращает ответ.
И когда я в интерфейсе нажимаю «покажи мне синие вещи», на самом деле внутри отправляется запрос с блоком color = blue. Если цвет красный, то color = red. Если несколько, то перечислением, если цвет не указан — в запросе параметра тоже нет. И все эти ситуации надо проверить:
И вот тут он может ошибиться. Например, скопипастить результат и в обоих случаях выводить синие вещи. Или случайно опечатался и усе, синий работает, розовый, белый, черный — все работает, а вот на красном упадет! Причем с ужасной ошибкой, если разработчик не предусмотрел такой исход и не обернул эксепшен.
Возможно, вы точно знаете, что код более умный и работает через параметры. If param = $param_1 then took thing with color = $param_1. Тут да, что красный, что синий — пофигу, один класс эквивалентности. Казалось бы. Но стоит задуматься, откуда берутся значения параметров. Наверное, где-то в коде есть класс, в котором перечислены допустимые величины — BLUE, RED, GREEN... И если разработчик опечатался в том классе, написав blie вместо blue, то хоть вы 100500 меток "синий" в админке на вещи повесите, поиск их никогда не найдет.
Тесты на данные
А тут мы уже проверяем сами данные. Правильно ли человек, отвечающий за наполнение базы, разместил метки. Это когда ты выбираешь "синее платье", а получаешь желтые шорты → это ошибка не в функционале, а в самих данных.
Нет, все бывает, конечно, может быть и ошибкой функционала. Но, если мы уже провели тесты на функционал и знаем, что сами по себе метки работают, значит, такой баг является багом данных. Для исправления которого нужен не разработчик, а любой человек, имеющий доступ к админке.
И вот тут уже тестирование — это трудоемкий процесс. Потому что надо проверить на адекватность все 100500 наименований в базе. А времени то обычно мало, релиз нужен «еще вчера», все как обычно ツ
Именно поэтому, когда ищешь что-то на сайте типа https://www.wildberries.ru/, порой поражаешься результатам поиска. Но виноваты в этом не разработчики, функционал есть и он работает. Баги допустили те, кто наполняет данные.
Это как если вы заметите ошибку в блог-посте, допустил ее автор поста. А сам блоггер или другая платформа, на которой поднят сайт, тут ни при чем.
Другой пример, но тоже почему-то на поиск. Студенты у меня пишут автотесты на систему Folks (Тут могла бы быть ссылка на ТЗ, но я пока морально не готова его в открытый доступ выкладывать, хотя я работаю над этим!).
Но перед тем, как автоматизировать, сначала составляется чек-лист проверок. И я постоянно вижу в ДЗ что-то похожее:
1. Писать тесты тяжело
Это же надо держать в уме, что с чем ты уже скомбинировал, а что нет. Имхо, если времени на проведение всех тестов нет, то лучше сначала накидать простые тесты на каждый параметр, а потом использовать pairwise. Так хоть покрытие нормальное будет и писать тесты не тяжело.
А то Вася то ворклогался в задачу «Мучился с автотестами (6ч)». Конечно, мучился! Это же надо все в голове держать, что проверил, а что нет.
2. Ревью провести тоже непросто
Вот если тесты пишет джуниор, а потом опытный коллега проводит ревью, это ж ему надо придется целую карту рисовать. Так, надо проверить то, то и то. Первый тест проверил это и это (ставим галочки), второй это и то (еще галочки), чего же не хватает?
Фактически тебе все равно придется расписать чек-лист простых проверок, просто чтобы убедиться, что они все где-нибудь да учтены. Почему бы тогда сразу так не сделать?
Я не могу окинуть тесты взглядом, чтобы понять текущее покрытие — это плохо.
3. Описание непонятное
Часто тесты выступают вместо документации. По крайней мере у нас, так как у нас не ручные чек-листы, а описания автотестов. Автотесты гоняются каждый день → они всегда актуальны, даже если документация где-то устарела, тесты не врут.
Проект большой, удержать в голове вообще все не получается. Поэтому иногда на вопрос «а как это работает?» тебе даже разработчик скажет «не знаю, проверь в тесте». И вот ты идешь в тесты, а там черт ногу сломит в описании. Где искать нужную тебе часть функционала — хз. И это тоже плохо.
4. Пропускаются баги
Как следствие из всего вышеперечисленного. Если пытаться все удержать в голове — что-нибудь да забудешь проверить.
В нашем случае косяки были в логике «возвращай разный результат: или только текстовое описание, или текст + картинку, или только картинку товара». В запросе есть блок result, там можно выбрать text, capture, или оба сразу. А потом оказалось, что картинка возвращается всегда, даже если ее не выбрали. А теста на "только текст" не оказалось. И простым ревью на полноту тестирования его не выловишь из-за пунктов 2 и 3.
5. Если тест нашел баг, его сложно локализовать
Вот не работает поиск по 10 параметрам — это что значит? Поиск не работает совсем? Или из-за параметра 1? Или из-за параметра 2, 3, 4, 5? Или их совокупности? Приходится копаться и разбираться.
Тест-анализ отлично применяется для комплексных тестов. Вот вы проверили каждый параметр и убедились, что сам по себе он работает. Теперь надо как-то комбинировать.
Но мы уже не проверяем каждый с каждым, а проверяем саму связку «И». Работает ли, когда у нас не один параметр, а два? А если максимум? Если знаем, что параметры влияют друг на друга, их тоже проверяем отдельно. Но все равно получается сильно меньше перебора «все со всем».
Тут снова напомню про pairwise. Если очень хочется сократить количество тестов, все равно распишите подробно по каждому параметру, а потом засуньте результат в pairwise. И вот вам и тестов мало, и все учтено.
А вообще стоит смотреть по ситуации, нужно ли сокращение базовых проверок. Одно дело — если их получилось 100, а времени есть только на 10. И совсем другое — если вы составляете автотесты. Один раз написал, и готово, дальше всю работу делает робот. И вот чтобы падения автотестов разработчик мог легко локализовать и исправить, они должны быть небольшими и простыми. Один тест — одна проверка, все по феншуй.
Я беру требования, блокнот и ручку. Читаю каждый пункт требований и накидываю тесты, которые можно на него написать. Например:
Вроде пара строк ТЗ, но уже 7 тестов получилось... Обратите внимание на приоритет. Сначала провели базовые проверки: параметр есть, мы его вызываем. Потом комплексные и граничные значения (несколько значений, все, вообще ни одного). Потом уже всякие извраты (картинки нет, а мы ее запрашиваем).
Когда так расписываешь проверки, сразу приходят интересные вопросы в голову по поводу ТЗ. Так-с, какой тут будет результат? А и правда, какой?? Идем к аналитику и уточняем. Причем эти вопросы приходят в голову только тогда, когда начинаешь выписывать тесты. Я проверяла — когда просто «тестируешь ТЗ» и типа в уме прикидываешь тесты, то находишь далеко не все подводные камни. По крайней мере, у меня так.
Поэтому лучше часик уделить тест-дизайну. Покумекать, какие тесты я в принципе хочу, задать все нужные вопросы, а потом уже садиться автоматизировать. А то написание автотестов иногда занимает несколько дней. Будет плохо, если вопросы появятся к концу срока... Ну и потом, если сразу не прикинуть все тесты, то снова есть шанс что-то продолбать. Голова то уже реализацией забита, а не идеями.
Конечно, когда я начинаю сами автотесты писать, то мне приходят в голову новые идеи. То стряпаешь запрос и вдруг понимаешь «ух ты, а еще можно сделать вот так и сяк». Дописываешь новые мысли в блокнотик и продолжаешь. Или получаешь результат — что-то не то, хм, хм... Изучаешь ожидания теста, понимаешь, что он прав, а ты нет, логика работы другая. Но тогда ведь можно проверить еще вот то и то!
В общем, доработки тестов неизбежны ツ
Ну и что? Все равно принцип «начинаем с простого» себя всегда оправдывает, потому что ты 90% тестов придумываешь сразу и задаешь 90% вопросов, которые стоит задать.
Не мудрите. Особенно если вы в тестировании еще новичок. Попробуете усложнять — обязательно что-нибудь да продолбаете. Пропустите баг и будет потом очень обидно.
Начинаем мы всегда с простого. Если базовый функционал работает — начинаем извращаться, комбинировать и вот это вот все. Но сложности — потом! А начинаем мы с простого.
См также:
С тремя параметрами работает, а с одним — NPE — еще пример из жизни, чем плоха религия «проверил все параметры, а по одиночке уже и не надо»
PS — это выдержка из моей книги для начинающих тестировщиков, написана в помощь студентам моей школы для тестировщиков
Когда люди узнают про тест-анализ, про то, что можно сокращать количество тестов, объединяя несколько в один и выкидывая малоприоритетные, в головах устанавливается какой-то флажок «Меньше тестов = круче!». Иначе не знаю, как это объяснить.
Мои тесты, не отдам! |
Смотрю на результат — получилось 30 тестов, описание которых не понимаю даже я. При том, что я на проекте работаю 6 лет и знаю его очень хорошо. А описание нам надо отправить заказчику.
Почему я не понимаю описание? Слишком много всего сразу проверяется. Первый же тест — комбинация десятка параметров. И результат, соответственно, на 10 строк текста. А ведь хороший тест — это маленькая конкретная проверка. Особенно когда это автотест, то есть время тратит робот, а не человек.
Ну что? Переделали. Тестов стало 150, поделенных на 9 папочек, но зато все простые и понятные. Пока писали простые тесты, нашлось несколько багов, пропущенных ранее... Почему в данном случае 150 лучше чем 30? И почему 30 тестов упустили баги?
Пример с магазином
Посмотрим на примере попроще, но близком к описываемому случаю. Вот, например, фильтры в интернет-магазине:
Платья / шорты / топики
Платья: вечерние, повседневные..
Цвет
Материал
Размер
...
Можно установить один фильтр, а можно несколько сразу. Вот вам и поиск с разными параметрами! Ведь где-то там, под пользовательским интерфейсом скрывается как раз некий API-метод, который умеет возвращать результат под разные входные условия.
У нас еще есть логика из серии «возвращай разный результат: или только текстовое описание, или текст + картинку, или только картинку товара». Но сути дела это не меняет.
Как мы начинаем тестировать? Какой будет первый тест? Сразу же понеслись «а вот выберу платья, цвет серый, материал плюш, размер 42...»? Нет, это уже комплексное тестирование. Оно тоже важно и нужно, но начинаем с простого.
Берем каждый фильтр и тестируем по отдельности. Вроде кажется, что тестов будет over дофига, в базе магазина ведь целая куча данных! Но тут надо разделять тесты на функционал и тесты на данные.
Тесты на функционал
Вот, допустим, цвет. У нас есть 7 цветов: синий, розовый, красный, зеленый, белый, черный, голубой. Нам надо проверить, как каждый цвет работает в отдельности + комплексные тесты (а что, если я выбрала не один цвет, а два? А что, если вообще все?).
При этом не надо упарываться на то, что в магазине есть 10 000 платьев синего цвета, «неужели мне все их проверять?». Нет, это уже будут тесты на данные. А нас сейчас интересует функционал. Возвращаются ли в принципе синие вещи?
Если мы тестируем свое приложение, у нас есть доступ или к коду, или к разработчикам. Мы можем выяснить сам механизм. И вряд ли разработчик каждую вещь добавляет в код через IF id = 101 THEN... Нет, скорее он делает набор меток типа color (цвет). Вот по значению метки и возвращает ответ.
И когда я в интерфейсе нажимаю «покажи мне синие вещи», на самом деле внутри отправляется запрос с блоком color = blue. Если цвет красный, то color = red. Если несколько, то перечислением, если цвет не указан — в запросе параметра тоже нет. И все эти ситуации надо проверить:
- Создаем вещи с метками blue и red — у тестировщика системы обычно есть такая возможность. Или прямо в базу добавить, или через админку, или в автотесте.
- Выбираем в интерфейсе фильтр «синий» или отправляем напрямую запрос с блоком color = blue → проверяем, что вернулась только одна из вещей, с правильной меткой.
- Выбираем фильтр «красный»
- Выбираем сразу оба фильтра
- Оставляем фильтрацию пустой
Вы сейчас можете сказать: стопэ, стопэ! А как же тест-анализ? Зачем тестировать каждый цвет? Достаточно проверить один, если функционал с метками работает — значит, все норм.
Не совсем. Да, разработчик не пишет обработку для каждой вещи. Но он пишет обработку для каждой метки:
- If параметр в запросе color = blue → возьми все вещи, у которых цвет blue
- If параметр в запросе color = red → возьми все вещи, у которых цвет red
Возможно, вы точно знаете, что код более умный и работает через параметры. If param = $param_1 then took thing with color = $param_1. Тут да, что красный, что синий — пофигу, один класс эквивалентности. Казалось бы. Но стоит задуматься, откуда берутся значения параметров. Наверное, где-то в коде есть класс, в котором перечислены допустимые величины — BLUE, RED, GREEN... И если разработчик опечатался в том классе, написав blie вместо blue, то хоть вы 100500 меток "синий" в админке на вещи повесите, поиск их никогда не найдет.
Тесты на данные
А тут мы уже проверяем сами данные. Правильно ли человек, отвечающий за наполнение базы, разместил метки. Это когда ты выбираешь "синее платье", а получаешь желтые шорты → это ошибка не в функционале, а в самих данных.
Нет, все бывает, конечно, может быть и ошибкой функционала. Но, если мы уже провели тесты на функционал и знаем, что сами по себе метки работают, значит, такой баг является багом данных. Для исправления которого нужен не разработчик, а любой человек, имеющий доступ к админке.
И вот тут уже тестирование — это трудоемкий процесс. Потому что надо проверить на адекватность все 100500 наименований в базе. А времени то обычно мало, релиз нужен «еще вчера», все как обычно ツ
Именно поэтому, когда ищешь что-то на сайте типа https://www.wildberries.ru/, порой поражаешься результатам поиска. Но виноваты в этом не разработчики, функционал есть и он работает. Баги допустили те, кто наполняет данные.
Это как если вы заметите ошибку в блог-посте, допустил ее автор поста. А сам блоггер или другая платформа, на которой поднят сайт, тут ни при чем.
Пример с Folks
Другой пример, но тоже почему-то на поиск. Студенты у меня пишут автотесты на систему Folks (Тут могла бы быть ссылка на ТЗ, но я пока морально не готова его в открытый доступ выкладывать, хотя я работаю над этим!).
Но перед тем, как автоматизировать, сначала составляется чек-лист проверок. И я постоянно вижу в ДЗ что-то похожее:
- Поиск по фамилии
- Поиск по фамилии + ДР + материнскому капиталу + номеру донора почки + ...
- Как работает подстановочный символ % в дате
Ээээ... Минуточку... Если второй тест упадет, то какие выводы мы сможем сделать? Что не работает поиск по двум словам? Или по ДР? Или по материнскому капиталу? Или еще по чему?
А если упадет третий тест? Это подстановочный символ не работает или поиск по дате?
Чтобы точно знать, отчего тест упал, сначала надо проверить основной функционал: поиск. Система должна искать по каким-то полям и не должна по лишним. А потом уже можно наворачивать всякие усложнения типа подстановочных символов.
Чем плохо объединять 10 проверок в 1
1. Писать тесты тяжело
Это же надо держать в уме, что с чем ты уже скомбинировал, а что нет. Имхо, если времени на проведение всех тестов нет, то лучше сначала накидать простые тесты на каждый параметр, а потом использовать pairwise. Так хоть покрытие нормальное будет и писать тесты не тяжело.
А то Вася то ворклогался в задачу «Мучился с автотестами (6ч)». Конечно, мучился! Это же надо все в голове держать, что проверил, а что нет.
2. Ревью провести тоже непросто
Вот если тесты пишет джуниор, а потом опытный коллега проводит ревью, это ж ему надо придется целую карту рисовать. Так, надо проверить то, то и то. Первый тест проверил это и это (ставим галочки), второй это и то (еще галочки), чего же не хватает?
Фактически тебе все равно придется расписать чек-лист простых проверок, просто чтобы убедиться, что они все где-нибудь да учтены. Почему бы тогда сразу так не сделать?
Я не могу окинуть тесты взглядом, чтобы понять текущее покрытие — это плохо.
3. Описание непонятное
Часто тесты выступают вместо документации. По крайней мере у нас, так как у нас не ручные чек-листы, а описания автотестов. Автотесты гоняются каждый день → они всегда актуальны, даже если документация где-то устарела, тесты не врут.
Проект большой, удержать в голове вообще все не получается. Поэтому иногда на вопрос «а как это работает?» тебе даже разработчик скажет «не знаю, проверь в тесте». И вот ты идешь в тесты, а там черт ногу сломит в описании. Где искать нужную тебе часть функционала — хз. И это тоже плохо.
4. Пропускаются баги
Как следствие из всего вышеперечисленного. Если пытаться все удержать в голове — что-нибудь да забудешь проверить.
В нашем случае косяки были в логике «возвращай разный результат: или только текстовое описание, или текст + картинку, или только картинку товара». В запросе есть блок result, там можно выбрать text, capture, или оба сразу. А потом оказалось, что картинка возвращается всегда, даже если ее не выбрали. А теста на "только текст" не оказалось. И простым ревью на полноту тестирования его не выловишь из-за пунктов 2 и 3.
5. Если тест нашел баг, его сложно локализовать
Вот не работает поиск по 10 параметрам — это что значит? Поиск не работает совсем? Или из-за параметра 1? Или из-за параметра 2, 3, 4, 5? Или их совокупности? Приходится копаться и разбираться.
А как же тест-анализ?
Но мы уже не проверяем каждый с каждым, а проверяем саму связку «И». Работает ли, когда у нас не один параметр, а два? А если максимум? Если знаем, что параметры влияют друг на друга, их тоже проверяем отдельно. Но все равно получается сильно меньше перебора «все со всем».
Тут снова напомню про pairwise. Если очень хочется сократить количество тестов, все равно распишите подробно по каждому параметру, а потом засуньте результат в pairwise. И вот вам и тестов мало, и все учтено.
А вообще стоит смотреть по ситуации, нужно ли сокращение базовых проверок. Одно дело — если их получилось 100, а времени есть только на 10. И совсем другое — если вы составляете автотесты. Один раз написал, и готово, дальше всю работу делает робот. И вот чтобы падения автотестов разработчик мог легко локализовать и исправить, они должны быть небольшими и простыми. Один тест — одна проверка, все по феншуй.
Как я пишу тесты
В запросе есть блок result:
- text — текстовое описание товара
- capture — картинка товара
Он определяет, что будет возвращаться в ответе.
Так, ок, наши тесты:
- У товара есть и описание, и картинка. В запросе — text
- У товара есть и описание, и картинка. В запросе — capture
- У товара есть и описание, и картинка. В запросе — text, capture
- У товара есть и описание, и картинка. В запросе — img (несуществующий параметр)
- У товара есть и описание, и картинка. В запросе блока result вообще нет
- У товара есть только описание. В запросе — capture
- У товара есть только описание. В запросе — text, capture
Вроде пара строк ТЗ, но уже 7 тестов получилось... Обратите внимание на приоритет. Сначала провели базовые проверки: параметр есть, мы его вызываем. Потом комплексные и граничные значения (несколько значений, все, вообще ни одного). Потом уже всякие извраты (картинки нет, а мы ее запрашиваем).
Когда так расписываешь проверки, сразу приходят интересные вопросы в голову по поводу ТЗ. Так-с, какой тут будет результат? А и правда, какой?? Идем к аналитику и уточняем. Причем эти вопросы приходят в голову только тогда, когда начинаешь выписывать тесты. Я проверяла — когда просто «тестируешь ТЗ» и типа в уме прикидываешь тесты, то находишь далеко не все подводные камни. По крайней мере, у меня так.
Поэтому лучше часик уделить тест-дизайну. Покумекать, какие тесты я в принципе хочу, задать все нужные вопросы, а потом уже садиться автоматизировать. А то написание автотестов иногда занимает несколько дней. Будет плохо, если вопросы появятся к концу срока... Ну и потом, если сразу не прикинуть все тесты, то снова есть шанс что-то продолбать. Голова то уже реализацией забита, а не идеями.
Конечно, когда я начинаю сами автотесты писать, то мне приходят в голову новые идеи. То стряпаешь запрос и вдруг понимаешь «ух ты, а еще можно сделать вот так и сяк». Дописываешь новые мысли в блокнотик и продолжаешь. Или получаешь результат — что-то не то, хм, хм... Изучаешь ожидания теста, понимаешь, что он прав, а ты нет, логика работы другая. Но тогда ведь можно проверить еще вот то и то!
В общем, доработки тестов неизбежны ツ
Ну и что? Все равно принцип «начинаем с простого» себя всегда оправдывает, потому что ты 90% тестов придумываешь сразу и задаешь 90% вопросов, которые стоит задать.
Итого
Начинаем мы всегда с простого. Если базовый функционал работает — начинаем извращаться, комбинировать и вот это вот все. Но сложности — потом! А начинаем мы с простого.
См также:
С тремя параметрами работает, а с одним — NPE — еще пример из жизни, чем плоха религия «проверил все параметры, а по одиночке уже и не надо»
PS — это выдержка из моей книги для начинающих тестировщиков, написана в помощь студентам моей школы для тестировщиков
"Не совсем. Да, разработчик не пишет обработку для каждой вещи. Но он пишет обработку для каждой метки:"
ОтветитьУдалитьтак а если 10000 меток то что проверять каждую?
Смотря какое нужно покрытие. Если "хай так будет, авось никто не ошибся, ставя метки", то не нужно. Но вообще это должно делаться в процессе. Разработчик же не напишет код на 100500 вещей за 1 секунду, пишет по 1-2 метки, их и проверяем. И, желательно, сразу в автотестах. Тогда это не будет казаться непосильной работой
УдалитьОчень полезная сатья и читается на одном дыхании
ОтветитьУдалитьСпасибо)
УдалитьСпасибо большое. Долго думал как нормально провоести декомпозицию фильтра, пришло более отчетливое понимание "КАК".
ОтветитьУдалить