web-dev-qa-db-ja.com

大規模なレガシー(C / C ++)コードベースに単体テストをどのように導入しますか?

Cで記述された大規模なマルチプラットフォームアプリケーションがあります(C++の量はわずかですが増加しています)。これは長年にわたって進化しており、大規模なC/C++アプリケーションで期待される多くの機能を備えています。

  • #ifdef地獄
  • テスト可能なコードの分離を困難にする大きなファイル
  • 複雑すぎて簡単にテストできない機能

このコードは組み込みデバイスを対象としているため、実際のターゲットで実行するのはかなりのオーバーヘッドです。したがって、ローカルシステム上で、開発とテストの多くを短いサイクルで実行したいと考えています。ただし、「システムの.cファイルにコピー/貼り付け、バグを修正、コピー/貼り付け」という従来の戦略は避けたいと思います。開発者がそれを行うために面倒に行くつもりなら、後で同じテストを再作成して、自動化された方法で実行できるようにしたいと思います。

ここに問題があります。コードをよりモジュール化するためにリファクタリングするには、よりテスト可能にする必要があります。しかし、自動化された単体テストを導入するには、よりモジュール化する必要があります。

1つの問題は、ファイルが非常に大きいため、ファイルを呼び出す関数同じファイル内を使用して、適切な単体テストを実行する必要があることです。コードがモジュール化されるので、これはそれほど問題にはならないようですが、それは長い道のりです。

私たちが考えたのは、「テスト可能であることがわかっている」ソースコードにコメントを付けることです。次に、テスト可能なコードのスクリプトスキャンソースファイルを記述し、別のファイルにコンパイルして、単体テストにリンクします。不具合を修正して機能を追加するため、ユニットテストを徐々に導入することができます。

ただし、このスキーム(および必要なすべてのスタブ関数)を維持するのが面倒になりすぎて、開発者がユニットテストの維持を停止することが懸念されます。したがって、別のアプローチは、すべてのコードのスタブを自動的に生成し、それにファイルをリンクするツールを使用することです。 (これを実行する唯一のツールは高価な商用製品です)しかし、このアプローチはrequireのように見えます。外部呼び出しのみが可能であるため、開始する前にすべてのコードをモジュール化する必要があります。スタブアウト。

個人的には、開発者に外部の依存関係について考えさせ、独自のスタブをインテリジェントに記述してもらいたいです。しかし、これは、ひどく大きくなりすぎた10,000行のファイルのすべての依存関係をスタブ化するのに、圧倒される可能性があります。すべての外部依存関係のスタブを維持する必要があることを開発者に納得させるのは難しいかもしれませんが、それを行う正しい方法はありますか? (私が聞いたもう1つの議論は、サブシステムのメンテナーがサブシステムのスタブを保守する必要があるということです。しかし、開発者に「自分のスタブを書くように強制する」ことは、より良い単体テストにつながるのでしょうか?)

#ifdefs、もちろん、別の全体の次元を問題に追加します。

C/C++ベースの単体テストフレームワークをいくつか見てきましたが、見栄えのよいオプションがたくさんあります。しかし、「単体テストのないコードの毛玉」から「単体テスト可能なコード」への移行を容易にするものは見つかりませんでした。

だからここにこれを経験した人への私の質問があります:

  • 良い出発点は何ですか?私たちは正しい方向に進んでいますか、それとも明らかなものを見逃していますか?
  • 移行に役立つツールはどれですか? (現在の予算はほぼ「ゼロ」であるため、フリー/オープンソースが望ましい)

ビルド環境はLinux/UNIXベースであるため、Windows専用のツールは使用できません。

72
mpontillo

「単体テストのないコードの毛玉」から「単体テスト可能なコード」への移行を容易にするものは何も見つかりませんでした。

どれほど悲しい-奇跡的な解決策はない-何年も蓄積された長年の修正 技術的負債 .

簡単な移行はありません。大規模で複雑な深刻な問題があります。

あなたは小さなステップでそれを解決することができます。各小さなステップには、以下が含まれます。

  1. 絶対に不可欠な個別のコードを選びます。 (がらくたの端をかじらないでください。)重要なコンポーネントを選択してください-どういうわけか-残りから切り分けることができます。単一の関数が理想的ですが、関数のもつれたクラスタまたは関数のファイル全体かもしれません。テスト可能なコンポーネントとしては、完璧ではないものから始めてもかまいません。

  2. それが何をすることになっているのかを理解してください。インターフェースがどうあるべきかを理解してください。これを行うには、ターゲットピースを実際に個別にするために、最初のリファクタリングを行う必要がある場合があります。

  3. 「今のところ」、ディスクリートコードが見つかったときの多かれ少なかれテストする「全体的な」統合テストを記述します。重要なものを変更しようとする前にこれをパスしてください。

  4. コードを、現在の髪の毛よりも意味のある、きちんとしたテスト可能なユニットにリファクタリングします。全体的な統合テストと(今のところ)下位互換性を維持する必要があります。

  5. 新しいユニットのユニットテストを記述します。

  6. すべてが合格したら、古いAPIを廃止し、変更によって何が壊れるかを修正します。必要に応じて、元の統合テストを修正します。古いAPIをテストします。新しいAPIをテストします。

繰り返す。

49
S.Lott

マイケルフェザーズはこれについて聖書を書きました レガシーコードを効果的に使用する

25

レガシーコードとテストを紹介する私の小さな経験は、 " Characterization tests "を作成することです。既知の入力でテストの作成を開始してから、出力を取得します。これらのテストは、実際には何をしているのかはわかりませんが、機能していることがわかっているメソッド/クラスに役立ちます。

ただし、単体テストを作成することがほとんど不可能である場合もあります(特性テストも)。その場合、受け入れテストを通じて問題を攻撃します(この場合、 Fitnesse )。

1つの機能のテストに必要な一連のクラス全体を作成し、fititeで確認します。 「特性テスト」に似ていますが、1つ上のレベルです。

ジョージが言ったように、レガシーコードで効果的に働くことはこの種のことの聖書です。

ただし、チームの他のメンバーが賛成する唯一の方法は、テストを継続して実行することのメリットを個人的に認めるかどうかです。

これを実現するには、できるだけ簡単に使用できるテストフレームワークが必要です。他の開発者が独自のテストを書くための例としてテストを計画します。ユニットテストの経験がない場合は、フレームワークの学習に時間を費やすことを期待しないでください。ユニットテストを作成すると、開発が遅くなるため、フレームワークがテストをスキップする口実であることがわからない場合があります。

クルーズコントロール、luntbuild、cdashなどを使用した継続的インテグレーションにしばらく時間をかけます。コードが毎晩自動的にコンパイルされてテストが実行される場合、ユニットテストがqaの前にバグをキャッチすると、開発者はメリットを見始めます。

奨励すべきことの1つは、共有コードの所有権です。開発者がコードを変更して他の誰かのテストに違反した場合、その人がテストを修正することを期待すべきではない場合、開発者はテストが機能しない理由を調査し、自分で修正する必要があります。私の経験では、これは達成するのが最も難しいことの1つです。

ほとんどの開発者は、何らかの形の単体テストを作成します。時には、チェックインしたりビルドを統合したりしない使い捨てのコードを作成します。これらをビルドに簡単に統合できるようにすれば、開発者は買い始めます。

私のアプローチは、新しいテストを追加することです。コードが変更されると、既存のコードをデカップリングしないと、必要な数のテストや詳細なテストを追加できない場合があります。

単体テストを主張する唯一の場所は、プラットフォーム固有のコードです。 #ifdefsがプラットフォーム固有の上位レベルの関数/クラスに置き換えられる場合、これらはすべてのプラットフォームで同じテストでテストする必要があります。これにより、新しいプラットフォームを追加する時間を節約できます。

テストを構造化するためにboost :: testを使用します。シンプルな自己登録関数により、テストの作成が簡単になります。

これらはCTest(CMakeの一部)でラップされており、ユニットテストの実行可能ファイルのグループを一度に実行し、簡単なレポートを生成します。

私たちの毎晩のビルドは、antとluntbuild(antグルーc ++ 、. net、Javaビルド)で自動化されています)

すぐに、ビルドに自動デプロイメントと機能テストを追加したいと思っています。

7
iain

私たちはまさにこれを実行している最中です。 3年前に、ユニットテスト、コードレビューがほとんどなく、かなり特別なビルドプロセスでプロジェクトに参加しました。

コードベースは、一連のCOMコンポーネント(ATL/MFC)、クロスプラットフォームのC++ Oracleデータカートリッジ、および一部のJavaコンポーネント、すべてクロスプラットフォームのC++コアライブラリを使用する)で構成されています。コードは10年近く前のものです。

最初のステップは、いくつかの単体テストを追加することでした。残念ながら、動作は非常にデータ駆動型であるため、データベースからのテストデータを使用する単体テストフレームワーク(当初はCppUnit、現在はJUnitとNUnitを持つ他のモジュールに拡張されています)を生成するための初期の努力がありました。最初のテストのほとんどは、実際の単体テストではなく、最外層を実行する機能テストでした。おそらく、テストハーネスを実装するために、ある程度の労力(予算を立てる必要があるかもしれません)を費やす必要があります。

単体テストを追加するコストをできるだけ低くすると、非常に役立ちます。テストフレームワークにより、既存の機能のバグを修正するときにテストを比較的簡単に追加できるようになりました。新しいコードは適切な単体テストを持つことができます。コードの新しい領域をリファクタリングして実装するときに、コードの非常に小さな領域をテストする適切な単体テストを追加できます。

昨年、CruiseControlとの継続的な統合を追加し、ビルドプロセスを自動化しました。これにより、テストを最新の状態に保ち、合格するインセンティブがさらに高まります。これは、初期の頃は大きな問題でした。したがって、開発プロセスの一環として、定期的な(少なくとも毎晩)単体テストの実行を含めることをお勧めします。

私たちは最近、コードレビュープロセスの改善に焦点を当てましたが、これはかなりまれで効果がありませんでした。その目的は、開発者がより頻繁に行うようにコードレビューを開始して実行することをはるかに安くすることです。また、プロセス改善の一環として、プロジェクトの計画に含まれるコードレビューと単体テストの時間をはるかに低いレベルで取得して、以前は一定の割合しかなかったのに対し、個々の開発者がそれらについてより深く考える必要があるようにしています。彼らに費やされた時間のうち、スケジュールで迷子になるのははるかに簡単でした。

5
Robert Tuck

私は、完全にユニットテストされたコードベースを使用したグリーンフィールドプロジェクトと、長年にわたって成長し、さまざまな開発者がいる大規模なC++アプリケーションの両方に取り組んできました。

正直なところ、ユニットテストとテストの最初の開発で多くの価値を追加できる状態にレガシーコードベースを取得しようとする気にはなりません。

レガシーコードベースが特定のサイズと複雑さになると、単体テストカバレッジが多くの利点を提供するようになるため、完全な書き換えと同等のタスクになります。

主な問題は、テスト容易性のためのリファクタリングを開始するとすぐに、バグが発生し始めることです。そして、高いテストカバレッジを取得して初めて、これらの新しいバグがすべて発見され、修正されることが期待できます。

つまり、非常にゆっくりと慎重に進む必要があり、十分にユニットテストされたコードベースのメリットを今後数年間得ることができません。 (おそらく合併などが発生するので、決してないでしょう。)それまでの間、おそらくソフトウェアのエンドユーザーに明らかな価値のないいくつかの新しいバグを導入しています。

または、すべてのコードの高いテストカバレッジに到達するまで、高速であるがコードベースが不安定である。 (つまり、2つのブランチが生成され、1つは本番環境、もう1つは単体テストバージョン用です。)

このため、一部のプロジェクトでは、これはすべて規模の問題であり、書き換えには数週間かかるだけで、確かにその価値があります。

4
Jeroen Dirks

検討する1つのアプローチは、統合テストの開発に使用できるシステム全体のシミュレーションフレームワークを最初に配置することです。統合テストから始めると直感に反するように思えるかもしれませんが、説明した環境で真の単体テストを実行する際の問題はかなり手ごわいものです。おそらく、ソフトウェアでランタイム全体をシミュレートするだけではありません...

このアプローチは、リストされた問題を単にバイパスします-それはあなたに多くの異なるものを与えるでしょうしかし実際には、堅牢な統合テストフレームワークを使用すると、ユニットを分離しなくても、ユニットレベルで機能を実行するテストを開発できることがわかりました。

PS:PythonまたはTclに基づいて構築されたコマンド駆動型シミュレーションフレームワークの作成を検討してください。これにより、テストを簡単にスクリプト化できます...

3
Jeff Kotula

G'day、

まず、明らかな点を確認することから始めます。 1つのヘッダーファイルでdecを使用します。

次に、コードがどのようにレイアウトされているかを調べます。それは論理的ですか?大きなファイルを小さなファイルに分解し始めるかもしれません。

たぶん、Jon Lakosの優れた本「Large-Scale C++ Software Design」( sanitised Amazon link )のコピーを入手して、それがどのように配置されるべきかについていくつかのアイデアを得てください。

コードベース自体、つまりファイルレイアウトと同様のコードレイアウトに対する信頼が高まり、悪臭の一部が解消されたら、たとえばヘッダーファイルでdecを使用すると、単体テストの作成を開始するために使用できるいくつかの機能を選択できます。

私はCUnitとCPPUnitが好きなプラットフォームを選び、そこから行きます。

しかし、それは長くて遅い旅になるでしょう。

HTH

乾杯、

3
Rob Wells

最初にモジュール化する方がはるかに簡単です。依存関係が非常に多いものを実際に単体テストすることはできません。いつリファクタリングするかは難しい計算です。コストとリスクとメリットを比較検討する必要があります。このコードは広範囲に再利用されるものですか?または、このコードは実際には変更されません。使い続けるつもりなら、おそらくリファクタリングしたいでしょう。

しかし、リファクタリングしたいようです。最も単純なユーティリティを作成して構築することから始める必要があります。あなたは無数のことをするあなたのCモジュールを持っています。たとえば、文字列を常に特定の方法でフォーマットしているコードがあるかもしれません。たぶん、これはスタンドアロンのユーティリティモジュールになる可能性があります。新しい文字列フォーマットモジュールができました。コードが読みやすくなりました。そのすでに改善。あなたは漁獲量22の状況にあると主張しています。あなたは本当にそうではありません。物事を移動するだけで、コードの可読性と保守性が向上します。

これで、この分割されたモジュールのユニットテストを作成できます。いくつかの方法があります。コードを含み、PCのメインルーチンで一連のケースを実行する別のアプリを作成するか、「UnitTest」と呼ばれる静的関数を定義して、すべてのテストケースを実行し、合格すると「1」を返すことができます。これはターゲットで実行できます。

たぶん、このアプローチで100%行くことはできないかもしれませんが、それは始まりであり、テスト可能なユーティリティに簡単に分解できる他のものを見るようになるかもしれません。

2
Doug T.

基本的には、2つの別々の問題があると思います。

  1. リファクタリングする大規模なコードベース
  2. チームで働く

モジュール化、リファクタリング、ユニットテストの挿入などは困難な作業であり、どのツールでもその作業の大部分を引き継ぐことはできないと思います。その珍しいスキル。一部のプログラマはそれを非常にうまく行うことができます。ほとんどはそれを嫌います。

チームでそのようなタスクを実行するのは面倒です。 「強制」開発者がこれまでに機能することを強く疑います。 Iainsの考えは非常によくできていますが、ソースを "クリーンアップ"でき、ソースをクリーンアップしたい1人または2人のプログラマを見つけることを検討します。バグ、aehm関数。 likeそのような仕事をした人だけがその仕事で成功します。

2
RED SOFT ADAIR

テストを簡単に使用できるようにします。

「自動実行」を配置することから始めます。開発者(自分を含む)にテストを記述させたい場合は、テストを簡単に実行して結果を確認してください。

3行のテストを記述し、最新のビルドに対して実行し、結果を確認するのはワンクリックだけで、開発者をコーヒーに送らないでください。機械。

つまり、最新のビルドが必要であり、コードの作業方法などのポリシーを変更する必要があるかもしれません。そのようなプロセスは組み込みデバイスを備えたPITAである可能性があることはわかっています。そのため、アドバイスはできません。しかし、テストの実行が難しい場合、誰もテストを作成しないことを知っています。

テストできるものをテストする

私はここで単体テストの一般的な哲学に反することを知っていますが、それが私が行うことです。テストしやすいもののテストを作成します。私はモッキングを気にせず、テスト可能にするためにリファクタリングもしていません。UIが関係している場合は、単体テストを行っていません。しかし、ますます私のライブラリルーチンに1つあります。

簡単なテストが見つけやすい傾向にかなり驚いています。ぶら下がっている果物を選ぶことは決して無駄ではありません。

別の見方をすると、もしそれが成功した製品でなければ、巨大なヘアボールの混乱を維持するつもりはありません。現在の品質管理は、交換が必要な完全な故障ではありません。むしろ、簡単に実行できるユニットテストを使用してください。

(ただし、それを実行する必要があります。ビルドプロセスの「すべてを修正する」ことにとらわれないでください。)

コードベースを改善する方法を教えてください

その歴史を持つすべてのコードベースは、改善のために絶叫します、それは確かです。ただし、すべてをリファクタリングすることは決してありません。

同じ機能を持つ2つのコードを見ると、ほとんどの人は、特定の側面(パフォーマンス、読みやすさ、保守性、テスト容易性など)でどちらが「優れている」かに同意できます。難しい部分は3つです。

  • さまざまな側面のバランスをとる方法
  • このコードが十分であることに同意する方法
  • 何も壊さずに悪いコードを十分なコードに変える方法.

最初の点はおそらく最も困難であり、エンジニアリングの質問と同じくらい社会的なものです。しかし、他の点は学ぶことができます。このアプローチを取る正式なコースは知りませんが、社内で何かを整理できるかもしれません。2人の男が一緒に作業しているところから、厄介なコードを取り、それを改善する方法について話し合う「ワークショップ」まで何でもできます。


1
peterchen

それにはすべて哲学的な側面があります。

テスト済みで完全に機能する整頓されたコードが本当に必要ですか?あなたの目的ですか?あなたはそれから何か利益を得ますか?.

はい、最初はこれはまったく愚かに聞こえます。しかし正直なところ、あなたが従業員だけでなくシステムの実際の所有者でない限り、バグは単により多くの作業を意味し、より多くの作業はより多くのお金を意味します。髪の毛に取り組んでいる間、あなたは完全に幸せになります。

私はここで推測しているだけですが、この巨大な戦いに参加することによってあなたが取っているリスクは、コードを整頓することによって得られる可能性のある見返りよりもはるかに高いでしょう。あなたがこれを乗り越えるための社会的スキルを欠いている場合、あなたはただトラブルメーカーとして見られます。私はこれらの人を見たことがあります、そして私も1人でした。しかし、もちろん、これを実行するのはかなりクールです。感動します。

しかし、整頓されていないシステムを動作させるために今すぐ余分な時間を費やすことにいじめられていると感じた場合、コードがきちんと整えられれば、それは本当に変わると思いますか?。いいえ。コードが適切に整頓されると、人々はこの空き時間をすべて利用して、最初の利用可能な期限にコードを完全に破棄します。

結局のところ、コードではなく、職場のニースを作成するのは管理者です。

1
Per Viberg