JUnit 테스트 - Account 컨트롤러, 서비스 생성

jeongjin-kim·2023년 7월 19일

JUnit5

목록 보기
6/11
post-thumbnail

최주호 강사님의 인프런 강좌 정리 및 실습한 기록

목표

  • 로그인 후, Account 객체를 등록한다.

구현사항

  • @Service 생성
  • @RestConroller 생성
  • 계좌 등록 테스트 용 Dummy 객체 추가
  • 테스트

서비스 생성

Database -> Service -> Controller 순으로 생성한다.

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class AccountService {

    private final AppUserRepository appUserRepository;
    private final AccountRepository accountRepository;

    @Transactional
    public AccountSaveResponseDTO registerAccount(AccountSaveRequestDTO accountSaveRequestDTO, Long appUserId) {
        // AppUser 가 DB 에 존재하는지 검증
		// --- 빈칸을 채우시오 (1) ---

        // 해당 계좌가 DB 에 존재하는 지 검증
		// --- 빈칸을 채우시오 (2) ---

        // DB 에 계좌 등록
        // DTO 를 Entity 로 변경
		// --- 빈칸을 채우시오 (3) ---

        // DTO 응답
        // --- 빈칸을 채우시오 (4) ---
    }

}

빈칸을 채워보겠습니다. 현재 상황을 한번 점검해보자면

  • spring data jpa 가 적용되어 있습니다.
  • 아주 간단하게 1건 입력, 1건 조회를 구현합니다.
  • 응답 시 사용할 DTO 를 만들어야 합니다.
  • DB 조회 시 에러가 나는 경우, 커스텀 에러를 컨트롤러에 전달합니다.

JPA 에 대한 기본적인 지식이 있어야 작성이 가능합니다.

AccountSaveResponseDTO

DTO 는 하나의 클래스에서 관리합니다.
inner static class 로 필요한 DTO 를 계속 추가해나갑니다.
@Valid 를 통한 간단한 유효성 검증만 적용되어 있습니다.


public class AccountResponseDTO {

    @Getter
    @Setter
    public static class AccountSaveResponseDTO {
        private Long id;
        private Long number;
        private Long balance;

        // 생성자
        public AccountSaveResponseDTO(Account account) {
            this.id = account.getId();
            this.number = account.getNumber();
            this.balance = account.getBalance();
        }
    }
}

AccountRequestDTO

Account 를 저장하고자 요청하는 DTO 객체입니다.
간단한 테스트 목적으로 작성되었으므로, 길이를 4로 고정합니다.

public class AccountRequestDTO {

    @Getter
    @Setter
    public static class AccountSaveRequestDTO {

        @NotNull
        @Digits(integer = 4, fraction = 4)  // 최소4자, 최대 4자
        private Long number;

        @NotNull
        @Digits(integer = 4, fraction = 4)
        private Long password;

        public Account toEntity(AppUser appUser) {
            return Account.builder()
                    .number(number)
                    .password(password)
                    .balance(1000L)
                    .appUser(appUser)
                    .build();
        }

    }
}



AccountService

JPA 를 알고 있다면 save, findBy... 으로 별다른 로직없이 작성할 수 있습니다.
강의에서는 JPA 가 기본적으로 제공하는 기능들에 대해서는 테스트하지 않습니다.
추후 fetch join 에 관한 리팩토링이 이루어집니다.


@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class AccountService {

    private final AppUserRepository appUserRepository;
    private final AccountRepository accountRepository;

    @Transactional
    public AccountSaveResponseDTO registerAccount(AccountSaveRequestDTO accountSaveRequestDTO, Long appUserId) {
        // AppUser 가 DB 에 존재하는지 검증
        AppUser appUserPS = appUserRepository.findById(appUserId).orElseThrow(
                () -> new CustomApiException("해당 유저를 찾을 수 없습니다.")
        );

        // 해당 계좌가 DB 에 존재하는 지 검증
        Optional<Account> account = accountRepository.findByNumber(accountSaveRequestDTO.getNumber());
        if (account.isPresent()) {
            throw new CustomApiException("해당 계좌가 이미 존재합니다.");
        }

        // DB 에 계좌 등록
        // DTO 를 Entity 로 변경
        Account accountPS = accountRepository.save(accountSaveRequestDTO.toEntity(appUserPS));

        // DTO 응답
        return new AccountSaveResponseDTO(accountPS);
    }

}

AccountController

로그인을 한 유저가 계좌를 등록하려고 합니다.
컨트롤러에서는 어떤 인자가 필요할까요?

현재 AOP 가 적용되어 있습니다. 그리고 1명의 유저가 n개의 계좌를 개설할 수 있습니다.


@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class AccountController {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    private final AccountService accountService;

    @PostMapping("/account/save")
    @ResponseStatus(HttpStatus.CREATED)
    public ResponseEntity<?> saveAccount(
            @RequestBody @Valid AccountSaveRequestDTO accountSaveRequestDTO,
            BindingResult bindingResult,
            @AuthenticationPrincipal LoginAppUser loginAppUser
    ) {
        AccountSaveResponseDTO accountSaveResponseDTO = accountService.registerAccount(accountSaveRequestDTO, loginAppUser.getAppUser().getId());
        return new ResponseEntity<>(new ResponseDTO<>(1, "계좌를 성공적으로 등록했습니다.", accountSaveResponseDTO), HttpStatus.CREATED);
    }

}

AOP 가 적용되어 BindingResult 에 에러가 담기고, 계좌 정보와 사용자 정보가 컨트롤러가 전달되는 상황입니다.

Dummy 생성

이전까지가 회원가입, 로그인이었다면, 그 이후에 사용자의 요청에는 매번 token 이 필요합니다.
회원가입의 간편화를 위해서 CommandLineRunner 를 사용합니다.

@Configuration
public class DummyDevInit extends DummyObject {

    @Bean
    @Profile("dev") // prod 에서는 실행되지 않도록 특정한 profile 을 지정한다.
    CommandLineRunner init(AppUserRepository appUserRepository) {
        return (args) -> {
            // 서버 실행  시 무조건 실행된다.
            appUserRepository.save(newAppUser("ssar", "bori"));
        };
    }

}

AppUser 와 동일하게 newAccount, newMockAccount 를 생성합니다.
Dummy 는 @Test 환경에서 실제 데이터가 아닌, 테스트 용 샘플 데이터 생성에 사용됩니다.

/*
* 편의를 위해 비밀번호는 1234 고정
* 해당 객체는 테스트 시 DB 에 저장하기 위한 테스트 객체이다.
*
* */
protected Account newAccount(Long number, AppUser appUser) {
    return Account.builder()
            .number(number)
            .password(1234L)
            .balance(1000L)
            .appUser(appUser)
            .build();
}


/*
* DB 에서 select 시 사용되는 테스트 객체이다.
*
* */
protected Account newMockAccount(Long id, Long number, Long balance, AppUser appUser) {
    return Account.builder()
            .id(id)
            .number(number)
            .password(1234L)
            .balance(balance)
            .appUser(appUser)
            .createdAt(LocalDateTime.now())
            .updatedAt(LocalDateTime.now())
            .build();
}

JUnit5 테스트

@SpringBootTest 는 통합 테스트를 위한 어노테이션입니다. 스프링 환경을 전부 메모리에 띄우므로 단위 테스트에서는 사용이 권장되지 않습니다.
@ExtendWith 어노테이션을 사용하여 Mockito 환경만을 불러옵니다.

@ExtendWith(MockitoExtension.class)
public class AccountServiceTest() extends DummyObject {
 	// ...
    
}

의존성 주입
계좌 등록을 위해서 AppUser, Account repo 를 불러옵니다.


@InjectMocks // 모든 Mock 들이 @InjectMocks 로 주입된다.
private AccountService accountService;

@Mock // 가짜 repo
private AccountRepository accountRepository;

@Mock
private AppUserRepository appUserRepository;

@Spy    // 가짜 환경에 주입되는 진짜 객체
private ObjectMapper objectMapper;

이후, 계좌 생성에 필요한 계좌 요청 정보, 사용자 정보 를 생성합니다.
계좌를 repo 에 등록 후, 해당 계좌의 정보가 방금 생성한 계좌의 정보와 일치하는 지 테스트하는 사실 크게 의미는 없는 테스트 입니다.
Mockito 테스트에 익숙해지기 위한 여러 과정 중 하나라고 생각하시면 될 것 같습니다.


@Test
public void account_register_test() throws JsonProcessingException {
    /* given */
    Long appUserId = 1L;

    AccountSaveRequestDTO accountSaveRequestDTO = new AccountSaveRequestDTO();
    accountSaveRequestDTO.setNumber(1000L);
    accountSaveRequestDTO.setPassword(1234L);

    /* stub 1 */
    AppUser appUser = newMockAppUser(appUserId, "ssar", "bori");
    when(appUserRepository.findById(any())).thenReturn(Optional.of(appUser));

    /* stub 2 */
    when(accountRepository.findByNumber(any())).thenReturn(Optional.empty());


    /* stub 3*/
    Account account = newMockAccount(1L, 1000L, 1000L, appUser);
    when(accountRepository.save(any())).thenReturn(account);


    AccountSaveResponseDTO accountSaveResponseDTO = accountService.registerAccount(accountSaveRequestDTO, appUserId);
    String responseBody  = objectMapper.writeValueAsString(accountSaveResponseDTO);
    System.out.println(responseBody);

    /* then */
    Assertions.assertThat(accountSaveResponseDTO.getNumber()).isEqualTo(1000L);

}

http 테스트

인텔리제이에서 제공하는 http 를 활용해서 테스트 해보겠습니다.
로그인 후 생성되는 토큰을 global 변수로 활용해서 복사, 붙여넣는 과정을 생략합니다.
JUnit 의 테스트와 마찬가지로 개별 테스트, 통합 테스트 처럼 실행할 수 있습니다.

로그인 요청

위에서 생성한 CommandLineRunner 로 서버 실행 순간에 sample 유저가 회원가입 됩니다.

client.log 를 통해서 콘솔에 출력할 수 있는데, 이는 상단에 Response Handler 탭에서 확인할 수 있습니다.
http header 에 'Authorization' 으로 담긴 'Bearer 어쩌고저쩌고' 를 global 변수로 등록합니다. 세션이 살아있는 동안에 유지됩니다.

POST 요청을 보내면, 다음과 같이 JWT 가 생성됩니다.

로그인 요청 - 테스트

다음과 같이 client.test 를 통해서 .http 파일 내에서 테스트를 진행할 수 있습니다.
이 사이트 에서 좀 더 자세한 내용을 확인할 수 있습니다.
예시처럼 간단한 테스트 말고, 꽤 정교한 테스트도 가능해보입니다.

토큰 길이가 -1 이라고 놓고 요청을 POST 요청을 보내보면 Tests 탭에 다음과 같이 나옵니다.

계좌 생성 요청

위에서 만들어진 JWT 를 사용해봅시다. 사용하는 방법은 {{ ... }} 입니다.

이제 생성된 토큰을 매번 복사, 붙여넣기 할 수고를 덜 수 있게 되었습니다!

사용의의

  • .env 파일을 만들어서 민감한 정보를 가릴 수 있습니다.
  • .http 파일로 만들기 때문에, 버전 관리를 할 수 있습니다 😄
  • HTTP 요청과 동시에 결과값을 검증할 수 있습니다.

0개의 댓글