Javaの設計とテストの困りごとから読む本を選ぶ記事のアイキャッチ。若手エンジニアが読む順番のチェックリストとテスト結果を確認するイラスト。

技術書・書評

Javaの設計・テストおすすめ本3冊|変更が怖いときの選び方と読む順番

AIによる要約

設計・テストの本は、いま説明できない変更から選びます。責務が混ざるなら設計入門、内部変更でテストが壊れるなら単体テスト、業務とDBの境界で迷うならシステム設計の原則が候補です。このページでは対象版と読む範囲を確認し、独自のJava・JUnit課題で失敗と修正、未保存の条件まで確かめます。

新人SE
新人SE
設計の本を読めば、変更するのが怖くなくなりますか?テストも何を確かめればよいか迷います。
ポンコツSE
ポンコツSE
まず、困っている変更を一つ選びましょう。読む範囲を絞り、同じ入力で結果を比べると、本の内容を自分の判断へ移しやすくなります。

Javaで機能を追加できるようになっても、「直す場所が読めない」「テストが毎回壊れる」という壁は残ります。注文画面に項目を一つ増やしただけなのに、Controller、Service、SQL、テストを順番に開き、どこまで直せばよいか分からなくなる。そんなときは、APIの使い方を増やすだけでなく、変更の理由と確かめ方を整理する本が候補になります。

この記事は、出版社の書誌と公開目次で対象範囲を確認し、Javaの独自課題と組み合わせた本選びの案内です。3冊を同時に買う前提にはしません。今のコードで説明できないことを一つ選び、その問題に対応する章を読み、手元の小さな変更で確かめる順序を示します。

選定は出版社の公開情報と独自課題に基づき、読了体験や全ページの品質評価ではありません。

この記事のポイント

  • 責務が混ざる、テストが壊れる、業務とDBの境界が曖昧、の3つから選ぶ
  • 本の例の言語と、自分が試すJava・JUnitの環境を分けて考える
  • 読む章を絞り、同じ業務入力で修正前後を比べる
  • 合格条件は読了ページ数より、変更理由とテスト範囲を説明できること

最初の1冊を、いま困っている変更から選ぶ

いまの症状最初の候補読んだ後に作るもの
送料の条件が画面・バッチ・請求処理に散らばっている改訂新版 良いコード/悪いコードで学ぶ設計入門業務判断を一か所へ寄せた小さなクラス
処理結果は同じなのに、内部メソッドの変更でテストが壊れる単体テストの考え方/使い方公開結果と外部への影響を確かめるテスト
ServiceにSQLや画面項目を渡すべきか判断できない現場で役立つシステム設計の原則入力、業務判断、保存の境界と担当を示すメモ

まず、直近のレビュー指摘を一つだけ書き出してください。「設計が苦手」より、「無料配送の条件を変えると3つの入口を直す」の方が、本を読む目的になります。テストで困っているなら、このページの主軸は『単体テストの考え方/使い方』です。責務の置き場所が読めない段階なら、先に設計入門の範囲を使います。

前提は、Javaでクラス、引数、戻り値、例外を読み、短いメソッドを自分で動かせることです。JUnitをまだ一度も実行していない人は、最初からモックの設計論へ進まず、下の課題で一つの期待値を確認するところから始めてください。購入の判断は、その小さな作業で何が分からなかったかを見てからでも間に合います。

責務が混ざって変更箇所が散るなら、設計入門から

『改訂新版 良いコード/悪いコードで学ぶ設計入門』は、2024年12月25日刊、ISBN 978-4-297-14622-1の版を対象にしています。出版社目次では、データと処理のまとめ方、関心の分離、既存コードの改善を扱います。技術評論社の書誌・目次で版を確認できます。

例えば、架空の受注システムで「1万円以上なら送料無料」を実装したとします。注文画面とCSV取込で別々に同じ条件を書き、翌月に対象金額が変わると、片方だけ古い条件が残る。修正漏れを気合いで防ごうとしても、次の担当者は条件が何か所にあるか知りません。この場面では、短く書く技法より先に、同じ判断をどこへ集めるかを考える必要があります。

読む順は、まず第3章でデータと判断のまとまりを確認し、第6・7章で別の関心を切り分け、第15章を小さな改善の手順として使う案です。これは本の全章要約ではなく、この受注課題に合わせた読み方です。最初から大規模なクラス構成を作るために読むのではありません。

課題では、送料の計算をDBから切り離した OrderPricing へ寄せます。数量の許容範囲もこの課題では1〜99と決め、どの入口から呼んでも同じ結果になるようにします。現実の業務では、その上限が画面都合なのか、受注ルールなのかを先に確認してください。画面だけの入力制約まで、根拠なく全業務のルールにしてはいけません。

合格条件は、無料配送の金額を変えるときの修正先を指せることと、修正しない入口を説明できることです。クラス数が増えたこと自体は合格理由になりません。画面ごとに独立した短い処理で、共通の業務判断も変更理由の混在もないなら、分割を増やすためだけにこの本を急いで買う必要はありません。

実装を少し変えるたびにテストが壊れるなら、単体テストの本へ

『単体テストの考え方/使い方』は、2022年12月28日刊、ISBN 978-4-8399-8172-3の日本語訳を対象にしています。書籍のコード例はC#です。JavaやJUnitの構文を順番に覚える入門書ではありません。 公開目次には、テストの価値、モックによる壊れやすさ、統合テストやDBの検証が含まれます。マイナビ出版の紹介・目次で範囲を確認してください。

向いているのは、テストを書けるようになった後で「何を確認すると変更に強いか」に迷う人です。例えば、送料計算を二つのprivateメソッドへ分けただけなのに、メソッドの呼び出し回数や途中の値を固定したテストが壊れる。一方で、本当の不具合である「ちょうど1万円に送料が付く」は見逃している。この二つを区別できるようにするのが、今回の学習目標です。

読む順は、第3・4章でテストの構造と価値を確認し、第5章でモックと内部実装の結び付きを点検、第7章を改善に使う案です。DBを含む範囲が問題になった時点で第8・10章へ進みます。章番号は対象版の公開目次に対応します。C#のフレームワーク呼び出しをそのままJavaへ貼るのではなく、「入力」「観察できる結果」「外部への影響」を自分のJUnitコードへ写します。

このページのテストは、料金を返す公開メソッドと、注文が保存対象になったかを確認します。送料を求めるprivateメソッドの名前、ローカル変数の数、計算を何回に分けたかは固定しません。内部を整理しても業務上の結果が同じなら、テストはそのまま使えます。反対に、数量の上限や送料条件という公開契約を変えたなら、期待値の変更にも業務上の理由が必要です。

モックを使わないことが目的ではありません。メール送信や外部決済のように、呼び出した事実が利用者に影響する境界を確認する価値はあります。何でもモックへ置き換える前に、今回の MemoryOrders のような小さな代役で、観察したい結果を表せないか考えます。単体テストが初めてなら、まず実行できる最小例を作り、その後に本で判断の理由を補う順序が取りやすいです。

SpringのServiceとRepositoryを使う既存コードへこの考え方を移す場合は、次の実装例がつながります。入力が正しい場合だけでなく、業務エラー時に何を返し、何を保存しないかも確認できます。

関連記事Spring BootのServiceをJUnit・Mockitoでテストする方法|Repositoryをモックする基本 本で考えたテストの境界を、SpringのServiceとRepositoryへ移すための実装例です。Mockitoを使った成功・失敗ケースと、保存しない条件の確かめ方を確認できます。 記事を読む

業務とDBの境界で迷うなら、システム設計の原則へ

『現場で役立つシステム設計の原則』は、2017年7月5日刊、ISBN 978-4-7741-9087-7を対象にします。公開目次では業務ロジック、アプリケーション機能、DBと業務モデルの関係を扱います。技術評論社の書誌・目次を確認できます。

読む入口の案は、第3章で業務判断の整理、第5章で機能の組み立て、第6章でDBとの関係、の順です。最新のSpring Bootの設定やJPAアノテーションを調べるための選定ではありません。既存の業務システムで「画面とテーブルが似ているから同じクラスでよいのか」と迷うときに、責務を考える材料として使います。

例えば、画面には確認用の文字列や選択肢の表示名が必要でも、料金判断に必要なのは単価と数量だけです。DBには受注日時や管理用の列があっても、それらすべてを送料計算へ渡す必要はありません。UIやテーブルに項目が増えるたびに業務計算も変更されるなら、どの都合が境界を越えたかを書き出します。

課題では、OrderService が料金判断を呼び、正常な結果だけを OrderRepository へ渡します。料金のクラスはSQLや画面名を知りません。実プロジェクトへ移すときは、トランザクションの開始位置、保存失敗時の扱い、複数処理が同時に動いた場合の制約を別に確認します。「インターフェースを付けたからDBの問題も解決した」とは考えないでください。

今は単一の小さなCRUDを覚えている段階で、Javaの型やHTTPの流れがまだ追えないなら、まず機能を一つ動かす方が先です。この本の購入を急ぐより、Form、Service、Entityがどの値を必要とするかを紙に並べるところから始められます。逆に、実務で変更理由の違うコードを何度も一緒に直しているなら、そのメモを持って該当範囲へ進むと読み方が具体的になります。

独自課題:同じ注文入力で、失敗と修正を確かめる

ここからは書籍の掲載コードではなく、この記事用に作成した小さなJava課題です。業務ルールは「単価0円以上」「数量1〜99」「商品小計1万円以上で送料0円、それ未満は500円」とします。税、割引、実決済、実際のDB、複数通貨は扱いません。金額の単位は円に固定し、実システムの価格設計へそのまま流用する前に業務条件を確認してください。

検証環境は Java 17.0.8、JUnit Jupiter 5.12.2、Maven 3.9.16 です。ZIPには pom.xml、3つの本体クラス、JUnitのテストクラス、READMEを入れています。生成されたclassファイルやtargetフォルダは含めません。各クラスはファイル名どおりに配置してから、プロジェクトのフォルダで mvn test を実行します。

この記事のサンプルコード

同じ注文入力で、故障検出と修正を確かめる

故障版と修正版の本体3クラス、JUnitテスト、pom.xml、実行手順のREADMEを含む独自課題です。料金の境界値、不正入力の未保存、正常な結果の保存を確認できます。

  • Java 17
  • JUnit 5.12.2
  • 4ケース検証済み

用意するものJava 17・Maven 3.9系

解凍後、design-testing-order-sampleのREADME.mdを開き、そのフォルダでmvn testを実行します。初回の依存ライブラリ取得にはネット接続が必要です。

架空の受注ルールです。Spring・実DB・実決済・HTTP画面は使わず、保存先はメモリ上の代役です。書籍掲載コードの転載ではありません。

Before:料金判断と保存が混ざり、境界値も間違う

次の故障版は、料金を決める条件と保存を同じメソッドへ置いています。さらに > になっているため、単価5,000円・数量2なら小計は1万円なのに送料500円が加わります。下のテストはこの同じ入力を修正版にも渡します。数量0のような不正入力を受け止める場所も、この版にはありません。

import java.util.ArrayList;
import java.util.List;

final class LegacyOrderService {
    private final List<Integer> savedTotals = new ArrayList<>();

    int place(int unitPrice, int quantity) {
        // Before: 料金の判断と保存が混在。ちょうど1万円でも送料が付く
        int subtotal = unitPrice * quantity;
        int total = subtotal + (subtotal > 10_000 ? 0 : 500);
        savedTotals.add(total);
        return total;
    }
}

After:業務判断と保存の役割を分ける

OrderPricing.java は、入力から見積結果を作るところに絞ります。送料の判断と数量範囲はここで一度だけ決め、結果を Quote へまとめます。整数の乗算・加算にはJavaの Math.multiplyExact と Math.addExact を使っています。桁あふれを別の値として通さないための選択です。Java 17のMath APIで仕様を確認できます。大きい金額を扱う実業務では、型や上限の設計を別途行ってください。

final class OrderPricing {
    private static final int FREE_SHIPPING_THRESHOLD = 10_000;
    private static final int SHIPPING_FEE = 500;

    static Quote quote(int unitPrice, int quantity) {
        if (unitPrice < 0 || quantity < 1 || quantity > 99) {
            throw new IllegalArgumentException("単価は0以上、数量は1〜99です");
        }
        int subtotal = Math.multiplyExact(unitPrice, quantity);
        int shippingFee = subtotal >= FREE_SHIPPING_THRESHOLD ? 0 : SHIPPING_FEE;
        return new Quote(subtotal, shippingFee);
    }

    record Quote(int subtotal, int shippingFee) {
        int total() {
            return Math.addExact(subtotal, shippingFee);
        }
    }
}

OrderService.java は、正常な見積結果ができてから保存を依頼します。入力が不正なら見積作成で例外になり、保存には進みません。保存先の実装をここに書き込まないので、この学習ではメモリ上の代役へ差し替えられます。この切り分け自体が目的ではなく、料金の条件を変えたときに保存処理まで触らずに済むかを確かめます。

import java.util.Objects;

final class OrderService {
    private final OrderRepository orders;

    OrderService(OrderRepository orders) {
        this.orders = Objects.requireNonNull(orders);
    }

    OrderPricing.Quote submit(int unitPrice, int quantity) {
        OrderPricing.Quote quote = OrderPricing.quote(unitPrice, quantity);
        orders.save(quote);
        return quote;
    }

    interface OrderRepository {
        void save(OrderPricing.Quote quote);
    }
}

JUnit:失敗を検出するケースも、テスト全体は成功する

OrderDesignTest.java の最初のケースは、故障版の戻り値が期待する1万円と一致しないことを assertThrows(AssertionFailedError.class, ...) で確認します。次に修正版を同じ入力で呼び、1万円になることを普通の assertEquals で確かめます。故障を囲んでいるアサーションと、修正後の成功を混同しないための構成です。アサーションの動きはJUnit 5.12.2のAPIも参照できます。

import java.util.ArrayList;
import java.util.List;

import org.junit.jupiter.api.Test;
import org.opentest4j.AssertionFailedError;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

class OrderDesignTest {
    @Test
    void sameInputExposesShippingBugAndFix() {
        LegacyOrderService before = new LegacyOrderService();
        assertThrows(AssertionFailedError.class,
                () -> assertEquals(10_000, before.place(5_000, 2)));

        MemoryOrders orders = new MemoryOrders();
        OrderService after = new OrderService(orders);
        assertEquals(10_000, after.submit(5_000, 2).total());
    }

    @Test
    void publicResultsCoverShippingAndQuantityBoundaries() {
        assertEquals(5_500, OrderPricing.quote(5_000, 1).total());
        assertEquals(10_000, OrderPricing.quote(5_000, 2).total());
        assertEquals(10_400, OrderPricing.quote(100, 99).total());
    }

    @Test
    void invalidInputIsNotSaved() {
        MemoryOrders orders = new MemoryOrders();
        OrderService service = new OrderService(orders);
        assertThrows(IllegalArgumentException.class, () -> service.submit(5_000, 0));
        assertThrows(IllegalArgumentException.class, () -> service.submit(5_000, 100));
        assertThrows(IllegalArgumentException.class, () -> service.submit(-1, 1));
        assertTrue(orders.saved().isEmpty());
    }

    @Test
    void acceptedOrderIsSavedAsItsPublicResult() {
        MemoryOrders orders = new MemoryOrders();
        OrderPricing.Quote result = new OrderService(orders).submit(5_000, 2);
        assertEquals(new OrderPricing.Quote(10_000, 0), result);
        assertEquals(List.of(result), orders.saved());
    }

    static final class MemoryOrders implements OrderService.OrderRepository {
        private final List<OrderPricing.Quote> quotes = new ArrayList<>();

        @Override
        public void save(OrderPricing.Quote quote) {
            quotes.add(quote);
        }

        List<OrderPricing.Quote> saved() {
            return List.copyOf(quotes);
        }
    }
}
確認した内容実行で確認した結果
同じ5,000円×2の注文故障版は10,500円になり期待値比較が失敗。修正版は10,000円
送料条件と数量の境界5,000円×1は5,500円、5,000円×2は10,000円、100円×99は10,400円
数量0・100、単価マイナスIllegalArgumentExceptionを検出し、保存先の代役は空のまま
正常な注文戻り値が見積内容と一致し、その結果1件だけが保存対象になる

実行結果は Tests run: 4, Failures: 0, Errors: 0, Skipped: 0 でした。故障版の比較が成功したのではありません。故障版では期待したアサーション失敗を検出でき、修正版では業務上の期待値が通ったので、テスト全体が成功しています。不正入力の例外も同じ考え方で検出しています。

この4件から分かるのは、メモリ上の保存先に対して正常な結果だけを渡すことまでです。SQL、DB制約、トランザクション、実際のHTTP画面は検証していません。業務ルールの単体テストが通った後、保存先を実装した段階では実DBとの統合テストを追加します。テストを種類ごとに分けると、失敗したときにどの境界を調べるかも説明しやすくなります。

読んだあと、レビューで何を説明できれば合格か

本を開く前に、直す範囲を一つの変更へ絞ります。最初の日は既存の挙動を確かめるテストを書き、次に送料判断を移し、その後で名前やメソッド構造を整理する、と分けて進めると差分を追えます。機能追加と大規模な構成変更を同時に行うと、どちらで挙動が変わったか調べにくくなります。

読んだ章を説明するだけでなく、変更前後のコードを並べ、「無料配送の条件はここだけを変える」「数量不正では保存されない」「DB制約はこのテストの対象外」と言えるかを確認してください。設計の良し悪しをクラス数やコード行数だけで評価せず、次の変更の入口と、壊れたときに気付く条件をレビューへ持ち込みます。

// レビューコメント例
送料条件が画面とCSVで重複しています。
共通の判断を一か所へ寄せ、同じ入力で結果を確認してください。

// レビューコメント例
privateメソッドの名前より、返す金額と未保存の条件を確かめたいです。
実DBの制約を確認したテストか、メモリ上の代役かも分けて説明してください。

レビューへの返信も、単に「本に書いてあったので分けました」で終わらせません。「画面とCSVで共通の送料条件を一つへ寄せました。5,000円×2と数量0の結果を確認しています。保存先のDB制約は次の統合テストで確認します」と書けば、今回の変更で確かめたことと残る範囲が伝わります。

本が今は不要な人もいます。短いJavaコードをまだ自分で実行できない、テストの期待値が業務上正しいか未確認、そもそも修正対象の仕様を聞けていない場合です。購入でその不足を飛び越えようとせず、コンパイル、最小テスト、仕様の確認の順に戻ります。逆に、同じ種類の迷いが続くなら、今回の小さな課題で引っかかった章を次に読む目的にできます。

購入前・提出前のセルフチェック

  • 困っている変更を一つの具体例で書けたか
  • 書名だけでなく、対象版と出版社目次を確認したか
  • C#の例と自分のJava環境の違いを理解しているか
  • 修正前と修正後へ同じ業務入力を渡しているか
  • 不正入力、金額や数量の境界、未保存の条件を確かめたか
  • privateメソッドの形を理由なくテストへ固定していないか
  • 実DB・HTTP・外部サービスを確認したか、未確認なら区別できるか
  • 読了や購入より先に、小さな変更を説明できるようになったか

テストの判断を体系的に整理したい人の1冊

今回の課題を動かした後、「テストは書けるが、どこまで固定すると壊れやすいか」に迷い続ける人には、『単体テストの考え方/使い方』を候補にします。選ぶ理由は、このページで扱った公開結果と実装の詳細の区別を、継続して判断するためです。まず出版社目次と自分の課題が合うかを確かめ、必要な章から読み進めてください。

書籍「単体テストの考え方/使い方」の表紙
商品画像:Amazon.co.jp
PR テストを学ぶ若手向け

単体テストの考え方/使い方

Vladimir Khorikov (著), 須田智之 (翻訳)

単体テストで「何を検証するか」の判断基準を学ぶ。

モックの使いどころや壊れにくいテストの考え方を整理できます。コード例はC#です。JUnitの操作入門ではなく、テストの設計と価値を深める本です。

  • テスト対象の切り分けに迷っている
  • 保守しやすい単体テストを書きたい

当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。

この記事とあわせて読みたい

画面入力のクラスをそのまま業務処理へ渡すか迷うときと、形式・業務チェックの責務を分けたいときの確認先です。本選びで終わらせず、今のSpringコードへ判断を移すために使えます。

関連記事Spring BootのFormとDTOの違い|Serviceへ渡すクラスを分ける判断 画面入力のFormと業務処理へ渡す値をどう分けるかを、変換コードで確認できます。画面都合が業務処理へ広がるときの次の確認先です。 記事を読む 関連記事Spring Bootで入力チェックが重複する|ControllerとServiceの分け方 入力形式と業務上の判断をController・Serviceへどう分担するかを整理します。責務の重複や、業務エラーを返す場所で迷うときに役立ちます。 記事を読む

まとめ

変更箇所が散るなら責務のまとまりから、テストが内部変更で壊れるなら観察する結果から、業務とDBの境界で迷うなら値と処理の受け渡しから考えます。3冊を最初から読み切るより、一つの困りごとに対応する範囲を選び、同じ入力で変更前後を確かめる方が、仕事の判断へつなげやすくなります。

今回の課題では、故障版の送料誤りを検出し、修正版の金額、不正入力の未保存、正常な結果の保存まで確認しました。次に自分のコードで行う変更と、まだ確かめていない境界を一つずつ書き出せれば、本を読む目的も具体的になります。

-技術書・書評
-, , , ,