Немного о тестировании программ для МК

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

С тем, что такое тестирование ПО в общем можно ознакомится для начала тут:
ru.wikipedia.org/wiki/Тестирование_программного_обеспечения

Зачем нужно тестирование.

В первую очередь тестирование нужно для того, чтобы удостовериться в работоспособности и соответствии определённым требованиям программы в целом и/или ее отдельных частей. Конечно, наличие тестов, само по себе не может гарантировать полное отсутствие ошибок в разрабатываемой системе, однако тесты существенно повышают вероятность раннего обнаружения ошибок, сокращают время их поиска. Как известно ошибка во время компиляции не стоит ничего, обнаруженная во время первичного тестирования — почти ничего (время разработчика/тестировщика), обнаруженная во время альфа/бета тестирования — стоит мало, а вот критическая ошибка найденная после серийного выпуска продукта может быть очень дорога.
Таким образом основную задачу тестирования (не только ПО) можно сформулировать так: как можно более раннее обнаружение ошибок, чтобы сэкономить деньги и время.

Особенности тестирования программ для МК

Чем отличается тестирование программ для МК от тестирование прикладного или серверного ПО для ПК?
На ПК все дорожки уже давно проторены (ну основные точно): окружения относительно стандартны — Windows/Unix, в каждом стеке технологий разработки есть свои библиотеки для поддержки тестирования, свои методы работы сними и т.д.
Для МК всё проще и сложнее одновременно. Проще потому, что сложность программ для МК, даже для толстых ARM-ов, на много меньше, чем для ПК. Сложнее, — потому, что окружение в котором работает программа может быть очень разнообразным. Программа должна работать со встроенной периферией МК и с различными внешними устройствами. Работа с большим разнообразием железа на низком уровне в купе с жесткими требованиями по быстродействию делает тестирование непростой задачей.

Ручное и автоматизированное тестирование.

Часто ПО МК тестируют вручную методом «тестового прогона». Скомпилированную прошивку загружают в целевую систему, или её макет, запускают и с помощью какого либо отладочного интерфейса/вывода (USART, светодиоды, осцилограф, JTAG и т.д.) наблюдают за ее работой. Также вручную можно имитировать внешние воздействия и менять по ходу выполнения различные параметры. Это получается ручное интеграционное/системное тестирование. Однако если программа не работает или работает не правильно, понять в чём причина бывает очень сложно. Отлаживать программу целиком по шагам очень долго и не продуктивно. Сюда-же можно отнести испытание на универсальных симуляторах типа Proteus-а и ему подобных, где симулируется не только МК с внутренней периферией, но и внешняя обвязка вцелом.

В большинстве случаев программа для МК выполняет много различных функций, которые можно выделить более-менее независимо от других, такие как вывод на дисплей, опрос клавиатуры, управление двигателем и т.д. Многие разработчики проектируют такие функции отдельно в специальным тестовых проектах, а потом копируют готовый отлаженный код в целевой проект. Это своего рода «ручное модульное тестирование».
Потом этот «отлаженный» код переносится в следующий проект, но поскольку он скорее всего не совсем соответствует новой задаче, то он модифицируется на месте под новые требования. А ведь любое даже самое аккуратное изменение может быть чревато ошибками. В идеале после каждого существенного изменения код должен быть протестирован заново. Вручную это делать крайне трудоёмко и многие неоправданно отказываются от частого модульного тестирования в пользу системного опять-же ручного тестирования.
На помощь приходит автоматизация большинство тестов как модульных, так и системных можно автоматизировать теми или иными способами. Для платформо-независимого кода, различных контейнеров, типа списков, очередей, деревьев и т.д., высокоуровневых протоколов обмена, конечных автоматов и др. необязательно выполнять тесты на целевом процессоре, их можно и очень удобно оформлять в виде тестовых программ для ПК. Этот процесс легко автоматизируется, запуск одного скрипта или программы позволяет быстро проверить такие модули на наличие ошибок.
С платформо-зависимым кодом всё несколько сложнее. Его естественно нужно тестировать в целевом окружении. Можно, конечно, проводить тестирование непосредственно на целевой платформе с необходимым внешним обвесом и отладочным оборудованием, но автоматизировать этот процесс гораздо тяжелее, чем для платформо-независимых модульных тестов. Этот подход очень тяжеловесен и лучше подходит для комплексного системного тестирования, чем для модульных тестов. Как вариант, можно попытаться использовать универсальные симуляторы (тот-же Proteus), но они монстроподобны и автоматизируются очень плохо.
К счастью для многих популярных семейств МК существуют специализированные программные симуляторы, которые симулируют ядро процессора, внутреннюю периферию и некоторые внешние устройства. Один такой симулятор для МК семейства AVR под названием «Simulavr» рассмотрим дальше.
Далее будем говорить преимущественно об автоматизированных тестах.

Тестируемость кода

Пока по этой статье всё выглядит так как будто всеобъемлющее автоматизированное тестирование — это какая-то серебреная пуля, позволяющая отловить, если не все, то почти все дефекты в разрабатываемой программе. В реальной жизни это далеко не так. Во первых если просто наклепать каких-никаких тестов для уже написанной программы, то это не даст ровным счетом ничего. Такие тесты будут просто показывать, что программа работает именно так, как она работает и будут пригодны только для проверки, что новые изменения не сломали старую функциональность (уже полезно, впринципе). Во-вторых далеко не всякий код можно и нужно тестировать независимо от всей остальной системы. Например, драйвера различных специфичных внешних устройств, которые нельзя симулировать, очевидно можно тестировать только на реальном оборудовании. Проще говоря для того чтобы протестировать фрагмент кода (модуль) нужно в тестовом окружении воспроизвести все его исходящие зависимости. Каждая вызываемая внешняя (по отношению к тестируемому коду) функция, каждая глобальная переменная является исходящей зависимостью. Здесь можно ввести понятие «шва». Шов — это код (исходящая зависимость), поведение которого можно изменить, не изменяя сам код.

volatile int value;
...
void Foo()
{
	// сделать какие-то нетривиальные вычисления
	int bar = (value * 10)  << 1024;
	// преобразовать результат в строку 
char  buffer[9];
itoa(bar, buffer, 10);
// вывести его на LCD 
lcd_home();
lcd_puts(buffer);
}
…
// где-то используем эту функцию
Foo();

Здесь у функции Foo есть четыре зависимости и все они не являются швами: глобальная переменная value и функции itoa, lcd_home, lcd_puts. В таком виде эта функция практически нетестируема, поскольку сложно гарантировать определённое значение глобальной переменной — оно может меняться в прерывании или другом потоке. То есть нужно тестировать и весь код использующий value. Также сложно проконтролировать результат, поскольку функция вывода lcd_puts жестко задана. Попробуем сделать некоторые зависимости в этом примере швами:

int value;
…
void Foo(int arg, void (*print_func)(char *) )
{
// сделать какие-то нетривиальные вычисления
	int bar = (arg * 10)  << 10;
// преобразовать результат в строку 
char  buffer[9];
itoa(bar, buffer, 10);
// вывести результат куда-то
print_func(buffer);
}
…
// вывод строки на LCD
void lcd_print(char *str)
{
lcd_home();
lcd_puts(str);
}
…
// где-то используем эту функцию
Foo(value, lcd_print);

Теперь у нас только одна зависимость — стандартная функция itoa, она нам не мешает. Чтобы протестировать нашу функцию теперь достаточно написать замену для lcd_print, которая будет проверять переданный аргумент:

void test_print(char *str)
{	
	const char *expected = "1234";
	if(strcmp(expected, str) != 0)
	{
		fprintf(stderr, "Foo test failed. Expected %s, got %s", expected, str);
		exit(1);
	}
}
void testFoo()
{
	Foo(126362);
	fprintf(stderr, "Foo test is OK");
}

Этот тест не зависит от платформы и его можно успешно запускать на ПК.
Из этого примера видно, что чем меньше у фрагмента кода исходящих зависимостей не являющихся швами, тем проще и эффективней его тестировать. Отсюда вывод: программы изначально нужно разрабатывать с учётом потребности в тестировании.
Однако, как видно из этого примера, тестируемость не всегда даётся бесплатно — во втором фрагменте на один вызов функции больше, причем это вызов по указателю.

Практический пример 1.

Задача: написать автоматизированный тест для примера описанного здесь:
we.easyelectronics.ru/Soft/kolcevoy-bufer-na-s-dlya-mk.html
Итак, у нас есть класс инкапсулирующий USART, он использует два кольцевых буфера и аппаратный модуль USART МК. С кольцевым буфером всё понятно, он не привязан к платформе и для него легко написать модульные тесты — это нас сейчас не интересует. Гораздо полезнее и интереснее написать тест проверяющий, что класс Usart корректно инициализируется и передаёт и принимает данные.
AVR симулятор Simulavr
Simulavr — это программный симулятор для МК семейств Tity и Mega AVR.
www.nongnu.org/simulavr/
Он эмулирует ядро AVR всю внутреннюю периферию, внешние выводы и некоторые внешние устройства, такие как клавиатуры, LCD дисплеи и что-то еще. Вообще Simulavr это скорее фреймворк для построения тестов и собственных специализированных симуляторов, чем готовый к использованию универсальный продукт. Для этого у него предусмотрен интерфейс для языков Tcl и Python. Собранную версию для ОС Windows можно найти в инсталляции WinAvr. Однако, там находится один единственный монолитный исполняемый файл, никаких подключаемых библиотек нет, и соответственно нет интерфейса для Tcl и Python. Но это-же OpenSorce (мать… мать… мать… — привычно откликнулось эхо), мы сами соберём, что нам нужно.
Надо сказать, что собрать Simulavr штатными средствами у меня не получилось (ни под cygwin, ни под mingw), так-же как и Tcl и Python интерфейсы. Зато удалось собрать dll, содержащую всю необходимую функциональность. Так, что тесты будем писать на С++.
Также нужно отметить, что Simulavr поддерживает достаточно ограниченное количество МК, в последней версии список такой:
at90canbase
at4433
at8515
atmega8
atmega16/32
atmega128
atmega668
atmega1284
attiny2313

Приступим. Библиотека Simulavr-а собрана с помощью Mingw, работать будем с этим пакетом. В качестве IDE выбран CodeBlocks.
Для начала модифицируем исходную программу, чтобы она стала тестом выполняемым с помощью Simulavr-а:

#include <avr/io.h>
#include <avr/interrupt.h>
#include "usart.h"
#include <util/delay.h>

// специальные переопределённые порты для тестовых и отладочных целей
#define special_output_port (*((volatile char *)0x24))
#define special_input_port  (*((volatile char *)0x25))
#define special_abort_port  (*((volatile char *)0x22))
#define special_exitcode_port  (*((volatile char *)0x020))

// коды завершения симуляции
typedef enum
{ 
	ExitSuccess = 0,
	ExitFailure = 1
} ExitStatus;

// принудительное завершение работы программы с данным кодом
void Exit(ExitStatus status)
{
	special_exitcode_port = status;
	special_abort_port = 1;
}

// вывод строки для отладки
void DebugPuts(const char *str) 
{
  const char *c;

  for(c = str; *c; c++)
    special_output_port = *c;
}

// вывод одного символа для отладки
static inline void DebugPutch(char c) 
{
	special_output_port = c;
}

// класс обработчика ошибок для USART
class MyErrHandler
{
public:
	static void RxOwerflow()
	{
		DebugPuts("\nRx buffer overflow\n");
		Exit(ExitFailure);
	}
	static void FramingError()
	{
		DebugPuts("\nFramingError\n");
		Exit(ExitFailure);
	}
	static void DataOverRun()
	{
		DebugPuts("\nDataOverRun\n");
		Exit(ExitFailure);
	}
	static void ParityError()
	{
		DebugPuts("\nParityError\n");
		Exit(ExitFailure);
	}
};
 
// 8 bytes tx fifo buffer, 
// 16 bytes rx fifo buffer
// interrupt driven USART
typedef Usart<16, 16, Usart0Regs, MyErrHandler> usart;

ISR(USART_UDRE_vect)
{	
	usart::TxHandler();
}

ISR(USART_RXC_vect)
{
	usart::RxHandler();
}

int main()
{
	// выводим приветствие
	DebugPuts("Running AVR buffered USART test\n\n");

	usart::Init<125000>();
	uint8_t c;
	sei();

	while(1)
	{
		if(usart::Getch(c))
		{
			// печатаем в отладочный вывод каждый принятый символ
			DebugPuts("Recived: ");
			DebugPutch(c);
			DebugPuts("\n");
			// инвертируем биты в принятом символе и отправляем обратно
			usart::Putch(~c);
		}
	}
}

В Simulavr-е есть возможность переопределить несколько портов для отладочных нужд, например:
special_output_port — символ, записанный в этот порт будет записан в указанный файл или выведен в консоль;
special_input_port — при чтении из этого порта символ будет прочитан из указанного файла или введен с клавиатуры
special_abort_port — запись в этот порт приводит к завершению симуляции;
special_exitcode_port — код возврата для симуляции, значение отличное от нуля означает завершение симуляции с ошибкой.
Такой подход очень удобен и позволяет делать отладочный ввод-вывод не расходуя на него аппаратных интерфейсов такких как USART. Приведённый пример предназначен для atmega16, к сожалению у неё нет ни единого свободного адреса в пространстве портов ввода-вывода, поэтому в качестве специальных отладочных портов пришлось переопределить регистры не используемой в этом примере периферии. Надо внимательно следить чтоб, переопределюнные регистры не использовались втестовой программе по прямому назначению.
Теперь используя библиотеку Simulavr-а напишем хост, который и будет выполнять наш тест. Это будет консольное приложение.

// стандартные заголовки
#include <iostream>
#include <string>

// заголовки Simulavr
#include "global.h"
#include "flash.h"
#include "avrdevice.h"
#include "avrfactory.h"
#include "systemclock.h"
#include "traceval.h"
#include "string2.h"
#include "helper.h"
#include "specialmem.h"
#include "irqsystem.h"

// классы имитирующие USART передатчик и приёмник из примеров к Simulavr
#include "serialtx.h"
#include "serialrx.h"

using namespace std;

int main(int argc, char *argv[])
{
// имя нашего МК
    const char * deviceName = "atmega16";
// адреса специальных отладочных регистров
// должны совпадать с теми, что писали в прошивке МК
    const unsigned writeToPipeOffset = 0x24;
    const unsigned readFromPipeOffset = 0x25;
    const unsigned writeToAbort = 0x22;
    const unsigned writeToExit = 0x20;
// частота, на которой работает МК, должна совпадать с F_CPU  в прошивке
    const unsigned fcpu = 4000000; // 4 MHz
// проверяем параметры командной строки
    if(argc < 2)
    {
        cerr << "You should pass *.elf file name as first argument" << endl;
        return 1;
    }
// считаем, что первый аргумент - это имя elf файла с нашей прошивкой МК
    const char * testFileName = argv[1];
// Создаем наше устройство.
// Кстати, в одной симуляции может быть больше одного устройства
    AvrDevice *dev1 = AvrFactory::instance().makeDevice(deviceName);

// Последовательные передатчик и приёмник
    SerialTxBuffered tx;
    SerialRxBuffered rx;

// устанавливаем скорости приёма/передачи
    tx.SetBaudRate(125000);
    rx.SetBaudRate(125000);

// цепи для подключения USART
    Net txNet;
    Net rxNet;

// подключаем к цепи вывод <b>tx</b> тестового передатчика
    txNet.Add(tx.GetPin("tx"));

// к этой-же цепи подключаем вывод <b>rx</b> mega16, который находится на ножке D0.
    txNet.Add(dev1->GetPin("D0"));

// подключаем к цепи вывод <b>rx</b> тестового приёмника
    rxNet.Add(rx.GetPin("rx"));

// подключаем к цепи вывод <b>rx</b> mega16 , который находится на ножке D1.
    rxNet.Add(dev1->GetPin("D1"));

// переопределяем отладочные регистры
    dev1->ReplaceIoRegister(readFromPipeOffset,
                            new RWReadFromFile(dev1, "FREAD", "-"));

    dev1->ReplaceIoRegister(writeToPipeOffset,
                            new RWWriteToFile(dev1, "FWRITE", "-"));

    dev1->ReplaceIoRegister(writeToAbort, new RWAbort(dev1, "ABORT"));
    dev1->ReplaceIoRegister(writeToExit, new RWExit(dev1, "EXIT"));

// загружаем прошивку устройства из elf файла
    dev1->Load(testFileName);
// устанавливаем частоту генератора устройства
// а точнее период в наносекундах
    dev1->SetClockFreq(1000000000ll / fcpu); // time base is 1ns!
// подключаем устройство к общей системе тактирования
    SystemClock::Instance().Add(dev1);
// запускаем симуляцию на 10 милли секунд
// чтобы контроллер инициализировался и выполнился стартовый код
    SystemClock::Instance().RunTimeRange(1000*1000*10); // тут время в наносекундах

// записываем в буфер тестового передатчика сообщение
// передавать он начнет как только мы снова запустим симуляцию
    string message = "Hello";
    typedef string::iterator iter;
    for(iter i = message.begin(); i != message.end(); ++i)
        tx.Send(*i);

// запускаем симуляцию на 100 милли секунд
// за это время МК должен успеть получить посылку
// и отправить ее обратно, инвертировав все биты
    SystemClock::Instance().RunTimeRange(1000*100000);
// проверяем, что у нас в буфере приемника
// там должно быть столько-же байт, сколько мы отправили
    if(rx.Size() != message.size())
    {
// ошибка - получили не стольклько байт, сколько отправили
        cerr    << "Transmission is truncated, " << message.size() << "bytes was send, and "
                << rx.Size() << "bytes was recived." << endl;
        return 1;
    }
// проверяем, что нам пришло наше сообщение с инвертированными битами.
    for(iter i = message.begin(); i != message.end(); ++i)
    {
        char c = rx.Get();
        if(c != ~*i)
        {
	// что-то не совпало
            cerr    << "Incorrect data, expected " << ios::hex << (int)~*i
                    << ", got " << (int)c << endl;
            return 1;
        }
    }
// все совпало - тест пройден.
    cout << "Test passed." << endl;
    return 0;
}


Симулавр — очень мощьная база для построения тестов, на нем удобно сестировать то, что для тестирования в «железе» требует наличия достаточно сложной отладочной инфраструктуры. Он позволяет делать симуляции с несколькими МК и отлаживать межпроцессорное взаимодействие с применением любых доступных протоколов. Поддерживаиет создание различного виртуального оборудования и графического пользовательского интерфейса(если удастся скомпилировать модули для Tcl и Python под Windows, под Linux всё впорядке)

Исходники к примеру тут [2.7 Мб] dl.dropbox.com/u/20372814/SimulavrTests.zip
Для его работы должны быть установлены WinAvr и MinGw. К их исполняемым должны быть добавлены в переменную окружения Path (или вручную подкорректировать Makefile-ы ).

Пример 2.

Теперь разберём тест для моей библиотеки для работы с портами ввода-вывода.
easyelectronics.ru/rabota-s-portami-vvoda-vyvoda-mikrokontrollerov-na-si.html
Автоматизированный тест для неё просто необходим, так как в ней используются довольно сложные механизмы оптимизации для вывода значений в порты. Ручное тестирование для этого практически не пригодно, потому, что очень сложно и утомительно проверять все осмысленные комбинации линий ввода вывода в списках — их многие десятки. Структура библиотеки такова, что ее отдельные компоненты очень слабо связаны между собой — зависимости в основном передаются через шаблонные параметры. Предполагается лишь, что переданные таким образом зависимости имеют определённую структуру, реализуют определенный интерфейс. Например, все что реализует интерфейс порта ввода-вывода может быть использовано в качестве ввода-вывода, и в нем можно объявить отдельные линии ввода вывода (TPin) и создать список линий (PinList). Это расширяет возможности библиотеки — позволяет единообразно использовать в качестве портов ввода-вывода различные расширители (например, сдвиговые регистры), облегчает портирование на новые аппаратные платформы — достаточно только написать относительно простой класс, реализующий интерфейс портов ввода-вывода, а также позволяет легко протестировать правильность функционирования библиотеки.
Для этого написан класс реализующий интерфейс порта ввода-вывода и хранящий три статических поля служащих «регистрами ввода-вывода», значения которых легко проверить:

template<class DataType, unsigned Identity>
class TestPort :public TestPortBase
{
public:
	typedef DataType DataT;
	typedef TestPortBase Base;
	
	static void SetConfiguration(DataT mask, Configuration configuration)
	{
		if(configuration)
			DirReg |= mask;
		else
			DirReg &= ~mask;
	}
...
	static void Write(DataT value)
	{
		OutReg = value;
	}
	static void ClearAndSet(DataT clearMask, DataT value)
	{
		OutReg &= ~clearMask;
		OutReg |= value;
	}
	static DataT Read()
	{
		return OutReg;
	}
...
	enum{Id = Identity};
	enum{Width=sizeof(DataT)*8};
// статические пременные, служащие регистрами ввода-вывода,
// по образу и поддобию портов в AVR
	volatile static DataType OutReg;
	volatile static DataType DirReg;
	volatile static DataType InReg;
};


Консольное приложение содержащее тесты:

#include <iostream>
#include <string>
#include "iopins.h"
#include "pinlist.h"
#include <stdlib.h>

using namespace std;
using namespace IO;
using namespace IO::Test;

// обяъвляем два тестовых порта
typedef TestPort<unsigned, 'A'> Porta;
typedef TestPort<unsigned, 'B'> Portb;

// объявляем линии ввода-вывода в тестовых портах
// чтобы они были доступны по коротким именам: Pa0, Pb2 и т.д.
DECLARE_PORT_PINS(Porta, Pa)
DECLARE_PORT_PINS(Portb, Pb)

// макрос проверяющий на равенство два значения и 
// если они не равны прерывающий программу с сообщением об ошибке
// и месте, где она произошла
#define ASSERT_EQUAL(value, expected) if((value) != (expected)){\
    std::cout << "\nAssertion failed! "  << "\n\tFile: " << __FILE__ << std::endl << "\tfunction: " << __FUNCTION__ << "\n\tline: " << __LINE__ << std::endl;\
    std::cout << std::hex << "\tExpacted: 0x" << (unsigned)(expected) << "\tgot: 0x" << (unsigned)(value);\
    exit(1);\
    }

// класс печатает список линий переданный в качестве параметра Pins
template<class Pins>
struct PrintPinList
{
    template<class List, int index>
    struct Iterator
    {
        static void Print()
        {
            Iterator<List, index-1>::Print();
            typedef typename List:: template Pin<index-1> CurrentPin;
            if(index == List::Length)
                std::cout << (char)CurrentPin::Port::Id << CurrentPin::Number;
            else
                std::cout << (char)CurrentPin::Port::Id << CurrentPin::Number << ", ";
        }
    };
    template<class List>
    struct Iterator<List, 0>
    {
        static void Print()
        {}
    };
    static void Print()
    {
        std::cout << "PinList<";
        Iterator<Pins, Pins::Length>::Print();
        std::cout << ">";
    }
};

// тест для списка линий, где все линии принадлежат одному порту
// listValue - значение записываемое в список линий
// portValue - значение, которое ожидается запишется в порт
// если в порт записывается другое значение - значит где-то ошибка
template<class Pins>
void TestOnePortPinList(unsigned listValue, unsigned portValue)
{
// получаем порт к которму принадлежат линиии из списка
    typedef typename Pins::template Pin<0>::Port Port;
    typename Pins::DataType val; // буферная переменная
// печатаем имя функции - теста
    cout << __FUNCTION__ << "\t";
// печатаем состав списка линий
    PrintPinList<Pins>::Print();

    Port::Write(0);
// далее тестируем все функции и проверяем, 
// что в порт записалось ожидаемое значение
    Pins::Write(listValue);
    ASSERT_EQUAL(Port::OutReg,  portValue);
    val = Pins::Read();
    ASSERT_EQUAL(val, listValue);

    Port::DirReg = 0;
    Pins::SetConfiguration(Pins::Out, listValue);
    ASSERT_EQUAL(Port::DirReg, portValue);

    Port::Write(0);
    Port::DirReg = 0;

    Port::InReg = portValue;
    val = Pins::PinRead();
    ASSERT_EQUAL(val, listValue);

    Port::InReg = 0;
    val = Pins::PinRead();
    ASSERT_EQUAL(val, 0);

    Pins::Write(0);
    ASSERT_EQUAL(Port::OutReg, 0);

    Pins::Set(listValue);
    ASSERT_EQUAL(Port::OutReg, portValue);

    Pins::Clear(listValue);
    ASSERT_EQUAL(Port::OutReg, 0);

    Pins::SetConfiguration(Pins::In, 0xff);
    ASSERT_EQUAL(Port::DirReg, 0);
// Тест пройден, для данного списка линий библиотека работает корректно
    cout << "\tOK" << endl;
}
...
// еще другие тесты

int main()
{
// пишем последовательные значения в последовательные списки
    for(int i=0; i< 16; i++)
    {
        cout << "Writing value: " << i << endl;
        TestOnePortPinList<PinList<Pa0, Pa1, Pa2, Pa3> >(i, i);
        TestOnePortPinList<PinList<Pa1, Pa2, Pa3, Pa4> >(i, i << 1);
        TestOnePortPinList<PinList<Pa2, Pa3, Pa4, Pa5> >(i, i << 2);
    }
// тесты для непоследовательных списков
    TestOnePortPinList<PinList<Pa1, Pa3, Pa2, Pa0> >(0x0f, 0x0f);
    TestOnePortPinList<PinList<Pa0, Pa2, Pa1, Pa3> >(0x0f, 0x0f);
    TestOnePortPinList<PinList<Pa2, Pa1, Pa3, Pa4, Pa6> >(0x1f, 0x5e);
...
// ещё много тестов для различных случаев
    return 0;
}

Внося какие-то изменения в библиотеку легко проверить, что все работает правильно и никакая функциональность не сломалась — достаточно запустить тест.
Исходники примера: dl.dropbox.com/u/20372814/PinListTests.zip
Среда разработки CodeBlocks, компилятор gcc из пакета MinGw.

Литература к размышлению

Bart Broekman and Edwin Notenboom: Testing Embedded Software.
Книжка довольно редкая и я потратил достаточно много времени, чтоб скачать ее полную версию, поэтому выкладываю ссылку на Dropbox-е:
dl.dropbox.com/u/20372814/Testing%20Embedded%20Software.pdf
Рекомендую ознакомиться.

  • +6
  • 30 марта 2011, 19:54
  • neiver

Комментарии (9)

RSS свернуть / развернуть
Респект за книгу!
0
  • avatar
  • Puff
  • 30 марта 2011, 21:53
… а что вы скажите по поводу «Crossworks IDE», я ею не пользовался но из чего я понял главная её изюминка как раз в тщательной отладке кода,… или вы с этой средой не имели дело
0
Ничего не скажу, я с ней не работал.
0
Тема интересная и очень полезная. Только все же описание какое-то разбросанное. Как-то очень мутно определены понятия «зависимость» и «шов» (последнее только вводится, но не применяется).

Первый пример в параграфе «Тестируемость кода» также недоописан. Во-первых, пример совсем оторванный и в то же время перенапичкан функционалом; во-вторых, идет жонглирование числами несоответствующих типов, а в-третьих, в последнем вызове фунции Foo отсутсвует второй параметр (указатель на функцию).

А отсюда два вопроса:
1. Насчет полноты теста. Тестировать функцию на одном значении аргумента — это не тестирование. Как, пускай даже на этом примере, обеспечить полноту теста? Или хотя бы протестировать на 100 разных значениях?
2. Что делать после того как функция оттестирована? Я имею ввиду как избавляться от аргумента в виде указателя на функцию?
0
Спасибо обстоятельный комментарий.
Согласен, статья получилась несколько сумбурной. В очередной раз не получилось объять необъятное :)
1. тестировать 100 значений в большинстве случаев не имеет смысла, если функция линейна, как в примере — для полноты достаточно граничных случаев и одно два значения в середине диапазона значений агрумента. Полнота и достаточность тестирования — это очень большая тема.
2. От аргумента в виде указателя в данном случае не нужно избавляться, функцию очевидно надо использовать. А если волнуют вопросы производительности и вызов по указателю слишком дорог, можно (если писать на С++) передавать внешнюю вынкцию как параметр шаблона. В этом случае никаких линих накладных расходов на вызов не будет.
0
Спасибо.

1. Функция линейна? Уберите операцию сдвига из своего выражения, и все станет намного сложнее:

int bar = value * 10;

При входных значениях value >= 3277 и value<-3278 результат будет совсем не такой, какой ожидается. Так что, хоть полный тест проводить здесь и бессмысленно (аргументов может быть и несколько, никакого машинного времени не хватит на проверку всех ОДЗ), но тесты по граничным значениям необходимы, хот бы так: 0, 1, -1, 32767, -32768.

Вот вопрос и был в том, как их проверять по описанной схеме. Тем более она не понятна вот еще чем: почему в качестве параметра не передается указатель на функцию-пустышку? Ведь нагляднее, когда проверки правильности/неправильности делаются в одном месте (т.е. внутри test_foo).

Собственно, с линейными все понятно и просто. Хотя во многих случаях более правильно будет доказательство их правильности, а не тестирование. А как быть с нелинейными? 5 if'ов и 1 switch на 3 варианта+дефаулт — и придется уже делать 128 тестов.

2. Производительность здесь не на первом месте. Больше беспокоит именно указатель на функцию. Даже если отложиться в сторону вопросы небезопасности такого кода (все-таки указатель — это данные, которые могут быть попорчены неправильной работой программы), остается вопрос наглядности и переносимости кода. А если все вызываемые функции (зависимости) не сконцентрированы в одном месте, а разбросаны в перемежку с вычислениями? Надо будет передавать несколько указателей на несколько функций. При тестировании это еще куда ни шло, но при работе вызывать функцию, а в параметрах ей передавать целый веер указателей — на мой взгляд очень неудобно.
0
P.S. Я не придираюсь. Самому часто приходится заниматься тестированием, но процесс происходит вручную и иногда очень неудобен (часто даже код с простой математикой приходится тестировать на целевой платформе из-за того, что все компиляторы по-своему понимают такие вещи как знаковость типов, размерность, преобразование типов и т.п.). Хотелось бы понять механизм автоматического тестирования.
0
1. Полное тестирование всех возможных комбинаций параметров или тестирование всех сочетаний условий в функции в большинстве случаев невозможно, да и не нужно. Если есть 5 if-ов и switch на 3 варианта+дефаулт обычно достаточно столько тестов, чтоб просто покрыть все пути кода, их будет не больше 8 в данном случае, а то и меньше.
2. Если данные испорчены неправильной работой программы, то чем быстрее она упадёт, тем лучше. Что лучше если программа тихо неправильно пережевывает неправильные данные и выдает неправильный результат, или переходит по неправильному указалелю и зависает? Ничего не лучше. Но второй случай будет, вероятно, быстрее обнаружен.
А если надо передавать несколько зависимостей, то они упаковываются в структуру, аля интерфейс:
struct SomeInterface
{
void (*func1)(arg_t arg);
result_t (*func2)(arg2_t arg);
...
void (*funcN)();
};
...
void Foo(SomeInterface * funcs, something else)
{
funcs->func1(...);
funcs->func2(...);
...
funcs->funcN(...);
}

А в С++ всё еще проще есть динамический и статический полиморфизм — виртуальные функции и шаблоны.
0
1. Как Вы получили 8? Чтобы пройти все пути полностью, нужно 128 проходов.
2. Лучше ни то и не другое. Лучше разделять этапы проектирования/тестирования/внедрения, или, где так сделать не удается, обнаруживать сбой, не позволяя ни работать с неправильными данными, ни тем более зависать неизвестно где. А надеяться на то, что вероятно сбой будет обнаружен уже после последствий, — это не инженерный подход.

Со структурой — понятно, это первое, что приходит на ум для реализации такого подхода. У меня просто сомнения относительно его правильности. Где можно почитать именно об этой методике? 1. Как Вы получили 8? Чтобы пройти все пути полностью, нужно 128 проходов.
2. Лучше ни то и не другое. Лучше разделять этапы проектирования/тестирования/внедрения, или, где так сделать не удается, обнаруживать сбой, не позволяя ни работать с неправильными данными, ни тем более зависать неизвестно где. А надеяться на то, что вероятно сбой будет обнаружен уже после последствий, — это не инженерный подход.

Со структурой — понятно, это первое, что приходит на ум для реализации такого метода. У меня просто сомнения относительно самого подхода. Где можно почитать именно об этой методике? Кто ее автор?
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.