본 포스팅은 백기선님의 "더 자바, 애플리케이션을 테스트하는 다양한 방법" 을 보고 정리한 글 입니다.
관심 있으신 분들은 https://www.inflearn.com/course/the-java-application-test 를 살펴보세요

개요

Junit5의 기본 애너테이션이라고 할 수 있는 @Test, @BeforeAll, @BeforeEach, @AfterAll, @AfterEach, @Disabled 를 알아보자.

@Test

본 어노테이션을 붙이면 Test 메서드로 인식하고 테스트 한다.
JUnit5 기준으로 접근제한자가 Default 여도 된다. (JUnit4 까지는 public이어야 했었다.)

    @Test
    void create1() {
        Study study = new Study();
        assertNotNull(study);
        System.out.println("create1()");
    }

    @Test
    void create2() {
        System.out.println("create2()");
    }

@BeforeAll

본 어노테이션을 붙인 메서드는 해당 테스트 클래스를 초기화할 때 딱 한번 수행되는 메서드다.
메서드 시그니쳐는 static 으로 선언해야한다.

    @BeforeAll
    static void beforeAll() {
        System.out.println("@BeforeAll");
    }

@BeforeEach

본 어노테이션을 붙인 메서드는 테스트 메서드 실행 이전에 수행된다.

    @BeforeEach
    void beforeEach() {
        System.out.println("@BeforeEach");
    }

@AfterAll

본 어노테이션을 붙인 메서드는 해당 테스트 클래스 내 테스트 메서드를 모두 실행시킨 후 딱 한번 수행되는 메서드다.
메서드 시그니쳐는 static 으로 선언해야한다.

    @AfterAll
    static void afterAll() {
        System.out.println("@AfterAll");
    }

@AfterEach

본 어노테이션을 붙인 메서드는 테스트 메서드 실행 이후에 수행된다.

    @AfterEach
    void afterEach() {
        System.out.println("@AfterEach");
    }

@Disabled

본 어노테이션을 붙인 테스트 메서드는 무시된다.

    @Disabled
    @Test
    void create3() {
        System.out.println("create3()");
    }

코드

/**
 * @author : Eunmo Hong
 * @since : 2020/08/13
 */

class StudyTest {

    @Test
    void create1() {
        Study study = new Study();
        assertNotNull(study);
        System.out.println("create1()");
    }

    @Test
    void create2() {
        System.out.println("create2()");
    }

    @Disabled
    @Test
    void create3() {
        System.out.println("create3()");
    }

    @BeforeAll
    static void beforeAll() {
        System.out.println("@BeforeAll");
    }


    @AfterAll
    static void afterAll() {
        System.out.println("@AfterAll");
    }

    @BeforeEach
    void beforeEach() {
        System.out.println("@BeforeEach");
    }

    @AfterEach
    void afterEach() {
        System.out.println("@AfterEach");
    }
}

실행 콘솔

  • @BeforeAll 이 실행된다.
    --------반복------
    1. @BeforeEach 가 실행된다
    2. @Test 를 붙인 메서드가 실행된다.
    3. @AfterEach 가 실행된다
    --------반복------
  • @AfterAll 이 실행된다

깃헙 주소

링크

https://github.com/code-chobo/dev-api 프로젝트를 진행하며 쓰는 회고입니다.

문제점

  • 테스트 시 현재 로그인된 사용자가 있다는 전제로 테스트를 한다면?
    • 테스트 내부에서 SecurityContextHolder.context 등에 UsernamePasswordAuthenticationToken 을 set 해주고.. 어쩌구 저쩌구.. 테스트가 몇 개 없다면 상관없지만, 테스트가 여러 개 있다면? 일일히 다 해줘야하나?
    • WithSecurityContextFactory 를 이용하자.

코드

WithAccountSecurityContextFactory.class

/**
 * @author : Eunmo Hong
 * @since : 2020/07/26
 */

@RequiredArgsConstructor
public class WithAccountSecurityContextFactory implements WithSecurityContextFactory<WithAccount> {

    private final AccountService accountService;

    @Override
    public SecurityContext createSecurityContext(WithAccount annotation) {
        String nickname = annotation.value();

        String email = nickname + "@email.com";
        String password = "11111111";
        JoinAccountRequest joinAccountRequest = new JoinAccountRequest(email, nickname, password, password);
        accountService.join(joinAccountRequest);

        UserDetails userDetails = accountService.loadUserByUsername(email);
        Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(authentication);
        return context;
    }
}

WithAccount

@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithAccountSecurityContextFactory.class)
public @interface WithAccount {
    String value();
}

테스트

StudyApiControllerIntegrationTest

...
    @DisplayName("스터디 참가 성공")
    @WithAccount("joiner")
    @Test
    void joinStudyTest() throws Exception {
        Account account = accountRepository.findByNickname("joiner").get();

        CreateStudyRequest request = createStudyRequest(10, 12);
        Long studyAccountId = studyService.createStudy(request, account);

        StudyAccount studyAccount = studyAccountRepository.findById(studyAccountId).get();
        Long studyId = studyAccount.getStudy().getId();

        mockMvc.perform(post("/api/study/member")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(objectMapper.writeValueAsString(joinStudyRequest(studyId))))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.studyAccountId").exists());
    }
...
  • 이미 WithAccountSecurityContextFactory에서 가입도 진행했고, SecurityCOntextHolderAuthentication을 set 해줬기 때문에 이와 같은 코드를 작성할 수 있다.

https://github.com/code-chobo/dev-api 프로젝트를 진행하며 쓰는 회고입니다.

org.hibernate.dialect.MySQL5Dialect

application-test.yml

spring:
  jpa:
    open-in-view: false
    hibernate:
      ddl-auto: create
    database-platform: org.hibernate.dialect.MySQL5Dialect

상황 :

지금까지 작성한 테스트를 모두 한 번에 실행할 때
Caused by: javax.persistence.NonUniqueResultException: query did not return a unique result: 2
같은 에러가 뜬다.

문제점 :

테스트 클래스 위에 @Transactinal 을 붙여서 테스트가 끝난 뒤 Rollback되기를 기대했지만 되지 않아서 NonUniqueResultException 같은 에러가 났다. 확인해보니 org.hibernate.dialect.MySQL5DialectMySQLDialect 를 상속받는데, 이 클래스는 hibernate.dialect.storage_engine 환경 설정이 null 이라면 getDefaultMySQLStorageEngine()으로 MySQLStorageEngine 필드에 값을 넣어준다. MySQL5Dialect 은 다음과 같다.

MySQL5Dialect.java

...
/**
     * Constructs a MySQLDialect
     */
    public MySQLDialect() {
        super();

        String storageEngine = Environment.getProperties().getProperty( Environment.STORAGE_ENGINE );
        if ( storageEngine == null ) {
            this.storageEngine = getDefaultMySQLStorageEngine();
        }
...

protected MySQLStorageEngine getDefaultMySQLStorageEngine() {
        return MyISAMStorageEngine.INSTANCE;
    }

MyISAMStorageEngine 을 쓴다!!!!!!!!!

Table 들이 MyISAM엔진으로 설정된 것을 볼 수 있다. MyISAMTransaction을 지원하지 않는다!
참고 : https://gracelove91.tistory.com/102

따라서 Rollback이 되지 않기 때문에 NonUniqueResultException 같은 에러가 난 것이다.

해결 :

  1. hibernate.dialect.storage_engine 를 설정해준다
  2. getDefaultMySQLStorageEngine()InnoDBdialect를 쓴다.

나는 2번으로 해결했다.

application-test.yml

spring:
  jpa:
    open-in-view: false
    hibernate:
      ddl-auto: create
    database-platform: org.hibernate.dialect.MySQL57Dialect
  • MySQL57DialectMySQL55Dialect 를 상속받는다.

MySQL55Dialect

public class MySQL55Dialect extends MySQL5Dialect {

    @Override
    protected MySQLStorageEngine getDefaultMySQLStorageEngine() {
        return InnoDBStorageEngine.INSTANCE;
    }
}
  • InnoDBStorageEngine 이 기본 디폴트 스토리지 엔진임을 볼 수 있다.

https://github.com/code-chobo/dev-api 프로젝트를 진행하며 쓰는 회고입니다.

@DataJpaTest

application-test.yml

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/codechobo_test?serverTimezone=Asia/Seoul&useSSL=false
    username: codechobo_user
    password: password
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    open-in-view: false
    hibernate:
      ddl-auto: create
    database-platform: org.hibernate.dialect.MySQL57Dialect

상황 :

@DataJpaTest 테스트를 실행했을 때
Caused by: org.h2.jdbc.JdbcSQLSyntaxErrorException: Table "ACCOUNT" not found; SQL statement:...
같은 에러가 뜬다.

문제점 :

분명히 application-test.ymldatasourcemysql로 잡아주었는데도 임베디드디비인 h2 에서 에러가 난다.
왜 이런 건지 알아보니, @DataJpaTest는 데이터 소스가 저렇게 되있어도 임베디드 디비를 쓴다!

해결 :

따라서 @DataJpaTest 를 사용하는 테스트 클래스 위에 @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)를 붙여줘서 임베디드 디비로 대체하지 않고, 설정 파일에 적시된 데이터소스를 사용하게끔 했다.

+ Recent posts