Tuesday, May 07, 2013

Стоит ли проверять свои изменения перед комитом? Оцениваем сколько может стоить ошибка.

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

Цель и ожидаемый результат.


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

Выводы предидущего исследования.

Согласно нашей модели если я, как разработчик, никогда не буду тестировать свои изменения перед тем, как отдать тестировщику, то в 58% случаев это сработает и качество системы не пострадает. Если же я всегда буду прогонять предкомитные тесты, то следует ожидать качественную систему в 78%.

Приводим проценты к денежному эквиваленту.

Ок, основываясь на оптимистичных ожиданиях и субьективных оценках мы получили разницу в 20% между вероятностями "хорошей" и "плохой" сборки приложения в зависимости от того, поленюсь я или нет запустить приложение перед комитом. 
Много это или мало? Кто-то наверняка скажет, что 20% - это много и лучше бы разработчику потратить лишних 10 минут чтобы убедиться, что он отдает неполоманную систему. Однако лично мне эти проценты не позволяют делать окончательные выводы. 

Я основываюсь на следующем предположении:

Предположение

Если в большинстве случаев мои изменения "проканают", то мне незачем тратить время на тестирование. Т.е. в том случае, если я буду тестировать, то я потрачу (к примеру) 10 минут + тестировщик тоже потратит 10 минут. Если тестировщик ничего не найдет, то свои 10 минут я потратил впустую. Если же тестировщик что-то найдет, то сэкономленного мной времени должно с лихвой хватить на исправление замечаний от тестера.

Метод оценки

В предидущем исследовании мы получили вероятностые зависимости между моим действием проверять/не проверять систему  (переменная Check, дальше C) и качеством приложения, выраженном через то, переоткроется ли баг (переменная Reopen, дальше R) и найдутся ли новые ошибки (переменная Introduced new issues, дальше I).
Как же, имея вышеперечисленные вероятностные характеристики оценить мои и тестировщика трудозатраты в вариантах, когда система мной проверена (C=1) и когда не проверена (C=0)?
Я воспользуюсь методом, который применяется в алгоритме поиска оптимального решения в дереве возможных вероятных действий/состояний expectimax search. Много умных слов, но смысл попытаюсь донести на дурацком примере.

Дурацкий пример

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

Выразим эту историю в виде структуры (дерево), которую можно загрузить в алгоритм поиска. Основными элементами дерева являются: состояние (state), действие (action), вероятность перехода в новое состояние (P) и выгода (Utility). Здесь многовато новых терминов в одном предложении, поэтому вот поясняющий рисунок.
Рис 1. Дерево вероятностных состояний

В текущем состоянии (очень жарко) можно сделать 2 действия (actions): пройти мимо киоска или выпить ледяной напиток. 

  • Если пройти мимо, то это заберет 10 "условных единиц" здоровья. -10 - это выгода (utility), которую мы получим, перейдя в новое состояние. Вероятность перехода (P) в это новое состояние = 1.0.
  • Если выпить напиток, то тут немного интереснее и возможны следующие варианты:
    • Можно заболеть с вероятностью 0.2, при этом здоровье пострадает на 100 условных единиц.
    •  Может все будет хорошо и здоровье улучшится на 10 пунктов. Вероятность такого варианта 0.8.
Теоретически дерево можно продолжить, т.к. в каждом новом состоянии могут быть доступны новые действия, каждое из которых может привести в очередное состояние.
Например, если в качестве прохладительного напитка выбрать пиво, то в удовлетворенном состоянии может появиться действие "взять водки", "взять еще пива" и т.д. Каждое действие в свою очередь может приводить в новое состояние, причем эмпирическим путем установлено, что вероятность попадания в состояние с крестиком после действия "взять водки" значительно возрастает. Но для демонстрации метода и целей нашего исследования достаточно одного уровня.

Имея набор вероятных состояний с ассоциированной выгодой (utility) можно определить какое действие предпринять. Для этого нужно посчитать математическое ожидание (expected value, дальше EV) для каждого действия и выбрать то, у которого значение EV выше. Посчитать EV довольно просто - нужно сложить все возможные выгоды, умноженные на вероятности их получения:
$$EV_a=\sum_{s'}{P(s,a,s') U(s')}$$
Здесь \(EV_a\) - ожидаемая выгода действия a. \(P(s,a,s')\) - вероятность попадания в состояние s' если выбираем действие a. \(U(s')\) - выгода, которую мы получим, когда перейдем в состояние s'.

В этом примере значения EV такие:
  • Если выбираем "пройти мимо": \(EV=1.0 (-10) = -10\)
  • Если выбираем "выпить" \(EV=0.8*10 + 0.2 (-100) = -12\)

Если сравнить значения ожидаемой выгоды, то очевидно, что лучше не пить холодных напитков в жару (\(-10 > -12\)).

Строим дерево

Возможные состояния

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

  • Изменение не принесло новых ошибок и мой фикс был успешно протестирован в новом билде (I=0 и R=0)
  • Баг отрепродюсился на новом билде (R=1), но новых багов не нашлось (I=0)
  • Мое изменение внесло новые баги (I=1), но изначальный баг успешно пофикшен (R=0)
  • Состояние "пж"  (п="полная"): изменение не пофиксило изначальную проблему плюс оно породило новые баги (I=1 и R=1)
На рисунке изображены все возможные состояния системы, в которые она может попасть после моих потенциальных действий.
Рис 2. Возможные состояния системы после действия.

Вероятности переходов

В предидущем посте я предположил следующие вероятности для событий "Баг переоткрыт" (переменная R - Reopen) и "Внесеные новые баги" (переменная I - Introduced new issues). Для того, чтобы освежить значения вот эти таблицы из предидущего поста:
Таблица 1Вероятность возникновения R при фиксированном C
Таблица 2. Вероятность возникновения I при фиксированном C
В таблице представлены бинарные переменные. Если значение переменной 1, то в колонке Probability указана вероятность того, что событие произойдет. Если 0, то вероятность того, что события не будет.

Например, вот как следует читать первую строчку таблицы #1:
Если проверить систему перед комитом (колонка C=1), то баг переоткроется с вероятностью 10% (R=1, Probability=0.1). 

Теперь посмотрим на возможные состояния в нашей системы (см. рис 2). Как видишь, каждое состояние - это один вариант из набора из всех возможных значений R и I. Чтобы получить вероятность перехода, нужно посчитать совместную вероятность (joint probability) возникновения событий R и I при известном C. Для этого просто перемножим соответствующие значения R и I*.

* Как только мы знаем, какое значение принимает C это делает события R и I независимыми и их совместная вероятность равна произведению вероятностей возникновения R и I.

Так, например, для действия "проверять" (To Check, где C = 1) вероятность попасть в состояние, когда изменение успешно проверено (R=0) и новых багов не обнаружено (I=0) равна \(0.9 * 0.8 = 0.72\), для C=1, R=1 и I=0 - \(0.1 * 0.8 = 0.08\) и т.д. В табличном виде это выглядит следующим образом:
Таблица 3. Совместные вероятност событий R и I при фиксированном C.

Определяем выгоду

Ок, на данным момент у нас есть варианты возможных действий (проверять/не проверять), список состояний, в которые можем попасть (см. рис 2) и вероятности попадания в новое состояние при выборе того или иного действия (см. таблицу 3). Осталось совсем немного - определить выгоду (utility) каждого из состояний.
Как можно оценить utility? Что "выгоднее" - чтобы тестировщик переоткрыл багу (R=1) или чтобы закрыл, но обнаружил новые (I=1)? Интуитивно кажется, что состояние (R=1,I=0) выгоднее чем (R=0,I=1) но насколько? Я предлагаю оценить трудозатраты тестировщика и разработчика для каждого состояния, а после расчета мат. ожидания (EV) выбрать то действие, которое его минимизирует.
Оценка трудозатрат основана исключительно на моих субъективных ощущениях, которые я иногда включаю во время планирования очередного спринта. Оценю в абстрактных и никому до конца не понятных единицах - стори поинтах *. 

Поигрю немного сам с собой в "планнинг покер" дабы получить искомые значения :). 

Стори №1. Итак, самая минимальная задача, которую можно придумать - это проверка моего изменения. Ее возьмем за 1. В случае, если это будет делать тестировщик, то он прогонит основной набор тестов, относящийся к изменению за 1 единицу времени **. Я предполагаю, что набор этих кейзов небольшой и разработчик (если он не раздолбай) запустит такой же набор у себя перед комитом.

Стори №2. Бага переоткрыта. Тестировщик прогнал базовые тесты и обнаружил, что моего изменения в билде нет. Он переоткрывает багу и, скорее всего, проведет небольшое исследовательское тестирование дабы убедиться, что симптомы проблемы не поменялись. Предположу, что объем работы такой же как и в стори №1 (т.е. 1). 
Мне, как разработчику***, возможно понадобится чуть больше времени дабы заглушить муки совести, потом попробовать переубедить тестера, что ему показалось, затем провести исследование дабы обнаружить что тестер сделал все в правильном окружении, на правильных серверах и в правильную фазу лунного цикла. После этого я обычно быстро устраняю проблему. Рискну предположить что для разработчика это займет 2 единицы времени.

Стори №3. Тестировщик обнаружил новые проблемы. Скорее всего ему прийдется запустить более полный набор тестов, затем локализировать проблемы и зарепортить симптомы в виде новых багов. Предположу, что это сложнее стори №1 в 3 раза, следовательно займет 3 единицы времени.
Я уже чувствую разочарование разработчика, который находится в подобном состоянии :) Согласись, это довольно печально - пофиксить 1 баг, а взамен получить пачку новых :). Скорее всего здесь кроется что-то очень сложное, поскольку мое изменение повлияло на те части системы, о которых я и не думал во время работы. Следовательно фикс может затянуться, т.к. во всех этих кусках прийдется колупаться. Ставлю 5 сторипоинтов (упс.. сори - единиц времени :)) этой задаче.

* Далее будет несоответствие с "классическими" юзер стори, поскольку выдумывать роли и записывать каждую историю в виде "Как тестер я хочу чтобы не было багов" мне лень, да и на тему исследования это никак не влияет.
** Здесь может быть некоторое противоречие с теми сторипоинтами, которыми нас учат классики. В качестве сторипоинта я беру не сложность, как это принятно, а "идеальную" единицу времени, поскольку потом буду приводить эти единицы к денежному эквиваленту. Я  исхожу из предположения, что любой тестировщик потратит 1 единицу времени на эту задачу.
*** И еще одно расхождение с классиками. Обычно историю оценивает команда (тестеры и девелоперы) и дает общую оценку. Но в целях данного трактата я разделю тестерские и девелоперские оценки. Дальше поймешь зачем я это сделал.

Вот для наглядности варианты оценок в виде таблицы
Таблица 4. Оценка трудозатрат в сторипоинтах.
Осталось посчитать выгоду каждого действия. Напомню, что выгода = математическое ожидание, которое считается как сумма вероятности события помноженное на его выгоду: \(\sum_{s'}{P(s,a,s') U(s')}\)
Не буду загромождать статью текстом простейших вычислений, приведу сразу все значения в таблицах.

Оценка математического ожидания если программист параноидально добросовестен (C=1):
Таблица 5. Оценка трудозатрат в случае 100% проверки перед комитом.

Оценка мат. ожидания в случае абсолютного программиста-разгильдяя:
Таблица 6. Оценка трудозатрат если не делать проверок перед комитом.
Как видно из таблиц, если проверять свои изменения перед комитом, то изменение будет стоить 3.9 условных единиц - "попугаев". Если же изменения не проверять, то 4.8.
Следовательно можно было бы сделать вывод, что проверять изменения перед комитом обходится дешевле. Но раз уж мы говорим про стоимость, то давай переведем полученные мат ожидания (EV) в условные денежные единицы и посмотрим на окончательный результат.

Переводим в доллары

Здесь мы попытаемся получить оценки решений проверять/не проверять в денежном эквиваленте. Денежный эквивалент мат ожидания называется expected monetary value (EMV). На русском это звучит как-то нелепо, поэтому я буду пользоваться английским названием.

Известно, что зарплаты программистов отличаются от зарплат тестировщиков. Следовательно цена 1 единицы времени программиста не равна цене 1 единицы времени тестера. Для расчета соотношения зарплат я взял на DOU средние зарплаты по киеву QA Engineer и Software Engineer за май-июнь 2012 год. Там фигурируют такие цифры:
- Средняя зп QA Engineer с опытом 1-10 лет  = $1280
- Средняя зп Software Engineer с опытом 1-10 лет = $2000

Исходя из стредних зарплат соотношение зп программиста к зп тестера = 2000/1280 = 1.5625. Пересчитаю таблицу оценки трудозатрат из сторипоинтов в доллары *. 
Таблица 7. Относительная оценка трудозатрат в $
* Здесь я просто умножил оценки затрат для девелопера на полученное соотношение (1.5625).

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

Дальше остается расчитать EMV по известной формуле, где для подсчета Utility используем "девелоперские" и "тестерские" оценки из таблицы №7 *

Оценка EMV если программист параноидально добросовестен (C=1):
Таблица 8. Относительное значение EMV если делать проверку.
Оценка EMVв случае абсолютного программиста-разгильдяя:
Таблица 9. Относительное значение EMV если не проверять свои изменения.
* Я понимаю, что на картинках не показано как именно получаются значения Utility, поэтому в конце поста я добавил ссылку на экселевский документ, чтобы можно было детальнее разобрать этот пример. 

Выводы

Мы нашли два математических ожидания: оценили последствия безалаберности (если не проверять изменения) и абсолютной параноидальности (если всегда проверять свои изменения перед комитом). Результаты оказались не в пользу безалаберности. 
  1. Во временных трудозатратах разница составляет ~ 20% (4.8 сторипоинта в случае безалаберности и 3.9 в случае паранойи)
  2. В денежном эквиваленте разница между безалаберностью и паранойей составляет 10-12% (6.15 если не проверять свои изменения и 5.14 в случае проверки перед комитом). Видно, что на денежное ожидание (EMV) повлияло то, что программисты стоят дороже почти в 1.5 раза чем тестеры и экономия времени на проверке своих изменений дает такую разницу.
Мое изначальное предположение, что можно не проверять свои изменения перед комитом при наличии хорошего набора юнит тестов не подтвердилось. Впредь буду аккуратнее с использованием интуиции в подобных  предположениях, но полностью выключать ее тоже не буду, ведь погрешность моей интуиции, выражанная в денежном эквиваленте составила всего 10% ;).

Ссылки на источники, используемые файлы


2 comments:

  1. Серега,

    А если учесть возможность запуска приемочных авто-тестов перед тем как отдать тестеру на работу? Пускай это даже будет автоматически после коммита, но +5-10 минут после каждой сборки. Можно сократить время ответа от тестировщика и вероятность получить стандартный баг будет намного ниже.

    ReplyDelete
    Replies
    1. Чтобы смоделировать эту ситуацию нужно узнать еще несколько деталей. Например, какая степень покрытия и как наличие автоматических тестов влияют на запуск мануальных. Чтобы не заморачиваться исследованием и моделированием этой зависимости я предлагаю смоделировать следующий подход:
      - Автоматические тесты запускаются каждый раз после комита
      - Известна степень покрытия автотестов
      - Мануальные тесты не запускаем вообще (на самом деле это довольно жесткое ограничение, но предположим, что их запускают редко и это дает нам право вынести их из нашей модели)
      В результате сама модель не изменится, мы просто заменим ручную проверку автоматической с заранее известной степенью покрытия. Затем можно расчитать вероятности переоткрытия (R) и внесения новых багов (I). Для этого в модель внесем значение степени покрытия тестами. Вот картинка, которая у меня получилась для 80% покрытия: ( http://bit.ly/10piczV ). Вероятности остальных переменных я оставил неизмененными.
      Получили вероятности возникновения R (12% что бага переоткроется и 88% что нет) и I (24% что внесем новые изменения, 76% - все будет ок), после чего можно посчитать матожидание для 4-х состояний (см. п. "Строим дерево"). Это и будет величина стоимости исправления ошибки в случае, если всегда запускать автотесты, у которых покрытие 80%.
      Надеюсь не слишком запутано объяснил, если что непонятно - спрашивай :)

      Delete