Юнит-тестируем бинарный поиск с использованием Cobertura

Продолжаю просмотр лекций курса «Introduction to Algorithms» MIT: http://videolectures.net/mit6046jf05_demaine_lec03/

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

С нуля, впрочем, даже такой казалось бы простой, алгоритм писать смысла нет. Для начала надо поискать готовое решение. Готовое решение находится здесь: http://ru.wikipedia.org/wiki/Двоичный поиск.

Портирую код в Java. Получаю такой код:

public  class  BinarySearch<T> {

    public static class Result {
        public final boolean success;
        public final int index;

        public Result(boolean success, int index) {
            this.success = success;
            this.index = index;
        }
    }

    public static <T> Result search(T[] objects, int first, int last, T x, Comparator<? super T> comparator) {

        if(objects.length < last) {
            last = objects.length;
        }
        if (last == 0) {
            // not found, if you need to insert the element then in position 0
            return new Result(false, 0);
        }

        int cmp = comparator.compare(objects[first], x);
        if (cmp > 0) {
            // not found, if you need to insert the element then in first position
            return new Result(false, first);
        } else if(cmp == 0) {
            return new Result(true, first);
        }

        cmp = comparator.compare(objects[last - 1],x);

        if (cmp < 0) {
            // not found, if you need to insert element then in last position
            return new Result(false, last);
        } else if (cmp == 0) {
            return new Result(true, last-1);
        }

        while (first < last) {
            int mid = first + (last - first) / 2;

            cmp = comparator.compare(x, objects[mid]);
            if (cmp < 0) {
                last = mid;
            } else if (cmp == 0) {
                return new Result(true, mid);
            } else {
                first = mid + 1;
            }
        }

        if (comparator.compare(objects[last], x) == 0) {
            return new Result(true, last);
        } else {
            // Not found, if you need to insert the element then it's place - last
            return new Result(false, last);
        }
    }
}

Метод search в качестве параметров принимает массив объектов, диапозон индексов, внутри которого нужно делать поиск (в случае, если поиск осуществляется не по всему, а по части массива). Четвертый параметр — искомый объект. Пятый — класс Comparator, реализующий сравнение двух объектов. Параметров слишком много, их порядок не слишком удобно запоминать. Поэтому делаю дополнительные методы:

   T[] objects;
    Comparator<? super T> comparator;
    private int first;
    private int last;

    public BinarySearch<T> in(T[] objects) {
        this.objects = objects;
        this.first = 0;
        this.last = objects.length;
        return this;
    }

    public BinarySearch<T> range(int first, int size) {
        this.first = first;
        this.last = size;
        return this;
    }

    public BinarySearch<T> comparator(Comparator<? super T> comparator) {
        this.comparator = comparator;
        return this;
    }

    public Result search(T x) {
        return search(objects, first, last, x, comparator);
    }

Эти методы позволяют улучшить читаемость кода. Ниже юнит-тесты:

    @Test
    public void testSuccessSearch() {
        Integer[] a = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 };

        BinarySearch<Integer> search = new BinarySearch<Integer>()
            .in(a).comparator(new Comparator<Integer>() {
                @Override
                public int compare(Integer o1, Integer o2) {
                    return o1 - o2;
                }
            });

        for(int i=0; i<a.length; i++) {
            BinarySearch.Result result = search.search(i);
            assertTrue(result.success);
            assertEquals(result.index, i);
        }
    }

    @Test
    public void testSuccessSearchOddArraySize() {
        Integer[] a = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 };

        BinarySearch<Integer> search = new BinarySearch<Integer>()
            .in(a).comparator(new Comparator<Integer>() {
                @Override
                public int compare(Integer o1, Integer o2) {
                    return o1 - o2;
                }
            });

        for(int i=0; i<a.length; i++) {
            BinarySearch.Result result = search.search(i);
            assertTrue(result.success);
            assertEquals(result.index, i);
        }
    }

    @Test
    public void testForFails() {
        Integer[] a = { 0, 1, 2, 3, 4, 5, 6, 7, 10, 11, 12, 13 };
        BinarySearch<Integer> search = new BinarySearch<Integer>()
            .in(a).comparator(new Comparator<Integer>() {
                @Override
                public int compare(Integer o1, Integer o2) {
                    return o1 - o2;
                }
            });

        BinarySearch.Result result = search.search(-1);
        assertTrue(!result.success);
        assertEquals(result.index, 0);

        result = search.search(8);
        assertTrue(!result.success);
        assertEquals(result.index, 8);

        result = search.search(9);
        assertTrue(!result.success);
        assertEquals(result.index, 8);

        result = search.search(100500);
        assertTrue(!result.success);
        assertEquals(result.index, a.length);

    }

Юнит-тесты позволяют проверить отсутствие ошибок. А также, что интересно, они позволяют оптимизировать слегка код. Для этого подключаю к проекту Cobertura:
добавляю в pom.xml секцию build:

    <build>
        <plugins>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>cobertura-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <id>site</id>
                        <phase>pre-site</phase>
                        <goals>
                            <goal>cobertura</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

и добавляю в конец pom.xml секцию reporting:

    <reporting>
        <plugins>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>cobertura-maven-plugin</artifactId>
                <version>2.5.2</version>
            </plugin>
        </plugins>
    </reporting>

Создаю отчет, стартовав maven командой: maven clean site

Получаю отчет и в каталоге target/site получаю отчет. Нахожу в нем покрытие моего класса тестами. Нахожу вот что.

binary-search-cobertura

Зеленым цветом помечены протестированные строки. Цифрочки указывают количество раз, которое данная строка выполнилась. Красным показываются строки, в которые код из-за выполнения тех или иных условий не попал. Что тут видно. Видно, что я не протестировал что мой код вернет в случае пустого массива, не протестировал что вернется в случае если диапозон поиска будет указан за границами массива и, наконец, нашел, что условие в 86 строчке всегда возвращает false. Поэтому условие можно выбросить за ненадобностью.

Выводы…

Во-первых, Cobertura — замечательное средство для анализа кода.
Во-вторых, юнит-тестирование полезная вещь.
В третьих, не факт, что код из wiki: http://ru.wikipedia.org/wiki/Двоичный поиск будет работать. Хотя идея в нем в целом показана прекрасно, и замечания тоже весьма полезные. Однако свой код, написанный на основе кода из упомянутой статьи все равно стоит «вдоль и поперек» протестировать.

Оставьте комментарий