はじめに
【C++言語】 は、C言語を拡張して1980年代に登場したプログラミング言語です。
特徴としては、C言語の「速さ・低レベル制御」を残しつつ、「オブジェクト指向」「例外処理」「標準ライブラリの拡充」などが加わりました。
【注意】このメモは、C++言語を全くしらない人間が、ChatGPTに聞いて適当に書いている、C++言語の基本的な記載方法のまとめです。
(内容は確認しながら書いているつもりですが、間違ってたらごめんなさい)
🟦 GoogleTest の基本構成
C++ における代表的な単体テストフレームワークである
GoogleTest(gtest)を用いたテスト導入について整理していきます。
単体テストとは、
・ 関数単位
・ クラス単位
・ 処理の最小単位
で 「正しく動くか」 を自動で確認するための仕組みです。
単体テストを導入するメリット
・ 処理を 独立して確認 できる
・ 修正時に 既存の動作が壊れていないか即座に分かる
・ 手動確認を減らし、安心して変更できる
GoogleTest を使う以前に、
テストしやすいコード構造 が重要です。
最低限、次を意識します。
・ テスト対象は main ではなく 関数単位
・ 入力と出力が明確
・ 副作用(ファイル操作・標準出力など)が分離されている
これが守られていれば、
GoogleTest で 段階的にテストを追加 できます。
🟢 GoogleTest の基本構成
GoogleTest を使ううえで、まず押さえておくべき要素は多くありません。
・ TEST(テストスイート名, テスト名)
・ EXPECT_EQ / ASSERT_EQ などの検証マクロ
・ main 関数は GoogleTest 側が用意する
🔹 最小構成
#include <gtest/gtest.h>
TEST(SampleTest, Addition) {
EXPECT_EQ(2 + 3, 5);
}
🟢 解説:
✔ テストは自動登録される
✔ 実行すると自動で評価される
✔ 失敗すると理由が表示される
関数を呼ぶ → 結果を比較する
これだけで単体テストが成立します。
🟦 EXPECT と ASSERT の違い(重要)
GoogleTest には 2 種類の検証マクロがあります。
種類 失敗時の挙動
EXPECT_* 失敗してもテストを継続
ASSERT_* 失敗した時点でテストを中断
🔹 EXPECT 系
EXPECT_EQ(a, b);
EXPECT_TRUE(cond);
・ 失敗しても次の検証に進む
・ 状態確認向け
👉
「できるだけ多くの情報を一度に見たい」場合に使う
🔹 ASSERT 系
ASSERT_EQ(ptr, nullptr);
・ 失敗した瞬間にテスト終了
・ 前提条件チェック向け
👉
「ここが壊れていたら後続処理は意味がない」場合に使う
🔹 使い分けの目安
ケース 推奨
初期化・NULLチェック ASSERT
戻り値・状態確認 EXPECT
複数条件をまとめて確認 EXPECT
🔸 テストコードの配置方法(実務想定)
ディレクトリ構成例
project/
├─ src/
│ ├─ file_reader.cpp
│ ├─ file_reader.h
│
├─ tests/
│ ├─ test_file_reader.cpp
│
├─ CMakeLists.txt
配置の考え方
src
→ 本体コード(プロダクトコード)
tests
→ GoogleTest 用のテストコード
main 関数
→ tests 側でのみ使用する
👉
プロダクトコードにテスト用コードを混ぜないことが基本です。
これにより、実装とテストの責務を明確に分離できます。
🔸 関数を 1 つ対象にした単体テストの実装例
テスト対象関数
// file_reader.h
#pragma once
#include <string>
#include <vector>
struct FileData {
std::string fileName;
std::string content;
};
struct ResultData {
int resultCode;
std::string folderPath;
std::vector<FileData> files;
};
ResultData readTextFilesFromFolder(const std::string& folderPath);
テスト対象関数
// file_reader.h
#pragma once
#include <string>
#include <vector>
struct FileData {
std::string fileName;
std::string content;
};
struct ResultData {
int resultCode;
std::string folderPath;
std::vector<FileData> files;
};
ResultData readTextFilesFromFolder(const std::string& folderPath);
// file_reader.cpp
#include "file_reader.h"
#include <filesystem>
#include <fstream>
// std::filesystem を fs という短い名前で使えるようにする
namespace fs = std::filesystem;
/*
* 指定したフォルダ内の .txt ファイルをすべて読み込み、
* ファイル名と内容を ResultData として返す関数
*
* @param folderPath 読み込み対象のフォルダパス
* @return ResultData
* - resultCode : 0 = 成功 / -1 = 失敗
* - folderPath : 処理対象のフォルダ
* - files : 読み込んだ .txt ファイル一覧
*/
ResultData readTextFilesFromFolder(const std::string& folderPath) {
ResultData result;
// 呼び出し元から渡されたフォルダパスを結果に保存
result.folderPath = folderPath;
try {
// 指定フォルダ内のエントリ(ファイル・ディレクトリ)を順に処理
for (const auto& entry : fs::directory_iterator(folderPath)) {
// 拡張子が ".txt" のファイルのみ対象とする
if (entry.path().extension() == ".txt") {
FileData fd;
// ファイル名のみを取得(パスは含まない)
fd.fileName = entry.path().filename().string();
// ファイルを入力ストリームとしてオープン
std::ifstream file(entry.path());
std::string line;
// 1行ずつ読み込み、改行付きで content に追加
while (getline(file, line)) {
fd.content += line + "\n";
}
// 読み込んだファイル情報を結果リストに追加
result.files.push_back(fd);
}
}
result.resultCode = 0;
} catch (...) {
result.resultCode = -1;
}
return result;
}
テストコード
// tests/test_file_reader.cpp
#include <gtest/gtest.h>
#include "file_reader.h"
TEST(ReadTextFilesTest, InvalidFolderPath) {
ResultData result = readTextFilesFromFolder("invalid/path");
EXPECT_EQ(result.resultCode, -1);
}
確認していること
・ 関数が例外で異常終了しないこと
・ 異常系の戻り値が仕様どおりであること
🟦 単体テストで必ず考えるべき観点(汎用)
単体テストを書く際に重要なのは、
「すべてのコードを書く」ことではなく、
壊れやすいポイント・判断が分かれるポイントを必ず通すこと です。
ここでは、特定の関数に依らず、
どんな処理でも共通して考えるべきテスト観点を整理します。
🟢 if 文・条件分岐は必ずすべて通す
単体テストで最も基本かつ重要なのが
条件分岐(if 文)をすべて通すことです。
AND / OR 条件の考え方
例えば次のような条件がある場合:
if (A || B) {
// 処理
}
この場合、最低限以下を確認します。
A = true, B = false
A = false, B = true
A = false, B = false
👉
「OR でつながっているから 1 回通せば OK」ではなく、
どの条件で通ったのかを分けて確認します。
AND(&&)の場合も同様で、
両方 true
片方 false
両方 false
を意識します。
readTextFilesFromFolder に当てはめると
if (entry.path().extension() == ".txt")
この if に対して最低限必要なのは:
.txt ファイルがある場合
.txt 以外のファイルしかない場合
.txt とそれ以外が混在している場合
👉
if の真・偽の両方を必ず通すのが基本です。
🟢 件数に関するテスト(0 / 1 / 多)
次に重要なのが 件数(数が変わるケース) です。
これは「境界値テスト」と呼ばれる考え方の一種です。
なぜ件数を見るのか?
多くのバグは、
・0 件のとき
・1 件のとき
・想定より多いとき
に発生します。
readTextFilesFromFolder の場合
最低限、以下を確認します。
フォルダが空(0 件)
.txt が 1 ファイルだけ
.txt が複数存在する
確認ポイントは:
resultCode が正しいか
files.size() が期待通りか
例外で落ちないか
👉
中身の文字列チェックはこの段階では必須ではありません。
🟢 異常系(失敗するケース)は必ず入れる
単体テストで 最優先 すべきなのが異常系です。
理由は単純で、
実務では「正しくない入力」のほうが怖い
からです。
readTextFilesFromFolder の異常系
存在しないフォルダパス
アクセスできないフォルダ
確認するのは次の 2 点だけです。
プログラムが落ちないこと
戻り値が仕様どおりであること
EXPECT_EQ(result.resultCode, -1);
👉
「例外が起きないこと」+「状態で失敗を返すこと」が重要です。
🟢 大量データは「動くかどうか」だけでよい
大量ファイルや大きなデータ量については、
単体テストでは 性能評価までは行いません。
この段階で見るのは:
処理が途中で落ちないか
想定外の例外が出ないか
👉
時間計測や負荷試験は、
単体テストではなく別のテスト工程の役割です。
🟢 境界値とは何か(簡単な考え方)
境界値とは、
ちょうど 0
ちょうど 1
条件が切り替わる点
のことを指します。
readTextFilesFromFolder の境界値例
.txt が 0 件
.txt が 1 件
.txt が 2 件以上
👉
「普通に動きそうなところ」ではなく、
切り替わるポイントを意識してテストします。
🟢 この関数で最低限そろっていれば OK なテスト
まとめると、readTextFilesFromFolder に対して
この段階で揃っていれば十分なのは以下です。
異常パス指定で失敗する
空フォルダでも正常終了する
.txt の件数が正しく数えられる
.txt 以外が無視される
👉
これだけで、
if 文はすべて通っている
件数の境界値も押さえている
実務で安心して修正できる状態
になっています。
🟦 メモリリークは単体テストでどう確認するか?
結論から
GoogleTest 単体では、メモリリークは検出しません。
単体テストでやるのは:
「リークしにくい書き方になっているか」
「検出ツールと組み合わせて確認する」
です。
① コードレベルで意識すること(最重要)
単体テスト以前に、実装で次を守ることが前提です。
new / delete を直接使わない
std::vector, std::string など RAII に任せる
ファイル・リソースはスコープで解放される形にする
今回の readTextFilesFromFolder では:
std::ifstream file(entry.path());
スコープを抜けると自動で close
手動解放不要
例外が起きてもリークしない
👉
この形にできていれば、単体テストで個別にリーク確認を書く必要はありません。
② 単体テスト × ツールで確認する
実務では、次のように確認します。
Linux / macOS
・ Valgrind
・ AddressSanitizer(ASan)
Windows
・ Visual Studio の CRT Debug Heap
・ AddressSanitizer(最近の MSVC)
例(AddressSanitizer):
-fsanitize=address -g
👉
GoogleTest を ASan 付きで実行することで、
テスト実行中にリークがあれば即検出
「どのテストで漏れたか」も分かる
という形になります。
③ 単体テスト側で最低限できること
テストとしてやるのはこれだけです。
テストを 何度も呼び出しても落ちない
ループ実行しても異常が出ない
TEST(ReadTextFilesTest, RepeatExecution) {
for (int i = 0; i < 100; ++i) {
ResultData result = readTextFilesFromFolder("test_data");
EXPECT_EQ(result.resultCode, 0);
}
}
👉
リークがあれば、ASan / Valgrind がここで検出します。
🟦 例外処理は単体テストでどう扱うか?
結論から
単体テストでは、
例外を投げる設計か
例外を握りつぶして状態で返す設計か
どちらかを明確にし、その 仕様どおりか を確認します。
今回の設計(readTextFilesFromFolder)
try {
...
result.resultCode = 0;
} catch (...) {
result.resultCode = -1;
}
この関数は:
例外を外に投げない
失敗は resultCode で表現する
という設計です。
単体テストで確認すべきこと
この場合、確認すべきは次の 2 点だけです。
例外が外に出ないこと
異常時に戻り値が正しいこと
TEST(ReadTextFilesTest, ExceptionHandledInternally) {
EXPECT_NO_THROW({
ResultData result = readTextFilesFromFolder("invalid/path");
EXPECT_EQ(result.resultCode, -1);
});
}
EXPECT_NO_THROW は
GoogleTest(gtest)で使われる
アサーション(検証用マクロ)の一つで、
👉 「この処理は例外を投げずに正常終了するはず」ということを
確認するためのものです。
✅ 処理の実行中に例外が投げられなければテスト成功
❌ 例外が1つでも投げられたらテスト失敗
EXPECT_NO_THROW と EXPECT_THROW の違い
EXPECT_NO_THROW(expr)
例外が出ないことを期待
EXPECT_THROW(expr, std::exception)
指定した型の例外が出ることを期待
EXPECT_ANY_THROW(expr)
何かしら例外が出ること
使うべきケース(重要)
EXPECT_NO_THROW を使うべき関数
内部で例外処理している
エラーは戻り値やステータスで表現する
ライブラリ関数・ユーティリティ関数
👉 今回の readTextFilesFromFolder は該当
よくある勘違い
❌「例外が起きない=テスト成功」ではない
⭕「例外が起きないこと も テストする」
EXPECT_NO_THROW({
auto result = readTextFilesFromFolder(path);
EXPECT_EQ(result.resultCode, -1);
});
👉 安全性 + 振る舞いの両方を確認するのが正解
もし例外を投げる設計なら?
設計がこうなら:
ResultData readTextFilesFromFolder(...) {
if (...) {
throw std::runtime_error("error");
}
}
テストはこうなります。
TEST(ReadTextFilesTest, ThrowException) {
EXPECT_THROW(
readTextFilesFromFolder("invalid/path"),
std::runtime_error
);
}
👉重要なのは混在させないこと。
🟦 単体テストでの責務まとめ
観点 単体テスト
メモリリーク検出 △ ASan / Valgrind と併用
例外が出るか ✅ EXPECT_THROW / NO_THROW
リソース解放 △ RAII 前提
落ちないこと ✅ 最重要
戻り値の正当性 ✅ 状態で確認
💠C++まとめ💠
💠Cまとめ💠