on
설정 : Java + Spring + Console + JDBC + Gradle 테스트 용 설정 2
설정 : Java + Spring + Console + JDBC + Gradle 테스트 용 설정 2
728x90
0. 기본적인 스프링과 자바 개발 설정이 가물가물해서 다시 적어 보는 포스트이다.
1. 지난 번에 프로젝트 만들고, DB, 유저 만들고, 기본 라이브러리를 가져왔다. 이젠 코드를 붙일 부분이다.
1-1 여기에 붙이는 소스는 토비의 스프링 책의 DB와 User 클래스를 사용한다. 귀찮다.
1-2 다만, 책에 구현되지 않은 나머지 count, get의 내용을 추가로 구현한 부분이 있다.
2. 우선 테스트 코드이다. 기본적인 코드 없이 테스트 부터 생성하였다.
2-1 기본적인 테스트를 수행하는 코드이다. 어떻게 UserDao를 작성할지와 상관없이 입출력만으로 작성할 수 있다.
2-2 @ExtendWith(SpringExtension.class)는 ApplicationContext를 공유하기 위해서 사용한다.
2-2-1 메소드 마다 ApplicationContext를 생성하거나 @BeforeEach에서 매번 생성하기에는 부담이 되기 때문이다.
2-2-2 @Autowired를 통해 객체 주입이 가능하다. ApplicationContext를 받아왔다.
2-3 @ContextConfiguration은 어떤 설정파일을 사용할지를 지정해 주는 부분이다.
2-4 @BeforeEach에서는 2개의 User fixture를 만들어 두고 편하게 사용한다. UserDao객체도 받아와 테스트로 사용한다.
2-5 나머지는 모두 단순한 부분이지만 TestGetFailure라는 메소드가 있다. 오류가 정상적으로 발생하는지 체크를 한다.
2-5-1 JUnit4와 약간 다른 부분이다. assertThrow를 사용하는데, 어떤 예외를 기대하는지를 지정하고,
2-5-2 두번째 인자로 테스트를 수행할 callback을 지정한다. callback수행결과를 Exception으로 받아 확인하고 있다.
package basic_project; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.junit.jupiter.api.Assertions.assertThrows; import java.sql.SQLException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = {basic_project.TestDaoFactory.class}) public class UserDaoTest { @Autowired private ApplicationContext context; private UserDao dao; private User user; private User user2;; @BeforeEach void setUp() { // ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class); user = new User("heops79", "pilseong", "password"); user2 = new User("lilymii", "sangmi", "password"); dao = context.getBean("userDao", UserDao.class); } @Test void testAdd() throws SQLException { dao.deleteAll(); assertThat(dao.getCount(), is(0)); dao.add(user); assertThat(dao.getCount(), is(1)); } @Test void testDeleteAll() throws SQLException { dao.deleteAll(); dao.add(user); dao.add(user2); assertThat(dao.getCount(), is(2)); dao.deleteAll(); assertThat(dao.getCount(), is(0)); } @Test void testGet() throws SQLException { dao.deleteAll(); dao.add(user); User pilseong = dao.get("heops79"); assertThat(pilseong.getName(), is(user.getName())); assertThat(pilseong.getId(), is(user.getId())); } @Test void testGetFailure() throws SQLException { Exception exception = assertThrows(EmptyResultDataAccessException.class, () -> { dao.deleteAll(); dao.add(user); dao.get("testtest"); }); assertThat(exception.getMessage(), is("empty result")); } @Test void testGetCount() throws SQLException { dao.deleteAll(); assertThat(dao.getCount(), is(0)); dao.add(user); System.out.println(user.getId() + " enrolled successfully"); dao.add(user2); System.out.println(user2.getId() + " enrolled successfully"); User pilseong = dao.get("heops79"); System.out.println(pilseong.getName() + " is fetched from the DB"); assertThat(pilseong.getName(), is(user.getName())); assertThat(pilseong.getId(), is(user.getId())); User sangmi = dao.get("lilymii"); System.out.println(sangmi.getName() + " is fetched from the DB"); assertThat(sangmi.getName(), is(user2.getName())); assertThat(sangmi.getId(), is(user2.getId())); assertThat(dao.getCount(), is(2)); } @Test void testSetDataSource() { assertThat(this.dao.getDataSource(), notNullValue()); } }
3. 위의 테스트 코드를 만족하기 위한 코드를 작성하면 되는데, 데이터베이스 접근 로직을 간단하기 위해 코드를 구현한다.
3-1 우선 JdbcTemplate와 콜백 인터페이스를 정의하였다. 토비 책처럼 add, deleteAll, get, count를 구현한다.
3-1-1 add와 deleteAll을 위해서는 하나의 statement를 작성하는 콜백만 있으면 되지만, get과 count는 ResultSet을 요구한다.
3-1-2 ResultStrategy도 추가로 작성하였다. 람다를 사용하기 위해서는 인터페이스는 하나의 메소드를 정의해야 한다.
3-1-3 아래처럼 StatementStrategy는 PreparedStatement를 정의하는 콜백이고 실행 시 Connection이 필요하다.
3-1-4 ResultStrategy는 템플릿이 실행한 결과를 인자로 받아서 결과를 만들어 돌려준다.
3-1-4-1 어떤 타입인지 모르기 때문에 Generic으로 정의하였다.
package basic_project; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; public interface StatementStrategy { PreparedStatement makePreparedStatement(Connection c) throws SQLException; } package basic_project; import java.sql.ResultSet; import java.sql.SQLException; import org.springframework.dao.DataAccessException; public interface ResultStrategy { T extractData(ResultSet rs) throws SQLException, DataAccessException; }
3-2 이제 위의 인터페이스를 사용하는 템플릿을 정의할 부분이다. 이것이 JdbcTemplate이다.
3-2-1 토비 책에 일부 코드가 있지만, 거기에 없는 것도 그냥 참고로 구현했다. 아래의 코드가 핵심이다.
3-2-2 아래의 코드를 구현하면 사실 다 한 거다. 데이터베이서 설정과 나머지는 Configuration에서 하면 된다.
3-2-3 deleteAll, add 함수는 executeSQL를 사용하고 add의 경우를 위해 가변인자를 사용하였다.
3-2-3-1 두 기능을 한 템플릿에 넣기 위해 가변인자의 길이를 확인하는 부분이 있다.
3-2-4 get과 count는 결과 값이 다르기 때문에 두개로 분리하여 콜백을 작성하였다.
3-2-5 템플릿을 만든 이유는 다 알겠지만 지긋지긋한 close 때문이다.
package basic_project; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import javax.sql.DataSource; public class JdbcContext { private DataSource dataSource; public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } public DataSource getDataSource() { return this.dataSource; } public void executeSQL(String sql, String... values) throws SQLException { this.jdbcContextWithStatementStrategy((Connection c) -> { PreparedStatement ps = c.prepareStatement(sql); if (values.length > 0) { ps.setString(1, values[0]); ps.setString(2, values[1]); ps.setString(3, values[2]); } return ps; }); } public int query(String sql) throws SQLException { return this.jdbcContextWithStatementStrategyAndResultStrategy( (Connection c) -> c.prepareStatement(sql), (ResultSet rs) -> { if (rs.next()) { return rs.getInt(1); } return 0; }); } public User query(String sql, String... values) throws SQLException { return this.jdbcContextWithStatementStrategyAndResultStrategy((Connection c) -> { PreparedStatement ps = c.prepareStatement(sql); if (values.length > 0) { ps.setString(1, values[0]); } return ps; }, (ResultSet rs) -> { if (rs.next()) { return new User(rs.getString(1), rs.getString(2), rs.getString(3)); } return null; }); } private T jdbcContextWithStatementStrategyAndResultStrategy( StatementStrategy stmt, ResultStrategy rsst) throws SQLException { Connection c = null; PreparedStatement ps = null; ResultSet rs = null; try { c = dataSource.getConnection(); ps = stmt.makePreparedStatement(c); rs = ps.executeQuery(); return rsst.extractData(rs); } catch (SQLException e) { throw e; } finally { if (ps != null) { try { ps.close(); } catch (SQLException e) { } } if (c != null) { try { c.close(); } catch (SQLException e) { } } } } public void jdbcContextWithStatementStrategy(StatementStrategy stmt) throws SQLException { Connection c = null; PreparedStatement ps = null; try { c = dataSource.getConnection(); ps = stmt.makePreparedStatement(c); ps.executeUpdate(); } catch (SQLException e) { throw e; } finally { if (ps != null) { try { ps.close(); } catch (SQLException e) { } } if (c != null) { try { c.close(); } catch (SQLException e) { } } } } }
3-3 이제 가장 중심에 있는 User, UserDao를 작성한다.
3-3-1 아주 단순하다. 이미 JdbcTemplate을 만들어 두어, 한 줄 짜리 쿼리문이면 된다.
3-3-2 별로 할 말은 없고 get에서 외예 발생하는 부분 정도가 눈에 띈다. 토비 책에서 제시한 형식이다.
package basic_project; import java.sql.SQLException; import javax.sql.DataSource; import org.springframework.dao.EmptyResultDataAccessException; public class UserDao { JdbcContext jdbcContext = null; private DataSource dataSource; public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; this.jdbcContext = new JdbcContext(); this.jdbcContext.setDataSource(dataSource); } public DataSource getDataSource() { return this.dataSource; } public void setJdbcContext(JdbcContext jdbcContext) { this.jdbcContext = jdbcContext; } public void add(User user) throws SQLException { jdbcContext.executeSQL("insert into users(id, name, password) values(?,?,?)", user.getId(), user.getName(), user.getPassword()); } public User get(String id) throws SQLException { User user = jdbcContext.query("select * from users where id = ?", id); if (user == null) { throw new EmptyResultDataAccessException("empty result", 1); } return user; } public void deleteAll() throws SQLException { jdbcContext.executeSQL("delete from users"); } public int getCount() throws SQLException { return jdbcContext.query("select count(*) from users"); } }
4. 이제 설정파일을 작성한다. 실전용 설정과 테스트 설정 두 개를 만들었다. 거의 동일하지만 다른 DataSource를 사용하도록 했다.
4-1 테스트에서는 TestDaoFactory 설정을 사용하였다.
4-2 설정 클래스에는 반드시 @Configuration이 있어야 한다.
4-3 두 설정의 차이는 사용하는 DB이름이 springbook과 testdb의 차이가 있고, DataSource가 다른 방식이다.
4-4 테스트에 사용된 SingleConnectionSource는 suppress close 설정을 해주어야 제대로 동작한다.
4-4-1 Connection이 꼴랑 하나이기 때문에 동작 방식을 지정해 주는 것이다.
package basic_project; import javax.sql.DataSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.datasource.SimpleDriverDataSource; @Configuration public class DaoFactory { @Bean public UserDao userDao() { UserDao userDao = new UserDao(); userDao.setDataSource(simpleDriverDataSource()); return userDao; } @Bean public DataSource simpleDriverDataSource() { SimpleDriverDataSource dataSource = new SimpleDriverDataSource(); dataSource.setDriverClass(com.mysql.cj.jdbc.Driver.class); dataSource.setUrl("jdbc:mysql://localhost/springbook?characterEncoding=UTF-8"); dataSource.setUsername("pilseong"); dataSource.setPassword(""); return dataSource; } }
package basic_project; import javax.sql.DataSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.datasource.SingleConnectionDataSource; @Configuration public class TestDaoFactory { @Bean public UserDao userDao() { UserDao userDao = new UserDao(); userDao.setDataSource(singleConnectionDataSource()); return userDao; } @Bean public DataSource singleConnectionDataSource() { SingleConnectionDataSource dataSource = new SingleConnectionDataSource(); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setUrl("jdbc:mysql://localhost/testdb?characterEncoding=UTF-8"); dataSource.setUsername("pilseong"); dataSource.setPassword(""); dataSource.setSuppressClose(true); return dataSource; } }
5. 이제 테스트를 수행해 본다. 왼쪽의 전체 트리 구조와 아래 테스트 결과를 보면 구조와 결과를 알 수 있다.
5-1 이전 포스트에 적어왔지만 gradle test는 아무 결과를 보여주지 않는다. 아래 결과는 plugin 설정하여 정보가 나오는 것이다.
728x90
from http://kogle.tistory.com/354 by ccl(A) rewrite - 2021-11-29 02:02:14