2009-08-13 2 views
7

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

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

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

Самая большая проблема, с которой я столкнулся, заключается в модульном тестировании. Как я могу вставить макет-объект, когда класс, я хочу проверить глобальные переменные #include, которых у меня нет?

Вот ситуация в псевдокоде:

foo.h

#include "Task.h" 
class Foo : Task { 
public: 
    Foo(int n); 
    ~Foo(); 
    doStuff(); 
private: 
    // copy and assignment operators here 
} 

bar.h

#include <pthread.h> 
#include "Task.h" 

enum threadIndex { THREAD1 THREAD2 NUM_THREADS }; 
struct tThreadConfig { 
    char  *name, 
    Task  *taskptr, 
    pthread_t threadId, 
    ... 
}; 
void startTasks(); 

bar.cpp

#include "Foo.h" 

Foo foo1(42); 
Foo foo2(1337); 
Task task(7331); 

tThreadConfig threadConfig[NUM_THREADS] = { 
    { "Foo 1", &foo1, 0, ... }, 
    { "Foo 2", &foo2, 0, ... }, 
    { "Task", &task, 0, ... } 
}; 

void FSW_taskStart() { 
    for (int i = 0; i < NUMBER_OF_TASKS; i++) { 
     threadConfig[i].taskptr->createThread(); 
    } 
} 

Что делать, если я хочу больше или меньше задач? Разный набор аргументов в конструкторе foo1? Я думаю, что мне придется иметь отдельный bar.h и bar.cpp, который кажется намного больше работы, чем необходимо.

+0

Я предполагаю, что вы имеете в виду '& foo1', а не '% foo1' (оператор модуля)? – DaveR

+0

Спасибо. Это то, что я получаю для переписывания вместо копирования/вставки. –

ответ

4

Если вы хотите сначала протестировать такой код, я бы рекомендовал прочитать Working Effectively With Legacy Code Также см. this.

В основном использование компоновщика для вставки ложных/поддельных объектов и функций должно быть в последнюю очередь, но по-прежнему совершенно справедливо.

Однако вы также можете использовать инверсию управления без рамки, что может нанести некоторую ответственность на клиентский код. Но это действительно помогает тестированию. Например, чтобы проверить FSW_taskStart()

tThreadConfig threadConfig[NUM_THREADS] = { 
    { "Foo 1", %foo1, 0, ... }, 
    { "Foo 2", %foo2, 0, ... }, 
    { "Task", %task, 0, ... } 
}; 

void FSW_taskStart(tThreadConfig configs[], size_t len) { 
    for (int i = 0; i < len; i++) { 
     configs[i].taskptr->createThread(); 
    } 
} 

void FSW_taskStart() { 
    FSW_taskStart(tThreadConfig, NUM_THREADS); 
} 

void testFSW_taskStart() { 
    MockTask foo1, foo2, foo3; 
    tThreadConfig mocks[3] = { 
      { "Foo 1", &foo1, 0, ... }, 
      { "Foo 2", &foo2, 0, ... }, 
      { "Task", &foo3, 0, ... } 
     }; 
    FSW_taskStart(mocks, 3); 
    assert(foo1.started); 
    assert(foo2.started); 
    assert(foo3.started); 
} 

Теперь вы можете можете можете передать макет версию вы будете резьб на «FSW_taskStart», чтобы убедиться, что функция не фактически начать нити по мере необходимости. К сожалению, вы должны полагаться на то, что исходный FSW_taskStart передает правильные аргументы, но теперь вы тестируете намного больше своего кода.

+0

Ну, мне повезло, что это еще не устаревший код, поэтому я хочу получить все прямо сейчас. У меня уже был МОК, но, увидев пример, я убедился, что это возможно. Таким образом, производственный код может использовать глобальные переменные, поэтому вы можете предсказать, где они будут в памяти, но тестовый код может просто не включать bar.cpp. Я все еще не уверен. Как вы предложили, мне не нужно будет менять какие-либо файлы .h, просто включите в файл unit test различные файлы .cpp? –

+1

+1. DIP и IOC - хороший образец архитектуры, особенно для ... тестирования с макетным объектом. Если вы не можете изменить свою подпись функций, вы можете сделать с некоторой косвенностью. Вместо передачи объекта контекста вы можете вызвать функцию, которая возвращает ее. Эта функция может использовать IOC и возвращать реальные объекты контекста или макет объекта в зависимости от его инициализации ... – neuro

+1

@drhorrible, название книги может быть немного обманчиво ;-) Его определение устаревшего кода - это код без модульного теста. Это в основном сборник методик для модульного тестирования в разных сложных сценариях. – iain

-1

Можно выделить память с помощью таНос, а затем получить новый оператор, чтобы сделать объект в этой позиции

void* mem = malloc(3*sizeof(SomeClass)); 
SomeClass *a = new(mem) SomeClass(); 
mem += sizeof(SomeClass); 
SomeClass *b = new(mem) SomeClass(); 
mem += sizeof(SomeClass); 
SomeClass *c = new(mem) SomeClass(); 

так что вы можете таНос вся память, то выделить его, как вы хотите. Примечание: убедитесь, что вы вызываете деконструкцию вручную, как обычно, при вызове delete.

+0

-1, malloc так же запрещен как новый в этих средах. Это добавляет боль без выгоды. – MSalters

+0

Так mallocing один блок памяти в начале запрещен? Кажется, это глупое правило. – Lodle

3

Помогла ли вам помощь в зависимости от ситуации?Это может избавиться от всех глобальных переменных и обеспечить легкую замену зависимостей в ваших модульных тестах.

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

Для каждой среды (целевой, симулятор, модульный тест ...) вы создаете одну «конфигурационную» функцию, которая создает все необходимые объекты, драйверы и все потоки, предоставляя потокам их список зависимостей. Например, целевая конфигурация может создать USB-драйвер и вставить его в какой-либо поток сообщений, в то время как конфигурация тестового блока может создать драйвер USB-заглушки, который контролирует тесты.

Если вам нужна эта «прозрачность» для важной переменной, создайте для них классы, которые будут содержать их по известному адресу, и при необходимости вводят эти классы.

Это немного больше, чем статические списки объектов, но гибкость фантастична, особенно когда вы сталкиваетесь с некоторыми сложными проблемами интеграции и хотите обменять компоненты для тестирования.

Грубо:

// Config specific to one target. 
void configure_for_target_blah(System_config& cfg) 
{ // create drivers 
    cfg.drivers.push_back("USB", new USB_driver(...)) 
    // create threads 
    Thread_cfg t; 
    t.main = comms_main; // main function for that thread 
    t.drivers += "USB"; // List of driver names to pass as dependencies 
    cfg.threads += t; 
} 

// Main function for the comms thread. 
void comms_main(Thread_config& cfg) 
{ 
    USB_driver* usb = cfg.get_driver("USB"); 
    // check for null, then store it and use it... 
} 

// Same main for all configs. 
int main() 
{ 
    System_config& cfg; 
    configure_for_target_blah(cfg); 
    //for each cfg.drivers 
    // initialise driver 
    //for each cfg.threads 
    // create_thread with the given main, and pass a Thread_config with dependencies 
}