Pull to refresh

Comments 54

Как-то тема не особо раскрыта. Из статьи я так и не понял, почему программисты не любят юнит-тесты, и что с этим делать. Писать тесты на основе поведения и открытого апи? И все?
И вот это про какие именно тесты говорится?
«Проваленные» тесты совсем не означают, что какая-то функциональность не работает

В общем про костыльные?
Как-то тема не особо раскрыта.
Согласен, но мне показалось, что делать пост еще длиннее уже не прилично.
почему программисты не любят юнит-тесты
потому, что далеко не всегда умеют их писать, и как следствие — не видят в них смысла (просто из личного опыта, никого не хочу обидеть)
и что с этим делать
Учиться, учиться, еще раз учиться.
Писать тесты на основе поведения и открытого апи? И все?
Поверьте, это не мало.

«Проваленные» тесты совсем не означают, что какая-то функциональность не работает
Это вот о чем. Если тесты основаны на подробном знании внутренностей модуля, то банальное переименование list_head в listHead «провалит» нам все тесты. Но модуль как был работоспособным, так и остался.
Работал в разных конторах и ещё нигде за мою карьеру мне не приходилось писать тесты.
Ну это нормально. Бизнес, и все такое. Клиенту ведь не тесты нужны, а продукт. Если у Вас получается делать его хорошо без тестов, то проблемы то и нет.
Я — не очень внимательный человек, по своей природе. И тесты часто меня страхуют от ошибок. Я без них уже не могу работать. К хорошему быстро привыкаешь :)
По-моему, это не вопрос симпатии разработчика, а вопрос официально закреплённой методики.
Не повезло, в хреновых конторах работали. Я вот вспоминаю предыдущие места работы и понимаю, что бОльшую часть бесконечной карусели «Аа, у нас опять ХХХ сломалось» можно было бы решить именно тестами.
Как это обычно бывает
Полезность и необходимость юнит-тестов демонстрируется на синтетических примерах в вакууме, вроде коллекций и калькуляторов.
Принято.
Разработку какой системы на микроконтроллере Вы хотели бы видеть с тестами в рамках одной статьи?
Я бы хотел видеть тестирование погодной станции следящей за показателями температуры, влажности, давления и содержания CO2 в жилых помещениях и являющейся частью системы умного дома(связь, допустим, по 2.4GHz каналу) с ведением истории.
Признаться, в данном случае меня больше интересует не столько тестирование, сколько тестирование именно систем на микроконтроллерах(у самого недавно встал такой вопрос, и внятных решений пока так и не смог найти).
Я думаю, мы оба понимаем, что Вы хотите видеть полноценный проект. Разработку ПО от начала и до конца плюс тесты.
Это во-первых не реально уместить в одну статью, во-вторых, я пока не готов проделать такой объем работы ради плюсиков или минусиков в Карму :)

Но помочь обещаю. Постараюсь объединить пару периферийных модулей в некое рабочее приложение и оформить в виде статьи. С тестами разумеется.

А пока рекомендую установить ceedling. После установки Вы сможете подробно изучить пример, который идет в комплекте с этим инструментом (ceedling examples). Если не ошибаюсь, там какой-то измеритель температуры (прощу прощения за неточную информацию, давно туда не смотрел).
А было бы действительно круто увидеть пример разработки хотя бы одной настоящей функции «через тесты».
Кстати именно на таких примерах в вакууме я никак не мог въехать. А как только на реальных багах коллега садился со мной и мы вместе писали тесты, так сразу и пошло.
Просто TDD многие продвигают, а по факту ими можно адекватно покрыть только мизерную часть функционала. Это как раз контейнеры, и model в MVC. И то с model не всегда, ибо она бывает часто меняется в процессе развития проекта, и тесты замахаешься поддерживать.
Это не так. И данная статья именно про это.
За «вакуум» прощу прощения. Думал, что пример связанного списка будет всем понятен как академический. Но выстрел оказался мимо. Теперь ясно о чем еще следует написать.
В наших продуктах покрыто больше 70% юнит- и интеграционными тестами. А если посчитать еще UI тесты, то и 80-90 будет. Что мы делаем не так?
Что мы делаем не так?
Ммм, смешиваете в одну кучу интеграционные тесты и TDD?
Ну хорошо, на юнит тестах около 50% покрыто.
Ну я же не говорю, что TDD прям вообще бесполезен. TDD он для очень узкого круга задач. Если ваши продукты — базы данных, то там можно и больше 50% тестов покрыть. А если ваши продукты — фронт-энды сайтов, то тут TDD уже не шибко поможет.
Я из ваших слов не понял, что вы говорите о фронтендах сайтов. А для продуктов, где бОльшая часть кода — бизнес-логика, фраза «по факту ими можно адекватно покрыть только мизерную часть функционала» звучит очень странно, потому я и прокомментировал :).
Простите ради Бога.
Я не очень понял. В вопросе сесть сарказм? Или же Вы серьезно спрашиваете «Что мы делаем не так»?
Если сарказма нет, то мне не ясно, что Вас беспокоит? Почему Вы решили, что что-то делаете не так?

Какой-то сеанс психотерапии получается, к сожалению :(
А если писать сначала тесты, а потом код. Это поможет?
К сожалению нет. Или далеко не всегда.

Э-э, я так и не понял почему «к сожалению нет. Или не всегда»? То, что лично вы время от времени нарушаете основополагающий принцип TDD, не может быть подтверждением правоты этого высказывания… Без принципа test-first это может быть и unit-testing, но уже по любому не TDD.
Я как бы и не говорю, что TDD — это наше все. Здесь мой выпад во вполне конкретную сторону.
Есть пред-история. Написание тестов, что называется «по-коду» не дает понимания преимуществ TDD. И если это не «переключить» в голове, то первая строчка теста написанного до написания кода будет

extern void * list_head;

Ну а дальше уже по известной схеме (сам так начинал). И тут уже не важно, тестами вперед или, в…
Выводы странные. Что значит «правильно писать тесты»?

Если вы придерживаетесь test-first, то они никак не могут быть неправильными. В этом случае неправильный unit-test будет означать неправильную постановку задачи…
Если честно, то хуже реализацию списков придумать сложно…
Если честно, то хуже реализацию списков придумать сложно…

Первое, что приходит в голову: список храним в непрерывном массиве, при каждом добавлении или удалении элемента перезахватываем массив (увеличив или уменьшив длину на 1) и копируем в него нужные элементы. По-моему, получится хуже, чем в статье. И никаких сложностей.
Да это-то то, как раз нормально. Два простых вопроса:
1) Что делать, если понадобится второй аналогичный список?
2) Что делать, если понадобится сохранить в списке значение -1?

Все таки паттерны с использованием функций eof()/eol() не спроста возникли. Да и функции с побочными эффектами меняющие глобальные переменные это зло. В эмбеддед иногда себе такое позволяется, при дефиците производительности и памяти. Но в общем случае тоже не приветсвуется…
Если понадобится второй список, то разработчику достаточно добавить в функции параметр node_t *&list_head. Для совместимости со старыми функциями можно оставить и реализации без параметров, работающие с дефолтным списком. Это можно сделать добавлением шести строчек и изменением ещё трёх — не такое уж большое изменение.

Про -1 — да, спорное решение. Но какие альтернативы?
— три функции вместо двух. Да, функция на проверку «очередь пуста» нужна, но никто не гарантирует, что пользователь будет ей строго пользоваться, и никогда не попытается взять элемент из пустой очереди (особенно в нынешнем многопоточном мире). Так что, какое-то значение для пустой очереди вернуть надо, почему бы не -1? Ниоткуда не следует, что оно будет использоваться именно как индикатор отсутствия элементов. Возможно, в конкретном проекте это значение — команда «ничего не делать».
— Exception при попытке взять элемент. Не знаю, как остальные, но я считаю, что исключения допустимы только когда возникла действительно нештатная ситуация. А в остальных случаях должна быть возможность написать программу так, чтобы ни одного исключения при нормальном выполнении не было. Иначе теряется возможность отладки «до первого исключения». А в данном примере — не исключено, что пользовательский код уже использует обращения к пустой очереди. Просто потому, что так удобнее.
— Функция с двумя результатами (аналог TryParse или scanf). В общем, неплохой вариант, но получать результат отдельным вызовом по ссылке не всегда удобно.

Что мне не очень понравилось — что при добавлении элемента приходится просматривать список от начала. Квадратичное время вместо линейного. Нужен второй указатель — на последний элемент, но код с ним станет сложнее.
Именно три функции вместо двух… Это типовое решение. Никакой необходимости изобретать велосипед нет.
Вы ни как не сможете заставить программиста использовать правильно ваше api.
Это типовое решение. Никакой необходимости изобретать велосипед нет.

В условиях ограниченных ресурсов и конкретного проекта, в котором будет использоваться эта очередь, такая необходимость очень даже может возникнуть. Но, конечно, в этих ситуациях решения, содержащие malloc на каждый вызов, не будут рассматриваться даже в виде предварительных вариантов. Скорее уж мысль пойдёт в сторону кольцевого буфера, лежащего по фиксированному адресу, с использованием аппаратной модулярной арифметики, или другой подобной экзотики.
eof() вообще практически ничего не добавил бы. Он бы тупо инлайнился в коде… По стоимости это ничем не отличается от сравнения результата двух int'ов, который пришлось бы делать в варианте без eof()…
Хороший должен быть компилятор, чтобы заинлайнить вызов функции, описанной в другом файле. Если он, к тому же, соптимизирует два последовательных обращения к ячейке list_head, тогда действительно проигрыша не будет.
достаточно добавить в функции параметр node_t *&list_head.

Вот именно. И тогда все эти костыли для тестирования становятся не нужны…
Перехват malloc/free всё равно нужен. Кстати, он понадобится и в боевом проекте: для объектов фиксированного размера лучше использовать свой alloc, построенный, опять же, на односвязных списках. Если, конечно, нет информации, что в библиотеке такая ситуация тоже предусмотрена, и захват 8 байтов там почти всегда выполняется за считанные такты.
0_о а перехват зачем? всё что вам нужно — это запихнуть число и вернуть его обратно… Утечки памяти, IMHO, лучше отслеживать нагрузочным тестированием, а не юнит-тестами…
Уже не в первый раз читаю статьи про написание тестов для микроконтроллеров. Однако мне нигде не попадалась информация, о том, как проверять код, работающий с периферией. Как имитировать работу таймеров, прерываний, DMA?
У нас в проекте, мы тестировали работу таймера достаточно просто. Включаешь таймер на какое то время устанавливаешь обработчик и крутишься проверяя какую нибудь переменную. В обработчике изменяешь эту переменную и теле цикла должна сработать проверка. Цикл делается какое то заведомо большее количество итераций чем установленное время и если переменная не изменилась тест провален.
По поводу DMA думаю можно придумать такой же принцип.

Думаю тестировать аппаратуру на контроллере не очень актуально, обычно она работает, хотя… Мы это делали для разработчиков ПЛИС которые как раз часто сталкиваются с подобными проблемами.
Часто достаточно заглянуть в errata, чтобы понять насколько она «работает». Иногда волосы дыбом встают)
Полностью согласен с коллегой
Тщательно проектировать API и мокать всю периферию.
В этой фразе нет ни одного лишнего слова.
Для этого используют JTAG и отладчики… Это уже к программированию по скольку по скольку относится…

Легче всего начать использовать C++ и не мучиться со всякими штуками, вроде link-time substitution. Тем более, что все современные компиляторы его поддерживают. К примеру, если обернуть список в класс и использовать параметр шаблона в качестве аллокатора, то тестирование упрощается во много раз, без какого-либо ущерба для производительности.

Список
struct NewAlloc
{
    static void* malloc(int size)
    {
        return ::malloc(size);
    }

    static void free(void* item)
    {
        ::free(item);
    }
};

template <typename Alloc = NewAlloc>
class LList
{
    typedef struct node
    {
        int val = 0;
        struct node* next = nullptr;
    } node_t;

    node_t* list_head = nullptr;

public:
    void list_push(int val)
    {
        node_t* current = list_head;
        if (list_head == nullptr) {
            list_head = (node_t*)Alloc::malloc(sizeof(node_t));
            list_head->val = val;
            list_head->next = NULL;
        }
        else {
            while (current->next != NULL) {
                current = current->next;
            }
            current->next = (node_t*)Alloc::malloc(sizeof(node_t));
            current->next->val = val;
            current->next->next = NULL;
        }
    }

    int list_pop(void)
    {
        if (list_head == nullptr) {
            return -1;
        }

        node_t* next_node = list_head->next;
        int retval = list_head->val;
        Alloc::free(list_head);
        list_head = next_node;
        return retval;
    }
};


Тесты на утечку памяти
struct CountingAlloc
{
    static int mallocCounter;
    static int freeCounter;

    static void reset()
    {
        mallocCounter = 0;
        freeCounter = 0;
    }

    static void* malloc(int size)
    {
        mallocCounter ++;
        return ::malloc(size);
    }

    static void free(void* item)
    {
        freeCounter --;
        ::free(item);
    }
};

int CountingAlloc::mallocCounter = 0;
int CountingAlloc::freeCounter = 0;

namespace UnitTest1
{
    TEST_CLASS(UnitTest1)
    {
    public:

        TEST_METHOD(test_push)
        {
            CountingAlloc::reset();
            LList<CountingAlloc> l;
            l.list_push(1);
            Assert::AreEqual(1, CountingAlloc::mallocCounter);
            Assert::AreEqual(0, CountingAlloc::freeCounter);
        }

        TEST_METHOD(test_pop)
        {
            CountingAlloc::reset();
            LList<CountingAlloc> l;
            l.list_push(1);
            l.list_pop();
            Assert::AreEqual(1, CountingAlloc::mallocCounter);
            Assert::AreEqual(1, CountingAlloc::freeCounter);
        }
    };
}

Кстати, сразу стало видно, что тесты в статье нарушают один из принципов TDD: тесты должны быть повторяемы. То есть, при изменении порядка запуска тестов, к примеру, результат не должен меняться.
Проблемы при изменении порядка запуска тестов никак не относятся к свойству повторяемости, они относятся к свойству независимости.
Да, Вы правы. Есть грешок.
Просто ceedling гарантирует порядок выполнения тестов. Вот я позволил себе слабость :)
Спасибо за версию на C++. Впечатляет.
К сожалению, есть проекты которые не приемлют C++. И это не хорошо и не плохо. Просто такие требования заказчика.
Я вот очень люблю юнит-тесты.
Может я не разработчик? :)
А сделать mock-и на функции стандартной библиотеки не самая простая задача.
man 3 malloc_hook (если GNU-расширения в данном проекте допустимы).
Проще и чище, чем подгонять тестируемый код под тесты.
За способ спасибо. Не знал, что так можно.
Но с утверждением
Проще и чище, чем подгонять тестируемый код под тесты.
позволю себе не согласится. Это только кажется, что код «подгоняется» под тесты. А по-факту, получается очень качественный и легко поддерживаемый код (это я не про #ifdef UNIT_TEST конечно же).
Ещё способ в копилку — man 3 mallinfo.
int before, after;
before = mallinfo().uordblks;

call_functions_you_want_to_test_for_leaks();

after = mallinfo().uordblks;
TEST_ASSERT_EQUAL_INT( before, after );
чтобы не делать #ifdef UNIT_TEST… можно обьединить исходные тексты теста и тестируемого модуля например через #include «module.c» в исходнике теста.
Да… и за этот способ тоже благодарен.
Мне и в голову не приходило. Но возьму на заметку. Иногда коллеги-разработчики не хотят менять код ни на символ, а тестировать надо. Вот и пригодится :)
Sign up to leave a comment.

Articles