Тесты: 100% покрытия и юниты не нужны

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

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

Меня зовут Максим Вишневский, я Senior Frontend-разработчик в Циан. В этой статье поделюсь историей, как наша команда реформировала подходы к тестированию: как мы отказались от 100% покрытия и unit-тестов, чем их заменили и какой получили результат. Поговорим о проблемах с Enzyme, пользе Playwright, мокинге данных для бэка и взаимодействии с QA.

Циан — огромная компания. Ежемесячно наши ресурсы посещают 19 миллионов уникальных пользователей и размещают более 1,9 миллионов актуальных объявлений. Это большая кодовая база, которую нужно тестировать. Раньше мы подходили к этому вопросу с точки зрения «покрыто, значит, надёжно». Однако такое убеждение может привести к проблемам. Чтобы гарантировать стабильную работу наших сервисов, мы решили разобраться в том, как всё должно работать, и как мы можем это реализовать у себя.

Почему unit-тесты — это боль

Unit-тесты внутри компонентов — не лучшая затея. Приведу несколько причин.

  1. Тестируя отдельные компоненты, мы не всегда можем сосредоточиться на их функциональности и проверить, как они работают.

Ниже пример кода на Enzyme. Берём компонент из контейнера и проверяем его наличие:

describe('Koмпонент ActionComponent’, () => {
  it('Рендерит компонент’, () => {
    const wrapper = shallow(<ActionComponent />);
    const button = wrapper.find(<SomeButton />);

    expect(button.exists()).toBeTruthy();
  });
});

Coverage есть, значит, код покрыт тестами и всё работает. Но по сути мы получаем лишь уверенность в том, что в продакшене компонент стабильно встраивается. При этом не проверяем реальное поведение прода, corner-кейсы, не следим за соблюдением контракта между контейнером и компонентом, а также за тем, как он взаимодействует с окружающими компонентами.

  1. Unit-тесты плохо работают на проверку качества.

Получив мифические 100%, мы фактически покрыли код позитивными сценариями и запушили в продакшен. Написанные тесты валидно отработают, дадут нам покрытие и галочку в метриках, но результат не обязательно будет отвечать требованиям к качеству.

  1. В unit-тестах не тестируются user story.

Вместо пользовательских сценариев внимание направлено на отдельные компоненты. Это может привести к проблемам, так как ценность компонента проявляется только в контексте общего сценария, а не в изоляции.

В unit-тестах для компонентов мы используем Enzyme, а значит тестируем только изолированно. Этот подход делает компонент похожим на «волшебную коробочку», которую можно настроить и протестировать, но это не всегда отражает, как он реально себя ведёт и какова его ценность в контексте бизнес-логики и пользовательского опыта. Поэтому такие тесты не несут большой пользы в продакшене.

  1. Нет уверенности в работе композиции компонентов.

При раздельном тестировании не становится понятно, как компоненты интегрируются между собой и как работают в рамках сценария.

  1. Нет гарантии качественной работы компонентов в проде.

Когда код успешно протестировали и выкатили, остаётся непонятным, качественно ли он работает. Да, мы можем полагаться на E2E (end-to-end) тесты от тестировщиков, но лично мне, как разработчику, хочется быть уверенным, что это действительно будет работать.

  1. Из-за unit-тестов накапливается избыточное количество кода.

Это происходит, когда тесты становятся объёмнее, чем сами компоненты или модули, которые они проверяют. В итоге приходится создавать множество имитаций окружения. Мы не только пытаемся замокать практически весь Интернет вокруг компонента или функции, но также не поддерживаем актуальность этих моков. Из-за этого не работаем с реальными данными, что приводит к постоянным проблемам в продакшене.

Ещё одна проблема в unit-тестировании — это снэпшоты. Команды начинают их использовать, когда хотят закрыть coverage. Но работать с ними неудобно, они плохо читаются. 

В таких кейсах мы просто пытаемся закрыть вопрос с покрытием. Проверяем структуру, которая может оказаться моком компонента, как например в случае с RTL, и будь что будет. 

Отказ от 100% покрытия

Многим важно покрыть код на 100%. Мы в компании тоже старались достичь этой цели, но в итоге это не принесло существенных результатов. Поясню на примере функции:

export function testedFunc(obj: Record<string, number>) {
  obj.x = 42;
}

it('100% coverage test’, () => {
  const obj: Record<string, number> = {};
  testedFunc(obj);

  expect (obj.x). toEqual(42);
});

Представим, нам нужно пропатчить объект. Мы провели тестирование и достигли 100% покрытия. Всё отлично, пока не возникает необработанное исключение:

it('exception', () => {
  const obj: Record<string, number> = {};
  Object.preventExtensions(obj);

  testedFunc(obj);

  expect (obj.x).toEqual(42); // TypeError
});

Получается, есть позитивный сценарий со 100% покрытием и негативный сценарий, который мы не проверили.

Негативные сценарии нужно проверять, потому что они, во-первых, гарантируют продолжение работы в случае ошибки, а во-вторых, проверяют непредвиденное поведение, на которое мы можем адекватно реагировать.

Ещё может произойти знакомая многим ситуация: есть функция, осуществляющая простое деление. Мы проверили 2 числа, они отработали. Потом получили на экране наше любимое: «Стоит NaN рублей».

Стали искать решение, как это побороть, и первым делом отказались от 100% покрытия, занизили его до 80%. Так мы решили проверить, будет ли прирост в статистике багов и каким будет dx (developer experience). Что это дало:

  • Общая статистика метрик не просела.

Мы следили по графикам, как ведёт себя приложение и насколько оно отказоустойчиво. Все показатели были на хорошем уровне, что дополнительно указывает: 100% покрытие — не гарант того, что в продакшене не возникнут проблемы.

  • Количество обращений в клиентскую службу не увеличилось.

Для нас это важный показатель, потому что клиентская служба сильно перегружается, если мы ошибёмся или что-то пойдёт не так.

  • Уровень багов normal и выше не увеличился.

Мы не рассматриваем минорные баги как критичные, поэтому они просто лежат в бэклоге. И так как сократилось количество тестов, которые нужно писать, скорость поставки фичей в продакшен немного выросла.

  • Качество поставляемых фичей не упало. На это указали метрики change fail rate, позволяющие смотреть динамику прироста багов при раскатке.

Прежде всего нам нужно было решить, с какими инструментами мы не сможем идти дальше, чтобы реализовать наши планы. Первой блокирующей проблемой был Enzyme.

Отказ от Enzyme

Enzyme — это инструмент, предоставляющий гибкие возможности для тестирования компонентов React. Понятие гибкости здесь относительное:

  • Enzyme имеет прямой доступ к пропсам и внутренностям других компонентов.

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

describe('Koмпонент ActionComponent', () = {
  it('Обрабатывает клик', () => {
    const wrapper = shallow(<ActionComponent />);
    const button = wrapper.find(<SomeButton />);
    const onClick = button.prop('onClick")

    onClick();

    expect (someHandler).toBeCalled();
  });
});

Так можно добраться до пропсов, выполнить клик и что-то проверить. Но это не продакшен-ready компонент — он не отрендерился, это всё ещё функциональное состояние.

  • Это уже не unit-тест, но ещё не интеграционный.

Мы не проверяем атомарное состояние компонента, ничего не интегрируем, мы просто какой-то магией куда-то достучались. Зачем мы это пишем? Закрываем coverage. Это даёт хоть какую-то надежду на то, что сбой не случится.

  • Низкая скорость работы при большом количестве тестов.

Для меня, как разработчика, ключевой недостаток Enzyme в скорости работы. Он очень медленный. Когда у вас 505 секунд на более полутора тысяч тестовых сценариев, медлительность становится проблемой.

  • Отсутствие поддержки

На момент выступления, в 2023 году, в Enzyme было открыто более двухсот issue без ответов. Команда разработчиков React официально отказалась от поддержки этого инструмента, ни одна версия поддерживаться не будет.

Мы решили отказаться от использования Enzyme и вместо этого по рекомендации от разработчиков React попробовали React Testing Library (RTL).

React Testing Library (RTL)

В целом, как инструмент RTL оказался неплохим, с несколькими заметными плюсами:

  • Более приближенное к реальности тестирование компонентов.

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

  • Управляемость в мокинге.

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

  • Лучше производительность.

RTL работает довольно быстро, хотя не всегда на уровне, которого нам хотелось, но в целом достаточно эффективно.

На фоне плюсов у RTL есть минусы:

  • Увеличение количества моков.

Иногда моков больше, чем кода. С этим минусом можно работать, но он существенный.

  • Использование jsdom вместо продакшен-структуры.

RTL использует jsdom, который не всегда в точности соответствует реальной продакшен-структуре, что может привести к неожиданному поведению. Это всё ещё не реальные данные и не полноценная проверка наших сценариев.

  • Затруднения при тестировании функциональной части и хуков.

Некоторые сложности возникают при тестировании функциональной части компонентов и хуков. Мы пробовали решить эту проблему, разделяя логику и компоненты: внутри контейнера оставили только jsx, сделали хук use component. Вся логика находится внутри контейнера, соответственно последний тестировали снэпшотами, хук протестировали unit-тестом. Как будто бы просто переложили всё в разные места, но суть осталась той же.

Несмотря на минусы, мы оставили RTL. Он особенно пригодился для покрытия отдельных сценариев (corner-кейсов) и для тестирования интеграций.

Инструмент оказался полезным для тестирования сложных функциональных сценариев, которые обычно требуют значительных ресурсов. Чтобы не замедлять основную инфраструктуру, целесообразно выделять corner-кейсы и тестировать их отдельно.

Нет необходимости включать их в функциональные тесты, это накладно. С помощью RTL можно покрыть такие сценарии unit-тестами с минимальным воздействием на инфраструктуру. 

Также RTL позволил нам проводить интеграционное тестирование, собирая компоненты вместе и проверяя их взаимодействие. Это оказалось полезным средством для проверки работы композиций компонентов.

После анализа возможностей и ресурсов, мы пришли к вопросу о первостепенном и важном в тестировании. Каковы наши ценности: какой результат хотим получить, какие есть ограничения, что важно и что вторично. Так у нас получилось сформулировать ценности, которых компания придерживается в работе с тестированием.

Ценности при тестировании

Чтобы писать хорошие тесты, нужна системность. Нужен подход, который регулирует, что мы тестируем и что для нас важно. Вот к каким ценностям мы пришли:

  • Важен конечный сценарий, а не атомы.

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

  • Нужно мыслить негативными и позитивными сценариями.

Хотя позитивные сценарии могут быть очевидными, часто, когда мы пишем код, мы уже уверены в их правильности, особенно если используем TypeScript с его хорошей типизацией. Однако при написании unit-тестов мы не должны полагаться только на TypeScript, а также проверять разные сценарии.

  • Разделение зон применения подходов («ваза тестирования»).

Мы поняли, что полезно разделять зоны применения различных подходов к тестированию. Выбрали вазу (так назвали форму), чтобы определить, какие подходы будем использовать.

Пирамида и ваза тестирования

На начальном этапе много говорили о пирамиде тестирования. Это способ группировки и организации тестов на разные уровни, в зависимости от их назначения. Пирамида группируется по принципу: основа — уровень unit-тестов, ему отводится самое большое место; средний уровень — интеграционные тесты; на верхушке — E2E-тесты.

Пирамида справа на рисунке — это то, как она должна выглядеть. Слева — изображение пирамиды тестирования в нашей компании. Видно, что мы уделяем много внимания unit-тестам, которые всё покрывают, но крайне мало — user story, хотя это включено в ценности тестирования.

Ваза тестирования (или бубен, или также бриллиант тестирования), на структуре которой мы в итоге остановились, выглядит иначе. Это тот же способ абстрактно описать слои, которыми мы тестируем код, но с новой зоной приоритета.

Unit-тесты остаются в основании, так как у нас ещё есть функции, которые стоит ими тестировать. Например, селекторы, редьюсеры, хуки и ещё хелперы — различные утилиты, в тестировании которых интеграционное и функциональное тестирование не помогут.

Больше места теперь выделяем интеграционным/функциональным и E2E-тестам, мы фокусируемся на них. И наконец-то тестируем user story — пришли к ценностям! Но пока только на бумаге.

Мы задались вопросом, что такое user story? Что это за ценность и как мы должны для себя её определять?

User story

Пользовательская история — это способ описания высокоуровневого поведения пользователя или функциональности, которую он ожидает в приложении.

Хорошего разработчика, пишущего хорошие тесты, отличает системное мышление. Он думает, как работает функционал, понимает логику приложения и способен описать user story.

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

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

Когда мы говорим о том, что есть тот самый бубен, то подразумеваем использование интеграционных и функциональных тестов. Чем они отличаются?

Интеграционное тестирование

Интеграционное тестирование проверяет взаимодействие между несколькими компонентами приложения.

Вот что включает в себя интеграционный тест:

  • Проверка работы нескольких компонентов.

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

  • Проверка корректности исполнения отдельных методов.

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

  • Проверка валидности передаваемых параметров.

Это важно, чтобы убедиться, что данные, которые мы передаём между компонентами, остаются валидными. Несмотря на типизацию в языках программирования, таких как TypeScript, могут возникать ситуации, когда данные между компонентами мутируют. В React это не такая частая ситуация как при работе с Vue.js.

Функциональное тестирование

Функциональное тестирование занимается проверкой работы системы с точки зрения бизнес-требований и пользовательских сценариев. Вот что оно включает:

  • Проверка корректности работы с точки зрения бизнес-требований.

Это сложное и всестороннее тестирование, которое охватывает всю бизнес-логику приложения. Мы проверяем, соответствует ли работа приложения заданным требованиям.

  • Проверка валидности пользовательских сценариев.

Мы тестируем приложение с точки зрения пользовательских сценариев и таким образом понимаем, соответствуют ли результаты ожиданиям пользователя.

  • Обширное покрытие логики.

Функциональные тесты позволяют охватить большой объём функциональности приложения одним тестом. Интересно, что большой функциональный тест закроет очень большую часть того самого покрытия, которое нужно получить, потому что он проходит большое количество кнопок, форм, кликает во все участки.

  • Даёт полноценную картину поведения приложения.

В отличие от других видов тестирования, функциональные тесты могут служить в качестве документации, описывающей поведение приложения.

Итак, получается, поскольку подход к тестированию изменился, нам больше не нужны unit-тесты для отдельных компонентов.

Отказ от unit-тестов компонентов

Мы выпилили unit-тесты компонентов — это было для нас своеобразным прыжком веры, выбросили их и ладно.

Что изменилось:

  • Увеличилась скорость сборки и локальных проверок.

Нет тестов — нет проблем, это удобно. Локальные проверки стали быстрее, потому что нет необходимости запускать и проверять множество unit-тестов. Наша инфраструктура также немного разгрузилась, хотя это продолжалось лишь до тех пор, пока мы не начали экспериментировать с функциональными тестами.

  • Написание тестов стало более осознанным процессом.

Теперь, когда мы тестируем функциональность, мы лучше понимаем, как работает продукт. Мы перешли от позиции «дайте мне ТЗ, и я сверстаю кнопочку» к ответственности за функциональность. Это классно, потому что это развивает навыки.

Пока в этом подходе мы не обнаружили минусов. Надеюсь, у вас будет также.

Для написания функциональных тестов пробовали два инструмента — сначала Cypress, затем Playwright.

Cypress: плюсы и минусы

У этого инструмента есть несомненные плюсы:

  • Имеет режим визуального тестирования. Вы видите реальное время, в течение которого Cypress производит клики и взаимодействует с элементами.

  • Позволяет описать поведение для бизнес-логики. Эта возможность как раз соответствует нашим ценностям. Мы к этому шли.

  • Имеет богатый набор возможностей для функциональных тестов, в том числе и платные плагины.

Мы написали тесты, запустили их, но хоть всё и поднялось, мы столкнулись с рядом проблем:

  • Скорость выполнения тестов не улучшилась.

Наши тесты стали выполняться так же медленно, как раньше unit-тесты. Это было не круто. Как будто мы убрали все юнит-тесты и запустили эту новую штуку, и ничего не работает быстро.

  • Начали падать билды, вырос reopen rate задач.

Из-за сбоев в билдах мы столкнулись с увеличением числа ошибок, а также с повышением частоты пересмотра задач. Все были недовольны, и мы не могли понять, в чём именно проблема. Возможно, виновата инфраструктура, но перестраивать её из-за проблем с Cypress нам не хотелось.

  • Режим отладки медленный и неудобный.

Плагины для vs code оказались неудобными и медленными. Асинхронный синтаксис оказался своеобразным.

Код в сценарии на Cypress выглядит не очень дружелюбно по отношению к разработчику. Приходится оперировать большим количеством строковых аргументов, а это легко может привести к путанице и семантическим ошибкам. Особенно тяжело становится после долгого дня работы над функционалом — приступая к написанию теста, хочется просто смотреть в потолок и ничего не делать.  

Мы рассмотрели эту ситуацию и пришли к выводу, что нужно пересмотреть наши подходы и ценности. И выбрали Playwright.  

Playwright: лучше, чем Cypress

Инструмент Playwright похож на Cypress, только новее, быстрее и круче. Предлагает множество преимуществ:

  • Значительно быстрее.

При запуске локально на компьютере с процессором M1 (не Intel), Playwright показывает реальное ускорение в 3-4 раза по сравнению с другими инструментами.

  • Удобный дебаг и визуальный режим.

С Playwright удобно работать. Вы можете легко перемещаться по тестовым сценариям, проверять точки и даже останавливаться на нужных моментах для отладки. Однако есть небольшая проблема — Playwright не сохраняет файл напрямую. Если вы внесли изменения, а потом закрыли, вы их потеряете, поэтому сохраняйте свои наработки.

  • Стабильные билды без изменений в инфраструктуре.

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

  • Более приятный и читаемый синтаксис.

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

Результаты этих тестов нужно было где-то хранить. Для такой задачи решили использовать Allure.

Allure для хранения результатов тестов

Какие возможности мы получили с Allure:

  • Централизация результатов тестирования.

Allure собирает все результаты функциональных тестов в одном месте, что удобно для их анализа и сравнения. Вы можете комбинировать результаты своих функциональных тестов с E2E-тестами, проведёнными тестировщиками.

  • Простой доступ к информации о покрытии тестами.

Команда QA, а также разработчики, могут легко проверить, какие сценарии уже покрыты тестами и где нужно что-то описать дополнительно. В будущем есть возможность мёржить покрытия между E2E-тестами тестировщиков и функциональными тестами.

  • Доступ к понятной истории изменений.

Теперь существует одна общая точка в истории изменений и можно навигироваться к истории коммитов — когда мы запустили тесты, по какой причине они упали и т.д. Выглядит это так:

Это удобнее, чем раньше, когда нужно было сходить в Slack и спросить, как там с тестированием.

  • Легко читаемые проблемы.

Логи ошибок в Allure представлены в более понятном и читаемом формате по сравнению с  логами Jenkins.

Как вы понимаете, проблемы ещё остались, но не глобальные.

Мокинг серверных данных

  • Сложность моков для SSR.

У нас SSR, и бывают состояния, когда нужно поднять приложение, чтобы проверить негативный сценарий. Было бы удобно, чтобы для тестирования база содержала валидные данные, с которыми работает приложение.

  • Отсутствие встроенных инструментов в Playwright.

Playwright, Cypress и подобные инструменты не предоставляют встроенных средств для адекватного мокирования серверных данных. Это означает, что нужно писать собственные решения. Надеюсь, кто-нибудь напишет и выложит что-то подобное на GitHub, но пока этого нет.

  • Необходимость постоянной актуализации данных.

Работая с unit-тестами, мы могли не актуализировать данные, и это не было большой проблемой. В случае с функциональным тестированием актуальность данных стала выше. Сломалась кнопка, с кем не бывает, но вместе с ней ломается весь пользовательский сценарий.

  • Изменение рабочего процесса.

Проблемы разработки — изменения флоу. Переход на новые инструменты и методики работы проходит со скрипом. Нам приходится привыкать к новым интерфейсам и менять рабочий процесс. Это может быть непросто и даже проблемно, а чтобы привыкнуть, нужно время.

  • Отказ от unit-тестов.

Это самая большая проблема. Для разработчиков, которые привыкли писать unit-тесты, смена мышления на функциональные тесты становится болью. Возможное решение — развивать корпоративную культуру функциональных тестов у себя в компании, выводить лучшие практики, писать технические статьи и доклады на эту тему, проводить митапы и пр.

  • Риск пробить SLO (Service Level Objective).

Отказ от unit-тестов может повлечь за собой риск пробития SLO. Это набор метрик, которые определяют желаемое поведение вашего продукта. Допустим, у вас Grafana с «ёлкой», вы вывели туда графики, и ваше приложение должно рендериться минимум 10 раз в час. Если оно отрендерилось два раза в час в прайм-тайме, то эта штука сообщит вам. А ещё она может звонить ночью голосом Робо-бабы и мешать вам спать. Поэтому лучше для всех, чтобы SLO не пробивало, и следить сразу же при раскатке.

Планы на будущее

Что хотелось бы реализовать в перспективе и что мы ещё не успели сделать:

  • Нарастить интеграции с QA для работы на одном поле.

Playwright — довольно гибкий инструмент, поддерживающий много полезных конфигураций, включая возможность использования Python, что органично для QA. Это позволит работать вместе на одной платформе и обещает много перспектив.

  • Удалить ненужные unit-тесты везде, куда дотянутся руки.

Удаление unit-тестов — не самая простая задача, потому что просто выпилить недостаточно. Нужно отслеживать параметры, понимать, где узкие места, чтобы не провалиться. К тому же у нас множество микрофронтов, около 200, но обкатали мы всего на 3-4 сервисах.  Поэтому работы будет много.  

  • Увеличить качество функциональных сценариев, создать понятную базу знаний. 

Мы собираемся активно развивать в компании культуру правильных функциональных тестов. И надеемся, что взлетит.

Вы можете посмотреть выступление на Frontend Conf 2023:

Источник: https://habr.com/ru/companies/oleg-bunin/articles/802785/


Интересные статьи

Интересные статьи

Fujifilm презентовала новую модель беззеркальной камеры X100VI на базе решений X100V. Внешне она напоминает предшественника, но включает значительные улучшения, поддерживая различные условия съёмки.&n...
Случай беспрецедентный, компания Яндекс, единственный техно. гигант, не использующий конструкцию UX/UI в наименовании вакансий. Анализ проведен по данным 10 000 записей WayBack Machine страницы h...
landing page для наглядностиНет часов. Нет минут. Нет часовых поясов. До свидания.Данный счёт времени достался нам по наследию от наших предков. Если говорить грубо, то вероятнее всего какой-то один ч...
Не так давно наткнулся на статью в журнале Forbes. Основной ее посыл - молодым ИТ-специалистам все сложнее найти работу. Если раньше на это требовалось один-три месяца, то сейчас полгода и больше. Что...
А вы знали что метрики покрытия вашего кода врут? В 2003 году Дерик Ретанс (Derick Rethans) выпустил Xdebug 1.2. Впервые в экосистеме PHP появилась возможность собирать данные о п...