Visual Studio 2022のC++(Microsoft Unit Testing Framework / MSTest for C++)でテストが増えて実行が遅い。『テストを並列実行』やvstest.console.exeの/Parallelを有効にしても逐次実行のまま…というときに、原因と最短で効く高速化手順をまとめます。
起きていること:設定を入れてもC++テストだけ時間が縮まらない
Visual Studioのテストエクスプローラーには「テストを並列実行」という設定があります。またCLIでは次のように実行できます。
vstest.console.exe MyTests.dll /Parallel
ところが、C++のMicrosoft Unit Testing Framework(CppUnitTestFramework)で作ったテストは、テスト数が多くても実行時間がほぼ変わらず、CPUもあまり使われないことがあります。C#のMSTestや他言語のテストでは速くなったように見えるため、余計に混乱しやすいポイントです。
結論:/Parallelは「同一DLL内のテストケースを並列化」しない
/Parallelが並列化する単位は、基本的に「テストアセンブリ(= テストDLL)」です。 つまり、1つのDLLの中に全テストを詰め込んでいる構成では、/Parallelを付けても並列で走らせる“別の作業”が存在しないため、結果として逐次実行に見えます。
| 区分 | イメージ | 例 | vstestの/Parallelが狙う並列化 |
|---|---|---|---|
| テストケース | 1つのテストメソッド | TEST_METHOD(Foo_返り値が正しい) | 対象外(同一DLL内は基本逐次) |
| テストクラス | テストケースのまとまり | TEST_CLASS(FooTests) | 対象外(同一DLL内は基本逐次) |
| テストアセンブリ | テストDLL(テストコンテナー) | MyTestsA.dll / MyTestsB.dll | 対象(DLL同士を並列に実行) |
この仕様を押さえると、現象はシンプルです。
- 対象DLLが1つだけ → 並列に実行できる相手がいない → 体感が変わらない
- 対象DLLが複数 → DLLごとに並列実行される → 全体時間が短くなりやすい
/Parallelの仕組みをもう一段だけ噛み砕く
vstest.console.exeは「テストを見つけて実行するランナー」で、テストDLL(テストコンテナー)ごとに実行の単位が切られます。/Parallelは、その複数コンテナーを同時に走らせる方向に働きます。言い換えると、テストメソッドをスレッドで同時実行する機能ではなく、複数のテスト実行を“別枠”で進める機能です。
そのため、単一DLLに対して/Parallelを付けても「中身を分解して勝手に並列化」してくれるわけではありません。C++のテストで実行時間を縮めたいなら、まずは“並列に回せる粒度(= 複数DLL)を用意する”のが最短ルートです。
なぜC#だと「効いている」ように見えるのか
同じvstestでも、C#側は「テストプロジェクトが複数ある」「フレームワーク側がメソッドレベル並列に対応している」などの条件が揃うと、並列化の恩恵が見えやすくなります。一方、C++のMicrosoft Unit Testing Frameworkは、少なくとも一般的な構成ではテストメソッド単位の並列実行を属性で有効化するといった運用は取りにくいのが実情です。結果として、/Parallelに期待しても挙動が変わらない、という落とし穴になります。
解決策:テストを複数プロジェクトに分割して「複数DLL」にする
最も現実的で、効果が出やすい対処は次の2点です。
- テストを複数のテストプロジェクト(= 複数DLL)に分割して配置する
- vstest.console.exeに複数DLLを渡し、/Parallel付きで実行する
コマンド例:複数DLLをまとめて渡す
vstest.console.exe MyTests.Core.dll MyTests.IO.dll MyTests.API.dll /Parallel
これでDLL(テストアセンブリ)単位の並列実行が働き、特に「テストが多い」「各テストが数百ms〜数秒かかる」ような環境では、体感できるレベルで短縮することが多いです。
並列度を調整したいとき:runsettingsでMaxCpuCountを指定する
環境によっては、並列度が高すぎてI/Oや外部依存が詰まり、逆に遅くなったり不安定になったりします。そういうときはrunsettingsでCPU数(同時実行数の上限)を制御します。
<?xml version="1.0" encoding="utf-8"?>
<RunSettings>
<RunConfiguration>
<MaxCpuCount>4</MaxCpuCount>
<ResultsDirectory>TestResults</ResultsDirectory>
</RunConfiguration>
</RunSettings>
実行例は次の通りです。
vstest.console.exe MyTests.Core.dll MyTests.IO.dll MyTests.API.dll /Parallel /Settings:runsettings.runsettings
「まずは全部使って最速にしたい」ならMaxCpuCountを0(利用可能なCPUを最大限使う)にする、という運用もありますが、外部リソースを叩く統合テストが混ざる場合は、4〜8程度に固定して安定性を優先した方が、トータルでは速いことがよくあります。
分割の効果をざっくり見積もる
例えば合計60秒かかるテストが、3つのDLLに均等に分かれ、I/O待ちや初期化の重なりも許容できるなら、理想的には20秒前後まで短縮できる可能性があります。もちろん現実には「偏り」や「共有資源の待ち」があるので理想通りにはいきませんが、分割前に“重い塊”を見つけて別DLLに逃がすだけでも効果が出やすいです。
| 構成 | 合計時間のイメージ | ボトルネック |
|---|---|---|
| 1DLLに全テスト | 60秒(逐次) | 常に1本で実行される |
| 3DLLに分割(バランス良) | 20〜30秒(並列) | 最も遅いDLLが支配する |
| 3DLLに分割(偏り大) | 40〜60秒(並列でも微妙) | 重いDLLが残る |
分割の切り方:迷ったら“機能単位”か“レイヤー単位”
テスト分割はやりすぎると管理コストが上がります。まずは次のどちらかが扱いやすいです。
| 分割方針 | 例 | メリット | 注意点 |
|---|---|---|---|
| 機能単位 | AuthTests / PaymentTests / SearchTests | 責務が明確で、所有者も決めやすい | 横断的な共通部品の置き場が必要 |
| レイヤー単位 | CoreTests / InfraTests / UITests | 依存関係が整理され、ビルドも安定しやすい | 機能追加時にどこへ置くか判断が必要 |
| 実行特性で分割 | FastUnit / SlowIntegration / External | 「速いものは並列」「遅い・不安定は隔離」がしやすい | 分類ルールをチームで揃える必要 |
Visual Studioでの進め方:分割しても運用が破綻しない形にする
プロジェクト分割のときにありがちな困りごとは「共通テストヘルパー」「テストデータ」「モックやスタブ」の置き場です。おすすめは次の形です。
- 共通ヘルパーは別プロジェクト(静的ライブラリ or ヘッダーオンリー)に切り出す
- テストデータは各テストDLLの出力先にコピー(または実行時に生成)
- 依存が重い統合テストは、専用のDLLに隔離して並列化の影響を限定する
手順例:新しいC++テストプロジェクトを追加してテストを移す
- ソリューションに新しいC++単体テストプロジェクトを追加する
- テスト対象コードへの参照(プロジェクト参照/ライブラリリンク)を揃える
- 既存テストのうち、まとまりの良いものから移動する
- 共通コードは「TestCommon」などの形で集約し、各テストから参照する
- 最終的に、vstest.console.exeに渡すDLLが複数になる状態にする
おすすめのディレクトリ例
Solution
src/
MyProduct.Core/
MyProduct.IO/
tests/
TestCommon/
MyProduct.Core.Tests/ (速いユニットテスト中心)
MyProduct.IO.Tests/ (I/O周りのユニットテスト)
MyProduct.Integration.Tests/ (外部依存があるので隔離)
並列化を“効かせる”ための実務的なチェックポイント
DLL分割で並列実行が始まっても、テストが互いに干渉すると失敗が増えます。並列化の前後で特に確認したい点をまとめます。
| 症状 | よくある原因 | 対策 |
|---|---|---|
| 並列化するとテストがたまに落ちる | 一時ファイル名が固定、同じフォルダを共有 | テストごとに作業ディレクトリを分け、GUID等で一意名にする |
| ポート競合で失敗する | 固定ポートでサーバー起動 | 空きポートを動的確保、または該当テストを専用DLLへ隔離 |
| 外部DB/外部API依存で不安定 | 共有リソースに同時アクセス | テスト用DBを分離、トランザクション、スタブ化、実行ジョブ分割 |
| 初期化が重くて結局遅い | クラス初期化/グローバル初期化が過大 | 初期化対象を最小化、遅い初期化は統合テスト側へ移す |
観測のコツ:本当に並列になっているかを見分ける
- vstest.console.exe実行中にCPU使用率が上がる(コアが複数使われる)
- TRXやログで、複数DLLの実行開始時刻が重なる
- 「遅いテストDLL」を分割すると、ボトルネックが目に見えて分かれる
もし変化がない場合は、まず「本当に複数DLLを指定しているか」「テストDLLが1つにまとまっていないか」を疑うと近道です。さらに掘るなら、診断ログを出すのも有効です。
vstest.console.exe MyTests.Core.dll MyTests.IO.dll /Parallel /Diag:vstest.log
それでも並列にならないときの確認リスト
- 実行対象が本当に複数DLLか(ワイルドカード指定やパス指定で1つしか拾っていない、などが起きやすい)
- ビルド構成が一致しているか(x64/x86混在で片方がスキップされると並列数が減る)
- 統合テストだけが残っていないか(外部待ちが支配するとCPUは上がらず、短縮も小さい)
- 同一マシン上の他プロセスが重くないか(ビルド・ウイルススキャン・I/O競合など)
どうしてもテストメソッド単位で並列化したい場合の考え方
Microsoft Unit Testing Framework(C++)の範囲では、メソッド単位の並列実行を属性で制御する、といった運用は取りにくいことが多いです。そこで、目的に応じて次の代替案が現実的です。
- プロセス並列(テストDLL/ジョブ分割)を徹底する:今回の解決策をさらに進め、CIでもテストDLLごとにジョブを分ける
- 分割実行を複数プロセスで同時に走らせる:/TestCaseFilterや/Testsで範囲を切り、複数のvstest.console.exeを並列実行する
- 別フレームワークに寄せる:GoogleTest等に移行し、CTestの-jや外部の並列実行基盤でテストを分割して走らせる
- 遅い統合テストは設計から見直す:I/Oや外部依存を減らし、ユニットテストでカバーできる範囲を増やす
“メソッド単位の並列化”は一見強力ですが、テストの独立性(スレッドセーフ/リソース分離)が不十分だと失敗率が上がり、トータルでは開発速度が下がることがあります。まずはDLL分割+/Parallelで安全に効果を出し、必要に応じて次の手を検討するのがおすすめです。
よくある質問
テストDLLを分割すると、ビルド時間が増えませんか?
増える場合はあります。ただし、実務では「ビルドよりテストが遅い」ケースが多く、並列実行による短縮が上回りやすいです。共通コードをTestCommonに寄せ、各テストプロジェクトの依存を軽くしておくと増加を抑えられます。
遅いテストが混ざっていても、全部を並列にして大丈夫?
外部リソース(DB、ネットワーク、共有フォルダなど)に依存するテストは、並列化で失敗率が上がることがあります。まずは統合テストを専用DLLに分離し、ユニットテストDLLだけを/Parallelで回す運用が安全です。
Visual Studioの「テストを並列実行」をONにすれば、DLL分割なしでも速くなる?
基本的にはなりません。並列化の単位がDLLである以上、1つのテストDLLしかないと並列に走らせる対象がありません。設定をONにするのは前提として有効ですが、効果を出す鍵は複数DLLに分けることです。
まとめ:/Parallelが効かないのは仕様、効かせるには「複数DLL」が近道
- /Parallelは同一DLL内のテストケースを並列化する機能ではなく、複数テストDLLを並列に実行する機能
- 1つのテストDLLに集約していると、/Parallelを付けても基本的に速くならない
- テストを複数プロジェクトに分割し、vstest.console.exeに複数DLLを渡して/Parallelで実行すると効果が出やすい
テスト実行時間が伸びてきたら、まずは「速いテスト」と「遅いテスト」を分けるところから始め、並列化しやすい構造へ段階的に整えると、運用コストを抑えつつ継続的に短縮できます。

コメント