스프링 테스트(2) - JUnit 단일 클래스 단위 테스트

2024. 2. 20. 23:39spring/TEST

과제

애플리케이션 모듈을 따로 분리해 테스트한 후 다시 조합해서 테스트하는 것이 가장 흔한 테스트 방식이다. 이 방식으로 애플리케이션을 테스트 해보자

 

해결책

단위 테스트의 쓰임새는 하나의 프로그램 단위를 테스트하는 것이다. 객체 지향 언어에서 단위란 보통 클래스나 메서드를 가리킨다. 단위 테스트의 범위는 하나의 단위 하나로 국한되지만 실제로 단위가 홀로 움직이는 일은 거의 없고 다른 단위와 함께 작동되는 경우가 대부분이다. 다른 단위와 의존 관계를 지닌 단위를 테스트할 때에는 보통 스텁이나 목 객체로 단위 간 의존 관계를 모방해서 테스트의 복잡도를 낮춘다.

 

스텁은 테스트에 필요한 최소한의 메서드만으로 의존 객체를 시뮬레이션한 객체로, 보통 메서드는 사전에 정해진 로직으로 하드 코딩한 데이터를 이용해 구현한다. 스텁은 테스트 코드가 그 내부 상태를 확인할 수 있도록 어떤 메서드를 표출한다. 스텀과 대조적으로, 

 

목 객체는 자신의 메서드를 테스트 코드가 어떤 식으로 호출할 거라 기대한다.따라서 실제로 호출된 메서드와 호출되리라 기대했던 메서드를 비교 검증할 수 있다.

 

스텁은 대개 상태를 검증할 때 쓰이며 목 객체는 수행 로직을 검증할 때 쓰인다는 점에서 차이가 있다.

 

단일 클래스 대한 단위 테스트 작성하기

 

은행 시스템의 핵심은 대부분 고객 계정 관리에 관한 기능이다. 다음과 같이 도메인 클래스 Account를 작성한다.

@SequenceGenerator(
        name = "ACCOUNT_SEQ_GENERATOR",
        sequenceName = "ACCOUNT_SEQ",
        initialValue = 1, allocationSize = 50
)
@Entity
@Getter @Setter
public class Account {

    @Id @GeneratedValue(
            strategy = GenerationType.SEQUENCE,
            generator = "ACCOUNT_SEQ_GENERATOR"
    )
    private Long id;

    private String accountNo;
    private double balance;

    public Account() { }

    public Account(String accountNo, double balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }
}

 

 

다음은 은행 시스템의 퍼시스턴스 레이어에서 계정 객체를 처리할 리포티토리이다.

public interface AccountRepository {
    void createAccount(Account account);
    void updateAccount(Account account);
    void removeAccount(Account account);
    List<Account> findAccount(String accountNo);
}

 

 

위 리포지토리를 구현한다.

@RequiredArgsConstructor
@Repository
public class AccountRepositoryImpl implements AccountRepository {

    private final EntityManager em;

    @Override
    public void createAccount(Account account) {
        List<Account> accountList = findAccount(account.getAccountNo());
        if (!accountList.isEmpty()) {
            throw new IllegalArgumentException("이미 있는 계좌입니다.");
        }

        em.persist(account);
    }

    @Override
    public void updateAccount(Account account) {
        List<Account> accountList = findAccount(account.getAccountNo());
        if (accountList.isEmpty()) {
            throw new NoSuchElementException("없는 계좌입니다.");
        }
        em.merge(account);
    }

    @Override
    public void removeAccount(Account account) {
        List<Account> accountList = findAccount(account.getAccountNo());
        if (accountList.isEmpty()) {
            throw new NoSuchElementException("없는 계좌입니다.");
        }
        em.remove(account);
    }

    @Override
    public List<Account> findAccount(String accountNo) {
        return em.createQuery("SELECT a FROM Account a WHERE a.accountNo = :accountNo", Account.class)
                .setParameter("accountNo", accountNo)
                .getResultList();
    }
}
 

 

간단히 구현한 코드라서 트랜잭션은 어노테이션 설정을 하지 않고 관리는 @Before, @After 어노테이션에게 맡겼다.

 

JUnit으로 리포지토리 단위 테스트를 작성하자.

 

@Slf4j
class AccountRepositoryImplTest {

    private AccountRepository accountRepository;
    private EntityManagerFactory emf;
    private EntityManager em;
    private EntityTransaction tx;
    private static final String EXISTING_ACCOUNT_NO = "A";
    private static final String NEW_ACCOUNT_NO = "B";

    @BeforeEach
    void init() {
        emf = Persistence.createEntityManagerFactory("hello");
        em  = emf.createEntityManager();
        tx  = em.getTransaction();
        accountRepository = new AccountRepositoryImpl(em);
        tx.begin();

        Account account = new Account();
        account.setAccountNo(EXISTING_ACCOUNT_NO);
        account.setBalance(0.2);

        accountRepository.createAccount(account);
    }

    @AfterEach
    void close() {
        tx.commit();
        em.close();
        emf.close();
    }

    @Test
    void accountExists() {
        assertEquals(1, accountRepository.findAccount(EXISTING_ACCOUNT_NO).size());
        assertEquals(0, accountRepository.findAccount(NEW_ACCOUNT_NO).size());
    }

    @Test
    void createNewAccount() {
        Account account = new Account(NEW_ACCOUNT_NO, 0.2);
        accountRepository.createAccount(account);
        assertEquals(accountRepository.findAccount(account.getAccountNo()).get(0), account);
    }

    @Test
    void createDuplicateAccount() {
        Account account = new Account();
        account.setAccountNo(EXISTING_ACCOUNT_NO);
        account.setBalance(0.2);

        assertEquals(1, accountRepository.findAccount(EXISTING_ACCOUNT_NO).size());
        assertThrows(IllegalArgumentException.class,
                () -> accountRepository.createAccount(account));
    }
}