権限セット付与時のCRUD/FLSのテストを書く

# はじめに

AppExchangeアプリ開発時のTIPSです。他のケースではたぶん使わないかなと思います。

権限セットにあるカスタムオブジェクト/項目の権限を設定しており、あるCRUD/FLS権限チェックを伴う処理に対して、以下のことをテストしたいということはそれなりにあるかなと思います。

* 権限セットを付与したユーザはその処理ができること
* 権限セットを付与していないユーザはその処理ができないこと

そのときのテストの書き方を説明します。

# テストの書き方

例えば、下のような`SomeLogic.readPrivateObjects`というCRUD/FLS権限チェックを伴うメソッドがあるとします。

```java:SomeLogic.cls
public with sharing class SomeLogic {
  public static PrivateObject__c[] readPrivateObjects() {
    // 何らかのCRUD/FLS権限チェックを伴う処理
    return [
      SELECT Id, PrivateField__c FROM PrivateObject__c
      WITH SECURITY_ENFORCED
    ];
  }
}
```

テストコードは以下のように書けます。アサーションは本題ではないので適当です。

```java:SomeLogicTest.cls
@isTest
private class SomeLogicTest {
  /**
   * 権限セットを付与したユーザで実行し成功するケース
   */
  @isTest
  private static void testReadPrivateObjectsSuccess() {
    PrivateObject__c[] expectedRecords = TestHelper.createPrivateObjects();
    User standardUser = TestHelper.createUser('standard', '標準ユーザ');
    TestHelper.assignPermissionSet(standardUser.Id, 'SinePermSetName');

    PrivateObject__c[] actualRecords;

    Test.startTest();
    System.runAs(standardUser) {
      actualRecords = SomeLogic.readPrivateObjects();
    }
    Test.stopTest();

    System.assertEquals(expectedRecords.size(), actualRecords.size());
    // 他にもアサーションを書く
  }

  /**
   * 権限セットを付与しないユーザで実行し失敗するケース
   */
  @isTest
  private static void testReadPrivateObjectsError() {
    PrivateObject__c[] expectedRecords = TestHelper.createPrivateObjects();
    User standardUser = TestHelper.createUser('standard', '標準ユーザ');

    QueryException actualEx;

    Test.startTest();
    System.runAs(standardUser) {
      try {
        SomeLogic.readPrivateObjects();
      } catch (QueryException e) {
        actualEx = e;
      }
    }
    Test.stopTest();

    System.assertNotEquals(null, actualEx);
  }
}
```

`testReadPrivateObjectsSuccess`が権限セットを付与したユーザで実行し成功するケースです。権限セットを付与したユーザを作成し、`System.runAs`でそのユーザでテストしたいメソッドを実行するのがポイントです。`System.runAs`はテストコードのみで使用できるメソッドで、指定したユーザで処理を実行することができます。
`testReadPrivateObjectsError`は権限セットを付与していないユーザで実行し、きちんとエラーが出ることをアサートします。

```java:TestHelper.cls
@isTest
public class TestHelper {
  public static PrivateObject__c[] createPrivateObjects() {
    // PrivateObject__cレコードを適当に作る
    // 省略
  }

  public static User createUser(String emailPrefix, String profileName) {
    Profile p = [SELECT Id FROM Profile WHERE Name = :profileName];
    User u = new User(
      Alias = emailPrefix.substring(0, 3),
      Email = emailPrefix + '@somedomain.example.com',
      EmailEncodingKey = 'UTF-8',
      FirstName = 'User',
      LastName = emailPrefix,
      LanguageLocaleKey = 'ja',
      LocaleSidKey='ja_JP',
      ProfileId = p.Id,
      TimeZoneSidKey = 'Asia/Tokyo',
      UserName = emailPrefix + '@somedomain.example.com'
    );
    insert u;
    return u;
  }

  public static void assignPermissionSet(Id userId, String permSetName) {
    User adminUser = [SELECT Id FROM User WHERE Id = :UserInfo.getUserId()];
    System.runAs(adminUser) {
      PermissionSet permset = [
        SELECT Id, Name FROM PermissionSet
        WHERE Name = :permSetName AND NamespacePrefix = 'somenamespace'
      ];
      insert new PermissionSetAssignment(
        PermissionSetId = permset.Id,
        AssigneeId = userId
      );
    }
  }
}
```

先ほど権限セットを付与するために呼んでいた`TestHelper.assignPermissionSet`ですが、上のように実装しています。基本的には`PermissionSetAssignment`というオブジェクトのレコードを作成すればいいだけです。ただし、素直に行うと以下のエラーが発生します。

```
MIXED_DML_OPERATION, 非設定オブジェクトを更新した後の設定オブジェクト上の DML 操作 (またはその逆) は、許可されていません
```

それを回避するために再び`System.runAs`を使っています。`System.runAs`を使うとトランザクションを分けることができるので、このエラーを回避することができます。権限セット以外でも上のエラーが出たときはこのテクニックは使えます。

# 終わりに

上記コードは実際使っているコードから記事用に再編したもので実際動かしていないので、動かないかもしれません。誤字とかもあるかもしれません。それくらい試せというところですが、記事を書き上げたら面倒になりました。手抜きですみません:sweat:ミスがあればコメントください。
とりあえず基本的なやり方を参考にしていただければと思います。