Apexテストのベストプラクティス

Apexのテストは、**本番環境でのバグを防ぐ** ために欠かせません。  
適切なテストを書くことで、**アプリケーションの品質を向上** させることができます。  
今回は、**4つのベストプラクティス** について、**具体的なコード例** を使って整理しました。

---

## **1. データ条件のテストをする（有効、無効、一括など）**
### **✅ 理由**
- **本番環境では様々なデータパターンが存在するため、すべてのケースを考慮したテストが必要。**
- **有効なデータ（正常系）、無効なデータ（異常系）、一括データ（大量処理）をテストすることで、エラーを未然に防げる。**

---

### **📌 例：コース開催 (`Course_Delivery__c`) の `Start_Date__c` のテスト**
```apex
@isTest
private class CourseDeliveryTest {

    // ✅ 1. 正常なデータ（有効な開始日）
    @isTest
    private static void testValidCourseDelivery() {
        Course_Delivery__c courseDelivery = new Course_Delivery__c(
            Name = 'Valid Course',
            Start_Date__c = Date.today().addDays(10) // ✅ 未来の日付（正常）
        );

        Test.startTest();
        Database.SaveResult result = Database.insert(courseDelivery, false);
        Test.stopTest();

        System.assertEquals(true, result.isSuccess(), '正常なデータは登録できるはず');
    }

    // ❌ 2. 無効なデータ（過去の日付でエラー）
    @isTest
    private static void testInvalidCourseDelivery() {
        Course_Delivery__c courseDelivery = new Course_Delivery__c(
            Name = 'Invalid Course',
            Start_Date__c = Date.today().addDays(-1) // ❌ 過去の日付（エラー）
        );

        Test.startTest();
        Database.SaveResult result = Database.insert(courseDelivery, false);
        Test.stopTest();

        System.assertEquals(false, result.isSuccess(), '過去の日付のコースは登録できないはず');
    }

    // 📦 3. 一括データのテスト（10件登録）
    @isTest
    private static void testBulkInsertCourseDeliveries() {
        List<Course_Delivery__c> courses = new List<Course_Delivery__c>();
        for (Integer i = 0; i < 10; i++) {
            courses.add(new Course_Delivery__c(
                Name = 'Bulk Course ' + i,
                Start_Date__c = Date.today().addDays(i + 1) // ✅ すべて未来の日付
            ));
        }

        Test.startTest();
        Database.SaveResult[] results = Database.insert(courses, false);
        Test.stopTest();

        for (Database.SaveResult result : results) {
            System.assertEquals(true, result.isSuccess(), 'すべてのデータは正常に登録されるはず');
        }
    }
}
```

✅ **正常系、異常系、大量データ処理のすべてをテストすることで、バグを未然に防げる！**

---

## **2. クラスとテストクラスは1対1**
### **✅ 理由**
- **クラスごとに対応するテストクラスを用意すると、管理がしやすくなる。**
- **どのテストがどのクラスを検証しているのかが明確になる。**

---

### **📌 ❌ 悪い例（複数の対象クラスを1つのテストクラスで扱う）**
```apex
@isTest
private class AllTests {
    @isTest private static void testCourseDelivery() { /* ... */ }
    @isTest private static void testOtherFunctionality() { /* ... */ }
}
```
⛔ **問題点**
- **どのクラスのテストなのか不明確** で、コードが煩雑になる。

---

### **📌 ✅ 良い例（1つのクラスに対して1つのテストクラス）**
```apex
@isTest
private class CourseDeliveryTest {
    @isTest private static void testValidCourseDelivery() { /* ... */ }
}
```
✅ **対応関係が明確になり、管理しやすくなる！**

---

## **3. テストデータが適切に作成されているか**
### **✅ 理由**
- **本番環境とSandbox環境ではレコードIDが異なるため、IDをハードコードするとエラーの原因になる。**
- **テストメソッド内でデータを作成することで、環境に依存しないテストが可能。**

---

### **📌 ❌ 悪い例（IDをハードコードしている）**
```apex
@isTest
private class BadTestExample {
    @isTest
    private static void testWithHardcodedId() {
        Course_Delivery__c course = [SELECT Id FROM Course_Delivery__c WHERE Id = 'a0123456789XYZ']; // ❌ IDをハードコード
        System.assertNotEquals(null, course);
    }
}
```
⛔ **問題点**
- **本番環境とSandboxでIDが異なるため、テストが失敗する可能性がある。**

---

### **📌 ✅ 良い例（テストデータを動的に作成）**
```apex
@isTest
private class GoodTestExample {
    @isTest
    private static void testWithDynamicallyCreatedData() {
        Course_Delivery__c course = new Course_Delivery__c(Name = 'Test Course', Start_Date__c = Date.today().addDays(5));
        insert course;

        Course_Delivery__c retrieved = [SELECT Id FROM Course_Delivery__c WHERE Id = :course.Id];
        System.assertNotEquals(null, retrieved, 'コースが存在するはず');
    }
}
```
✅ **環境に依存しない、安定したテストコードになる！**

---

## **4. SOSL検索のテスト**
### **✅ 理由**
- **SOSLの検索結果はテスト環境では通常空になるため、明示的に結果をセットする必要がある。**
- **`Test.setFixedSearchResults()` を使用して、テストデータの検索結果を指定する。**

---

### **📌 ✅ 良い例（`Test.setFixedSearchResults()` を使用）**
```apex
@isTest
private class SearchHelperTest {

    @testSetup
    private static void setupTestData() {
        Contact contact1 = new Contact(FirstName = 'Taro', LastName = 'Yamada', Email = 'taro@example.com');
        insert contact1;
    }

    @isTest
    private static void testSOSLSearch() {
        // 検索結果として返したいレコードのIDをリストに格納
        List<Id> expectedIds = new List<Id>();

        List<Contact> contacts = [SELECT Id FROM Contact WHERE LastName = 'Yamada'];
        if (!contacts.isEmpty()) {
            expectedIds.add(contacts[0].Id);
        }

        // SOSLの検索結果を固定
        Test.setFixedSearchResults(expectedIds);

        Test.startTest();
        List<sObject> results = [FIND 'anytest' IN ALL FIELDS RETURNING Contact(Id, Name)];
        Test.stopTest();

        System.assertEquals(expectedIds.size(), results.size(), '検索結果の件数が一致するはず');
    }
}
```
✅ **テスト環境でも確実にSOSLの検索結果を取得できるようになる！**

---

## **🎯 まとめ**
| ベストプラクティス | 理由 | 具体例 |
|-----------------|-----------------|-----------------------------|
| **1. データ条件のテスト** | 正常系・異常系・一括処理を確認 | **未来/過去の日付のバリデーションをテスト** |
| **2. クラスとテストクラスを1対1** | テストの管理がしやすい | **`CourseDeliveryTest` を作成** |
| **3. レコードIDをハードコードしない** | 環境に依存せずにテストできる | **`insert` でデータを作成** |
| **4. SOSL検索のテスト** | `Test.setFixedSearchResults()` を使用 | **確実に検索結果を取得** |