Spring Boot と Scalar DB を用いた API の作り方②

この記事は[Spring Boot と Scalar DB を用いた API の作り方①](https://qiita.com/yoshi-kinoshita/items/5b2c5c713c9cc2fa2915)の続きになります。

- [Spring BootとScalar DBを用いたAPIの作り方①](https://qiita.com/yoshi-kinoshita/items/5b2c5c713c9cc2fa2915)
- Spring BootとScalar DBを用いたAPIの作り方②(本記事)
- [Spring BootとScalar DBを用いたAPIの作り方③](https://qiita.com/yoshi-kinoshita/items/1cb6c0b11be5ab087ed6)

# 目次
1. [モデル・DTOの作成](#モデル・DTOの作成)
    1. [モデル](#モデル)
    1. [DTO](#dto)
1. [リポジトリクラスの作成](#リポジトリクラスの作成)
    1. [Scalar DBを使ったCRUD処理の実装](#scalar-dbを使ったcrud処理の実装)
    1. [リポジトリクラスのユニットテスト](#リポジトリクラスのユニットテスト)
1. [サービスクラスの作成](#サービスクラスの作成)
    1. [Scalar DBを使ったトランザクションの実装](#scalar-dbを使ったトランザクションの実装)
    1. [サービスクラスのユニットテスト](#サービスクラスのユニットテスト)
1. [コントローラークラスの作成](#コントローラークラスの作成)
    1. [コントローラークラスの実装](#コントローラークラスの実装)
    1. [コントローラークラスのユニットテスト](#コントローラークラスのユニットテスト)

# モデル・DTOの作成
## モデル
データベースに保持するためのデータ構造を保持するクラスとしてモデルを作成します。

Repositoryクラスで、スキーマファイルで定義したカラムに対応したデータを保存するため、クラス定数としてスキーマファイルで定義したカラムを記述します。

``` java:model/Group.java
@Value
@Builder
public class Group {
  public static final String GROUP_ID = "group_id";
  public static final String GROUP_NAME = "group_name";
  public static final String GROUP_USERS = "group_users";
  public static final String COMMON_KEY = "common_key";

  String groupId;
  String groupName;
  List<GroupUser> groupUsers;
  String commonKey;
}

```

``` java:model/User.java
@Value
@Builder
public class User {
  public static final String USER_ID = "user_id";
  public static final String EMAIL = "email";
  public static final String FAMILY_NAME = "family_name";
  public static final String GIVEN_NAME = "given_name";
  public static final String USER_GROUPS = "user_groups";
  public static final String USER_DETAIL = "user_detail";
  public static final String COMMON_KEY = "common_key";

  String userId;
  String email;
  String familyName;
  String givenName;
  List<UserGroup> userGroups;
  UserDetail userDetail;
  String common_key;
}

```
## DTO
DTOはAPIで受付け・返却するデータを保持するクラスです。APIの利用者に見せるためのデータを保持します。
JSONでデータを受け付けるため、Jacksonでデシリアリズを行っております。

以下が実装したDTOの例です。

``` java:dto/GetUserDto.java
@Getter
@Builder
@JsonDeserialize(builder = GetUserDto.GetUserDtoBuilder.class)
public class GetUserDto {
  @JsonProperty("user_id")
  String userId;

  @JsonProperty("email")
  String email;

  @JsonProperty("family_name")
  String familyName;

  @JsonProperty("given_name")
  String givenName;

  @JsonProperty("user_groups")
  List<GetGroupDto> userGroups;

  @JsonProperty("user_detail")
  UserDetailDto userDetail;
}
```

それ以外にも以下のDTOを作成しました。
- CreateUserDto.java
- CreateGroupDto.java
- GetGroupDto.java
- GroupUserDto.java
- UpdateUserDto.java
- UserDetailDto.java

# リポジトリクラスの作成
## Scalar DBを使ったCRUD処理の実装
Scalar DB でデータベースに対してCRUD操作を実行する方法は以下の手順になります。
- レコードを特定するKeyオブジェクトを生成する
- Get, Scan, Put, Deleteなどのオブジェクトを生成し、CRUD処理を行いたいレコードのKey、NameSpace、テーブル名、値を渡す
- トランザクションを介して、Get, Scan, Put, Deleteなどのオブジェクトを渡し、CRUD処理を実行する

取得したレコードは`Optional<Result>`として返却され、getPartitionKey(), getClusteringKey(), getValue(”カラム名”)などのメソッドで特定の値を取得できます。

取得した値は、スキーマファイルに定義したデータ型に従って、TextValue, IntValue, BigIntValue, BooleanValueなどのデータ型として返却されます。
モデルに変換する際に、getAsString(), getAsInt(), getAsBoolean()などのメソッドを使ってモデルで定義したデータ型に変換します。

実装は以下のようになります。

``` java:repository/UserReposiotry.java
@Repository
public class UserRepository extends ScalarDbReadOnlyRepository<User> {
  public static final String NAMESPACE = "demo";
  public static final String TABLE_NAME = "users";
  public static final String COMMON_KEY = "common_key";

  public String createUser(DistributedTransaction tx, CreateUserDto createUserDto, String userId) {
    try {
      Key pk = createPk(userId); // パーティションキーの作成
      getAndThrowsIfAlreadyExist(tx, createGet(pk)); // 同じIDを持つレコードが存在しないかをチェック
      Put put =
          new Put(pk)                                // usersテーブルに登録/更新するPutオブジェクトを作成
              .forNamespace(NAMESPACE)               // NameSpaceを指定
              .forTable(TABLE_NAME)                  // テーブルを指定
              .withValue(User.EMAIL, createUserDto.getEmail()) // 登録/更新したいデータを指定
              .withValue(User.COMMON_KEY, COMMON_KEY);
      tx.put(put);                                  // トランザクションを介して、データを登録/更新
      return userId;
    } catch (CrudConflictException e) {
      throw new RepositoryConflictException(e.getMessage(), e);
    } catch (CrudException e) {
      throw new RepositoryCrudException("Adding User failed", e);
    }
  }

  public void updateUser(
      DistributedTransaction tx,
      UpdateUserDto updateUserDto,
      List<GetGroupDto> userGroups,
      String userId)
      throws CrudException {
    try {
      Key pk = createPk(userId);                  // パーティションキーの作成
      getAndThrowsIfNotFound(tx, createGet(pk));  // 同じIDを持つレコードが存在しないかをチェック
      Put put =
          new Put(pk)                             // usersテーブルに登録/更新するPutオブジェクトを作成
              .forNamespace(NAMESPACE)            // NameSpaceを指定
              .forTable(TABLE_NAME)               // テーブルを指定
              .withValue(User.EMAIL, updateUserDto.getEmail()) // データオブジェクトをJSON文字列に変換
              .withValue(User.FAMILY_NAME, updateUserDto.getFamilyName())
              .withValue(User.GIVEN_NAME, updateUserDto.getGivenName())
              .withValue(
                  User.USER_DETAIL,
                  ScalarUtil.convertDataObjectToJsonStr(updateUserDto.getUserDetail()))
              .withValue(User.USER_GROUPS, ScalarUtil.convertDataObjectToJsonStr(userGroups))
              .withValue(User.COMMON_KEY, COMMON_KEY);
      tx.put(put);                                // トランザクションを介して、データを登録/更新。
    } catch (CrudConflictException e) {
      throw new RepositoryConflictException(e.getMessage(), e);
    } catch (CrudException e) {
      throw new RepositoryCrudException("Updating User failed", e);
    }
  }

  public void updateUserGroups(
      DistributedTransaction tx, String userId, List<UserGroup> userGroups) {
    try {
      Key pk = createPk(userId);
      User user = getAndThrowsIfNotFound(tx, createGet(pk));
      Put put =
          new Put(pk)
              .forNamespace(NAMESPACE)
              .forTable(TABLE_NAME)
              .withValue(User.EMAIL, user.getEmail())
              .withValue(User.FAMILY_NAME, user.getFamilyName())
              .withValue(User.GIVEN_NAME, user.getGivenName())
              .withValue(
                  User.USER_DETAIL, ScalarUtil.convertDataObjectToJsonStr(user.getUserDetail()))
              .withValue(User.USER_GROUPS, ScalarUtil.convertDataObjectToJsonStr(userGroups))
              .withValue(User.COMMON_KEY, COMMON_KEY);
      tx.put(put);
    } catch (CrudConflictException e) {
      throw new RepositoryConflictException(e.getMessage(), e);
    } catch (CrudException e) {
      throw new RepositoryCrudException("Updating UserGroups failed", e);
    }
  }

  public void deleteUser(DistributedTransaction tx, String userId) {
    try {
      Key pk = createPk(userId); 
      getAndThrowsIfNotFound(tx, createGet(pk));
      Delete delete = new Delete(pk).forNamespace(NAMESPACE).forTable(TABLE_NAME); // usersテーブルのレコードを削除するDeleteオブジェクトを作成
      tx.delete(delete);  // トランザクションを介して、レコードを削除。
    } catch (CrudConflictException e) {
      throw new RepositoryConflictException(e.getMessage(), e);
    } catch (CrudException e) {
      throw new RepositoryCrudException("Deleting User failed", e);
    }
  }

 public List<User> listUsers(DistributedTransaction tx) {
    try {
      Scan scan =  // インデックスキーを作成し、usersテーブルの中で同じキーを持つレコードスキャンするScanオブジェクトを作成
          new Scan(new Key(new TextValue(User.COMMON_KEY, COMMON_KEY))) 
              .forNamespace(NAMESPACE)
              .forTable(TABLE_NAME);
    List<Result> results = tx.scan(scan);  // Scanを実行させ、結果をResultのリストに保持
    return results.stream().map(this::parse).collect(Collectors.toList()); //ResultのリストからUserのリストを作成し、返却する
    } catch (CrudException e) {
      throw new RepositoryException("Reading Users failed", e);
    }
  }

  public User getUser(DistributedTransaction tx, String userId) {
    try {
      Key pk = createPk(userId);
      return getAndThrowsIfNotFound(tx, createGet(pk));
    } catch (CrudConflictException e) {
      throw new RepositoryConflictException(e.getMessage(), e);
    } catch (CrudException e) {
      throw new RepositoryCrudException("Reading User failed", e);
    }
  }

  private Key createPk(String userId) {
    return new Key(new TextValue(User.USER_ID, userId));
  }

  private Get createGet(Key pk) {
    return new Get(pk).forNamespace(NAMESPACE).forTable(TABLE_NAME);
  }

  @Override
  User parse(@NotNull Result result) {
    UserBuilder builder = User.builder();
    return builder
        .userId(ScalarUtil.getTextValue(result, User.USER_ID))
        .email(ScalarUtil.getTextValue(result, User.EMAIL))
        .familyName(ScalarUtil.getTextValue(result, User.FAMILY_NAME))
        .givenName(ScalarUtil.getTextValue(result, User.GIVEN_NAME))
        .userGroups(
            ScalarUtil.convertJsonStrToDataObjectList(
                ScalarUtil.getTextValue(result, User.USER_GROUPS), UserGroup[].class))
        .userDetail(
            ScalarUtil.convertJsonStrToDataObject(
                ScalarUtil.getTextValue(result, User.USER_DETAIL), UserDetail.class))
        .build();
  }
}
```

## リポジトリクラスのユニットテスト
Scalar DBでCRUD処理が実行されたか検証します。

ArgumentCaptor.forClassメソッドでArgumentCaptorのインスタンスを生成し、 verify()でモックオブジェクトのメソッドが呼び出されていることをテストするのと同時に、その呼出引数をcapture()します。 

``` java:test/**/repository/UserRepositoryTest.java
@SpringBootTest
public class UserRepositoryTest {
  private static final String MOCKED_USER_ID = UUID.randomUUID().toString();
  private static final String MOCKED_EMAIL = "mockedEmail";
  @MockBean DistributedTransactionManager manager;
  @MockBean DistributedTransaction tx;
  @MockBean Result result;
  @Autowired UserRepository repository;

  @BeforeEach
  private void setUp() throws TransactionException {
    when(manager.start()).thenReturn(tx); 
  }

  @Test
  public void createUser_shouldSuccess() throws TransactionException {
    CreateUserDto createUserDto = createUserDto();
    repository.createUser(tx, createUserDto, MOCKED_USER_ID);
    ArgumentCaptor<Put> argumentCaptor = ArgumentCaptor.forClass(Put.class);

    verify(tx, times(1)).put(argumentCaptor.capture());

    Put arg = argumentCaptor.getValue();
    TextValue email = new TextValue(User.EMAIL, createUserDto.getEmail());
    assertEquals(email, arg.getValues().get(”email”));
    verify(tx).get(any());
  }

  @Test
  public void createUser_userAlreadyExists() throws TransactionException {
    CreateUserDto createUserDto = createUserDto();
    when(tx.get(any())).thenReturn(Optional.of(result));

    Assertions.assertThrows(
        ObjectAlreadyExistingException.class,
        () -> repository.createUser(tx, createUserDto, MOCKED_USER_ID));
  }

  @Test
  public void createUser_dbSomeProblems_CrudExceptionThrown() throws TransactionException {
    CreateUserDto createUserDto = createUserDto();

    doThrow(CrudException.class).when(tx).put(any(Put.class));

    Assertions.assertThrows(
        RepositoryException.class, () -> repository.createUser(tx, createUserDto, MOCKED_USER_ID));
  }

  private CreateUserDto createUserDto() {
    CreateUserDtoBuilder builder = CreateUserDto.builder();
    return builder.email(MOCKED_EMAIL).build();
  }
```

テストを実行します。

``` bash
$ cd api
$ ./gradlew test --tests UserRepositoryTest

BUILD SUCCESSFUL in 3s
```
# サービスクラスの作成
## Scalar DBを使ったトランザクションの実装
サービスクラスで実装するものは以下の２つになります。
- Scalar DBのトランザクションマネージャでトランザクションを開始し、成功した場合の処理と失敗した場合の処理
- アプリケーションのロジック

まず、トランザクションマネージャの設定を行う必要があります。サービスクラス全体でトランザクションマネージャを利用できるようにするための設定クラスを作成します。

``` java:config/ApiConfig.java
@Configuration
public class ApiConfig {
  @Bean
  @Scope("singleton")
  DistributedTransactionManager createScalarDBTransactionManager() throws IOException {
    String databaseProp = "database.properties";
    DatabaseConfig scalarDBConfig =
        new DatabaseConfig(new URL("classpath:" + databaseProp).openConnection().getInputStream());
    TransactionFactory factory = new TransactionFactory(scalarDBConfig);
    return factory.getTransactionManager();
  }
}
```

その後、各サービスクラスでトランザクションマネージャを開始し、トランザクションを実装していきます。

以下のコードではグループを新規登録するメソッドと、グループにユーザーを新たに追加するメソッドを実装しています。

ポイントとしては、
- トランザクションマネージャをスタート (tx = db.start();)
- １つ以上のリポジトリクラスのメソッドを呼び出す
- 全てのリポジトリクラスのメソッドの呼び出しに成功したらトランザクションをコミットする(tx.commit())
- １つでも呼び出しに失敗したら、トランザクションを中止する (tx.abort())

``` java:service/GroupService.java

// グループを新規登録する
public String createGroup(CreateGroupDto createGroupDto, String userId)
      throws InterruptedException {
    while (true) {
      if (retryCount > 0) {
        if (retryCount == 3) {
          throw new ServiceException("An error occurred when adding a group");
        }
        TimeUnit.MILLISECONDS.sleep(100);
      }

      try {
        tx = manager.start(); // トランザクションマネージャをスタート
      } catch (TransactionException e) {
        try {
          tx.abort();
        } catch (AbortException ex) {
          log.error(ex.getMessage(), ex);
        }
        throw new ServiceException("An error occurred when adding a group", e);
      }

      try {
        String groupId = UUID.randomUUID().toString();　// IDの生成
        groupRepository.createGroup(tx, createGroupDto, groupId, groupUsers);　// groupRepositoryのcreateGroupメソッドを呼び出す。

        tx.commit();　// 処理が成功した場合はコミットする。
        return groupId;
      } catch (CommitConflictException
          | RepositoryConflictException
          | UnknownTransactionStatusException e) {
        try {
          tx.abort();　　// // 処理に失敗した場合はトランザクションを中止
        } catch (AbortException ex) {
          log.error(ex.getMessage(), ex);
        }
        retryCount++;
      } catch (CommitException | RepositoryCrudException | ObjectAlreadyExistingException e) {
        try {
          tx.abort();
        } catch (AbortException ex) {
          log.error(ex.getMessage(), ex);
        }
        throw new ServiceException("An error occurred when adding a group", e);
      }
    }
  }

  // グループにユーザーを追加する
  public void addGroupUser(String groupId, GroupUserDto groupUserDto) throws InterruptedException {
    while (true) {
      if (retryCount > 0) {
        if (retryCount == 3) {
          throw new ServiceException("An error occurred when adding groupUser");
        }
        TimeUnit.SECONDS.sleep(100);
      }

      try {
        tx = manager.start(); // トランザクションマネージャをスタート
      } catch (TransactionException e) {
        try {
          tx.abort();
        } catch (AbortException ex) {
          log.error(ex.getMessage(), ex);
        }
        throw new ServiceException("An error occurred when adding a group", e);
      }

      try {
        Group group = groupRepository.getGroup(tx, groupId);
        List<GroupUser> groupUsers =
            Optional.ofNullable(group.getGroupUsers()).orElse(new ArrayList<GroupUser>());  //グループIDからグループ情報の呼び出し
        groupUsers.forEach(  // // すでに所属しているユーザーならば例外をスロー
            (existingGroupUser -> {
              if (existingGroupUser.getUserId().equals(groupUserDto.getUserId())) {
                throw new ObjectAlreadyExistingException(this.getClass(), groupUserDto.getUserId());
              }
            }));
        groupUsers.add( // 新たにユーザーを追加
            ScalarUtil.convertDataObjectToAnotherDataObject(groupUserDto, GroupUser.class));
        groupRepository.updateGroupUsers(tx, groupUsers, groupId); // // groupRepositoryのupdateGroupUsers()メソッドを呼び出し、グループ情報を更新する

        User user = userRepository.getUser(tx, groupUserDto.getUserId()); //ユーザー情報を取得
        List<UserGroup> userGroups =
            Optional.ofNullable(user.getUserGroups()).orElse(new ArrayList<UserGroup>());
        userGroups.add(mapUserGroup(group)); // ユーザーの所属グループ一覧に新たにグループを追加
        userRepository.updateUserGroups(tx, groupUserDto.getUserId(), userGroups); // userRepositoryのupdateUserGroups()を呼び出し、ユーザー情報を更新する
        tx.commit(); // 処理に成功した場合はコミットする
        break;
      } catch (CommitConflictException
          | RepositoryConflictException
          | UnknownTransactionStatusException e) {
        try {
          tx.abort(); // 失敗した場合はトランザクションを中止しする
        } catch (AbortException ex) {
          log.error(ex.getMessage(), ex);
        }
        retryCount++;
      } catch (CommitException | RepositoryCrudException | ObjectAlreadyExistingException e) {
        try {
          tx.abort();
        } catch (AbortException ex) {
          log.error(ex.getMessage(), ex);
        }
        throw new ServiceException("An error occurred when adding a groupUser", e);
      }
    }
  }
```

## サービスクラスのユニットテスト
サービスクラスでトランザクションが正しく実装されているか、ユニットテストを行います。

verify()メソッドを使い、正常系・異常系でトランザクションがそれぞれ`commit`されているか`abort`されているかを検証します。

``` java:test/**/service/GroupServiceTest.java
@SpringBootTest
public class GroupServiceTest {
  private static final String MOCKED_GROUP_ID_1 = "mockedGroupId";
  private static final String MOCKED_GROUP_ID_2 = "mockedGroupId2";
  private static final String MOCKED_USER_ID_1 = "mockedUserId";
  private static final String MOCKED_USER_ID_2 = "mockedUserId2";

  @Mock UserRepository userRepository;
  @Mock GroupRepository groupRepository;
  @MockBean DistributedTransactionManager manager;
  @MockBean DistributedTransaction tx;
  @Autowired GroupService groupService;
  @Autowired ModelMapper mapper;

  @BeforeEach
  private void setUp() throws TransactionException {
    groupService = new GroupService(groupRepository, userRepository, manager, mapper);

    when(manager.start()).thenReturn(tx);
  }

    @Test
  public void createGroup_shouldSuccess() throws TransactionException, InterruptedException {
    CreateGroupDto createGroupDto = GroupStub.getCreateGroupDto();
    User user = UserStub.getUser(MOCKED_USER_ID_1);

    when(userRepository.getUser(tx, MOCKED_USER_ID_1)).thenReturn(user);
    groupService.createGroup(createGroupDto, MOCKED_USER_ID_1);

    verify(tx, times(1)).commit();
  }

  @Test
  public void createGroup_whenCommitFailed_shouldServiceException() throws TransactionException {
    CreateGroupDto createGroupDto = GroupStub.getCreateGroupDto();
    User user = UserStub.getUser(MOCKED_USER_ID_1);
    when(userRepository.getUser(tx, MOCKED_USER_ID_1)).thenReturn(user);

    doThrow(CommitException.class).when(tx).commit();

    assertThrows(
        ServiceException.class, () -> groupService.createGroup(createGroupDto, MOCKED_USER_ID_1));
  }

  @Test
  public void
      createGroup_whenCommitConflictExceptionThrows_shouldThrowServiceExceptionAndAbortTransaction3Times()
          throws TransactionException {
    CreateGroupDto createGroupDto = GroupStub.getCreateGroupDto();
    User user = UserStub.getUser(MOCKED_USER_ID_1);
    when(userRepository.getUser(tx, MOCKED_USER_ID_1)).thenReturn(user);

    doThrow(CommitConflictException.class).when(tx).commit();

    assertThrows(
        ServiceException.class, () -> groupService.createGroup(createGroupDto, MOCKED_USER_ID_1));
    verify(tx, times(3)).abort();
  }

  @Test
  public void addGroupUser_shouldSuccess() throws TransactionException, InterruptedException {
    Group group = GroupStub.getGroup(MOCKED_GROUP_ID_1);
    User user = UserStub.getUser(MOCKED_USER_ID_2);
    GroupUserDto groupUserDto = GroupStub.getGroupUserDto(MOCKED_USER_ID_2);

    when(groupRepository.getGroup(tx, MOCKED_GROUP_ID_1)).thenReturn(group);
    when(userRepository.getUser(tx, MOCKED_USER_ID_2)).thenReturn(user);

    groupService.addGroupUser(MOCKED_GROUP_ID_1, groupUserDto);

    verify(tx, times(1)).commit();
  }

  @Test
  public void addGroupUser_whenAlreadyBelongingUser_shouldThrowServiceException()
      throws TransactionException {
    Group group = GroupStub.getGroup(MOCKED_GROUP_ID_1);
    User user = UserStub.getUser(MOCKED_USER_ID_1);

    when(groupRepository.getGroup(tx, MOCKED_GROUP_ID_1)).thenReturn(group);
    when(userRepository.getUser(tx, MOCKED_USER_ID_1)).thenReturn(user);

    GroupUserDto groupUserDto = GroupStub.getGroupUserDto(MOCKED_USER_ID_1);

    Assertions.assertThrows(
        ServiceException.class, () -> groupService.addGroupUser(MOCKED_GROUP_ID_1, groupUserDto));
    verify(tx).abort();
  }
```
テストを実行します。

``` bash
$ cd api
$ ./gradlew test --tests GroupServiceTest

BUILD SUCCESSFUL in 3s
```

# コントローラークラスの作成
## コントローラークラスの実装
[エンドポイントの設計](https://qiita.com/yoshi-kinoshita/private/5b2c5c713c9cc2fa2915#%E3%82%A8%E3%83%B3%E3%83%89%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88%E3%81%AE%E8%A8%AD%E8%A8%88)で定義したエンドポイントに従い、Httpリクエストごとに関連するサービスクラスのメソッドを呼び出し、Httpレスポンスを返します。

実装の流れとしては
- @〇〇MappingアノテーションでパスをHTTPメソッドを指定
- @RequestBodyアノテーションでHttpRequestからのJSONデータをDTOにマップし、Javaオブジェクトにデシリアライズ
- @PathVariableアノテーションでパス内の変数を受け取る
- 受け取った値をサービスクラスに渡す

```java:controller/UserController.java
@RestController
@RequestMapping("/users")
public class UserController {
  private static final String PATH_USER_ID = "user_id";

  @Autowired UserService userService;

  @PostMapping()
  @ResponseStatus(HttpStatus.CREATED)
  public String createUser(@RequestBody CreateUserDto createUserDto) throws TransactionException {
    return userService.createUser(createUserDto);
  }

  @PutMapping("/{user_id}")
  @ResponseStatus(HttpStatus.OK)
  public void updateUser(
      @PathVariable(PATH_USER_ID) String userId, @RequestBody UpdateUserDto updateUserDto)
      throws TransactionException {
    userService.updateUser(userId, updateUserDto);
  }

  @DeleteMapping("/{user_id}")
  @ResponseStatus(HttpStatus.OK)
  public void deleteUser(@PathVariable(PATH_USER_ID) String userId) throws TransactionException {
    userService.deleteUser(userId);
  }

  @GetMapping("/{user_id}")
  @ResponseStatus(HttpStatus.OK)
  public GetUserDto getUser(@PathVariable(PATH_USER_ID) String userId) throws TransactionException {
    return userService.getUser(userId);
  }

  @GetMapping()
  @ResponseStatus(HttpStatus.OK)
  public List<GetUserDto> listUsers() throws TransactionException {
    return userService.listUsers();
  }
}
```

```java:controller/GroupController.java
@RestController
@RequestMapping("/groups")
public class GroupController {
  private static final String PATH_GROUP_ID = "group_id";
  private static final String PATH_USER_ID = "user_id";

  @Autowired GroupService groupService;

  @PostMapping()
  @ResponseStatus(HttpStatus.CREATED)
  public String createGroup(
      @RequestBody CreateGroupDto createGroupDto)
      throws TransactionException {
    return groupService.createGroup(createGroupDto);
  }

  @PutMapping("/{group_id}/group-users")
  @ResponseStatus(HttpStatus.OK)
  public void addGroupUsers(
      @PathVariable(PATH_GROUP_ID) String groupId, @RequestBody GroupUserDto groupUser)
      throws TransactionException {
    groupService.addGroupUser(groupId, groupUser);
  }

  @PutMapping("/{group_id}/group-users/{user_id}")
  @ResponseStatus(HttpStatus.OK)
  public void deleteGroupUser(
      @PathVariable(PATH_GROUP_ID) String groupId, @PathVariable(PATH_USER_ID) String userId)
      throws TransactionException {
    groupService.deleteGroupUser(groupId, userId);
  }

  @DeleteMapping("/{group_id}")
  @ResponseStatus(HttpStatus.OK)
  public void deleteGroup(@PathVariable(PATH_GROUP_ID) String groupId) throws TransactionException {
    groupService.deleteGroup(groupId);
  }

  @GetMapping("/{group_id}/group-users")
  @ResponseStatus(HttpStatus.OK)
  public List<GroupUserDto> listGroupUsers(@PathVariable(PATH_GROUP_ID) String groupId)
      throws TransactionException {
    return groupService.listGroupUsers(groupId);
  }

  @GetMapping()
  @ResponseStatus(HttpStatus.OK)
  public List<GetGroupDto> listGroups() throws TransactionException {
    return groupService.listGroups();
  }
}

```

## コントローラークラスのユニットテスト

``` java:test/**/controller/UserControllerTest.java
@WebMvcTest(UserController.class)
public class UserControllerTest {
  private static final String BASE_URL_PATH = "/users";
  private static final String MOCKED_USER_ID = "6695bdd7-ccb3-0468-35af-e804f79329b2";

  private MockMvc mockMvc;
  @MockBean private UserService userService;
  @MockBean private UserRepository userRepository;
  @MockBean DistributedTransactionManager manager;
  @Autowired UserController userController;
  @Autowired private ObjectMapper objectMapper;
  @Autowired private WebApplicationContext context;
  @MockBean private AuthenticationService authenticationService;

  @BeforeEach
  public void setup() {
    mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
  }

  @Test
  void createUser_shouldSuccess() throws Exception {
    CreateUserDto createUserDto = UserStub.getCreateUserDto();

    when(userService.createUser(createUserDto)).thenReturn(MOCKED_USER_ID);

    mockMvc
        .perform(
            post(BASE_URL_PATH)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(createUserDto)))
        .andExpect(status().isCreated());
  }

  @Test
  void updateUser_shouldSuccess() throws Exception {
    UpdateUserDto updateUserDto = UserStub.getUpdateUserDto();

    mockMvc
        .perform(
            put(BASE_URL_PATH + "/" + MOCKED_USER_ID)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(updateUserDto)))
        .andExpect(status().isOk());
  }

  @Test
  void deleteUser_shouldSuccess() throws Exception {
    mockMvc.perform(delete(BASE_URL_PATH + "/" + MOCKED_USER_ID)).andExpect(status().isOk());
  }

  @Test
  void getUser_shouldSuccess() throws Exception {
    mockMvc.perform(get(BASE_URL_PATH + "/" + MOCKED_USER_ID)).andExpect(status().isOk());
  }

  @Test
  void listUsers_shouldSuccess() throws Exception {
    mockMvc.perform(get(BASE_URL_PATH)).andExpect(status().isOk());
  }
}
```

``` bash
$ cd api
$ ./gradlew test --tests UserControllerTest

BUILD SUCCESSFUL in 3s
```

次回はSpring Securityを使ったアクセス制御とCucumberを使ったE2Eテストの仕方について説明します。

[Spring BootとScalar DBを用いたAPIの作り方③](https://qiita.com/yoshi-kinoshita/items/1cb6c0b11be5ab087ed6)