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

こちらの記事は[Spring Boot と Scalar DB を用いた API の作り方②](https://qiita.com/yoshi-kinoshita/private/44363a4aeacf83efcff3)の続きになります。

前回までの記事をご覧になってない方は先にそちらに目を通すようにお願いします。

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

# 目次
1. [Spring Securityによるアクセス制限](#spring-securityによるアクセス制限)
    1. [実装方針](#実装方針)
    1. [実装方法](#実装方法)
    1. [Spring Security Testの実装](#spring-security-testの実装)
1. [E2Eテストの導入](#e2eテストの導入)
    1. [Cucumberの導入](#cucumberの導入)
    1. [テストシナリオの作成](#テストシナリオの作成)
    1. [E2Eテストの実装](#e2eテストの実装)
1. [まとめ](#まとめ)

# Spring Securityによるアクセス制限
Spring Securityを使い、ユーザーを所属しているグループによって、管理者権限と一般権限を割り当てアクセス制御を行います。

以下の表にしたがって、権限ごとにアクセスできるリソースを分けていきます。

|対象|機能|管理者|一般ユーザー|Anonymous|
|---|---| ----| ---------|---------|
|ユーザー(自分自身)|登録する|⭕️|⭕️|⭕️|
|同上|更新する|⭕️|⭕️||
|同上|取得する|⭕️|⭕️||
|同上|削除する|⭕️|⭕️||
|ユーザー（自分以外）|更新する|⭕️|||
|同上|取得する|⭕️|||
|同上|削除する|⭕️|||
|同上|一覧取得する|⭕️|||
|グループ(所属グループ)|メンバーを追加する|⭕️|⭕️||
|同上|メンバーを削除する|⭕️|⭕️||
|同上|メンバーを一覧取得する|⭕️|⭕️||
|同上|削除する|⭕️|⭕️||
|グループ（全て）|登録する|⭕️|⭕️||
|同上|メンバーを追加する|⭕️|||
|同上|メンバーを削除する|⭕️|||
|同上|メンバーを一覧取得する|⭕️|||
|同上|グループを一覧取得する|⭕️|⭕️||

## 実装方針
APIに対する認証、認可をリクエスト中のAuthorizationヘッダの値で行います。この認可はリクエストごとに行います。

ヘッダーによる認証方式は、Spring Security では[事前認証シナリオ](https://spring.pleiades.io/spring-security/site/docs/5.0.1.RELEASE/reference/html/preauth.html)のケースに該当します。
Spring Securityでは、事前認証用に提供されているクラス群が存在するため、そのクラス群を活用する必要があります。

実装方法の概要は以下の様になります。
- リクエスト中のAuthorizationヘッダの値によって認証、認可処理を行うためのフィルター、サービスを作成する
- Spring Securityでリクエストごとに認可処理を行うためにセッションを使用しないよう設定する。
- コントローラークラスのメソッドに必要な権限を持っているかをチェックするために`@PreAuthorize`(“hasAuthority('権限名’)")のように`@PreAuthorize`アノテーションをつける

## 実装方法
Spring Securityを利用するためのbuild.gradleのdependenciesは以下の通りです

``` build.gradle
dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	testImplementation 'org.springframework.security:spring-security-test:5.6.0'
}
```

リクエスト中のAuthorizationヘッダから値を取り出すためのフィルタを以下のように作成します。
以下の実装例ではWebSecurityConfigクラスを作成し、そのサブクラスとして実装しています。

``` java:config/WebSecurityConfig.java
  static class MyPreAuthenticatedProcessingFilter extends AbstractPreAuthenticatedProcessingFilter {
    @Override
    protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
      return Optional.ofNullable(request.getHeader(HttpHeaders.AUTHORIZATION)).orElse("");
    }

    @Override
    protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
      return "";
    }
  }
```

次にフィルタで取り出したAuthorizationの値を使い認証済みユーザとユーザに権限を与える処理を作成します。

認証済みユーザの作成はorg.springframework.security.core.userdetail.Userクラスを拡張したAccountUserを作成し使用します。また、ユーザに与える権限はAuthorityUtils.createAuthorityListメソッドに権限名を与えることで生成しています。

以下の例ではAuthorizationヘッダの値から、リポジトリクラスのメソッドでユーザー情報を呼び出し、ユーザーが`admin`グループに所属している場合に管理者権限を与え、それ以外の場合に一般権限を与えています。

``` java:service/AuthenticationService.java
@Service
public class AuthenticationService
    implements AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {
  @Autowired UserRepository userRepository;
  @Autowired DistributedTransactionManager db;

  @Override
  public UserDetails loadUserDetails(PreAuthenticatedAuthenticationToken token)
      throws UsernameNotFoundException {
    try {
      String userId = token.getPrincipal().toString();
      List<GrantedAuthority> authorities =
          new ArrayList<GrantedAuthority>();

      DistributedTransaction tx = db.start();
      User user = userRepository.getUser(tx, userId);
      authorities.add(new SimpleGrantedAuthority("ROLE_USER"));

      List<String> groupNameList = new ArrayList<String>();
      List<String> groupIdList = new ArrayList<String>();
      List<UserGroup> userGroups =
          Optional.ofNullable(user.getUserGroups()).orElse(new ArrayList<UserGroup>());
      userGroups.forEach(
          (userGroup -> {
            groupNameList.add(userGroup.getGroupName());
            groupIdList.add(userGroup.getGroupId());
          }));
      if (groupNameList.contains("admin")) {
        authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
      }

      return new AccountUser("user", "password", authorities, user.getUserId(), groupIdList);
    } catch (TransactionException | ObjectNotFoundException e) {
      throw new UsernameNotFoundException("Invalid authorization header.");
    }
  }

  public static class AccountUser extends org.springframework.security.core.userdetails.User {
    String userId;
    List<String> groupIdList;

    public AccountUser(
        String username,
        String password,
        Collection<? extends GrantedAuthority> authorities,
        String userId,
        List<String> groupIdList) {
      super(username, password, authorities);
      this.userId = userId;
      this.groupIdList = groupIdList;
    }

    public List<String> getGroupIdList() {
      return groupIdList;
    }

    public String getUserId() {
      return userId;
    }
  }
}
```

作成したフィルタとユーザサービスがインジェクションされるようにBean定義を行います。また、Spring Securityでセッションを使用しないよう設定します。セッションを使用しないことでWeb APIに対するリクエストごとに認可処理が行われる様にします。

また、@PreAuthorizeアノテーションを有効化するためには @EnableGlobalMethodSecurity(prePostEnabled = true)をつける必要があります。

``` java:config/WebSecurityConfig.java

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
  @Autowired private AuthenticationService authenticationService;

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable();
    http.authorizeRequests()
        .anyRequest()
        .authenticated()
        .and()
        .addFilter(preAuthenticatedProcessingFilter())
        .sessionManagement()
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
  }

  @Bean
  public AbstractPreAuthenticatedProcessingFilter preAuthenticatedProcessingFilter()
      throws Exception {

    MyPreAuthenticatedProcessingFilter preAuthenticatedProcessingFilter =
        new MyPreAuthenticatedProcessingFilter();
    preAuthenticatedProcessingFilter.setAuthenticationManager(authenticationManager());

    return preAuthenticatedProcessingFilter;
  }

  @Bean
  PreAuthenticatedAuthenticationProvider tokenProvider() {
    PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
    provider.setPreAuthenticatedUserDetailsService(authenticationService);
    provider.setUserDetailsChecker(new AccountStatusUserDetailsChecker());
    return provider;
  }

  static class MyPreAuthenticatedProcessingFilter extends AbstractPreAuthenticatedProcessingFilter {
    @Override
    protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
      return Optional.ofNullable(request.getHeader(HttpHeaders.AUTHORIZATION)).orElse("");
    }

    @Override
    protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
      return "";
    }
  }
}
```

コントローラ内のメソッドに@PreAuthorize(“hasAuthority('権限名’)")をつけ、メソッド実行前に必要な権限を持っているかをチェックします。

``` 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}")
  @PreAuthorize("hasRole('ROLE_ADMIN') or principal.userId == #userId")
  @ResponseStatus(HttpStatus.OK)
  public void updateUser(
      @PathVariable(PATH_USER_ID) String userId, @RequestBody UpdateUserDto updateUserDto)
      throws TransactionException {
    userService.updateUser(userId, updateUserDto);
  }

  @DeleteMapping("/{user_id}")
  @PreAuthorize("hasRole('ROLE_ADMIN') or principal.userId == #userId")
  @ResponseStatus(HttpStatus.OK)
  public void deleteUser(@PathVariable(PATH_USER_ID) String userId) throws TransactionException {
    userService.deleteUser(userId);
  }

  @GetMapping("/{user_id}")
  @PreAuthorize("hasRole('ROLE_ADMIN') or principal.userId == #userId")
  @ResponseStatus(HttpStatus.OK)
  public GetUserDto getUser(@PathVariable(PATH_USER_ID) String userId) throws TransactionException {
    return userService.getUser(userId);
  }

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

## Spring Security Testの実装
Spring Security には、 JUnit によるテストをサポートする仕組みが用意されています。

`@WithMockUser`、`@WithMockUser`、`@WithUserDetails`のアノテーションを使用して認証されたモックユーザーを作成できます。

また、`@WithSecurityContext` を使用して必要な SecurityContext を作成する独自のアノテーションを作成できます。
以下の例では、`@WithMockCustomUser` という名前のアノテーションを作成できます。

``` java:test/**/security/SecurityUtil.java
public class SpringSecurityUtil {
  @Retention(RetentionPolicy.RUNTIME)
  @WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
  public @interface WithCustomMockUser {

    String username() default "user";

    String password() default "password";

    String userId() default "userId";

    String role() default "role";

    String groupId() default "groupId";
  }
}
```

`@WithMockCustomUser` に `@WithSecurityContext` アノテーションが付けられていることがわかります。これは、Spring Security テストサポートに、テスト用に SecurityContext を作成する予定であることを示すものです。`@WithSecurityContext` アノテーションでは、`@WithMockCustomUser` アノテーションを指定して、新しい SecurityContext を作成する SecurityContextFactory を指定する必要があります。

WithMockCustomUserSecurityContextFactory の実装は以下のとおりです。

``` java:test/**/security/WithMockCustomUserSecurityContextFactory.java
public class WithMockCustomUserSecurityContextFactory
    implements WithSecurityContextFactory<WithCustomMockUser> {

  @Override
  public SecurityContext createSecurityContext(WithCustomMockUser user) {
    List<GrantedAuthority> authorities =
        new ArrayList<GrantedAuthority>(Arrays.asList(new SimpleGrantedAuthority(user.role())));
    List<String> groupIdList = new ArrayList<String>(Arrays.asList(user.groupId()));
    SecurityContext context = SecurityContextHolder.createEmptyContext();
    AccountUser principal =
        new AccountUser(user.username(), user.password(), authorities, user.userId(), groupIdList);
    Authentication authentication =
        new UsernamePasswordAuthenticationToken(
            principal, principal.getPassword(), principal.getAuthorities());
    context.setAuthentication(authentication);
    return context;
  }
}

```
これで、テストクラスまたはテストメソッドに新しいアノテーションを付けることができ、Spring Security の WithSecurityContextTestExecutionListener により、SecurityContext が適切に読み込まれます。

コントローラークラスで作成した[ユニットテスト](#コントローラークラスのユニットテスト)に`@WithCustomMockUser`アノテーションを付与して、テストを追加していきます。

``` java:test/**/UserControllerTest.java
@ContextConfiguration
@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
  @WithAnonymousUser
  void createUser_byAnonymousUser_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
  @WithCustomMockUser(role = "ROLE_ADMIN")
  void updateUser_byAdminUser_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
  @WithCustomMockUser(userId = MOCKED_USER_ID)
  void updateUser_byOwnSelf_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
  @WithCustomMockUser(userId = MOCKED_USER_ID)
  void getUser_byOwnSelf_shouldSuccess() throws Exception {
    mockMvc.perform(get(BASE_URL_PATH + "/" + MOCKED_USER_ID)).andExpect(status().isOk());
  }

  @Test
  @WithCustomMockUser(role = "ROLE_ADMIN")
  void getUser_byAdminUser_shouldSuccess() throws Exception {
    mockMvc.perform(get(BASE_URL_PATH + "/" + MOCKED_USER_ID)).andExpect(status().isOk());
  }

  @Test
  @WithCustomMockUser(userId = "inValidUserId")
  void getUser_byInvalidUser_thenAccessDenied() throws Exception {
    assertThrows(AccessDeniedException.class, () -> userController.getUser(MOCKED_USER_ID));
  }

  @Test
  @WithCustomMockUser(role = "ROLE_ADMIN")
  void listUsers_shouldSuccess() throws Exception {
    mockMvc.perform(get(BASE_URL_PATH)).andExpect(status().isOk());
  }

  @Test
  @WithCustomMockUser
  void lisUsers_byGeneralUser_thenAccessDenied() throws Exception {
    assertThrows(AccessDeniedException.class, () -> userController.listUsers());
  }
}

```

# E2Eテストの導入
Cucumberを利用したE2Eテストの仕方を紹介します。

## Cucumberの導入
build.gradleに追加するdependenciesは以下の通りです。

``` build.gradle
testImplementation  group: 'io.rest-assured', name: 'rest-assured', version: '4.3.1'
testImplementation 'io.cucumber:cucumber-java:6.10.4'
testImplementation 'io.cucumber:cucumber-spring:6.10.4'
testImplementation 'io.cucumber:cucumber-junit:6.10.4'
```

Cucumberをコマンドラインから実行するためのTaskを作成します。

build.gradleに以下のConfigurationを追加します。

``` build.gradle
configurations {
    cucumberRuntime {
        extendsFrom testImplementation
    }
}
```

次にCucumberを実行するためのTaskを作成します。

``` build.gradle
task cucumber() {
    dependsOn assemble, testClasses
    doLast {
        javaexec {
            main = "io.cucumber.core.cli.Main"
            classpath = configurations.cucumberRuntime + sourceSets.main.output + sourceSets.test.output
            args = ['--plugin', 'pretty', '--plugin', 'html:target/cucumber.html', '--glue', 'com.example.api.cucumber', 'src/test/resources']
        }
    }
}
```

このTaskは、src/test/resourcesディレクトリの下の.featureファイルにあるすべてのテストシナリオを実行するように構成されています。

Mainクラスの–glueオプションで、シナリオの実行に必要なステップ定義ファイルの場所を指定します。
–pluginオプションで、テストレポートの形式と場所を指定します。

## テストシナリオの作成
それでは、src/test/resources以下にアプリケーションのテストシナリオ作成していきます。

CucumberでのテストシナリオはGherkinと呼ばれるテスト記述言語フォーマットで書く必要があります。* .featureファイルを作成し、1つ以上のシナリオが含まれている必要があります。
各シナリオにはCucumberが実行・検証する`steps`を記述します。 

``` test/resources/users.feature

Feature: Test /users CRUD endpoints

  Background: Admin user and General user already existed
    When the user "general" already existed
    When the user "admin" already existed
    And the user "admin" creates Admin Group
    Then it returns a status code of 201 for user

  Scenario: Admin user updates General user information
    When the user "admin" updates the user "general" information
    Then it returns a status code of 200 for user

  Scenario: General user updates his information
    When the user "general" updates the user "general" information
    Then it returns a status code of 200 for user
  
  Scenario: Admin user gets General user information
    When the user "admin" updates the user "general" information
    When the user "admin" gets the user "general" information
    Then it returns a status code of 200 for user
    And it returns user "general"

  Scenario: General user gets his information
    When the user "general" updates the user "general" information
    When the user "admin" gets the user "general" information
    Then it returns a status code of 200 for user
    And it returns user "general"
  
  Scenario: Admin user gets all users
    When the user "admin" gets all users
    Then it returns a status code of 200 for user
```

##  E2Eテストの実装
各Cucumberテストの設定を行うクラスを作成します `@RunWith`（Cucumber.class）でアノテーションを付けて、このランナーを使用するようにJUnitに指示し、すべてのCucumber機能を使用できるようにします。

```java:test/**/cucumber/CucumberIntegrationTest.java
@RunWith(Cucumber.class)
@CucumberOptions(
    plugin = {"pretty", "html:target/cucumber-report.html"},
    glue = {"com.example.api.cucumber"},
    features = {"src/test/resources"})
public class CucumberIntegrationTest {}
```

次にCucumberテストに依存性注入を行うCucumberSpringConfigurationクラスを作成します。

`@CucumberContextConfiguration`アノテーションは、このクラスをSpringのテストコンテキスト構成として使用するようにCucumberに指示します。 

```java:test/**/CucumberSpringConfiguration.java
@CucumberContextConfiguration
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class CucumberSpringConfiguration {}
```

各E2Eテストシナリオで使用するメソッドをクラスメソッドとして作成しておきます。

``` java:test/**/cucumber/E2eMethods.java
public class E2eMethods {
  public static String contentType = "application/json";
  public static String BASE_URL = "http://localhost:8080";
  private static E2eMethods e2eMethods = null;
  private static Response response;
  private static String authorizationHeader = "";

  public static E2eMethods getInstance() {
    if (e2eMethods == null) {
      e2eMethods = new E2eMethods();
    }

    return e2eMethods;
  }

  public static RequestSpecification initializeRequest() {
    RestAssured.baseURI = BASE_URL;
    RequestSpecification request = RestAssured.given();
    request.header("Content-Type", contentType);
    request.header("Authorization", authorizationHeader);
    return request;
  }

  public Response post(String endPointURL, String body, String userId) {
    authorizationHeader = userId;
    RequestSpecification request = initializeRequest();
    response = request.body(body).post(endPointURL);
    return response;
  }

  public Response postUser(String endPointURL, String body) {
    RequestSpecification request = initializeRequest();
    response = request.body(body).post(endPointURL);
    return response;
  }

  public Response put(String endPointURL, String body, String userId) {
    authorizationHeader = userId;
    RequestSpecification request = initializeRequest();
    response = request.body(body).put(endPointURL);

    return response;
  }

  public Response deleteGroupUser(String endPointURL, String userId) {
    authorizationHeader = userId;
    RequestSpecification request = initializeRequest();
    response = request.put(endPointURL);

    return response;
  }

  public Response get(String getFormat) {
    RequestSpecification request = initializeRequest();
    response = request.get(getFormat);
    return response;
  }

  public Response delete(String endPointURL, String userId) {
    authorizationHeader = userId;
    RequestSpecification request = initializeRequest();
    response = request.delete(endPointURL);

    return response;
  }

  public Response getWithUserId(String endPointURL, String userId) {
    authorizationHeader = userId;
    RequestSpecification request = initializeRequest();
    response = request.get(endPointURL);
    return response;
  }

  public String getJsonString(Object object) {
    ObjectMapper mapper = new ObjectMapper();
    try {
      return mapper.writeValueAsString(object);
    } catch (JsonProcessingException e) {
      throw new RuntimeException("Error while parsing response body json object", e);
    }
  }
}
```

また、各シナリオで使用するテストデータを取得するためのクラスも作成しておきます。

```java:test/**/cucumber/E2eConstants.java
public class E2eConstants {
  public static final String USERS_ENDPOINT_URL = "/users";
  public static final String GROUPS_ENDPOINT_URL = "/groups";
  public static final String GROUP_USERS = "group-users";
  public static final String STRING_FORMAT_SINGLE_ID = "%s/%s";
  public static final String STRING_FORMAT_TWO_SLASH = "%s/%s/%s";
  public static final String STRING_FORMAT_THREE_SLASH = "%s/%s/%s/%s";
  private static final String MOCKED_EMAIL = "mockedEmail";
  private static final String MOCKED_FAMILY_NAME = "mockedFamilyName";
  private static final String MOCKED_GIVEN_NAME = "mockedGivenName";
  private static final String MOCKED_PREFERRED_LANGUAGE = "mockedPreferredLanguage";
  private static final String MOCKED_PHONE_NUMBER = "mockedPhoneNumber";
  private static final String MOCKED_USER_ID = "mockedUserId";
  private static final String MOCKED_TYPE = "mockedType";

  public static CreateUserDto getCreateUserDto() {
    CreateUserDtoBuilder builder = CreateUserDto.builder();
    return builder.email(MOCKED_EMAIL).build();
  }

  public static UpdateUserDto getUpdateUserDto() {
    UpdateUserDtoBuilder builder = UpdateUserDto.builder();
    UserDetailDto userDetail = getUserDetailDto();
    return builder
        .email(MOCKED_EMAIL)
        .familyName(MOCKED_FAMILY_NAME)
        .givenName(MOCKED_GIVEN_NAME)
        .userDetail(userDetail)
        .build();
  }

  public static UserDetailDto getUserDetailDto() {
    UserDetailDtoBuilder builder = UserDetailDto.builder();
    return builder
        .preferredLanguage(MOCKED_PREFERRED_LANGUAGE)
        .phoneNumber(MOCKED_PHONE_NUMBER)
        .build();
  }

  public static GetUserDto getGetUserDto(String userId) {
    GetUserDtoBuilder builder = GetUserDto.builder();
    return builder
        .userId(userId)
        .email(MOCKED_EMAIL)
        .familyName(MOCKED_FAMILY_NAME)
        .givenName(MOCKED_GIVEN_NAME)
        .userDetail(getUserDetailDto())
        .build();
  }

  public static CreateGroupDto getCreateGroupDto(String groupName) {
    CreateGroupDtoBuilder builder = CreateGroupDto.builder();
    return builder.groupName(groupName).build();
  }

  public static GroupUserDto getGroupUserDto(String userId) {
    GroupUserDtoBuilder builder = GroupUserDto.builder();
    return builder.userId(userId).type(MOCKED_TYPE).build();
  }

  public static List<GroupUser> getGroupUsers() {
    GroupUser groupUser = GroupUser.builder().userId(MOCKED_USER_ID).type(MOCKED_TYPE).build();
    return new ArrayList<GroupUser>(Arrays.asList(groupUser));
  }

  public static GetGroupDto getGetGroupDto(String groupId, String groupName) {
    GetGroupDtoBuilder builder = GetGroupDto.builder();
    return builder.groupId(groupId).groupName(groupName).build();
  }
}

```

それでは.featureファイルで定義したシナリオに記載したステップを検証する機能を実装していきます。

Cucumberは、`@When`、`@And`、`@Then`などのアノテーションが付けられたメソッドを実行します。 

アノテーションにテストシナリオで定義した各ステップを記載します。

また、変数として用いたい箇所は{}で変数の型を括ると、テストシナリオで用いた変数を各テスト内で受け取れます。
 
```java:test/**/cucumber/UsersStepdefs.java
public class UsersStepdefs extends CucumberSpringConfiguration {

  private static E2eMethods e2eMethods = E2eMethods.getInstance();
  private String userId;
  private final HashMap<String, String> userIds = new HashMap<>();
  private Response response;
  private final String ADMIN_GROUP = "admin";

  @When("the user {string} already existed")
  public void theUserIsCreated(String user) {
    CreateUserDto createUserDto = E2eConstants.getCreateUserDto();
    String body = e2eMethods.getJsonString(createUserDto);
    response = e2eMethods.postUser(USERS_ENDPOINT_URL, body);
    userId = response.getBody().asString();
    userIds.putIfAbsent(user, userId);
  }

  @And("the user {string} creates Admin Group")
  public void adminGroupIsCreated(String executionUser) {
    CreateGroupDto createGroupDto = E2eConstants.getCreateGroupDto(ADMIN_GROUP);
    String groupBody = e2eMethods.getJsonString(createGroupDto);
    response = e2eMethods.post(GROUPS_ENDPOINT_URL, groupBody, userIds.get(executionUser));
  }

  @When("the user {string} updates the user {string} information")
  public void theUserUpdatesUserInformation(String executionUser, String targetUser) {
    UpdateUserDto updateUserDto = E2eConstants.getUpdateUserDto();
    String body = e2eMethods.getJsonString(updateUserDto);
    response =
        e2eMethods.put(
            String.format(STRING_FORMAT_SINGLE_ID, USERS_ENDPOINT_URL, userIds.get(targetUser)),
            body,
            userIds.get(executionUser));
  }

  @When("the user {string} gets the user {string} information")
  public void theUserGetTheUserInformation(String executionUser, String targetUser) {
    response =
        e2eMethods.getWithUserId(
            String.format(STRING_FORMAT_SINGLE_ID, USERS_ENDPOINT_URL, userIds.get(targetUser)),
            userIds.get(executionUser));
  }

  @And("it returns user {string}")
  public void itReturnsUser(String targetUser) {
    GetUserDto getUserDto =
        e2eMethods.convertJsonStrToDataObject(response.getBody().asString(), GetUserDto.class);
    GetUserDto expectedGetUserDto = E2eConstants.getGetUserDto(userIds.get(targetUser));
    assertThat(getUserDto.getUserId()).isEqualTo(userIds.get(targetUser));
    assertThat(getUserDto.getEmail()).isEqualTo(expectedGetUserDto.getEmail());
    assertThat(getUserDto.getFamilyName()).isEqualTo(expectedGetUserDto.getFamilyName());
    assertThat(getUserDto.getGivenName()).isEqualTo(expectedGetUserDto.getGivenName());
    assertThat(getUserDto.getUserDetail().getPhoneNumber())
        .isEqualTo(expectedGetUserDto.getUserDetail().getPhoneNumber());
    assertThat(getUserDto.getUserDetail().getPreferredLanguage())
        .isEqualTo(expectedGetUserDto.getUserDetail().getPreferredLanguage());
  }

  @When("the user {string} gets all users")
  public void adminUserGetsAllUsers(String executionUser) {
    response = e2eMethods.getWithUserId(USERS_ENDPOINT_URL, userIds.get(executionUser));
  }

  @Then("it returns a status code of {int} for user")
  public void validateStatusCode(int statusCode) {
    assertThat(response.getStatusCode()).isEqualTo(statusCode);
  }
}
```

# まとめ
Scalar DBとSpring Bootを使ったAPIの開発方法について説明しました。

Scalar DBを使ってみたいという方のご参考になれば幸いです。

# 追記
Scalar DB と Spring Boot を使った API 開発を行う際の例外処理について[こちら](https://qiita.com/yoshi-kinoshita/items/8ddbc5fa1b4a325be759)にまとめました。

# 参考
- [Scalar DB](https://github.com/scalar-labs/scalardb)
- [Scalar DB design document](https://github.com/scalar-labs/scalardb/blob/master/docs/design.md)
- [Scalar DB supported databases](https://github.com/scalar-labs/scalardb/blob/master/docs/scalardb-supported-databases.md)
- [Database schema in Scalar DB](https://github.com/scalar-labs/scalardb/blob/master/docs/schema.md)
- [Getting Started with Scalar DB](https://github.com/scalar-labs/scalardb/blob/master/docs/getting-started.md)
- [Spring Security事前認証シナリオ](https://spring.pleiades.io/spring-security/site/docs/5.0.1.RELEASE/reference/html/preauth.html)
- [Gherkin Reference](https://cucumber.io/docs/gherkin/reference/)
- [Getting Started with Cucumber](https://www.raywenderlich.com/26211276-getting-started-with-cucumber)