forDevLife
JUnit5 Extension 본문
먼저 Extension에 대해 몇 가지 구현된 예시를 알아보고, 간단히 데이터베이스를 롤백하는 Extension을 직접 만들어보도록 하자.
실습 코드
https://github.com/leejohy-0223/junit_study
GitHub - leejohy-0223/junit_study
Contribute to leejohy-0223/junit_study development by creating an account on GitHub.
github.com
1. Extension 이란?
JUnit 5에 추가된 extension(FunctionalInterface)를 통해 Test class, method를 확장할 수 있다. 동작 방식은 테스트의 이벤트, Life Cycle에 관여하게 되며, ExtendWith를 통해 Extension 구현체를 지정해 테스트를 실행하는 방법을 커스터마이징 할 수 있다.
우리가 사용하는 스프링 부트 제공 테스트용 애노테이션(@SpringBootTest, @WebMvcTest, @DataJpaTest 등)에 @ExtendWith(SpringExtension.class)이 메타 애노테이션으로 적용되어 있기 때문에 생략이 가능하며, 해당 어노테이션이 붙은 테스트는 SpringExtension.class에 구현된 기능에 영향을 받게 된다.
구체적으로 SpringExtension에 어떤 것들이 구현되어있는지 살펴보자.
[SpringExtension.class]
여러 Extension을 구현하고 있는데, 몇 가지만 살펴보자.
그 중 BeforeEachCallback이라는 클래스가 무언가 익숙한 느낌이니(?) 먼저 확인해보도록 하자.
[BeforeEachCallback.class]
개별 테스트, user-defined setUp method(문맥상 어노테이션 @BeforeEach를 의미하는 듯 하다)가 실행되기 전에 호출되는 callback이라고 작성되어 있다. 즉 테스트에 작성하는 BeforeEach보다 먼저 실행되며 매번 테스트 실행 전 수행하고 싶은 내용을 구현하면 될 것이다. 그게 맞는지는 뒤의 실습에서 확인해보도록 하자.
[TestInstancePostProcessor.class]
또 다른 한 가지를 살펴보자. 해당 인터페이스는 테스트 인스턴스에 종속성 주입 또는 사용자 지정 초기화 메서드 호출 등을 호출할 수 있는 lifecycle이다. SpringExtension에서는 이 부분을 어떻게 구현했을까?
- validateAutowiredConfig() : test method와 test lifecycle이 @Autowired로 처리되었는지를 검증해주는 메서드이다.
- getTestContextManager.prepareTestInstance() : 의존성을 주입(예를 들면 Mocking)하는 등, test instance들이 초기화 된 후에 즉시 호출되어야 하는 부분이다.
우선 validateAutowiredConfig 메서드의 설명이 감이 잘 오질 않는다. 테스트 메서드에 @Autowired가 붙었는지를 체크해주는 메서드인듯 한데, 한번 테스트 메서드에 @Autowired를 붙이고 실행해보자.
실행 결과는 다음과 같이 예외가 발생했다.
정상적으로 해당 메서드가 동작을 검증한 것으로 보이며, 바로 아래에서 getTestContextManager.prepareTestInstance()를 실행하기 전에 정상적으로 @Autowired가 작성되었는지를 체크하는 것 같다. (테스트 메서드에 이걸 실수로 붙이는 걸 방지하기 위함일까?!)
바로 다음 메서드인 getTestContextManager.prepareTestInstance()도 가볍게 살펴보자.
주목해야 할 부분은 TestExecuterListner.prepareTestInstance() 부분이다. 여러 TestExecutionListner를 대상으로 해당 메서드가 실행되며, 그 중 MockitoTestExecutionListener의 prepareTestInstance는 다음처럼 작성되어 있다.
직관적으로 testContext의 특정 필드(Mockito관련 Listener이기 때문에 @MockBean으로 작성된) 대상으로 Mock 객체를 주입해주는 부분이라는 느낌이 들었다. injectFields()의 내부를 따라가보면 @MockBean으로 작성된 필드를 대상으로 아래처럼 하나씩 Mock 객체를 주입해주는 코드를 확인할 수 있다. (자세한 건 MockitoTestExecutionListener의 postProcessFields를 참고하자.)
다시 본론으로 와서, 이처럼 SpringExtension은 여러 Extension 구현체를 override해서 기본적인 스프링부트 테스트 환경을 구성함을 알 수 있다. 각 구현된 메서드의 실행 순서는 다음 내용을 참고하자.
https://dzone.com/articles/lifecycle-of-junit-5s-extension-model
Lifecycle of JUnit 5's Extension Model - DZone Java
JUnit 5's Extension API promises to consolidate JUnit's various extensions into one place. See how it works in action as we map out a test suite's lifecycle.
dzone.com
2. Extension을 활용한 데이터베이스 rollback 적용 실습
실습에 앞서 간단한 클래스와 Dao, JdbcConnectionUtil을 다음과 같이 정의한다.
public class Employee {
private long id;
private String firstName;
// getter, setter
}
다음은 EmployeeJdbcDao이다. save의 sql은 편의를 위해 하드코딩했다.
public class EmployeeJdbcDao {
private Connection con;
public EmployeeJdbcDao(Connection con) {
this.con = con;
}
public void save() throws SQLException {
String sql = "INSERT INTO employee VALUES(10, 'name')";
Statement stmt = con.createStatement();
stmt.execute(sql);
}
public List<Employee> findAll() throws SQLException {
String sql = "SELECT id, first_name FROM employee";
Statement stmt = con.createStatement();
ResultSet resultSet = stmt.executeQuery(sql);
List<Employee> employees = new ArrayList<>();
while (resultSet.next()) {
Employee employee = new Employee();
employee.setId(resultSet.getLong("id"));
employee.setFirstName(resultSet.getString("first_name"));
employees.add(employee);
}
return employees;
}
}
마지막으로, Connection을 담당하는 JdbcConnectionUtil을 아래와 같이 구현했다.
public class JdbcConnectionUtil {
private static Connection connection;
private static String url = "jdbc:h2:~/extension;AUTO_SERVER=TRUE";
private static String id = "sa";
public static Connection getConnection() {
if (connection == null) {
try {
connection = DriverManager.getConnection(url, id, "");
} catch (SQLException e) {
throw new RuntimeException(e);
}
return connection;
}
return connection;
}
}
테스트는 아래와 같으며, 실행 전 H2 데이터베이스에 기본 데이터 2건을 넣어두었다. rollback이 적용되어 있지 않으므로 테스트 후에는 실제 DB에 save된 employee가 추가되어 있을 것으로 예상해볼 수 있다.
public class EmployeeJdbcDaoTest {
private EmployeeJdbcDao employeeJdbcDao = new EmployeeJdbcDao(JdbcConnectionUtil.getConnection());
@Test
void save_test() throws SQLException {
List<Employee> all = employeeJdbcDao.findAll();
employeeJdbcDao.save();
List<Employee> all2 = employeeJdbcDao.findAll();
System.out.println("after save " + all2.size());
Assertions.assertEquals(all2.size(), 3);
}
}
테스트 실행 전 & 후
이제 반복 실행하면 테스트가 실패한다. (10, name)이 계속 쌓이게 되어 employee가 점점 늘어나기 때문에 더 이상 3이 아니기 때문이다. (좀 이상한 테스트이긴 하지만,,)
여기에 앞서 배운 Extension을 활용해보자.
public class EmployeeDatabaseSetupExtension implements
BeforeEachCallback, AfterEachCallback, AfterAllCallback, {
private Connection con = JdbcConnectionUtil.getConnection();
private Savepoint savepoint;
@Override
public void beforeEach(ExtensionContext context) throws Exception {
con.setAutoCommit(false);
savepoint = con.setSavepoint("before");
System.out.println("call back in extension : beforeEach");
}
@Override
public void afterEach(ExtensionContext context) throws Exception {
con.rollback(savepoint);
System.out.println("call back in extension : afterEach");
}
@Override
public void afterAll(ExtensionContext context) throws Exception {
if (con != null) {
con.close();
}
System.out.println("call back in extension : afterAll");
}
}
- BeforeEachCallback, AfterEachCallback, AfterAllCallback을 구현했으며, 위에서부터 순서대로 해당 메서드를 Override하였다.
- beforeEach : (개별)테스트 시작 전, 커넥션을 대상으로 AutoCommit을 False로 설정하고, 현재 시점을 savePoint로 지정한다.
- afterEach : (개별)테스트가 끝난 후 기 저장된 savePoint로 rollback을 수행한다.
- afterAll : 전체 테스트가 끝난 후 connection을 종료시킨다.
이걸 우리의 테스트 위에 ExtendWith로 적용하면 된다.
@ExtendWith(EmployeeDatabaseSetupExtension.class)
public class EmployeeJdbcDaoTest {
// 기존과 동일
결과는 다음과 같다.
call back in extension : beforeEach
after save 3
call back in extension : afterEach
call back in extension : afterAll
테스트가 끝난 후에 롤백이 발생하기 때문에 더 이상 DB에 반영이 되지 않는다. 따라서 반복 테스트도 성공함을 확인할 수 있다.
추가로, 테스트 클래스 내에 @BeforeEach를 추가해보았다.
@BeforeEach
void setUp() {
System.out.println("before each in Single Class");
}
그리고 다시 실행하면 다음과 같다.
call back in extension : beforeEach // Extension method
before each in Single Class // Extension 다음에 호출된다.
after save 3
call back in extension : afterEach
call back in extension : afterAll
즉, extension을 활용하면 각 테스트 클래스의 @beforeEach, @afterEach 등에 들어갈 내용이 중복될 때 유용하게 사용될 수 있을거라고 생각된다.
참고
https://junit.org/junit5/docs/current/user-guide/#extensions-registration
JUnit 5 User Guide
Although the JUnit Jupiter programming model and extension model will not support JUnit 4 features such as Rules and Runners natively, it is not expected that source code maintainers will need to update all of their existing tests, test extensions, and cus
junit.org
https://www.baeldung.com/junit-5-extensions
A Guide to JUnit 5 Extensions | Baeldung
JUnit 5 Extensions can be used to extend the behavior of test classes or methods.
www.baeldung.com
'Spring' 카테고리의 다른 글
Springboot의 Connection Pool (0) | 2022.05.20 |
---|---|
JPA 사용 시 Entity에 기본 생성자가 필요한 이유 (0) | 2022.05.13 |
스프링 입문 - 웹 개발 기초 <4> (0) | 2021.05.24 |
스프링 입문 - 웹 개발 기초 <3> (0) | 2021.05.20 |
스프링 입문 - 웹 개발 기초 <2> (0) | 2021.05.18 |