2009-08-13 8 views
7

実行コードの決定論と透過性は、私が取り組んでいる組み込みプログラミングのタイプにおいて非常に重要です。私が透明性を意味するのは、例えば、メモリの任意のセクションを見てそこにどの変数が格納されているかを知ることができるということです。したがって、組み込みプログラマが期待しているように、可能な限り新しいものは避け、回避できない場合は初期化に限定します。組み込みプログラミングでのグローバル変数の回避

私はこれの必要性を理解していますが、私の同僚がこれをやり遂げたやり方に同意せず、より良い選択肢もわかりません。

私たちが持っているのは、いくつかのグローバル構造体配列といくつかのグローバルクラスです。ミューテックスの構造体の配列は、セマフォー用とメッセージキュー用の2つです(これらはメインで初期化されています)。実行されるスレッドごとに、それを所有するクラスはグローバル変数です。

私がこれをもっている最大の問題は単体テストです。クラスをテストしたいときにモックオブジェクトを挿入するにはどうすればいいですか?#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

私はそれがレガシーコードではないほど幸いだ。だから私は今すぐ物事をしたい。私はすでにIOCを念頭に置いていましたが、例があると私はそれが可能であると信じました。このようにして、プロダクションコードはグローバル変数を使用できるため、メモリ内のどこにあるかを予測できますが、テストコードにはbar.cppを含めることはできません。私はまだ確信しています。あなたが提案したやり方で、私は.hファイルを変更する必要はなく、ユニットテストのプロジェクトに異なる.cppファイルを含めるだけです。 –

+1

+1。 DIPとIOCは、特にモックオブジェクトを使ったテストのための良いアーキテクチャパターンです。関数シグネチャを変更できない場合は、間接的に行うことができます。コンテキストオブジェクトを渡す代わりに、それを返す関数を呼び出すことができます。この関数は、IOCを使用して、実際のコンテキストオブジェクトを返したり、その初期化に応じてオブジェクトを模擬することができます。 – neuro

+1

@drhorrible、本の名前は少し誤解を招くかもしれません;-)彼のレガシーコードの定義はユニットテストのないコードです。これは主に、異なるハードシナリオでの単体テスト技術の集まりです。 – iain

-1

あなたはmalloc関数を使用してメモリを割り当て、そのように、あなたが望むようにそれを割り当て、すべてのメモリををmallocすることができ、その位置

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(); 

でオブジェクトを作成するための新しい演算子を得ることができます。注意:削除を呼び出すときに手動で分解を呼び出すことを忘れないでください。

+0

-1、mallocはこれらの環境では新しいものと同じように禁止されています。これにより、痛みがなくなります。 – MSalters

+0

開始時に1ブロックのメモリをmallocすることは禁止されていますか?愚かなルールと思われる。 – Lodle

3

あなたの状況に依存性注入が役立つでしょうか?これにより、すべてのグローバル変数が取り除かれ、単体テストの依存関係を簡単に置き換えることができます。

各スレッドのメイン関数は、依存関係(ドライバ、メールボックスなど)を含むマップに渡され、(グローバル変数にアクセスする代わりに)それらを使用するクラスに格納されます。

環境(ターゲット、シミュレータ、ユニットテスト...)ごとに、必要なすべてのオブジェクト、ドライバ、およびすべてのスレッドを作成する1つの「構成」機能を作成し、スレッドに依存関係のリストを提供します。たとえば、ターゲット構成で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 
} 
関連する問題