• Услуги
  • Новости
  • Тренинги
  • Расписание
  • Материалы
  • Проекты
  • О нас

2013 September

А хотят ли тестировщики знать правду?

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

Я несколько раз выступал на конференции SQA Days и каждый раз был чуть ли не белой вороной – единственным разработчиком (именно реальным разработчиком, а не в прошлом), который делал доклад. Да и среди участников разработчиков мной было замечено немного. Почему так происходит? Во всех веселых историях про разработчиков и тестировщиков первые всегда выставляются в дураках. И некому защитить альтернативную точку зрения. А как же интеграция и совместная работа?

А за границей все гораздо более радостно. Много докладов об автоматизации как от разработчиков так и от тестировщиков. Мало докладов о ручном тестировании во всех его формах. Инструменты часто представляют разработчики а не тестировщики. Что же у нас то так все несовременно?

Я уже на третью конференцию для тестировщиков пытаюсь подать свой доклад “Бытовая классификация тестировщиков с точки зрения разработчика” и его не берут в программу. 🙁 Тестировщики часто говорят о противостоянии и конфликтах с разработчиками. Но ведь есть команды, где все живут в мире и согласии. Видимо что-то тут не так? Я хочу поговорить о том, как тестировщиков видят сами разработчики. В докладе будет проведена забавная классификация. Кроме известного всем тестировщика-обезьянки будут представлены тестировщик-муха, тестировщик-нацист, тестировщик-панда и многие другие герои. Вы сможете лишний раз задуматься над тем, как вас видят со стороны и, возможно, изменить ситуацию к лучшему. Доклад может быть полезен не только тестировщикам, но и менеджерам проектов, лидерам команд. Вы сможете быстрее распознавать те или иные шаблоны поведения тестировщиков и принимать меры по повышению уровня командной работы.

Я пока думаю где еще его можно было бы рассказать, чтобы целевая аудитория была более соответствующей и смеялась над очередной шуткой формата “разработчик сказал, что это не баг а фича”. Чтобы открыть глаза та то, как выглядят тестировщики с другой стороны, с точки зрения разработчиков. Без осознания этой правды очень тяжело работать вместе. Ведь совместная работа должна основываться на прозрачности и доверии с обеих сторон.

Так что ждите в ближайшее время на одной из сцен Украины или ближнего зарубежья! Заглянем по ту сторону правды… 🙂

Не хочешь пропускать ничего интересного? Подпишись на ленту RSS или следи за нами в Twitter!

event26/09/2013
personНиколай Алименков
mode_comment14
Далее
Почему я больше не хожу на Agile тусовки и конференции

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

Итак, когда-то давно мне очень нравилась одна фишка – мы собирались на встречи, участвовали в различных Agile мероприятиях и везде были реальные проблемы от реальных людей. Собирались и выступали люди, которые действительно работали над реальным внедрением гибких подходов и практик у себя на проекте. Было круто услышать реальные истории из нашего мира аутсорсинга, какие трудности испытывают команды в реальных условиях на реальных проектах в нашей стране. Сами темы выступлений и обсуждаемых проблем были гораздо интереснее и ярче, люди делились своими успехами, победами и поражениями. И создавалось приятное ощущение, что все движется вперед, развивается, не стоит на месте. Можно было на следующей конференции встретить тех же докладчиков и послушать как изменилась ситуация у них, чему они научились, что нового применили. Это давало толчок к собственному развитию, а также возможность поделиться своими опытом и знаниями с увлеченной аудиторией.

А что происходит теперь? В подавляющем большинстве выступают консультанты, коучи, тренера и евангелисты. Серьезно, около 90%. Кроме шуток, эта тенденция прослеживается не только в Украине, но и за границей. Только в Украине она более усугублена погоней за известными именами в программе – ведь мы все еще страна третьего мира, жители которой сильно ведутся на блестящие бусики (вспоминаем туземцев, а также как наш народ “хавает” золотые айфоны по невероятным ценам). Изредка консультанты разбавляются менеджерами (которых в грамотно построенном Agile процессе вообще и быть не должно, по крайней мере в этой роли). И что при таком составе выступающих можно услышать? Чем могут поделиться зарубежные “гуру”? Обычно, своими измышлениями и философскими рассуждениями, которые они вынесли из опыта консультирования НЕ В НАШЕЙ СТРАНЕ.

Я каждый раз смотрю на список тем докладов и задаю себе вопрос о практической применимости для меня лично и для людей, которых я знаю. И не вижу ее. Все философствования и идеи консультанты давно описали у себя в блогах, поэтому с ними я давно знаком. Вероятнее всего, видео подобного выступления проскакивало у меня в RSS ленте и я его уже видел, если конечно оно хоть чем-то меня заинтересовало. Так что мне там делать? Я теперь даже не подаю заявки на доклады – мои темы не вписываются в тематику. Я люблю рассказывать практические вещи, которые помогают людям что-то менять у себя и которые я попробовал сам.

Я в какой-то момент даже задумался, может реально уровень сильно вырос в Украине и у всех все хорошо. Но, общаясь с очень многими представителями различных компаний, я понимаю, что это не так. У многих как были проблемы с внедрением Agile, так и остались. Средняя температура по палате немного улучшилась, но незначительно. Зато сильно выросло число разочаровавшихся в Agile. И неужели им помогут очередные философские рассуждения от зарубежных “гуру”? Сомневаюсь.

Вот такая вот история… А вы что думаете по этому поводу?

Не хочешь пропускать ничего интересного? Подпишись на ленту RSS или следи за нами в Twitter!

event25/09/2013
personНиколай Алименков
mode_comment6
Далее
Как сделать быстрые счетчики в MySQL

Я буду время от времени писать интересные технические заметки, мало ли разработчики тоже читают наш блог. 🙂 Итак, сегодня попытаемся решить очень простую задачку – организовать счетчики в MySQL. Для чего это нужно? Примеров использования очень много: нужно для каждой страницы сайта хранить количество обращений, при обработке большого объема данных нужно посчитать частоты встречаемости элементов, нужно контролировать количество запросов для каждого пользователя в системе… В общем, задача частенько встречается.

Используем “ненадежные” хранилища

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

  • Храните все счетчики в памяти и периодически сбрасывайте контрольные значения на диск в инкрементальные файлы с указанием временной метки. Такой способ позволяет потерять только часть данных от последнего сохранения.
  • Используйте Redis с его возможностью ведения счетчиков. Работает очень быстро и достаточно надежно. Но, если у вас еще не используется Redis, то добавлять его только для счетчиков достаточно спорно. И вторая проблема заключается в транзакционности – счетчик нужно обновлять при успешном прохождении транзакции (в идеале в рамках транзакции), а иначе иногда при падениях или остановках системы счетчик может рассинхронизироваться с реалиями.
  • Используйте ZooKeeper с его распределенными счетчиками. Он предлагает функциональности больше чем вам нужно, но в некоторых случаях работает очень даже неплохо. Снова таки, вводить ZooKeeper только для счетчиков я не рекомендую. И при большой нагрузке он может стать узким местом, так как работает в один поток.

Во всех этих вариантах добиться идеальной точности и избежать потенциальной потери данных или двойного подсчета не удастся. Но для многих задач этого хватает с головой.

Тупое решение в лоб

Вам все таки нужны точные счетчики, привязанные к бизнес-транзакции и вы решили реализовать их на уровне БД. От этой точки и до конца статьи все примеры будут приводиться для MySQL. Решение напрашивается, потому что БД у вас уже есть и почему бы не использовать ее и для этой цели. Первым решением, которое приходит в голову, будет создание отдельной таблички:

CREATE TABLE resource_counter (
   resource_id BIGINT NOT NULL,
   count_of INT UNSIGNED NOT NULL,
   PRIMARY KEY (resource_id)
) ENGINE=InnoDB;

Ну и чтобы обновить счетчик, нужно вызвать банальный код:

UPDATE resource_counter SET count_of = count_of + :delta WHERE resource_id = :resourceId;

У этого решения есть несколько “сюрпризов”. Первый из них очень простой и понятен каждому, кто знает как работают базы данных. В случае обновления счетчика из нескольких потоков все они будут заблокированы и выполнятся в порядке очереди. Поэтому производительность будет не совсем радостной. Но есть и еще один интересный “сюрприз” – наличие deadlock-ов в банальном коде. Они могут проявляться сразу в нескольких случаях:

  • Вы в одной транзакции обновляете сразу несколько счетчиков. Параллельно подобных транзакций может выполняться несколько. Если вы не контролируете порядок resource_id, то в силу специфики захвата lock-ов по индексу (в примере он PK, но в любом случае он понятное дело у вас должен быть, чтобы поиск счетчика по resource_id работал быстро) вы получите deadlock.
  • Еще один сценарий deadlock-а появится, если вы будете производить действия с самой таблицей ресурса в той же транзакции для другой записи (например, дочерней или родительской) а вместо простого индекса будете использовать FK на таблицу ресурса.

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

Только вставки, ничего кроме вставок

И тут вам приходит в голову оптимизация. Вы вспоминаете, что вставки практически не блокируют друг друга, в отличие от обновлений данных. Поэтому вы решаете убрать PK с resource_id и вместо обновлений счетчика добавлять новые данные. Для подсчета же общего значения счетчика вы будете просто использовать следующий запрос:

SELECT sum(count_of) FROM resource_counter WHERE resource_id = :resourceId;

Некоторое время все даже будет работать достаточно быстро, но со временем производительность будет потихоньку деградировать, а общее количество “мусорных” исторических данных будет все время расти. И вы начинаете думать дальше…

Подсчитываем промежуточные итоги

Чтобы избавиться от “мусорных” исторических данных существует множество однотипных простых техник. Нужно время от времени агрегировать данные и удалять или “виртуально удалять” старые. Давайте рассмотрим немного детальнее. Вы запускаете job в отдельном потоке либо на уровне БД либо на уровне вашего приложения. Этот job бежит по таблице счетчиков, подсчитывает сумму по каждому ресурсу и вместо набора записей оставляет ровно одну со значением суммы. Как это реализовать технически:

  • Блокировать доступ к определенному ресурсу на время проведения операции. Проблема заключается в том, что другие потоки продолжают вставлять значения. А это значит, что без блокировки вы удалите старые записи, среди которых могут быть и новые. Тем самым рискуете поломать счетчик. Блокировку можно делать как на уровне БД так и в коде. Способ не очень хороший, потому что блокировки всегда замедляют работу.
  • Второй способ заключается в добавлении новой колонки с временем добавления записи (тип TIMESTAMP). Теперь вы можете безопасно посчитать сумму за 5 минут в прошлое и удалить записи, которые уже не нужны в той же транзакции. Сумма добавляется как новая запись за текущее время. На самом деле такой способ тоже чреват дополнительными блокировками при удалении и deadlock-ами при параллельной вставке и удалении по индексу, который у вас есть на resource_id.
  • Третий способ заключается в том, чтобы использовать отдельный поток для чистки, а агрегированные записи помечать специальным маркером. В этом случае правильное значение счетчика будет равно сумме всех записей, начиная с последнего агрегированного значения. Исторические данных могут удаляться по одной или блоками в любое время безо всякого риска.

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

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

Чистим за собой быстро

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

Для этого нам понадобятся вместо одной сразу 2 или 3 таблички одинаковой структуры: resource_counter, resource_counter_shadow, resource_counter_total (эта табличка является необязательной). Каждая из них будет поддерживать только вставки. Ваше приложение пишет все изменения значения счетчика в виде дополнительных записей в таблицу resource_counter и только в нее. Параллельно работает отдельный поток, который раз в определенное время производит замену таблиц:

RENAME TABLE resource_counter TO resource_counter_tmp, 
           resource_counter_shadow TO resource_counter,
                resource_counter_tmp TO resource_counter_shadow;

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

INSERT INTO resource_counter (resource_id, count_of) 
SELECT resource_id, sum(count_of) FROM resource_counter_shadow 
GROUP BY resource_id ORDER BY NULL;

Работать подобный запрос будет достаточно быстро, потому что таблица resource_counter_shadow маленькая и контролируется интервалом агрегации. Можно также использовать необязательную таблицу resource_counter_total, которая заведена для оптимизации (чтобы избежать переливания данных из одной таблички в другую, если они не меняются). Сделать это можно следующим запросом:

UPDATE TABLE resource_counter_total rct INNER JOIN 
(SELECT resource_id, sum(count_of) AS delta FROM resource_counter_shadow 
GROUP BY resource_id ORDER BY NULL) deltas ON rct.resource_id = deltas.resource_id
SET rct.count_of = rct.count_of + deltas.delta;

Есть еще более симпатичная версия этого же запроса:

INSERT INTO resource_counter_total (resource_id, count_of) 
SELECT resource_id, sum(count_of) FROM resource_counter_shadow rcs 
GROUP BY resource_id ORDER BY NULL
ON DUPLICATE KEY UPDATE resource_counter_total.count_of = resource_counter_total.count_of + rcs.count_of;

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

Применяем мульти-счетчик

В какой-то момент времени вы задумаетесь, а не слишком ли все стало сложно и обратитесь к изначальной проблеме. Она заключалась в том, что запись в таблице счетчика блокируется при обновлении. Но в то же время, формирование отдельных записей для каждого обновления приводит к росту их количества и необходимости чистить данные. Тогда надо использовать золотую середину. Для этого в таблицу счетчика добавляется новая колонка counter_index, которая включается в состав PK:

CREATE TABLE resource_counter (
   resource_id BIGINT NOT NULL,
   counter_index INT UNSIGNED NOT NULL,
   count_of INT UNSIGNED NOT NULL,
   PRIMARY KEY (resource_id, counter_index)
) ENGINE=InnoDB;

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

INSERT INTO resource_counter (resource_id, counter_index, count_of) 
VALUES (resource_id, rand() * 10, :countOf) 
WHERE resource_id = :resourceId
ON DUPLICATE KEY UPDATE count_of = count_of + :countOf;

Для получения значения счетчика по определенному ресурсу по-прежнему нужно будет использовать сумму, но по фиксированному количеству строк (максимум 10 в нашем примере):

SELECT sum(count_of) FROM resource_counter WHERE resource_id = :resourceId;

Работает быстро как на обновление так и на получение значения. Балансировать можно количеством счетчиков на один ресурс.

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

P.S. Все запросы писались напрямую в текстовом редакторе, поэтому мелкие ошибки и опечатки просьба указывать в комментариях, но в нежной форме. 😉

Не хочешь пропускать ничего интересного? Подпишись на ленту RSS или следи за нами в Twitter!

event24/09/2013
personНиколай Алименков
mode_comment6
Далее
Рубрика «Полезное чтиво». Выпуск 56

полезное чтиво

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

На заметку разработчикам

  • Release Notes for Cucumber-JVM 1.1.5 – большой релиз Cucumber-JVM с кучей новых фичей и исправлений
  • All Scalability Problems Have Only a Few Solutions – отличная статья с решениями проблем масштабирования
  • Four Myths of In-Memory Computing – In-Memory вычисления становятся все более актуальными с падением цен на память
  • Вы понимаете Hadoop неправильно – а вот тут кратко и емко описано, почему подавляющему большинству из вас Hadoop не нужен
  • Вам не нужен Hadoop – у вас просто нет столько данных – Hadoop нужен, если у вас реально много данных, а много – это не 1GB, не 100GB и часто даже не 1TB
  • Сравнение эффективности минимизаторов CSS- и JavaScript-кода (Сентябрь 2013) – минимизируйте свои CSS и JavaScript файлы, заботьтесь о скорости загрузки страницы и ее объеме!
  • Java SE 7 Update 40 Released – важное обновление Java
  • Top 10 Websites for Advanced-level Java Developers – 10 крутых сайтов с полезняшками для Java разработчика
  • Чем поможет архитектору «NoSQL» и… поможет ли? – NoSQL – это не магическая пилюля, вам придется много придумывать и хитрить, зато может работать отлично!
  • New AWS Command Line Interface (CLI) – новая крутая версия консольных тулов от Amazon для облачных сервисов
  • Development and Deployment at Facebook – как разрабатывают и деплоят в командах Facebook
  • Cassandra 2.0 Goes GA – Requires Java 7 – вышла Cassandra 2.0
  • Модульного тестирования недостаточно. Нужна статическая типизация! – на экранах Хабра статическая типизация и модульное тестирование
  • G1: One Garbage Collector To Rule Them All – G1 заставляет более серьезно задуматься о миграции на Java 7
  • Database Versioning and Delivery with Upgrade Scripts – отличное описание подхода к версионированию БД
  • Simple JavaScript Testing with QUnit – начать тестировать ваш JavaScript код с QUnit очень и очень просто, чего ждете?
  • Should it be readable or should it work? – работающий код может быть запутанным и благодаря этому якобы без дефектов, упрощения вскрывают карты
  • Почему изучать TDD трудно и что с этим делать. Часть 1 – на Хабре очередной спор про TDD
  • 10 Subtle Best Practices when Coding Java – полезные практические советы для Java разработчиков

Тестировщикам о тестировании

  • Selenium IDE 2.4.0 release notes – Selenium IDE продолжает выпускаться
  • Capturing JavaScript Errors in WebDriver – Even on Page Load! – как получать информацию о JavaScript ошибках на странице с помощью WebDriver
  • Ещё о тестировании в Яндексе роботами – как в Яндексе тестируют роботы
  • Тестовое задание QA – детальное описание тестирования карандаша для ответов на собеседовании
  • Writing Tests Against Page Objects – отличная статья об использовании Page Objects, это реально самый полезный шаблон в тестировании
  • После обновления Chrome перестал работать ChromeDriver – как починить ваши тесты для Chrome при переходе на свежую версию WebDriver
  • Вышел релиз Selenium 2.35 – очередной релиз WebDriver
  • SWD Page Recorder: Записывает PageObject-классы для Selenium WebDriver – облегчаем написание Page Objects с помощью автоматизации
  • Кто такой Жёсткий тестировщик? – тестировщики бывают жесткими

Процессы, подходы и менеджмент для менеджеров

  • Your story cards are limiting your agility – очень интересный подход формулировать User Story как предположение с критериями проверки
  • In Praise of “Master of One, Jack of Two” Agile Team Member – полной кросс-функциональности добиться невозможно, а вот частичной достаточно просто
  • By-the-book Agile Is No Longer Agile – работать по каноническим процессам должны только начинающие, опытные подстраивают их под себя
  • The Real Cost of Change in Software Development – вполне реально сделать стоимость изменений в ПО линейной, а не экспоненциальной, как принято считать
  • Why You Need to Customize Your Agile Methods – канонические Agile методологии надо подстраивать под себя как вы настраиваете операционную систему
  • О чем молчит диаграмма Ганта или почему проекты всегда опаздывают – диаграмму Ганта имеет право использовать только знающий теорвер менеджер, готовый подстраиваться на ходу
  • Painful Lessons – сказочка про Agile: жил-был злой waterfall, но из лесу вышел Agile и всех победил
  • STORY SPLITTING – A PLAY – “SPIKE SHERMAN” – пошаговая инструкция как разбить User Story на несколько, даже если это кажется сначала невозможным
  • Should Management Use Velocity as a Metric? – каждую неделю кто-то просыпается и пишет статью на тему вреда от использования Velocity в качестве метрики
  • Can’t Measure Productivity – если мы до сих пор не научились измерять продуктивность разработки, так может лучше и не пытаться?

А вот список интересного видео для просмотра:

  • Redis: Why and How – если вы еще не используете Redis, то вам стоит попробовать
  • Java Concurrent Animated – кто хочет лучше разобраться с java.util.concurrent, посмотрите на анимированную имитацию
  • Клуб анонимных разработчиков – видео с последних встреч клуба
  • Что я хотел бы услышать о разработке и разработчиках 20 лет назад – неплохой доклад Максима Дорофеева для студентов

Читайте и набирайтесь новых знаний!

Не хочешь пропускать ничего интересного? Подпишись на ленту RSS или следи за нами в Twitter!

event23/09/2013
personНиколай Алименков
mode_comment0
Далее
1 октября пройдет мастер-класс по TDD на Java для новичков

Только вчера прошла 32-ая встреча “Клуба анонимных разработчиков” и мы уже готовы анонсировать следующую встречу. Она состоится 1 октября и пройдет в формате практического мастер-класса.

Темой мастер-класса будет “TDD в Java для начинающих”. Многие слышали о том, что TDD – это круто и с помощью этой практики можно писать код лучше, быстрее и качественнее. Но в теории все звучит просто, на практике оказывается не так уж легко начать. Виктор Кучин подготовил практическое введение в TDD для тех, кто хотел бы попробовать этот подход, используя Java. На мастер-классе вы услышите небольшое теоретическое введение, а остальное время проведете за реальной разработкой по TDD. Поэтому все участники должны иметь с собой ноутбук с предустановленным ПО.

Встреча пройдет во вторник 1 октября. Местом проведения мы выбрали уютный Киевский офис компании DataArt. Этот офис полюбился членам клуба своей обстановкой и наличием всего необходимого для продуктивного общения.

Официальное начало встречи по-прежнему в 19:00, завершение в 23:00. Стоимость участия 80 гривен при оплате заранее, 120 гривен при оплате на месте. Пива, пиццы и кофе с печеньками хватит на всех. Регистрация обязательна. Все детали по оплате будут высланы вам после успешного прохождения регистрации. Количество мест ограничено 30 участниками.

Не хочешь пропускать ничего интересного? Подпишись на ленту RSS или следи за нами в Twitter!

event18/09/2013
personНиколай Алименков
mode_comment2
Далее
Так ли плохо безопасное планирование?

Недавно прочитал любопытную статейку от Максима Дорофеева с письмом от одного из его знакомого менеджера. В двух словах проблема примерно выглядит так:

  • есть команда, которая работает по Scrum (причем сравнительно недавно);
  • эта команда планирует итерации так, чтобы успевать сделать все запланированное;
  • в команде считается “смертным грехом” не выполнить обязательства и провалить спринт;
  • автор письма очень переживает, что команда может работать гораздо эффективнее, но за процессом эта эффективность теряется.

Scrum

Как и практически любой человек, который прочитал книжку Голдратта, менеджер из письма во всем видит потенциальное поле для улучшения эффективности. Причем, сразу и с текущего положения. В разы, чтобы как в книжке…

Лирическое отступление. Я когда-то давно прочитал “Цель” и “Цель-2” (кстати, всем настоятельно рекомендую) и до такой степени проникся идеями эффективности, что планировал чуть ли не по руководителям IT компаний ходить и “открывать им глаза” на неэффективность. Но когда начал больше расспрашивать в процессе консалтинга о деталях работы некоторых компаний, команд и проектов, осознал, что не все так просто и быстро. У некоторых царил полный хаос и они не были готовы к культуре “экономного эффективного производства”. Низкий уровень квалификации, бюрократия, навязанные извне правила и обязательства, специфические детали культуры компании и ситуация на рынке – все это требовало гораздо более примитивных мер на первых этапах борьбы за эффективность…

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

Scrum пытается научить команду давать осязаемые оценки своей продуктивности, выбрать самостоятельно объем работ на итерацию и сделать его успешно. Это очень важно! Во-первых, появляется предсказуемость для представителей бизнеса. Они теперь могут хоть как-то планировать. Во-вторых, появляется понятие командной ответственности за свои собственные решения и обратная связь по результатам выполнения работ (успели или нет, была ли возможность взять еще работ, нет ли проблем с качеством). Команда начинает работать как команда. И тут очень важно, чтобы начало хоть что-то получаться. Для некоторых команд непростой задачей является взять одну User Story и довести ее до конца в итерации. ОДНУ! Scrum же не исправляет ваши проблемы, а показывает их как в зеркале. Он дает вам данные, чтобы задуматься о причинах провала и неудач. Поэтому нет ничего зазорного или плохого в том, что команда ввела для себя (или кто-то другой ввел) жесткий критерий “все к концу итерации должно быть готово и качество должно быть на уровне”.

И вот, когда команда научилась давать нормальные оценки и делать хоть какую-то работу в срок от итерации к итерации, стоит задуматься об эффективности. Для этого в Scrum есть замечательные инструменты – burndown chart и ретроспектива. Первый может подтвердить описанное в статье подозрение, что команда первые две трети времени занимается чем угодно но не работой. А второй позволяет задать вопрос об эффективности и обсудить картину burndown chart за предыдущую итерацию. И команда сама примет решение, может ли она увеличить количество задач на итерацию или это слишком рискованно. В конце концов иногда проще брать на себя надежные обязательства и, если остается время, добавлять новые задачи. Чтобы ни при каких обстоятельствах не поломать ожидания представителей бизнеса. Все проекты разные и для некоторых это ну оооочень важно.

Все перечисленное будет работать, если в команде есть хотя бы кто-то ответственный. Если вся команда безответственная, то она будет филонить при любом процессе. Любая задача может быть искусственно затянута и оценки повышены. Поэтому сам процесс или подход не является решением. В Agile очень многое зависит от “правильных людей”, а иначе просто не работает, как бы грамотно не насаживались сверху практики замера и улучшения эффективности… 🙂

Через некоторое время необходимость в итерациях перестанет быть такой острой, и тут уже можно начать применять другие подходы к планированию. Еще через некоторое время итерации начнут просто мешать работать более эффективно для хороших команд. И тут время искать или строить более эффективный процесс разработки, например Kanban. Но рубить с плеча сразу может быть очень вредным для команды…

Не хочешь пропускать ничего интересного? Подпишись на ленту RSS или следи за нами в Twitter!

event13/09/2013
personНиколай Алименков
mode_comment1
Далее
О вреде наследования

Мы обсуждали тему вреда наследования в рамках недавней встречи клуба (уже доступно видео для желающих послушать про беды ООП, а также правильные подходы к дизайну и архитектуре). Я решил для закрепления темы и разжигания очередного холивара повторить основные тезисы в статье. 🙂

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

Дело в том, что наследование не учитывает будущие изменения родительского класса. Ведь когда вы наследуете новый класс от существующего, вы подписываете контракт о том, что новый класс всегда будет вести себя как существующий, возможно расширяя его поведение в некоторых местах. Для простоты понимания представьте себе, что вы разработчик и встретили своего друга, тоже разработчика. Поболтали, обсудили работу и поняли, что занимаетесь одним и тем же, только вы еще немного управляете командой. И тут вы можете сделать вывод – вы такой же как друг (вот оно наследование), но только делаете немного больше (расширение в дочернем классе). Прошло время и ваш друг немного забросил разработку и начал заниматься больше менеджментом (но по-прежнему время от времени пишет код, да и писать не разучился). Получается, у родительского класса появились новые функции, которые все дочерние классы по умолчанию наследуют. И вы нежданно-негаданно начинаете тоже заниматься менеджментом (тут может ничего плохого и нет), по крайней мере так следует из вашего “отношения наследования”. 🙂

В языках программирования такой эффект наблюдается за счет отсутствия необходимости переопределять публичные методы родительского класса. А это значит, что их с момента наследования могут добавить сколько угодно и вам никто, включая компилятор, об этом не скажет. А это, в свою очередь, может поломать вашу логику наследования с расширением возможностей родительского класса. На самом деле варианты есть – написать модульный тест, который посчитает количество публичных и полупубличных методов и проверит их количество. Это тоже не даст полной гарантии уведомления, но уже лучше. В идеале, стоило бы при наследовании во избежание сторонних эффектов добавлять тест, фиксирующий сигнатуры методов родительского класса. Но кто это делает? Я не встречал на практике. Более подробно с примерами о плохом наследовании можно прочитать в “Effective Java”. Надеюсь, эту книгу и так все Java разработчики прочли. 🙂

Это не все минусы наследования. Когда у класса появляется несколько наследников, то у них есть общее поведение, а есть расширенное. И зачастую приходится извращаться с тестами, чтобы избежать дубликатов. Это вырождается в абстрактный тест, который каждый тест на дочерний класс должен наследовать. А что делать, если вы наследуете класс из сторонней библиотеки. В этом случае нужно ли писать тесты на базовое поведение, дублируя существующие тесты в библиотеке? Или стоит оставить их как “надежные” без тестов?

Но ведь есть же шаблоны проектирования, которые предрасполагают нас к использованию абстрактных классов и наследования. Есть, и это большое зло. Мало кто правильно реализует тот же Template Method, делая публичные методы final и давая ровно столько точек для расширения дочерним классам, сколько требует базовый алгоритм. Но и правильная реализация не защитит от добавления новых точек для расширения при модификации алгоритма или рефакторинге. И ни один из наследников не получит уведомления от компилятора об этом. В данном случае гораздо лучше работает шаблон Strategy, где шаги алгоритма расписаны в интерфейсе, и обычный класс с реализацией самого алгоритма. Таким образом, каждая реализация интерфейса стратегии будет давать конкретную реализацию шагов алгоритма.

В Java нельзя разорвать наследование состояния от наследования поведения. Получается, что при желании наследовать состояние (набор полей и действия над ними) вы вынуждены тянуть за собой и все поведение, что приводит к нежелательным эффектам при будущих изменениях. Возможно, наличие других типов данных наподобие структур решило бы эту проблему.

Практически любое использование наследования можно заменить на композицию. Для этого нужен только интерфейс. Тестировать становится значительно проще, гибкости становится больше, уведомления об изменениях будет давать компилятор, код становится менее связанным… Одни преимущества! Почему же так не делают на практике? Ответ простой – из-за лени. Гораздо проще добавить еще одного наследника и дописать строчку кода, перекрыв один из методов. Это требует меньших усилий. А на то, что есть какие-то сложности в будущем, плевать. В будущем поддерживать этот код будет кто-то другой. 🙂

Может быть у вас есть возражения или удачные применения наследования? Напишите о них в комментариях!

Не хочешь пропускать ничего интересного? Подпишись на ленту RSS или следи за нами в Twitter!

event11/09/2013
personНиколай Алименков
mode_comment4
Далее
Как на самом деле выгодно тратить бюджет на образование

В последнее время кое-кто нам твердит, что выгоднее всего для компании “привозить” конференцию или выступление к себе в офис посредством онлайн вещания. Ведь тогда сотрудники компании находятся в безопасности: их никто не схантит, никто не узнает их контакты и им никуда не нужно ехать… И можно сэкономить еще на билетах, отелях, накладных расходах и т.д. Заметьте, ни слова про эффективность самого участия в такой “конференции”. Ни слова про то, насколько это будет вдохновляющим и полезным для сотрудников. Только экономия и безопасность. 🙂

Я поделюсь с вами страшным секретом от Капитана Очевидность. На самом деле самым выгодным с точки зрения как затрат так и принесенной пользы является приглашение экспертов к себе в офис на семинар. Алгоритм очень простой:

  • Вы выбираете тех экспертов на рынке, которые наиболее интересны вашим сотрудникам с учетом текущих проблем, используемых технологий, методологий, подходов и практик (проводите опросы, голосования, предлагаете разных кандидатов).
  • Связываетесь с ними и предлагаете выступить в вашей компании. Оплачиваете им перелет, проживание и гонорар за выступление на несколько часов.
  • При желании и возможностях, дополняете выступление приглашенного “гуру” выступлениями своих сотрудников на рядом стоящие темы.
  • Готовите и проводите мероприятие у себя в офисе, возможно даже с приглашением людей из других компаний.

Вуаля! Вы имеете отличное локальное мероприятие при минимальном бюджете. Из моего опыта, $1-2K хватает с головой, если вы не приглашаете “звезду” международного масштаба. А если вы будете отслеживать различные мероприятия в вашей местности, то можете еще больше сэкономить, совместив прилет эксперта к кому-то со своим мероприятием. Некоторым, благодаря таким хитростям, удавалось заполучить даже Боба Мартина и Мартина Фаулера. Важной особенностью будет живое общение с приглашенным экспертом, возможность позадавать вопросы и обсудить проблемы именно вашей компании или команды, программа выступления с учетом ваших потребностей и пожеланий. И главное, все в безопасности и комфортно себя чувствуют.

Не хочешь пропускать ничего интересного? Подпишись на ленту RSS или следи за нами в Twitter!

event09/09/2013
personНиколай Алименков
mode_comment0
Далее
Контролируем доступ к ресурсу в Java посекундно

JavaConcurrency in Practice

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

Итак, начнем с задачи. Она звучит очень просто. Есть некий сервис, к которому не рекомендуется делать больше чем X обращений в секунду. Понятное дело, что от небольшой погрешности никто не умрет и задача не имеет супер-критичной формулировки. Но надо приложить максимум возможных усилий, чтобы избежать перегрузки сервиса. Обращения к сервису идут с многих потоков (для примера 50), поэтому механизм контроля должен максимально избегать тяжелой синхронизации.

Звучит действительно несложно. В первую очередь на ум тем, кто не сачковал на курсе по теории алгоритмов или уже не первый год пишет код для многопоточного доступа, приходит Semaphore. В Java есть его реализация в пакете java.util.concurrent. Он отлично подходит для синхронизации доступа к ресурсу – можно задать количество разрешенных заходов, а когда они закончатся, остальные потоки мирно уснут в ожидании освободившихся мест. Проблема заключается в том, что в отличие от классического использования, поток не может сам по завершению задачи вернуть свой заход назад в семафор. Ведь на каждую секунду количество заходов должно быть лимитировано. То есть, нам нужен некий временной семафор.

Прежде чем идти дальше, любой опытный разработчик постарается поискать, не решал ли кто-то уже подобную задачу. Поиск привел нас к TimedSemaphore из commons-lang и RateLimiter из google-guava. Первый имеет сразу две проблемы: слишком прямолинейная реализация с синхронизированным медленным методом aquire и добавление ресурсов в семафор с помощью ScheduledThreadPoolExecutor, что совершенно не гарантирует добавление в начале секунды и отсутствие простоев по несколько секунд. Второй использует несколько странное решение для поставленной задачи – при необходимости обеспечить лимит в N запросов в секунду он ждет после каждого из них по 1/N части секунды. Получается, что наши запросы будут распределяться по секунде вместо того, чтобы заниматься обработкой результатов. Это явно не наш кандидат.

Как же обновлять в семафоре количество возможных заходов? Первой решили опробовать все тот же выделенный поток с расписанием “раз в секунду”. Поток ни с кем не конкурирует, поэтому синхронизировать ничего не нужно. Он просто возвращает значение семафора в равное лимиту на секунду. Данному потоку для пущего устрашения был дан максимальный приоритет. Реализация заведомо ненадежная, но интересовало насколько именно в наших условиях. Оказалось, что немного раз дистанция между обновлениями составляла несколько секунд (потоку просто не давали времени на выполнение) и очень много раз она сбивалась на сотни миллисекунд. А для нас это сулило две проблемы: простой работающих потоков или двойная норма запросов в одну секунду. Обе проблемы совершенно не радовали.

В запасе была оптимизация – семафор пополнялся не только по расписанию, но и по запросу любого работающего потока, который пришел узнать можно ли ему сделать запрос и обнаружил, что секунда с прошлого обновления уже прошла. Реализация требует синхронизации, но она может быть облегчена за счет использования AtomicInteger и его метода compareAndSet. Каждый поток проверяет время последнего обновления по отношению к текущему. Если секунда прошла, то пытается установить свое время вместо старого. Это удастся только одному потоку – именно он и будет отвечать за добавления запросов в семафор. Но получается из одной легковесной структуры для синхронизации мы уже используем две, причем в большой части случаев делая проверки по времени совершенно напрасными. Но в среднем случае ситуация улучшилась. Зато, если все потоки успели заснуть на семафоре в ожидании свободных заходов, то ничего не изменилось и проблемы с обновлением по расписанию все те же.

И тут на помощь пришло альтернативное решение, которое рассматривалось с самого начала – работа через счетчики. Заводим AtomicInteger для хранения количества осуществленных запросов в секунду и дополнительно храним текущую секунду (мы потом придумали как кодировать информацию о текущей секунде сразу в счетчик, чтобы операции были атомарными, но это не так важно). Теперь любой поток проверяет не изменилась ли секунда. Пробует ее обновить и если у него получается, то он сбрасывает счетчик все тем же методом compareAndSet. Иначе, он просто увеличивает счетчик и смотрит, не превышен ли лимит на данную секунду. Если превышен, то засыпает на время до конца секунды. Код такого решения явно сложнее и каждый поток теперь должен сам засыпать вместо ожидания на объекте синхронизации. Вот пример реализации такого подхода:

private static final int LIMIT = 20;

private final AtomicInteger second = new AtomicInteger(getCurrentSecond());
private final AtomicInteger counter = new AtomicInteger(0);

public void accessResource() {
   int currentSecond = checkCurrentSecondIsCorrect();
   int requestsCount = counter.incrementAndGet();
   if (requestsCount > LIMIT) {
      sleepTillNextSecond(currentSecond);
   }
}

private int checkCurrentSecondIsCorrect() {
   while (true) {
      int currentSecond = getCurrentSecond();
      int lastSecond = second.get();
      if (currentSecond == lastSecond || tryToChangeSecond(currentSecond, lastSecond)) {
         return currentSecond;
      }
   }
}

private boolean tryToChangeSecond(int currentSecond, int lastSecond) {
   boolean changed = second.compareAndSet(lastSecond, currentSecond);
   if (changed) {
       counter.set(0);
   }
   return changed;
}

Это решение можно улучшить, если завести не один AtomicInteger, а N. Представьте для простоты их в виде массива и пусть N = 60 (количество секунд в минуте). Тогда каждый поток приходит и первым делом определяет какая сейчас секунда. После этого берет из массива счетчик по индексу равному секунде и дальше работает с ним. Получается, что все потоки работают просто со счетчиками и нет необходимости им их сбрасывать. А кто же тогда будет их обнулять? Эту задачу как раз и можно повесить на отдельный поток, который будет раз в 5 секунд просыпаться и обнулять счетчики в “безопасной зоне” (скажем для примера, все кроме пары секунд до и после текущей секунды). Ему тоже не нужно ни с кем синхронизироваться, а размер массива и “безопасной зоны” могут быть подобраны так, что даже опоздание этого потока на 30-40 секунд ни на что не влияет.

И тут пришла идея скомбинировать данный подход с семафорами. Алгоритм получился очень простой. Вначале заводится массив из N семафоров и все они сразу имеют установленное количество заходов равное лимиту в секунду. Когда поток приходит, то он высчитывает секунду, берет по ней из массива семафор и пытается получить заход с его помощью tryAcquire, передавая туда время до конца текущей секунды. Если метод вернет TRUE, то можно смело делать запрос. Если он вернул FALSE (не получилось захватить заход за заданное время), то надо просто вернуться на первый шаг и взять другой семафор. Реализация алгоритма представляет простейший цикл. Параллельно действует восстанавливающий семафоры поток. Он представляет массив в виде замкнутого кольца (так как после 59 секунды снова идет 0 секунда) и раз в 5 секунд восстанавливает значения семафоров в “безопасной зоне” (все кроме 5 секунд до и после по кольцу от текущей секунды). Пример реализации:

private static final int RING_SIZE = 60;
private static final int LIMIT = 20;

private final Semaphore[] ring = new Semaphore[RING_SIZE];

public AccessManager() {
   for (int i = 0; i < ring.length; i++) {
      ring[i] = new Semaphore(LIMIT);
   }
}

public void accessResource() {
   while (!acquire()) {}
}

private boolean acquire() {
   try {
      return tryToAcquire(getActiveRingCell());
   } catch (InterruptedException e) {
      return false;
   }
}

private boolean tryToAcquire(int cell) throws InterruptedException {
   Semaphore semaphore = ring[cell];
   long timeoutMs = calculateDelay();
   return semaphore.tryAcquire(timeoutMs, TimeUnit.MILLISECONDS);
}

private int getActiveRingCell() {
   return (int) ((Clock.getTimeMillis() / 1000) % RING_SIZE);
}

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

Не хочешь пропускать ничего интересного? Подпишись на ленту RSS или следи за нами в Twitter!

event04/09/2013
personНиколай Алименков
mode_comment0
Далее
17 сентября новая “архитектурная” встреча в “Клубе анонимных разработчиков”

Тематика архитектуры и дизайна похоже очень популярна – второй раз на встречу «Клуба анонимных разработчиков» собирается около 100 человек. Мы уже выложили видео последних двух встреч и уже готовы огласить темы следующей встречи. Она состоится во вторник 17 сентября.

На встрече будут представлены две темы. С первым докладом под названием “Объективное чувство стиля в ООП и закон Деметры” выступит постоянный член клуба Владимир Цукур. Цель доклада – познакомить аудиторию с законом Деметры. Это очень простое правило, следуя которому можно значительно понизить связанность классов и уменьшить количество зависимостей, а значит сократить количество ошибок и упростить тестирование. Хорошие ООП разработчики следуют этому закону в полной или частичной мере. Владимир покажет пример на Java, расскажет “правила игры” и осветит практическую сторону вопроса. Также аудитории будут представлены результаты исследований, доказывающих положительное воздействие следования этому закону в реальных проектах.

Второй доклад называется “Священный уровень DAO” и представит его Николай Алименков. С приходом JPA в мире Java многие начали писать о том, что уровень DAO умер и больше нет смысла поддерживать его в архитектуре приложений. Николай постарается убедить аудиторию в обратном, продемонстрировав многочисленные преимущества использования данного уровня, а также на практических примерах раскроет суть использования DAO в современных приложениях.

Итак, 17 сентября пройдет 32-ая встреча клуба. Местом проведения пока выбран офис компании ЕПАМ, который располагается по адресу ул. Кудряшова 14-Б. Наши встречи собирают все больше и больше участников, поэтому мы снова начинаем не влезать даже в более просторные залы. Возможно, ближе к дате встречи место будет изменено.

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

Официальное начало встречи по-прежнему в 19:00, завершение в 23:00. Стоимость участия 80 гривен при оплате заранее, 120 гривен при оплате на месте. Пива, пиццы и кофе с печеньками хватит на всех. Регистрация обязательна. Все детали по оплате будут высланы вам после успешного прохождения регистрации. Количество мест ограничено 90 участниками.

Не хочешь пропускать ничего интересного? Подпишись на ленту RSS или следи за нами в Twitter!

event02/09/2013
personНиколай Алименков
mode_comment0
Далее
Важное
XP Days Ukraine 2018: вспоминаем как это было
folder
label
event
star
forum
Категории
  • Club (50)
  • Material (179)
  • Project (6)
  • Review (3)
  • Schedule event (452)
  • Trainer (7)
  • Общие (267)
  • Полезное чтиво (57)
  • Статьи (242)
  • Тренинги (62)
Tags
.NET agile Agile Base Camp AgileDays Agileee 2010 build automation code review continuous delivery continuous integration DevOps exploratory testing Hibernate IT Brunch IT talk Java JavaScript JEE JEEConf kanban QA refactoring scrum selenium Selenium Camp spring tdd test automation testing unit testing webdriver XP XP Days Ukraine автоматизация тестирования архитектура видео инженерные практики клуб анонимных разработчиков команда конференция полезное чтиво презентация проектирование тестирование тренинги управление рисками
Archives
  • February 2021
  • October 2020
  • March 2020
  • February 2020
  • January 2020
  • December 2019
  • November 2019
  • October 2019
  • September 2019
  • June 2019
  • April 2019
  • March 2019
  • January 2019
  • December 2018
  • September 2018
  • July 2018
  • June 2018
  • May 2018
  • April 2018
  • March 2018
  • February 2018
  • January 2018
  • December 2017
  • November 2017
  • October 2017
  • September 2017
  • August 2017
  • July 2017
  • June 2017
  • May 2017
  • April 2017
  • March 2017
  • February 2017
  • January 2017
  • December 2016
  • November 2016
  • October 2016
  • September 2016
  • August 2016
  • July 2016
  • June 2016
  • May 2016
  • April 2016
  • March 2016
  • February 2016
  • December 2015
  • November 2015
  • October 2015
  • September 2015
  • August 2015
  • July 2015
  • June 2015
  • May 2015
  • April 2015
  • March 2015
  • February 2015
  • January 2015
  • December 2014
  • November 2014
  • October 2014
  • September 2014
  • August 2014
  • July 2014
  • June 2014
  • May 2014
  • April 2014
  • March 2014
  • February 2014
  • January 2014
  • December 2013
  • November 2013
  • October 2013
  • September 2013
  • August 2013
  • July 2013
  • June 2013
  • May 2013
  • April 2013
  • March 2013
  • February 2013
  • January 2013
  • December 2012
  • November 2012
  • October 2012
  • September 2012
  • August 2012
  • July 2012
  • June 2012
  • May 2012
  • April 2012
  • March 2012
  • February 2012
  • January 2012
  • December 2011
  • November 2011
  • October 2011
  • September 2011
  • August 2011
  • July 2011
  • June 2011
  • May 2011
  • April 2011
  • March 2011
  • February 2011
  • January 2011
  • December 2010
  • November 2010
  • October 2010
  • September 2010
  • August 2010
  • July 2010
  • June 2010
  • May 2010
  • April 2010
  • March 2010
  • February 2010
  • January 2010
  • December 2009
  • September 2009
Recent Posts
  • Байки про Архитектуру, микросервисы и монолиты.
  • Rise and Fall of story points. Capacity-based planning from the trenches.
  • Static analysis tools as the best friend of QA
  • Modern CI/CD in the microservices world with Kubernetes
  • Тренинг “Kubernetes for Java developers”
  • Тренинг “Test automation strategy for microservices-based systems”
  • Тренинг “Test automation strategy for microservices-based systems”
  • Тренинг “Efficient Selenium Infrastructure with Selenoid”
  • Конференция JEEConf 2020
  • Конференция Simplicity Day: Agile Magic
Recent Comments
  • Микола on Справедливо ли зарабатывают представители IT Украины?
  • Николай Алименков on Страсти по Crossover
  • Pavel on Страсти по Crossover
  • Василий on Опубликована программа JEEConf 2018
  • Николай Алименков on Опубликована программа JEEConf 2018
НАШ twitter
Tweets by @xpinjection
© XP Injection, 2019
  • Услуги
  • Новости
  • Тренинги
  • Материалы
  • Расписание
  • Проекты
  • О нас
© XP Injection, 2019