0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

四日では覚えられないC++言語【Day 6: GoogleTest の基本構成】

0
Last updated at Posted at 2026-01-18

はじめに

【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まとめ💠








0
0
0

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?