Javaにおけるテスト容易性1の話をするとき、避けて通れないのがアクセス修飾子2の問題です。特にpublicメソッド3とprivateメソッド4のテストのしやすさの差は、まるで天国と地獄ほどの違いがあります。
本記事では、なぜpublicメソッドがこれほどまでにテストしやすいのか、そしてprivateメソッドの呪縛から抜け出すための設計手法について、実例を交えながら探求していきます。さらに、見落とされがちなパッケージプライベートの活用法、Java 9以降のモジュールシステムへの対応、そして上級者向けの高度なテスト戦略まで、包括的に解説します。
Publicメソッドの圧倒的なテスト容易性
まずは、publicメソッドがいかにテストしやすいかを実感していただきましょう。以下のコードを見てください。
public class PriceCalculator {
// Publicメソッド:シンプルで美しいテストが可能
public Money calculateSubtotal(Money unitPrice, int quantity) {
// BigDecimalベースのMoneyクラスで精度を保証
return unitPrice.multiply(quantity);
}
public Money applyDiscount(Money subtotal, DiscountRate discountRate) {
return subtotal.multiply(discountRate.getMultiplier());
}
public Money addTax(Money amount, TaxRate taxRate) {
return amount.multiply(BigDecimal.ONE.add(taxRate.getRate()));
}
}
// Moneyクラス:金額計算の精度を保証
public final class Money {
private final BigDecimal amount;
private final Currency currency;
public static Money of(String amount, String currencyCode) {
return new Money(
new BigDecimal(amount),
Currency.getInstance(currencyCode)
);
}
public Money multiply(int quantity) {
return new Money(
amount.multiply(BigDecimal.valueOf(quantity)),
currency
);
}
public Money multiply(BigDecimal factor) {
return new Money(
amount.multiply(factor)
.setScale(currency.getDefaultFractionDigits(), RoundingMode.HALF_UP),
currency
);
}
}
// テストコード:これ以上ないほどシンプル
@Test
void testPriceCalculation() {
PriceCalculator calculator = new PriceCalculator();
// 直接呼び出し可能、型安全、IDEサポート完備
Money subtotal = calculator.calculateSubtotal(
Money.of("100.00", "JPY"), 5
);
assertEquals(Money.of("500.00", "JPY"), subtotal);
Money discounted = calculator.applyDiscount(
subtotal, DiscountRate.of(0.1)
);
assertEquals(Money.of("450.00", "JPY"), discounted);
Money withTax = calculator.addTax(
discounted, TaxRate.of(0.08)
);
assertEquals(Money.of("486.00", "JPY"), withTax);
}
このテストコードの美しさは、以下の点にあります。publicメソッドは直接呼び出し可能であり、コンパイラ5による型チェック6が効き、IDE7の自動補完8やリファクタリング機能9もフルに活用できます。メソッド名を変更しても、IDEが自動的にテストコードも更新してくれるため、保守性が非常に高いのです。
さらに重要なのは、テストの意図が明確であることです。何をテストしているのかが一目瞭然で、新しくプロジェクトに参加したメンバーでもすぐに理解できます。これは、チーム開発において非常に大きなメリットとなります。
見落とされがちな第3の選択肢:パッケージプライベート
多くの開発者は「public vs private」の二元論に陥りがちですが、Javaにはパッケージプライベート(修飾子なし)という強力な選択肢があります。
public class OrderService {
// Public: 外部に公開するAPI
public Order createOrder(Customer customer, List<Product> products) {
validateOrder(customer, products);
Order order = processOrder(customer, products);
notifyOrderCreated(order);
return order;
}
// Package-private: 同一パッケージからテスト可能
Order processOrder(Customer customer, List<Product> products) {
// パッケージ内のテストクラスから直接アクセス可能
// リフレクション不要でテスト容易性を確保
Money total = calculateOrderTotal(products);
applyCustomerDiscounts(customer, total);
return new Order(customer, products, total);
}
// Package-private: 複雑なビジネスロジックも安全にテスト可能
Money calculateOrderTotal(List<Product> products) {
return products.stream()
.map(Product::getPrice)
.reduce(Money.ZERO, Money::add);
}
void applyCustomerDiscounts(Customer customer, Money total) {
// 顧客ランクに応じた割引ロジック
// テストで直接検証可能
}
// Private: 真に内部的な実装詳細のみ
private void notifyOrderCreated(Order order) {
// 通知の実装詳細は隠蔽
}
}
// 同じパッケージ内のテストクラス
package com.example.order; // OrderServiceと同じパッケージ
@Test
class OrderServiceTest {
private OrderService service = new OrderService();
@Test
void testProcessOrder() {
// パッケージプライベートメソッドを直接テスト
Order order = service.processOrder(customer, products);
assertNotNull(order);
assertEquals(expectedTotal, order.getTotal());
}
@Test
void testCalculateOrderTotal() {
// 複雑な計算ロジックを独立してテスト
Money total = service.calculateOrderTotal(products);
assertEquals(Money.of("1500.00", "JPY"), total);
}
}
リフレクションという悪魔の誘惑
一方で、同じロジックをprivateメソッドで実装した場合、テストは途端に複雑になります。
public class PaymentService {
public Invoice processPayment(PaymentRequest request) {
// publicメソッドは薄いラッパーに過ぎない
if (!validatePaymentRequest(request)) {
throw new InvalidPaymentException();
}
double amount = calculateFinalAmount(request);
PaymentResult result = executePayment(request.getCard(), amount);
return createInvoice(request, result);
}
// ビジネスロジックの核心部分がprivate
private boolean validatePaymentRequest(PaymentRequest request) {
// カード有効性チェック
if (!isValidCardNumber(request.getCard().getNumber())) {
return false;
}
// 利用限度額チェック
if (exceedsCreditLimit(request.getCard(), request.getAmount())) {
return false;
}
// ブラックリストチェック
if (isBlacklisted(request.getCard())) {
return false;
}
// セキュリティチェック(3Dセキュア等)
return performSecurityCheck(request);
}
private double calculateFinalAmount(PaymentRequest request) {
double baseAmount = request.getAmount();
double processingFee = calculateProcessingFee(baseAmount);
double tax = calculateTax(baseAmount + processingFee);
// ポイント利用
if (request.hasPoints()) {
baseAmount -= calculatePointDiscount(request.getPoints());
}
// クーポン適用
if (request.hasCoupon()) {
baseAmount = applyCouponDiscount(baseAmount, request.getCoupon());
}
return baseAmount + processingFee + tax;
}
// その他多数のprivateメソッド...
}
このprivateメソッドをテストしようとすると、リフレクション10を使わざるを得ません。
@Test
void testPrivateCalculation() throws Exception {
PaymentService service = new PaymentService();
// リフレクションを使った醜いテスト
Method method = PaymentService.class
.getDeclaredMethod("calculateFinalAmount", PaymentRequest.class);
method.setAccessible(true); // カプセル化の破壊
// 型安全性の喪失、実行時エラーの可能性
double amount = (double) method.invoke(service, testRequest);
// このテストは以下の問題を抱えている:
// 1. メソッド名の文字列に依存(タイポしてもコンパイルエラーにならない)
// 2. 引数の型が変わってもコンパイル時に検出できない
// 3. リファクタリング時に壊れやすい
// 4. IDEのサポートが受けられない
// 5. 例外処理が煩雑
// 6. Java 9+のモジュールシステムでは追加の設定が必要
}
// Java 9以降では、モジュールシステムによりさらに複雑に
@Test
void testPrivateMethodInModularSystem() throws Exception {
// モジュールシステムでは以下のような例外が発生する可能性
try {
method.setAccessible(true);
} catch (InaccessibleObjectException e) {
// --add-opens オプションが必要
// java --add-opens com.example/com.example.payment=ALL-UNNAMED
fail("Module system prevents reflection access");
}
}
リフレクションを使ったテストは、保守性が著しく低下します。メソッド名を変更しただけでテストが壊れ、しかもそれがランタイム11まで発覚しません。これは時限爆弾のようなもので、いつか必ず問題を引き起こします。
まさに「悪魔の誘惑」という名にふさわしく、一時的な解決をもたらすように見えて、長期的には大きな技術的負債を生み出すのです。
なぜPrivateメソッドをテストしたくなるのか
開発者がprivateメソッドをテストしたくなる理由は明確です。多くの場合、ビジネスロジックの核心部分がprivateメソッドに実装されているからです。publicメソッドは単なる窓口に過ぎず、実際の処理はprivateメソッドが担っています。
public class InventoryManager {
// Publicメソッドは単純な窓口
public InventoryUpdateResult updateInventory(Order order) {
if (!canProcessOrder(order)) {
return InventoryUpdateResult.rejected("Insufficient inventory");
}
List<InventoryMovement> movements = calculateMovements(order);
applyMovements(movements);
updateReorderPoints();
return InventoryUpdateResult.success(movements);
}
// 以下のprivateメソッドこそがビジネスロジックの本体
private boolean canProcessOrder(Order order) {
// 在庫チェックロジック
for (OrderItem item : order.getItems()) {
Stock stock = findStock(item.getProduct());
if (stock.getAvailable() < item.getQuantity()) {
// 予約在庫の確認
if (!checkReservedStock(item)) {
// 入荷予定の確認
if (!checkIncomingStock(item)) {
return false;
}
}
}
}
return true;
}
private List<InventoryMovement> calculateMovements(Order order) {
List<InventoryMovement> movements = new ArrayList<>();
for (OrderItem item : order.getItems()) {
// FIFO/LIFO/平均法などの在庫評価方法を適用
List<StockBatch> batches = selectBatches(item);
for (StockBatch batch : batches) {
InventoryMovement movement = new InventoryMovement(
batch.getId(),
item.getQuantity(),
batch.getCost(),
MovementType.SHIPMENT
);
movements.add(movement);
}
}
return movements;
}
private void updateReorderPoints() {
// 発注点の動的な更新
// 過去の需要データから安全在庫を計算
// リードタイムを考慮した発注点の設定
// 季節変動の考慮
// これら全てをテストで保証したい!
}
}
このような状況では、privateメソッドをテストしないことは品質リスクを抱えることになります。しかし、リフレクションを使ったテストは保守性を犠牲にします。これがJava開発者のジレンマです。
テストのためのPublic化という誤った解決策
このジレンマに直面した開発者が陥りがちなのが、「テストのためのpublic化」です。
public class ShippingCalculator {
// 悪い例:本来privateであるべきメソッドをpublicに
@VisibleForTesting // 言い訳のためのアノテーション
public double calculateDimensionalWeight(Package pkg) {
// 内部実装の詳細が外部に露出
double volumetricWeight = (pkg.getLength() * pkg.getWidth() * pkg.getHeight()) / 5000.0;
return Math.max(volumetricWeight, pkg.getActualWeight());
}
@Deprecated // 「使わないで」という願望
public double calculateZoneSurcharge(Address from, Address to) {
// でも公開APIなので誰かが使ってしまう可能性
int zoneDistance = calculateZoneDistance(from, to);
return zoneDistance * 50.0; // 内部の料金体系が露出
}
// さらに悪い例:内部状態まで公開
@TestOnly // 効果のないアノテーション
public Map<String, Double> getInternalRateTable() {
return Collections.unmodifiableMap(rateTable);
// 内部データ構造が露出し、将来の変更が困難に
}
}
この方法には重大な問題があります。カプセル化12の原則を破壊し、内部実装の詳細を外部に露出させてしまうのです。これにより、以下のような問題が発生します。
- 意図しない使用:外部のコードがこれらのメソッドを使い始める可能性
- 変更の困難化:一度公開したAPIは簡単に変更できない
- 責任の曖昧化:クラスの公開インターフェースが肥大化し、責任が不明確に
- 依存関係の増加:他のクラスがこれらのメソッドに依存し始める
- セマンティックバージョニングの破壊:パッチバージョンで削除できない
テストのためにpublicにすることは、まさに「悪魔に魂を売る」ような行為なのです。
Privateメソッドテストに関する哲学的議論
視点1:Martin FowlerとKent Beckの主張
テスト駆動開発(TDD)の提唱者たちは、「privateメソッドは直接テストすべきではない」という立場を取ります。
// Kent Beckのアプローチ:ビヘイビアをテストする
public class AccountServiceTest {
@Test
void shouldTransferMoneyBetweenAccounts() {
// Given
Account source = new Account("A001", Money.of("1000.00", "JPY"));
Account target = new Account("A002", Money.of("500.00", "JPY"));
AccountService service = new AccountService();
// When
TransferResult result = service.transfer(source, target, Money.of("300.00", "JPY"));
// Then
assertThat(result.isSuccessful()).isTrue();
assertThat(source.getBalance()).isEqualTo(Money.of("700.00", "JPY"));
assertThat(target.getBalance()).isEqualTo(Money.of("800.00", "JPY"));
// privateメソッドの動作は、publicインターフェースを通じて間接的に検証される
}
}
視点2:統合テストによるカバレッジ
privateメソッドは統合テストで自然にカバーされるべきという考え方です。
@SpringBootTest
@AutoConfigureMockMvc
public class OrderProcessingIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
void shouldProcessCompleteOrderFlow() throws Exception {
// API経由で注文を作成
mockMvc.perform(post("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content(orderJson))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.status").value("PENDING"));
// 支払い処理
mockMvc.perform(post("/api/orders/{id}/payment", orderId)
.contentType(MediaType.APPLICATION_JSON)
.content(paymentJson))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("PAID"));
// この統合テストは、多数のprivateメソッドを間接的にテストしている
// カバレッジツールで確認すると、privateメソッドも含めて高いカバレッジを達成
}
}
依存性注入によるテスタビリティの向上
では、どうすればよいのでしょうか。一つの解決策は依存性注入(DI)13の活用です。DIを使うことで、外部依存を分離し、テストしやすい設計を実現できます。
// Before: 密結合でテスト困難
public class NotificationService {
private final SmtpClient smtpClient = new SmtpClient("mail.server.com");
private final SmsGateway smsGateway = new SmsGateway("api.sms.com");
public void notifyCustomer(Customer customer, Order order) {
String message = generateOrderMessage(order); // privateメソッド
if (customer.prefersEmail()) {
sendEmail(customer.getEmail(), message); // privateメソッド
} else {
sendSms(customer.getPhone(), message); // privateメソッド
}
logNotification(customer, order, message); // privateメソッド
}
private String generateOrderMessage(Order order) {
// 複雑なメッセージ生成ロジック
StringBuilder sb = new StringBuilder();
sb.append("ご注文ありがとうございます。\n");
sb.append("注文番号: ").append(order.getId()).append("\n");
// ... 多くのビジネスロジック
return sb.toString();
}
private void sendEmail(String address, String message) {
// SMTP経由でメール送信
smtpClient.send(address, "ご注文確認", message);
}
}
// After: 依存性注入でテスタブルに
public class NotificationService {
private final MessageGenerator messageGenerator;
private final EmailSender emailSender;
private final SmsSender smsSender;
private final NotificationLogger logger;
// コンストラクタ注入
public NotificationService(
MessageGenerator messageGenerator,
EmailSender emailSender,
SmsSender smsSender,
NotificationLogger logger) {
this.messageGenerator = messageGenerator;
this.emailSender = emailSender;
this.smsSender = smsSender;
this.logger = logger;
}
public void notifyCustomer(Customer customer, Order order) {
NotificationMessage message = messageGenerator.generateOrderMessage(order);
if (customer.prefersEmail()) {
emailSender.send(customer.getEmail(), message);
} else {
smsSender.send(customer.getPhone(), message);
}
logger.logNotification(customer, order, message);
}
}
// MessageGeneratorは独立してテスト可能
public class MessageGenerator {
private final MessageTemplateEngine templateEngine;
private final LocalizationService localization;
public NotificationMessage generateOrderMessage(Order order) {
// ロジックがpublicメソッドとして分離された
Map<String, Object> variables = new HashMap<>();
variables.put("orderId", order.getId());
variables.put("items", order.getItems());
variables.put("total", order.getTotal());
String template = templateEngine.load("order-confirmation");
String localizedMessage = localization.localize(template, order.getCustomer().getLocale());
return new NotificationMessage(
"ご注文確認",
templateEngine.render(localizedMessage, variables)
);
}
}
// テストが簡単に
@Test
class MessageGeneratorTest {
@Test
void shouldGenerateOrderMessageWithCorrectContent() {
MessageGenerator generator = new MessageGenerator(
new SimpleTemplateEngine(),
new JapaneseLocalizationService()
);
Order order = OrderTestBuilder.aTypicalOrder().build();
NotificationMessage message = generator.generateOrderMessage(order);
assertThat(message.getSubject()).isEqualTo("ご注文確認");
assertThat(message.getBody()).contains(order.getId());
assertThat(message.getBody()).contains(order.getTotal().toString());
}
}
高度なDI:コンパイル時DIによる最適化
実行時のオーバーヘッドを避けるため、Dagger2やMicronautのようなコンパイル時DIを使用できます。
// Dagger2を使ったコンパイル時DI
@Module
public class ServiceModule {
@Provides
@Singleton
MessageGenerator provideMessageGenerator(
MessageTemplateEngine templateEngine,
LocalizationService localization) {
return new MessageGenerator(templateEngine, localization);
}
@Provides
@Singleton
EmailSender provideEmailSender(@Named("smtp.host") String smtpHost) {
return new SmtpEmailSender(smtpHost);
}
}
@Component(modules = {ServiceModule.class})
@Singleton
public interface ApplicationComponent {
NotificationService notificationService();
}
// テスト用のモジュール
@Module
public class TestServiceModule {
@Provides
@Singleton
EmailSender provideEmailSender() {
return new InMemoryEmailSender(); // テスト用実装
}
}
神クラスアンチパターンとその解決
もう一つの重要なアプローチは、単一責任の原則(SRP)14に従ったクラス設計です。一つのクラスが多くの責任を持ちすぎると、必然的にprivateメソッドが増えてしまいます。これが「神クラス」と呼ばれるアンチパターンです。
神クラスは、あらゆる機能を一つのクラスに詰め込んだ結果生まれる怪物です。その内部は複雑に絡み合ったprivateメソッドの迷宮となり、テストは困難を極めます。
// 神クラスの典型例:責任過多でprivateメソッドだらけ
public class OrderManager {
// 2000行を超える巨大クラス
public Order createOrder(Customer customer, List<Product> products) {
// 多数のprivateメソッドを呼び出す
validateCustomer(customer);
checkCreditLimit(customer);
validateProducts(products);
checkInventory(products);
double subtotal = calculateSubtotal(products);
double discount = calculateDiscounts(customer, products, subtotal);
double shipping = calculateShipping(customer.getAddress(), products);
double tax = calculateTax(subtotal, discount, shipping);
Order order = new Order();
order.setCustomer(customer);
order.setItems(createOrderItems(products));
order.setSubtotal(subtotal);
order.setDiscount(discount);
order.setShipping(shipping);
order.setTax(tax);
order.setTotal(subtotal - discount + shipping + tax);
persistOrder(order);
updateInventory(order);
sendNotification(customer, order);
updateAnalytics(order);
scheduleDelivery(order);
return order;
}
// 以下、大量のprivateメソッドが続く...
private void validateCustomer(Customer customer) {
if (customer == null) {
throw new IllegalArgumentException("Customer cannot be null");
}
if (!customer.isActive()) {
throw new BusinessException("Customer is not active");
}
// さらに多くの検証ロジック
}
private void checkCreditLimit(Customer customer) {
double currentCredit = getCreditUsage(customer);
double limit = customer.getCreditLimit();
if (currentCredit >= limit) {
throw new CreditLimitExceededException();
}
}
private void checkInventory(List<Product> products) {
for (Product product : products) {
int available = getAvailableQuantity(product);
if (available <= 0) {
throw new OutOfStockException(product);
}
}
}
private double calculateSubtotal(List<Product> products) {
return products.stream()
.mapToDouble(p -> p.getPrice() * p.getQuantity())
.sum();
}
private double calculateDiscounts(Customer customer, List<Product> products, double subtotal) {
double discount = 0;
// ボリュームディスカウント
if (products.size() > 10) {
discount += subtotal * 0.05;
}
// 会員ランク割引
switch (customer.getMembershipLevel()) {
case GOLD:
discount += subtotal * 0.15;
break;
case SILVER:
discount += subtotal * 0.10;
break;
case BRONZE:
discount += subtotal * 0.05;
break;
}
// 期間限定キャンペーン
if (isPromotionPeriod()) {
discount += subtotal * 0.10;
}
return Math.min(discount, subtotal * 0.30); // 最大30%割引
}
// まだまだ続く...全てprivateでテスト困難
}
// After: 責任ごとにクラスを分割し、神クラスを退治
public class OrderService {
private final CustomerValidator customerValidator;
private final CreditChecker creditChecker;
private final InventoryService inventoryService;
private final PricingService pricingService;
private final OrderRepository orderRepository;
private final NotificationService notificationService;
private final DeliveryScheduler deliveryScheduler;
// 依存性注入で各サービスを受け取る
public OrderService(/* 依存性の注入 */) {
// ...
}
public Order createOrder(Customer customer, List<Product> products) {
// 各責任を専門のサービスに委譲
customerValidator.validate(customer);
creditChecker.checkLimit(customer);
inventoryService.checkAvailability(products);
PricingResult pricing = pricingService.calculatePricing(customer, products);
Order order = Order.builder()
.customer(customer)
.items(products)
.pricing(pricing)
.build();
orderRepository.save(order);
inventoryService.reserve(order);
notificationService.notifyOrderCreated(order);
deliveryScheduler.schedule(order);
return order;
}
}
// 各サービスは単一責任を持ち、publicメソッドでテスト可能
@Service
public class CustomerValidator {
private final CustomerRepository repository;
private final BlacklistService blacklistService;
public ValidationResult validate(Customer customer) {
// publicメソッドとして独立してテスト可能
if (customer == null) {
return ValidationResult.error("Customer cannot be null");
}
if (!customer.isActive()) {
return ValidationResult.error("Customer is not active");
}
if (blacklistService.isBlacklisted(customer)) {
return ValidationResult.error("Customer is blacklisted");
}
return ValidationResult.success();
}
}
@Service
public class PricingService {
private final DiscountCalculator discountCalculator;
private final TaxCalculator taxCalculator;
private final ShippingCalculator shippingCalculator;
public PricingResult calculatePricing(Customer customer, List<Product> products) {
Money subtotal = calculateSubtotal(products);
Money discount = discountCalculator.calculate(customer, products, subtotal);
Money shipping = shippingCalculator.calculate(customer.getAddress(), products);
Money tax = taxCalculator.calculate(subtotal.subtract(discount).add(shipping));
return PricingResult.builder()
.subtotal(subtotal)
.discount(discount)
.shipping(shipping)
.tax(tax)
.total(subtotal.subtract(discount).add(shipping).add(tax))
.build();
}
// パッケージプライベートで適度な公開
Money calculateSubtotal(List<Product> products) {
return products.stream()
.map(p -> p.getPrice().multiply(p.getQuantity()))
.reduce(Money.ZERO, Money::add);
}
}
この設計により、各クラスは単一の責任を持ち、そのコア機能をpublicメソッドとして公開します。結果として、全てのビジネスロジックが自然にテスト可能になるのです。
ドメイン駆動設計における値オブジェクトの活用
DDD15のアプローチでは、**値オブジェクト16**を活用することで、複雑なロジックをテストしやすい形で実装できます。
// 値オブジェクト:immutableでテストしやすい
public final class Money {
private final BigDecimal amount;
private final Currency currency;
// プライベートコンストラクタ
private Money(BigDecimal amount, Currency currency) {
this.amount = Objects.requireNonNull(amount, "Amount cannot be null");
this.currency = Objects.requireNonNull(currency, "Currency cannot be null");
validateAmount(amount);
}
// ファクトリメソッド
public static Money of(String amount, String currencyCode) {
return new Money(
new BigDecimal(amount),
Currency.getInstance(currencyCode)
);
}
public static Money of(BigDecimal amount, Currency currency) {
return new Money(amount, currency);
}
// ゼロ値の定数
public static final Money ZERO = Money.of("0", "JPY");
// ビジネスロジックは全てpublicメソッド
public Money add(Money other) {
validateSameCurrency(other);
return new Money(amount.add(other.amount), currency);
}
public Money subtract(Money other) {
validateSameCurrency(other);
BigDecimal result = amount.subtract(other.amount);
if (result.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Money cannot be negative");
}
return new Money(result, currency);
}
public Money multiply(int factor) {
return multiply(BigDecimal.valueOf(factor));
}
public Money multiply(BigDecimal factor) {
return new Money(
amount.multiply(factor)
.setScale(currency.getDefaultFractionDigits(), RoundingMode.HALF_UP),
currency
);
}
public Money applyTaxRate(TaxRate taxRate) {
return multiply(BigDecimal.ONE.add(taxRate.getRate()));
}
// 比較メソッド
public boolean isGreaterThan(Money other) {
validateSameCurrency(other);
return amount.compareTo(other.amount) > 0;
}
public boolean isLessThan(Money other) {
validateSameCurrency(other);
return amount.compareTo(other.amount) < 0;
}
// privateメソッドは最小限に
private void validateSameCurrency(Money other) {
if (!currency.equals(other.currency)) {
throw new CurrencyMismatchException(
String.format("Cannot operate on different currencies: %s and %s",
currency, other.currency)
);
}
}
private static void validateAmount(BigDecimal amount) {
if (amount.scale() > 4) {
throw new IllegalArgumentException("Amount scale cannot exceed 4");
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Money money = (Money) o;
return amount.compareTo(money.amount) == 0 &&
currency.equals(money.currency);
}
@Override
public int hashCode() {
return Objects.hash(amount.stripTrailingZeros(), currency);
}
}
// 値オブジェクトのテストは純粋で簡単
@Test
class MoneyTest {
@Test
void testMoneyArithmetic() {
Money price = Money.of("100.00", "JPY");
Money tax = price.applyTaxRate(TaxRate.of(0.1));
assertEquals(Money.of("110.00", "JPY"), tax);
}
@Test
void testMoneyAddition() {
Money first = Money.of("100.50", "JPY");
Money second = Money.of("200.30", "JPY");
Money sum = first.add(second);
assertEquals(Money.of("300.80", "JPY"), sum);
}
@Test
void shouldThrowExceptionForDifferentCurrencies() {
Money jpy = Money.of("100", "JPY");
Money usd = Money.of("100", "USD");
assertThrows(CurrencyMismatchException.class, () -> jpy.add(usd));
}
// Property-based testing で不変条件を検証
@Property
void moneyAdditionIsCommutative(
@ForAll @BigRange(min = "0", max = "10000") BigDecimal a,
@ForAll @BigRange(min = "0", max = "10000") BigDecimal b
) {
Money m1 = Money.of(a, Currency.getInstance("JPY"));
Money m2 = Money.of(b, Currency.getInstance("JPY"));
assertEquals(m1.add(m2), m2.add(m1));
}
}
値オブジェクトは不変(immutable)17であり、副作用18がないため、テストが非常に簡単です。また、ビジネスロジックが自然にpublicメソッドとして表現されるため、privateメソッドの問題に悩まされることがありません。
ビルダーパターンによるテストデータの構築
テストを書きやすくするもう一つの重要な要素は、テストデータの構築方法です。ビルダーパターン19を使用することで、複雑なオブジェクトも簡単に作成できます。
public class CustomerTestBuilder {
private String id = UUID.randomUUID().toString();
private String name = "Test Customer";
private CustomerType type = CustomerType.REGULAR;
private Money creditLimit = Money.of("100000", "JPY");
private Money totalPurchases = Money.of("0", "JPY");
private LocalDate memberSince = LocalDate.now();
private Address address = AddressTestBuilder.aDefaultAddress().build();
private ContactInfo contactInfo = ContactInfoTestBuilder.aDefaultContactInfo().build();
public static CustomerTestBuilder aCustomer() {
return new CustomerTestBuilder();
}
public static CustomerTestBuilder aPremiumCustomer() {
return new CustomerTestBuilder()
.withType(CustomerType.PREMIUM)
.withCreditLimit(Money.of("500000", "JPY"))
.withTotalPurchases(Money.of("1000000", "JPY"))
.withMemberSince(LocalDate.now().minusYears(3));
}
public static CustomerTestBuilder aNewCustomer() {
return new CustomerTestBuilder()
.withType(CustomerType.REGULAR)
.withTotalPurchases(Money.of("0", "JPY"))
.withMemberSince(LocalDate.now());
}
public CustomerTestBuilder withId(String id) {
this.id = id;
return this;
}
public CustomerTestBuilder withName(String name) {
this.name = name;
return this;
}
public CustomerTestBuilder withType(CustomerType type) {
this.type = type;
return this;
}
public CustomerTestBuilder withCreditLimit(Money creditLimit) {
this.creditLimit = creditLimit;
return this;
}
public CustomerTestBuilder withTotalPurchases(Money amount) {
this.totalPurchases = amount;
return this;
}
public CustomerTestBuilder withAddress(Address address) {
this.address = address;
return this;
}
public CustomerTestBuilder inTokyo() {
this.address = AddressTestBuilder.anAddress()
.withCity("Tokyo")
.withPostalCode("100-0001")
.build();
return this;
}
public Customer build() {
return new Customer(id, name, type, creditLimit,
totalPurchases, memberSince, address, contactInfo);
}
}
// 読みやすく保守しやすいテストコード
@Test
class DiscountCalculatorTest {
@Test
void shouldApplyPremiumCustomerDiscount() {
// Given
Customer premiumCustomer = CustomerTestBuilder.aPremiumCustomer()
.withName("山田太郎")
.withTotalPurchases(Money.of("2000000", "JPY"))
.build();
List<Product> products = Arrays.asList(
ProductTestBuilder.aProduct().withPrice(Money.of("10000", "JPY")).build(),
ProductTestBuilder.aProduct().withPrice(Money.of("20000", "JPY")).build()
);
// When
DiscountCalculator calculator = new DiscountCalculator();
Money discount = calculator.calculate(premiumCustomer, products);
// Then
assertEquals(Money.of("4500", "JPY"), discount); // 15%割引
}
@Test
void shouldApplyVolumeDiscount() {
// Given
Customer customer = CustomerTestBuilder.aCustomer().build();
List<Product> manyProducts = ProductTestBuilder.multipleProducts(20);
// When
Money discount = calculator.calculate(customer, manyProducts);
// Then
assertThat(discount.isGreaterThan(Money.ZERO)).isTrue();
}
}
Lombokを使った簡潔なビルダー
Lombok20を使用することで、ビルダーパターンをさらに簡潔に実装できます。
@Value
@Builder(toBuilder = true)
public class Order {
String id;
Customer customer;
@Singular List<OrderItem> items;
Money subtotal;
Money discount;
Money tax;
Money total;
OrderStatus status;
Instant createdAt;
// Lombokが自動的にビルダーを生成
}
// 使用例
@Test
void testOrderWithLombok() {
Order order = Order.builder()
.id("ORD-001")
.customer(CustomerTestBuilder.aCustomer().build())
.item(OrderItem.of(product1, 2))
.item(OrderItem.of(product2, 1))
.subtotal(Money.of("30000", "JPY"))
.discount(Money.of("3000", "JPY"))
.tax(Money.of("2700", "JPY"))
.total(Money.of("29700", "JPY"))
.status(OrderStatus.CONFIRMED)
.createdAt(Instant.now())
.build();
// toBuilderでコピーして一部変更
Order modifiedOrder = order.toBuilder()
.status(OrderStatus.SHIPPED)
.build();
}
モックオブジェクトの適切な使用
外部依存を持つクラスのテストでは、**モックオブジェクト21**の使用が不可欠です。Mockito22などのモックフレームワーク23を使用することで、複雑な依存関係も簡単にテストできます。
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock private PaymentGateway paymentGateway;
@Mock private InventoryService inventoryService;
@Mock private NotificationService notificationService;
@Mock private OrderRepository orderRepository;
@InjectMocks
private OrderService orderService;
@Test
void shouldProcessOrderSuccessfully() {
// Given
Order order = OrderTestBuilder.aTypicalOrder().build();
PaymentRequest paymentRequest = new PaymentRequest(
order.getCustomer().getCreditCard(),
order.getTotal()
);
// モックの振る舞いを定義
when(paymentGateway.charge(paymentRequest))
.thenReturn(PaymentResult.success("TXN-12345"));
when(inventoryService.checkAvailability(order.getItems()))
.thenReturn(AvailabilityResult.allAvailable());
// When
OrderResult result = orderService.processOrder(order);
// Then
assertTrue(result.isSuccessful());
assertEquals("TXN-12345", result.getTransactionId());
// 相互作用を検証
verify(inventoryService).reserve(order.getItems());
verify(notificationService).sendOrderConfirmation(order);
verify(orderRepository).save(order);
// 呼び出し順序も検証
InOrder inOrder = inOrder(paymentGateway, inventoryService, orderRepository);
inOrder.verify(inventoryService).checkAvailability(any());
inOrder.verify(paymentGateway).charge(any());
inOrder.verify(orderRepository).save(any());
}
@Test
void shouldHandlePaymentFailure() {
// Given
when(paymentGateway.charge(any()))
.thenReturn(PaymentResult.failure("Insufficient funds"));
// When
OrderResult result = orderService.processOrder(order);
// Then
assertFalse(result.isSuccessful());
assertEquals("Payment failed: Insufficient funds", result.getErrorMessage());
// 失敗時は在庫予約やDBへの保存が行われないことを確認
verify(inventoryService, never()).reserve(any());
verify(orderRepository, never()).save(any());
}
}
スパイとパーシャルモック
時には実際のオブジェクトの一部だけをモック化したい場合があります。
@Test
void testWithSpy() {
// 実際のオブジェクトをスパイ
List<String> realList = new ArrayList<>();
List<String> spyList = spy(realList);
// 特定のメソッドだけモック化
when(spyList.size()).thenReturn(100);
// 他のメソッドは実際の実装を使用
spyList.add("test");
assertTrue(spyList.contains("test"));
assertEquals(100, spyList.size()); // モック化された値
}
@Test
void testPartialMockingOfService() {
// 一部のメソッドだけテスト用に置き換え
OrderService partialMock = spy(orderService);
// 外部API呼び出しだけモック化
doReturn(true).when(partialMock).callExternalValidationApi(any());
// 他のロジックは実際の実装を使用
Order result = partialMock.createOrder(customer, products);
assertNotNull(result);
}
高度なテスト戦略
1. Property-Based Testing
個別のテストケースを超えて、システムの不変条件を検証します。
@PropertyDefaults(tries = 1000)
class MoneyPropertyTest {
@Property
void additionShouldBeCommutative(
@ForAll @BigRange(min = "0", max = "1000000") BigDecimal a,
@ForAll @BigRange(min = "0", max = "1000000") BigDecimal b
) {
Money m1 = Money.of(a, Currency.getInstance("JPY"));
Money m2 = Money.of(b, Currency.getInstance("JPY"));
assertEquals(m1.add(m2), m2.add(m1));
}
@Property
void multiplicationShouldBeDistributive(
@ForAll @DoubleRange(min = 0, max = 1000) double a,
@ForAll @DoubleRange(min = 0, max = 1000) double b,
@ForAll @DoubleRange(min = 0, max = 10) double factor
) {
Money m1 = Money.of(String.valueOf(a), "JPY");
Money m2 = Money.of(String.valueOf(b), "JPY");
Money distributed = m1.add(m2).multiply(BigDecimal.valueOf(factor));
Money separated = m1.multiply(BigDecimal.valueOf(factor))
.add(m2.multiply(BigDecimal.valueOf(factor)));
// 丸め誤差を考慮した比較
assertTrue(distributed.getAmount()
.subtract(separated.getAmount())
.abs()
.compareTo(BigDecimal.valueOf(0.01)) < 0);
}
}
2. Mutation Testing
テストの品質を検証するミューテーションテストを活用します。
// build.gradle
plugins {
id 'info.solidsoft.pitest' version '1.9.0'
}
pitest {
targetClasses = ['com.example.core.*']
targetTests = ['com.example.core.*Test']
mutationThreshold = 85
coverageThreshold = 90
outputFormats = ['HTML', 'XML']
timestampedReports = false
}
// 重要なビジネスロジックには高い基準を設定
@Tag("critical")
@MutationCoverage(threshold = 0.95)
class PriceCalculatorTest {
@Test
void criticalPriceCalculationMustBeRobust() {
// このテストは95%以上のミュータントを検出する必要がある
PriceCalculator calculator = new PriceCalculator();
// 境界値テスト
assertEquals(Money.ZERO, calculator.calculate(Collections.emptyList()));
// 通常ケース
List<Product> products = Arrays.asList(
new Product("P1", Money.of("100", "JPY"), 2),
new Product("P2", Money.of("200", "JPY"), 1)
);
assertEquals(Money.of("400", "JPY"), calculator.calculate(products));
// エラーケース
assertThrows(IllegalArgumentException.class,
() -> calculator.calculate(null));
}
}
3. Contract Testing
マイクロサービス間の契約を検証する手法です。
@SpringBootTest
@AutoConfigureMockMvc
public class PaymentServiceContractTest {
@Pact(consumer = "OrderService", provider = "PaymentService")
public RequestResponsePact createPact(PactDslWithProvider builder) {
return builder
.given("Valid payment method exists")
.uponReceiving("A payment request")
.path("/api/payments")
.method("POST")
.headers("Content-Type", "application/json")
.body(new PactDslJsonBody()
.numberType("amount", 10000)
.stringType("currency", "JPY")
.object("card")
.stringMatcher("number", "\\d{16}", "4111111111111111")
.stringMatcher("expiry", "\\d{2}/\\d{2}", "12/25")
.stringType("cvv", "123")
.closeObject()
.stringType("orderId", "ORD-12345")
)
.willRespondWith()
.status(200)
.body(new PactDslJsonBody()
.uuid("transactionId")
.stringValue("status", "APPROVED")
.numberType("amount", 10000)
.stringType("currency", "JPY")
.datetime("processedAt", "yyyy-MM-dd'T'HH:mm:ss'Z'")
)
.toPact();
}
@Test
@PactVerification("PaymentService")
void testPaymentContract() {
// Pactに基づいたテスト実行
PaymentRequest request = PaymentRequest.builder()
.amount(Money.of("10000", "JPY"))
.card(testCreditCard())
.orderId("ORD-12345")
.build();
PaymentResult result = paymentClient.processPayment(request);
assertEquals("APPROVED", result.getStatus());
assertNotNull(result.getTransactionId());
}
}
パフォーマンスを考慮したテスタブル設計
テスタブルな設計がパフォーマンスを犠牲にするという誤解がありますが、適切な設計により両立可能です。
// JMHによるマイクロベンチマーク
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
@Fork(value = 2, jvmArgs = {"-Xms2G", "-Xmx2G"})
public class PriceCalculationBenchmark {
@Param({"10", "100", "1000"})
private int itemCount;
private List<OrderItem> items;
private PriceCalculator abstractCalculator;
private DirectPriceCalculator directCalculator;
@Setup
public void setup() {
items = generateItems(itemCount);
abstractCalculator = new PriceCalculator(
new StandardDiscountStrategy(),
new JapaneseTaxCalculator()
);
directCalculator = new DirectPriceCalculator();
}
@Benchmark
public Money calculateWithAbstraction() {
return abstractCalculator.calculate(items);
}
@Benchmark
public Money calculateDirect() {
return directCalculator.calculate(items);
}
@Benchmark
@CompilerControl(CompilerControl.Mode.INLINE)
public Money calculateWithForcedInlining() {
// JITコンパイラに最適化のヒントを提供
return items.stream()
.map(OrderItem::getTotal)
.reduce(Money.ZERO, Money::add);
}
}
// 結果:適切な抽象化はJITコンパイラにより最適化され、
// 直接実装とほぼ同等のパフォーマンスを実現
GraalVM Native Imageへの対応
リフレクションを避けた設計は、GraalVM Native Imageとの相性も良好です。
// リフレクションフリーな設計
@Configuration
@ImportRuntimeHints(ApplicationRuntimeHints.class)
public class NativeConfiguration {
@Bean
@RegisterReflectionForBinding({Order.class, Customer.class, Product.class})
public OrderService orderService(
OrderRepository repository,
PriceCalculator calculator
) {
return new OrderServiceImpl(repository, calculator);
}
}
// ランタイムヒントの提供
public class ApplicationRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
// シリアライゼーション用
hints.serialization()
.registerType(Order.class)
.registerType(Money.class)
.registerType(Customer.class);
// リソースファイル
hints.resources()
.registerPattern("templates/*.html")
.registerPattern("messages/*.properties");
// プロキシ
hints.proxies()
.registerJdkProxy(OrderService.class)
.registerJdkProxy(PaymentGateway.class);
}
}
// Native Image用のテスト
@NativeImageTest
class NativeOrderServiceTest {
@Test
void testOrderCreationInNativeImage() {
// Native Imageでも動作することを確認
OrderService service = new OrderServiceImpl(
new InMemoryOrderRepository(),
new SimplePriceCalculator()
);
Order order = service.createOrder(testCustomer(), testProducts());
assertNotNull(order);
}
}
実践的なガイドラインとまとめ
テスタブル設計のための決定木
段階的な改善アプローチ
-
レガシーコードの場合
// Step 1: 最小限の変更でテスタビリティを向上 // privateメソッドをpackage-privateに変更 // Step 2: 依存性の注入を導入 // コンストラクタ経由で依存を注入 // Step 3: 責任の分離 // 大きなクラスを段階的に分割 // Step 4: 値オブジェクトの導入 // プリミティブな値を値オブジェクトに置換
-
新規開発の場合
- 最初からDIを前提とした設計
- ドメインモデルを中心とした設計
- テストファーストアプローチ
チェックリスト
- ビジネスロジックは適切に分離されているか?
- クラスは単一の責任を持っているか?
- 依存関係は注入可能になっているか?
- 値オブジェクトで表現できるロジックはないか?
- テストデータの構築は簡単か?
- モックの使用は適切か(過度でないか)?
- パッケージプライベートを活用しているか?
- Java 9+のモジュールシステムに対応しているか?
- パフォーマンスへの影響を測定したか?
- Native Imageでの動作を考慮したか?
まとめ:Publicメソッドの価値とテスタブル設計への道
Javaにおけるpublicメソッドの圧倒的なテスト容易性は、単なる言語仕様の話ではありません。それは良い設計への指針でもあるのです。
privateメソッドのテストに苦労しているとき、それは設計を見直すべきサインかもしれません。パッケージプライベートの活用、依存性注入、単一責任の原則、値オブジェクトなどの設計手法を適切に組み合わせることで、自然とテストしやすいコードが生まれます。
重要なのは、テストのためだけにアクセス修飾子を変更するのではなく、設計そのものを改善することです。良い設計は自然とテストしやすく、テストしやすいコードは保守性も高いのです。
また、Kent BeckやMartin Fowlerが主張するように、「privateメソッドを直接テストする必要はない」という視点も重要です。publicインターフェースを通じた振る舞いのテストで十分な場合も多いのです。
最後に、現代のJava開発では以下の点も考慮すべきです。
- Java 9以降のモジュールシステムへの対応
- GraalVM Native Imageでの動作
- マイクロサービスアーキテクチャでのContract Testing
- Property-based TestingやMutation Testingなどの高度なテスト手法
これらを総合的に考慮することで、真にテスタブルで保守性の高いJavaアプリケーションを構築できるのです。
-
テスト容易性(Testability) - ソフトウェアがどれだけ簡単かつ効果的にテストできるかを示す品質特性。観測可能性(Observability)と制御可能性(Controllability)の2つの側面から成る。高いテスト容易性は、バグの早期発見、保守性の向上、開発速度の向上につながる。 ↩
-
アクセス修飾子(Access Modifiers) - Javaにおいてクラス、メソッド、変数のアクセス範囲を制御するキーワード。public(どこからでもアクセス可能)、protected(同一パッケージと継承クラス)、package-private(修飾子なし、同一パッケージ内のみ)、private(同一クラス内のみ)の4種類がある。 ↩
-
publicメソッド - どのクラスからでもアクセス可能なメソッド。クラスの公開APIを構成し、外部との契約を定義する。セマンティックバージョニングにおいて、publicメソッドの変更は破壊的変更となる。 ↩
-
privateメソッド - 定義されたクラス内からのみアクセス可能なメソッド。内部実装の詳細を隠蔽し、カプセル化を実現する。リファクタリング時に自由に変更可能。 ↩
-
コンパイラ(Compiler) - ソースコードを機械語やバイトコードに変換するプログラム。Javaの場合、javacコマンドがJavaソースコードをバイトコードに変換する。静的型チェックやエラー検出を行う。 ↩
-
型チェック(Type Checking) - プログラムの実行前に、変数や式の型が正しく使用されているかを検証する機能。Javaは静的型付け言語であり、コンパイル時に厳密な型チェックが行われる。 ↩
-
IDE(Integrated Development Environment) - 統合開発環境。コードエディタ、デバッガ、ビルドツールなどを統合したソフトウェア開発ツール。JavaではIntelliJ IDEA、Eclipse、NetBeans、Visual Studio Codeなどが主要。 ↩
-
自動補完(Auto-completion) - IDEが提供する機能で、入力中のコードを文脈に応じて自動的に補完する。メソッド名、変数名、インポート文などを効率的に入力できる。Ctrl+SpaceやCmd+Spaceで起動することが多い。 ↩
-
リファクタリング機能 - IDEが提供する、コードの振る舞いを変えずに構造を改善する機能。メソッド名の変更、メソッドの抽出、クラスの移動などを安全に実行できる。使用箇所の自動更新も行われる。 ↩
-
リフレクション(Reflection) - 実行時にクラスの構造を調査し、動的にメソッドを呼び出したり、フィールドにアクセスしたりする機能。強力だが、型安全性を失い、パフォーマンスも低下する。Java 9以降はモジュールシステムにより制限が強化。 ↩
-
ランタイム(Runtime) - プログラムの実行時。コンパイル時(Compile-time)と対比される。リフレクションのエラーはランタイムでしか検出できない。実行時例外(RuntimeException)もこの時点で発生する。 ↩
-
カプセル化(Encapsulation) - オブジェクト指向プログラミングの基本原則の一つ。データとそれを操作するメソッドを一つのユニットにまとめ、内部実装を隠蔽することで、変更の影響を局所化する。情報隠蔽とも呼ばれる。 ↩
-
依存性注入(Dependency Injection, DI) - オブジェクトが必要とする依存関係を外部から注入する設計パターン。テスタビリティの向上、疎結合の実現、柔軟性の向上などのメリットがある。Spring Framework、Google Guice、Dagger2などが代表的な実装。 ↩
-
単一責任の原則(Single Responsibility Principle, SRP) - SOLID原則の一つ。クラスは単一の責任のみを持つべきという原則。これにより、変更の理由が一つに限定され、保守性が向上する。Robert C. Martin(Uncle Bob)により提唱された。 ↩
-
DDD(Domain-Driven Design) - ドメイン駆動設計。エリック・エヴァンスが提唱した、複雑なソフトウェアの設計手法。ビジネスドメインの知識を中心に据え、ユビキタス言語、境界づけられたコンテキスト、集約などの概念を用いる。 ↩
-
値オブジェクト(Value Object) - DDDの構成要素の一つ。識別子を持たず、その属性によってのみ識別されるオブジェクト。不変であり、等価性は全ての属性の一致によって判断される。Money、Address、DateRangeなどが典型例。 ↩
-
不変(Immutable) - オブジェクトの状態が作成後に変更できない性質。不変オブジェクトはスレッドセーフであり、予期しない副作用を防ぐことができる。String、Integer、LocalDateなどが代表例。 ↩
-
副作用(Side Effect) - 関数やメソッドが、戻り値を返す以外に、外部の状態を変更すること。純粋関数は副作用を持たず、同じ入力に対して常に同じ出力を返す。関数型プログラミングでは副作用の最小化が重視される。 ↩
-
ビルダーパターン(Builder Pattern) - 複雑なオブジェクトの構築を段階的に行うデザインパターン。コンストラクタの引数が多い場合や、オプションパラメータが多い場合に有効。Joshua BlochのEffective Javaで推奨されている。 ↩
-
Lombok - Javaのボイラープレートコードを削減するライブラリ。アノテーションを使用して、getter/setter、コンストラクタ、ビルダー、equals/hashCodeなどを自動生成する。@Data、@Builder、@Valueなどが代表的。 ↩
-
モックオブジェクト(Mock Object) - テスト時に実際のオブジェクトの代わりに使用される偽のオブジェクト。期待される振る舞いを事前に定義し、テスト対象のコードが正しく動作するかを検証する。テストダブルの一種。 ↩
-
Mockito - Javaで最も広く使用されているモックフレームワーク。簡潔な文法で強力なモック機能を提供し、振る舞いの定義と検証を直感的に行える。when-thenReturn構文が特徴的。 ↩
-
モックフレームワーク - モックオブジェクトの作成と管理を簡単にするライブラリ。Mockito、EasyMock、PowerMockなどがJavaでは主要。テストダブル(Test Double)の一種であるモック、スタブ、スパイなどを提供する。 ↩