初めてのJUnit5単体テスト、最低限知っておきたいあれこれ

:writing_hand_tone1:初めてjavaでテストコードを書くという経験をし、わからないことだらけで
書き方から分からず大変だったので自分の備忘録や復習のため、記事を書きました。
同じような方の少しでも参考になればと思います。

※必要なプラグイン、設定、importは記載していません。

開発環境：　
intellij / java17 / junit5

### テストの実行方法

![スクリーンショット 2022-09-29 22.19.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/543815/4c58e858-2f75-aef6-fbfd-847e5d57bbdd.png)
左の<font color="green">▶︎</font>のマークを押してテストの実行が可能

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/543815/d8a589e6-adab-afb2-9f09-33e8c50e6e30.png)
テストに成功した場合、緑の✅が表示されます。

### アサーション

```CalculatorApp.java

// テスト対象のクラス
public class CalculatorApp {

    // 除算メソッド
    public int testDivision(int dividend ,int divisor) {
        return dividend/divisor;
    }
}
```


```CalculatorAppTest.java

// テスト実装クラス
class CalculatorAppTest {

    //テストメソッド
    @Test
    @DisplayName("除算メソッドに割り切れる整数が引数で渡された時の検証")
    void testDivision() {
        var calculator = new CalculatorApp();
        var result = calculator.testDivision(4,2);
        // アサーション
        assertEquals(2,result);
    }
}
```
:::note info
テストしたいメソッドが正常に動作するかどうか、および正しい結果を返すかどうかを確認するために、assertを使用する。与えられた入力パラメータで条件を評価する。
:::

##### アサーションの種類

http://www.ne.jp/asahi/hishidama/home/tech/java/junit/5/assertion.html



### テストメソッド内部の構造化、パターン

**AAA（トリプルエー）**

:::note info
テストコードを読みやすくするための指針です。AAA は arrange, act, assert の略語で、arrange は『準備』、act は『実行』、assertは『検証』を表します。
:::

* Arrange
    * テストメソッドを呼び出すためのインスタンス生成、入力パラメータの準備
* Act
    * テスト中のメソッドを呼び出し、実際の結果を返す
* Assert
    * 保持された値を検証する


```CalculatorAppTest.java

class CalculatorAppTest {

    @Test
    @DisplayName("除算メソッドに割り切れる整数が引数で渡された時の検証")
    void testDivision() {

        // Arrange　テストメソッドを呼び出すためのインスタンス生成、入力パラメータの準備
        var calculator = new CalculatorApp();
        var dividend = 4;
        var divisor = 2;
        var expectedResult = 2;

        // Act テスト中のメソッドを呼び出し、実際の結果を返す
        var actualResult = calculator.testDivision(dividend,divisor);

        // Assert　保持された値を検証する
        assertEquals(expectedResult,actualResult);
    }
}
```


### テストライフサイクル
※数字は実行される順を示しています。
![＠BeforeAll (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/543815/c04a876f-2e89-507d-d257-0054cc917be0.png)

* BeforeAll
全てのテストメソッドの前にメソッドを1回だけ実行させる

* AfterAll
全てのテストメソッド完了後にのみ1回だけ実行される

* BeforeEach
付与されているメソッドが各テストの実行前に実行される

* AfterEach
付与されているメソッドが各テストの完了後に実行される

:::note info
この中でもよく使用するのは`@BeforeEach`
:::

### 例外アサーション

```CalculatorAppTest.java

class CalculatorAppTest {

    CalculatorApp calculator;

    @Test
    @DisplayName("除算メソッドに割り切れない整数が引数で渡された時、例外がスローされる検証")
    void testDivision_error() {

        calculator = new CalculatorApp();

        // Arrange
        var dividend = 4;
        var divisor = 0;
        var exceptionMessage = "/ by zero";

        // Act & Assert
        // 例外が発生することを確認する例外アサート
        var arithmeticException = assertThrows(ArithmeticException.class, () -> {
            // Act
            calculator.testDivision(dividend,divisor);
        });

        // Assert
        assertEquals(exceptionMessage, arithmeticException.getMessage());
    }
}
```
:::note warn
assertThrowsの第1引数で「発生するであろう例外」のクラスを指定し、第2引数（ラムダ式）でテスト対象の処理を実行する。
:::

### モックオブジェクトの作成

```UserServiceTest.java

@ExtendWith(MockitoExtension.class) // このクラスでMockitoアノテーションを使えるようになる
public class UserServiceTest {
    // 処理省略
}

```
:::note warn
`@ExtendWith(MockitoExtension.class)`
このクラスでMockitoアノテーションを使えるようになる
:::

```UserServiceTest.java

@ExtendWith(MockitoExtension.class)
public class UserServiceTest {

    @InjectMocks
    UserService userService;

    @Mock
    UsersRepository userRepository;

    // 処理省略
}

```
:::note warn
`@Mock`
モック化するクラスにつける
`@InjectMocks`
テスト対象クラスにモックを注入
:::

```UserServiceTest.java

@ExtendWith(MockitoExtension.class)
public class UserServiceTest {

    // 上記の処理省略
    @DisplayName("ユーザー作成の検証")
    @Test
    void testCreateUser() {

    // Arrange
    Mockito.when(usersRepository.save(any(User.class)).thenReturn(true);

    // Act
    var user = userService.createUser(firstName, lastName, email, password, repeatPassword)

    // Assert
    assertNotNull(user);
    assertEquals(firstName, user.getFirstName());
    assertEquals(lastName, user.getLastName());
    assertEquals(email, user.getEmail());
    assertNotNull(user.getId());
    verify(usersRepository, Mockito.times(1)).save(any(User.class));
    }
}

```
:::note warn
**モック化したクラスのメソッドを実行したときの戻り値を設定する**

* 書き方は2パターン
    * `when(モックインスタンス.メソッド(引数)).thenReturn(戻り値);`
    * `doReturn(戻り値).when(モックインスタンス).メソッド(引数);`
:::

### メソッド呼び出しの検証

```UserServiceTest.java

@ExtendWith(MockitoExtension.class)
public class UserServiceTest {

// 上記の処理省略
    @Test
    @DisplayName("ユーザー作成の検証")
    void testCreateUser() {

    // Arrange
    // モック化したクラスのメソッドを実行したときの戻り値を設定する
    // Mockito.when(モックインスタンス.メソッド(引数)).thenReturn(戻り値)
    Mockito.when(usersRepository.save(any(User.class)).thenReturn(true);

    // Act
    var user = userService.createUser(firstName, lastName, email, password, repeatPassword)

    // Assert
    // メソッド呼び出しの検証
    verify(usersRepository, times(1)).save(any(User.class));
    // 一度もメソッド呼び出しがされない検証。never()はtimes(0)と同じ
    // verify(usersRepository, never()).save(any(User.class));

```
:::note warn
**メソッドが呼ばれた回数をテストする**
`verify(モックオブジェクト, times(回数)).テストするメソッド(引数);`

:::

### JPAエンティティのテスト

```UserEntityTest.java

@DataJpaTest
public class UserEntityTest {

    // エンティティマネージャの代替オブジェクト、データ層をテストするときのクラス
    // 情報を永続化し、すぐにデータベーステーブルと同期させることができる
    @Autowired
    private TestEntityManager testEntityManager;

    @Test
    void userEntitytest() {
        // Arrange
        var userEntity = new UserEntity();
        userEntity.setUserId(UUID.randomUUID().toString());
        userEntity.setFirstName("yamada");
        userEntity.setLastName("taro");
        userEntity.setEmail("test@test.com");
        userEntity.setPassword("pass");

        // Act
        // テストデータを設定
        var resultUserEntity = testEntityManager.persistAndFlush(userEntity);

        // Assert
        assertTrue(resultUserEntity.getId() > 0);
        assertEquals(userEntity.getUserId(), resultUserEntity.getUserId());
        assertEquals(userEntity.getFirstName(), resultUserEntity.getFirstName());
        assertEquals(userEntity.getLastName(), resultUserEntity.getLastName());
        assertEquals(userEntity.getEmail(), resultUserEntity.getEmail());
        assertEquals(userEntity.getPassword(), resultUserEntity.getPassword());
    }
}
```

:::note warn
**TestEntityManagerはEntityManagerの代わり**
情報を永続化し、すぐにデータベーステーブルと同期させることができる
:::

https://spring.pleiades.io/spring-boot/docs/current/api/org/springframework/boot/test/autoconfigure/orm/jpa/TestEntityManager.html


### JPQLクエリ、ソースコードのテスト

```UserRepository.java

// テスト対象のリポジトリ
@Repository
public interface UserRepository extends PagingAndSortingRepository<UserEntity, Long> {

    @Query("select user from UserEntity user where user.email like %:emailDomain")
    List<UserEntity> findUsersWithEmailEndingWith(@Param("emailDomain") String emailDomain);
}
    
```

```UserRepositoryTest.java

//コード省略

    @Autowired
    private TestEntityManager testEntityManager;
    private final String email = "test@test.com";
    private final String email2 = "test@test.com";

    @BeforeEach
    void setup() {
        // ユーザー作成
        // first user
        var userEntity = new UserEntity();
        userEntity.setUserId(userId1);
        userEntity.setEmail(email1);
        userEntity.setPassword("12345678");
        userEntity.setFirstName("tanaka");
        userEntity.setLastName("ichiro");
        // テストデータを設定
        testEntityManager.persistAndFlush(userEntity);

        // second user
        var userEntity2 = new UserEntity();
        userEntity2.setUserId(userId2);
        userEntity2.setEmail(email2);
        userEntity2.setPassword("abcdefgh");
        userEntity2.setFirstName("takeda");
        userEntity2.setLastName("hanako");
        // テストデータを設定
        testEntityManager.persistAndFlush(userEntity2);
    }

    @Test
    void testfindUsersWithEmailEndingWith() {
         // Arrange
        var userEntity = new UserEntity();
        userEntity.setUserId(UUID.randomUUID().toString());
        userEntity.setEmail("test@gmail.com");
        userEntity.setPassword("pass");
        userEntity.setFirstName("yamada");
        userEntity.setLastName("taro");
        testEntityManager.persistAndFlush(userEntity);
        
        var emailDomailName = "@gmail.com";

        // Act
        var users = useraRepository.findUsersWithEmailEndingWith(emailDomainName);

        // Assert
        // emailドメインが"@gmail.com"のユーザーは一件しか取得できないこと
        Assertions.assertEquals(1,users.size());
        //emailドメインが一致すること
        Assertions.assertTrue(users.get(0).getEmail().endsWith(emailDomainName));
    }

```