LoginSignup
1
2

More than 3 years have passed since last update.

【c++17】constexprで、ユニットテストが出来ないレガシーなコードに、ユニットテストを同居させる

Last updated at Posted at 2021-05-13

概要

 Visual Studio 2019 Professionalでの開発で実施したこと

背景

 大本営であるMicrosoftや各種ミドルウエアを提供している会社は、近年ではレガシーなコードを排除する動きがみられるが、特にハードウエアや計算ライブラリなどは、C/C++で書かれた未だにレガシーなコードでしか提供されていないことがよくあります。

 または、アップデートすら放置されている場合がある。基本的に制御や製品仕様を変更することで、ユーザに対して変更のリスクを負わせる危険性があるので、企業としてはコストも考えたうえで放置せざる得ないという状況もあるが・・・

 放置され続けることで、例えば近年盛んなCIツールやDockerとの連携を活用するチャンスを見逃したり、ハードウエア部分のユニットテストが全く出来なかったり、サポート切れの憂き目にあったりで、色々と問題があります。

 それを、少しでも良い方向にもっていく方法(個人的な案)。

 最近の案件で使用しているもので、Contec社のSMCシリーズという、IOとパルス信号が簡単に出せる製品があるので、それを例にして説明していく。PCIボードを使っているので、BIOSやデバイスマネージャーによる設定が必要。

 なおこの例に漏れず、特に産業製品のソフトウエアで有償のものの場合、次の問題点に示すような仕様のものは割と多いと思うので、適宜読み替えてください。

問題点

CSmc.h
#ifdef __cplusplus
extern"C"{
#endif

//---------------------------
// Initialization Functions
//---------------------------
long WINAPI SmcWInit(char * DeviceName, short *DevId);
long WINAPI SmcWExit(short DevId);
long WINAPI SmcWGetErrorString(long ErrorCode, char *ErrorString);

long WINAPI SmcWSetReady(short DevId, short AxisNo, short MotionType, short StartDir);
long WINAPI SmcWGetReady(short DevId, short AxisNo, short *MotionType, short *StartDir);

... (以下略) ...



#ifdef __cplusplus
}
#endif

 同社の製品はスタティックライブラリを使って関数を参照する仕組みになっていて、そのヘッダファイルを読み込むことで使用することが出来ます。
 返り値にエラーコードを出力、引数で例えば基板の番号だとか、制御したい軸(AxisNo)を与えたりします。

 もちろん、コードとしてC言語が基準に書かれていてレガシーなことも気になる点かもしれないが、それ以上に問題なのは、この関数はPCIに接続された基板の信号を見るという機能しかないので、ハードウエアがなければまともに実行できないという点です。

 例えば、↑のようなソースコードであれば、SmcWInitに失敗して、SmcWSetReadyを読み取っても「Readyの条件を調べる」どころか「初期化出来ていないというエラーコードを返すだけ」であり、実質的に動いていないのと変わらないのです。もちろん、デバイスの接続エラーを確認するのであればそのまま使って構わないのだが、それ以外の動きは接続してみないと分からない、つまりユニットテストやモジュールテストなんてそもそも出来ないと言っているようなもの。

 こういうとき、今までどうしてきたかと言えば、大体、

  • 結合テストでバグがないかを考える

  • ログ結果を分析して原因を考える。

  • 基板があればオシロスコープやデジタルマルチメーターを繋いで接続を確認する

みたいなことをやるのですが、それは実機があることが前提であるというのが難しい点なので、それを回避するためには、部品を予備で多く仕入れておくとか、そういうことしかできない状況です。安い部品であれば問題ないですが、近年コストダウン志向もあるかと思うので、そうそう簡単に複数台の購入を決断してもらうというわけにはいかないと思います。

 しかも、運用後に、お客さんのところで起きているバグの再現なんて難しいですしね。色々と問題があります。

ifdefを使う場合

 ビルドされるコードを切り替えるものは、今までであれば#ifdef~#else~#endifなどがありますが、#ifdefを使うのは実行される/実行されないコードが出てきてしまい、同じ条件で実行されないコードの部分が大きくなってしまうのが良くない点です。

 この場合、ビルド条件を設定する度に、プリプロセッサを使う運用ですが、#if~#elif~#else間で特殊化するコードが増えれば増えるほど、テストが難しくなります。

image.png

constexprを使う

constexprに関する解説記事は、こちらの方が詳しいと思うのでどうぞ・・・。

簡単に言えば、C++17より新たに実装された「ビルドされるコードを意図的に分けるための構文」です(したがって、Visual Studio 2017以降での採用が前提となります)。

今回の場合は、Facade(窓口役)を用意して、次のように実装しました。

CSmcFacade.h
class CSmcFacade {
public:
#if !defined(EMULATION)
    constexpr static bool _IS_EMULATION_ = false;
#else
    constexpr static bool _IS_EMULATION_ = true;
#endif

public:
    virtual long WINAPI SmcWInit(char* DeviceName, short* DevId);
    virtual long WINAPI SmcWExit(short DevId);
    virtual long WINAPI SmcWGetErrorString(long ErrorCode, char *ErrorString);

    virtual long WINAPI SmcWSetReady(short DevId, short AxisNo, short MotionType, short StartDir);
    virtual long WINAPI SmcWGetReady(short DevId, short AxisNo, short *MotionType, short *StartDir);
};

CSmcFacade.cpp

#include "CSmcFacade.h"
#include "CSmc.h"

long WINAPI CSmcFacade::SmcWInit(char* DeviceName, short* DevId) {
    if constexpr (_IS_EMULATION_) {
        // Emulationモードでの挙動
        return 0;
    } else {
        // 実機での挙動
        return ::SmcWInit(DeviceName, DevId);
    }
}

long WINAPI CSmcFacade::SmcWExit(short DevId) {
    if constexpr (_IS_EMULATION_) {
        // Emulationモードでの挙動
        return 0;
    } else {
        // 実機での挙動
        return ::SmcWExit(DeviceName, DevId);
    }
}

...(以下略)...

CSmcFacadeEmulation.h
class CSmcFacadeEmulation : public CSmcFacade {
    ...(実機なしの動作を書く)...
};
CSmcFacadeUnitTest.h
class CSmcFacadeUnitTest1: public CSmcFacadeEmulation {
    ...(テストの動作を書く)...
};

ビルド時には、プリプロセッサEMULATIONを付けるモードと付けないモードを用意します。これを用意すれば、実機がなくてもあっても動作確認することが出来るようになります。

image.png

image.png

 すると、こんな感じで、EMULATIONを付けさえすれば、テスト対象外になるのは結合テスト部分のみになります。そして、例えば、実際に処理を実行するときには、

short _device_id;
auto obj = new CSmcFacade();
if( !obj->SmcWInit("SMC000", &_device_id) ) {
    std::cerr << L"Init Error!" << std::endl;
}

という風に書いてしまえばよいのです。これで、実機あり・実機なしの区別が付きました。ユニットテスト時には(Google Testっぽく書くと)、

short _device_id;
auto obj = new CSmcFacadeUnitTest();
long ret = obj->SmcWInit("SMC000", &_device_id);
ASSERT_EQ(ret, 0);

と書いてしまえばよく、ユニットテストのためのビルドを必要としません。ユニットテストの時の「動き」をCSmcFacadeUnitTestに書けばよいのです。

メリット

実際は、CSmcFacadeなどに実態を入れる必要がありますが、こうすることで実機なし環境下でのデバッグがはかどること間違いないです。

  • レガシーコードの場合、インタフェースはDLLやスタティックライブラリで分割されて、かつそれなりの多くのユーザによってテストされている(ことが多い)。その部分にテストの境界を設定することで、実機なしと実機ありとの移行がやりやすくなる

  • 単に、実機がなくても簡単に作業が出来るので、テスト人員を割り振るときに役割分担しやすくなる。

所感

元ハード屋でソフトウエア開発する人が、ユニットテストに対する意識や関心が薄いのって、「実機で動いているものが全て」って考えてしまって、そこから思考停止してしまっているからじゃないのかなあ、と思ったり思わなかったり。

 

1
2
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2