Если нельзя, но очень хочется, то нужно обязательно и ничего в мире не стоит того, чтобы делать из этого проблему!


Интересна Java? Кликай по ссылке и изучай!
Если тебе полезно что-то из того, чем я делюсь в своем блоге - можешь поделиться своими деньгами со мной.
с пожеланием
столько времени читатели провели на блоге - 
сейчас онлайн - 

среда, 24 апреля 2013 г.

TDD что он может?

На прошлом Bomberman Cdenjoy всплыла задачка makeBricks из codingbat.com. Вопрос был в том, как бы я решал эту задачу ТДД. Ну, начал бы с самого простого кейса, а там по ситуации. Лучший способ демонстрации - парное программирование, потому мы с Виталиком сели в пару за комп и начали. Первый наш код получился таким.
public class Main {
    @Test
    public void test1() {
        assertBricks(true, 0, 1, 5);
        assertBricks(true, 1, 0, 1);
        assertBricks(false, 1, 0, 2);
    }

    public boolean makeBricks(int small, int big, int goal) {
        return small + big * 5 == goal;
    }

    private void assertBricks(boolean expected, int c1, int c5, int l) {
        boolean actual = makeBricks(c1, c5, l);
        assertEquals(String.format("C1 = %s, C5 = %s, L = %s, Actual = %s, But expected = True", c1, c5, l, actual, expected),
                expected, actual);
    }
}
Но уже после 4го теста (ассерта) мы пришли к тому, что начали путаться... И я предложил выйти в тестах на более высокий уровень абстракции. Код преобразился и стал понятнее описывать доменную область.
public class Main {
    private static final int HHHHH = 5;
    private static final int H = 1;
    public static final boolean NO = false;
    public static final boolean YES = true;

    @Test
    public void test1() {
        YES(5, HHHHH);
        YES(1, H);
        NO(2, H);
    }

    public boolean makeBricks(int small, int big, int goal) {
        return small + big * 5 >= goal;
    }


    private void YES(int l, int... bricks) {
        assertBricks(YES, l, bricks);
    }

    private void NO(int l, int... bricks) {
        assertBricks(NO, l, bricks);
    }

    private void assertBricks(boolean expected, int l, int... bricks) {
        int c1 = 0;
        int c5 = 0;
        for (int i : bricks) {
            if (i == H) {
                c1++;
            }
            if (i == HHHHH) {
                c5++;
            }
        }
        assertL(expected, c1, c5, l);
    }

    private void assertBricks(boolean expected, int c1, int c5, int l) {
        boolean actual = makeBricks(c1, c5, l);
        assertEquals(String.format("C1 = %s, C5 = %s, L = %s, Actual = %s, But expected = True", c1, c5, l, actual, expected),
                expected, actual);
    }
}
Работа пошла быстрее! В следующие пару минут мы написали еще пачку тестов
    @Test
    public void test1() {
        YES(5, HHHHH);
        YES(1, H);
        NO(2, H);
        YES(7, HHHHH, H, H, H);
        YES(14, HHHHH, HHHHH, H, H, H, H);
        NO(14, HHHHH, HHHHH,  H, H, H);
        YES(5, HHHHH, HHHHH);
        YES(6, HHHHH, H, H, H);
        NO(9, HHHHH, H, H, H);
    }
Реализация совсем немного изменилась, чтобы соответствовать тестам.
    public boolean makeBricks(int small, int big, int goal) {
        return small + big * 5 >= goal;
    }
И тут началось самое интересное. Мы больше не могли добавить ни одного красного теста. И так и сяк. Зависли. Но в то же время http://codingbat.com/prob/p183562 сказал нам, что мы забыли как минимум один кейс.
    @Test
    public void test2() {
        NO(9, HHHHH, HHHHH, H, H, H); 
    }
И тут возник заслуженный вопрос. Какова роль ТДД тут? Почему он не помогает решить эту задачу - найти еще один красный кейс? Ответ был таким - ТДД помогает убедиться в том что, что все кейсы которые ты придумал работают (более вероятно, чем если бы ты писал без тестов). ТДД помагает в поиске новых кейсов только тем, что сразу же сигнализирует о твоих ошибках, что со временем приводит к осознанию, что "я ошибаюсь часто", "я что-то недопроверил" - а этот скепсис по направлению к своему коду, помогает задуматься над поиском дополнительных кейсов. А вот как их искать, нам подсказал уже Лёня. Комментарий Виталика был такой (и я с ним полностью согласен)
Цікаво шо ці всі викрутаси можна почати тільки від одного "чувстуетсья что что-то не то", в тому випадку у нас були тести які явно показали, якшо їх нема, то може або провтикатись, або "почувствоваться", це з досвідом приходить, професійна подозрітєльность на такі штуки. Да і рішення в тих варіантах які запропоновано в основному інтуітивні, або направлені на то шоб допомогти інтуіції прийти до червоного кейса.
Только вот хотелось бы какой-то более конкретный математический аппарат для поиска красных кейсов. Уверен, что опытные тестировщики уладеют такими инструментами. На этом пока все. Пока буду искать ответ. Как появится - опишу его тут.... Спасибо Леня, Виталик! P.S. Вот та причина, почему я люблю блог. Описал в нем проблему, отложил ее в сторону и мозг твой сгенерил решение. Мой мне напомнил про то, что есть такая чудная библиотека, которая называется Approvals (кстати в прошлом месяце вышла новая версия). Что я с ней сделал? Ну кроме того, что скачал ее отсюда. А потом прописал maven dependency
<dependency>
 <groupId>net.sourceforge.approvaltests</groupId>
 <artifactId>approvaltests</artifactId>
 <version>0.1.3</version>
 <scope>system</scope>
 <systemPath>${project.basedir}/lib/ApprovalTests-0.1.3/ApprovalTests.jar</systemPath>
</dependency>
Я написал вот такой вот несложный тест
import org.approvaltests.legacycode.LegacyApprovals;
import org.approvaltests.legacycode.Range;
...
public class Main {
...
    @Test
    public void testApprovals() throws Exception {
        LegacyApprovals.LockDown(this, "approveMakeBricks", Range.get(0, 10), Range.get(0, 10), Range.get(0, 100));
    }

    public boolean approveMakeBricks(Integer small, Integer big, Integer goal) {
        return makeBricks(small, big, goal);
    }
Запустив его я увидел что возвращает метод во всех комбинациях входных параметров. Я стал критиковать эти результаты, и уже на 133 строчке (через минуту беглого просмотра) я первую увидел неточность.
[0, 0, 0] = true 
[1, 0, 0] = true 
[2, 0, 0] = true 
[3, 0, 0] = true 
[4, 0, 0] = true 
[5, 0, 0] = true 
[6, 0, 0] = true 
...
[6, 10, 0] = true 
[7, 10, 0] = true 
[8, 10, 0] = true 
[9, 10, 0] = true 
[10, 10, 0] = true // Все что выше всегда тру, это и ежу понятно - длину 0 мы составим из элементов любого набора
[0, 0, 1] = false // тут ок потому, как материала не хватает 
[1, 0, 1] = true // все что ниже тоже ок, потому как имея хоть одну H мы сможем составить длину 1 всегда 
[2, 0, 1] = true 
[3, 0, 1] = true 
[4, 0, 1] = true 
[5, 0, 1] = true 
[6, 0, 1] = true 
[7, 0, 1] = true 
[8, 0, 1] = true 
[9, 0, 1] = true 
[10, 0, 1] = true 
[0, 1, 1] = true // а вот тут интересно! Мой алгоритм считает, что можно взять HHHHH и c нее составить L=1, что не ок. 
[1, 1, 1] = true 
...
Вот вам и красный тест
    @Test
    public void test2() {
        NO(1, HHHHH); 
    }
Главная фишка аппрувалса - пусть каждый делает то, что ему свойственно: комп пусть считает комбинации, а человек пусть оценивает правильность результата. Решилось это как-то так
    public boolean makeBricks(int small, int big, int goal) {
        if (goal == 0) {
            return true;
        }
        if (small == 0) {
            return goal % 5 == 0 && goal / 5 <= big;
        }
        return small + big * 5 >= goal;
    }
И главное теперь тест написанный на аппрувалсе тоже стал говорить!
 Поехали дальше...
...
[0, 0, 2] = false // не хватает материала
[1, 0, 2] = false // не хватает материала
[2, 0, 2] = true  // тут и дальше все хватает
[3, 0, 2] = true 
[4, 0, 2] = true 
[5, 0, 2] = true 
[6, 0, 2] = true 
[7, 0, 2] = true 
[8, 0, 2] = true 
[9, 0, 2] = true 
[10, 0, 2] = true 
[0, 1, 2] = false // снова не хватает материала
[1, 1, 2] = true  // а тут интересно! Опять пытаемся поделить пятерку чтобы сделать из нее что-то... Прошлый фикс не совершенен!
[2, 1, 2] = true 
[3, 1, 2] = true 
[4, 1, 2] = true  
...
Итого красный тест таков. Я сразу добавил однотипные кейсы, потому что чувствую мощь!
    @Test
    public void test3() {
        NO(1, HHHHH);
        NO(2, HHHHH, H);
        NO(3, HHHHH, H, H);
        NO(4, HHHHH, H, H, H);
    }
Получилось как-то так
    public boolean makeBricks(int small, int big, int goal) {
        if (goal == 0) {
            return true;
        }
        return (goal - small) / 5 <= big && (goal - small) % 5 == 0 || (goal - big*5) <= small && goal >= big*5;
    }
Не спрашивай как я пришел к этому. Просто как-то уивделось такое решение. Кроме того оно не оптимальное, так как я поломал старое (то что засек approvals но не засекли мои тестики) потому я решил добавить в свой набор тестов этот кейс.
    @Test
    public void test4() {
        YES(1, HHHHH, H, H);
    }
Этот тест проверяет, что у меня и больших и маленьких в избытке.
    public boolean makeBricks(int small, int big, int goal) {
        if (goal == 0) {
            return true;
        }
        return (goal - small) / 5 <= big && (goal - small) % 5 == 0 
                || (goal - big*5) <= small && goal >= big*5 
                || goal < small; // фикс простой как двери
    }
А как на счет этого теста?
    YES(6, HHHHH, HHHHH, H, H);
Потом мне приспичило в туалет. А возвращался я уже с этой идеей
    public boolean makeBricks(int small, int big, int goal) {
        return goal / 5 <= big && goal % 5 <= small;
    }
И как я до этого сразу не додумался? Но это еще не все. Просмотр таблицы результатов остановила меня возле теста
    YES(11, HHHHH, H, H, H, H, H, H);
Блин! Так все идеально было :) Решение вот
    public boolean makeBricks(int small, int big, int goal) {
        return goal / 5 <= (big + small / 5) && goal % 5 <= small % 5;
    }
Но тест (судя по изменениям - я уже не так просматриваю таблицу, как слежу за diff)
 Напишем такой тестик
    YES(1, H, H, H, H, H);
Нате! Родил
    public boolean makeBricks(int small, int big, int goal) {
        return goal / 5 <= (big + small / 5) && goal % 5 <= ((((goal / 5 - big - small / 5) == 0) && (small / 5 != 0)) ? (small % 5) : small);
    }
Ну и задачка :) Я че сюда полез, потому что мне сказали что ее можно решить без циклов :) Я не нашел проблем в approvals тесте - обратился к codingbat.com и там тоже получил аппрув
 Теперь осталось заапрувить все в аппрувалс тесте и провести рефакторинг....
      public boolean makeBricks(int small, int big, int goal) {
        int needBig = goal / 5;
        int needSmall = goal % 5;
        int smallAsBig = small / 5;
        int smallWithoutBigGroups = small % 5;
        int stillNeedBig = needBig - big - smallAsBig;

        boolean usedSmallAsBig = stillNeedBig == 0 && smallAsBig != 0;

        boolean enoughSmall = needSmall <= (usedSmallAsBig ? smallWithoutBigGroups : small);
        boolean enoughBig = stillNeedBig <= 0;

        return enoughBig && enoughSmall;
    }
Как-то так...

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

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

2 комментария:

  1. У меня получилось как-то так

    return (small >= (goal % 5) && small >= goal - big * 5 );

    ОтветитьУдалить