728x90

이전에는 deleteAll()에서 변하지 않는 부분, 자주 변하는 부분을 전략 패턴을 사용해 분리했다.

독립된 JDBC 작업 흐름이 담긴 jdbcContextWithStatementStrategy()는 Dao 메서드들이 공유할 수 있게 되었다.

    public void jdbcContextWithStatementStrategy(StatementStrategy statementStrategy) throws SQLException {
        Connection connection = null;
        PreparedStatement preparedStatement;
        try{
            connection = dataSource.getConnection();

            preparedStatement = statementStrategy.makePreparedStatement(connection);

            preparedStatement.executeUpdate();
            preparedStatement.close();
        } finally{
            if(connection != null){
                connection.close();
            }
        }
    }

 

Dao 메서드는 전략 패턴의 클라이언트로서 컨텍스트에 해당하는 jdbcContextWithStatementStrategy() 메서드에 적절한 전략을 제공해주는 방법으로 사용할 수 있다.

 

deleteAll() 말고도 add() 메서드에도 저장해보자.

 

AddStatement라는 전략 클래스를 또 생성했다.

public class AddStatement implements StatementStrategy {

    User user;

    public AddStatement(User user) {
        this.user = user;
    }

    @Override
    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
        PreparedStatement preparedStatement = c.prepareStatement("insert into users(id, name, password) values (?, ?, ?)");

        preparedStatement.setString(1, user.getId());
        preparedStatement.setString(2, user.getName());
        preparedStatement.setString(3, user.getPassword());

        return preparedStatement;
    }
}

 

그리고 UserDao의 add() 메서드도 다음과 같이 수정한다.

    public void add(User user) throws SQLException {
        StatementStrategy statementStrategy = new AddStatement(user);
        jdbcContextWithStatementStrategy(statementStrategy);
    }

 

그리고 일단 테스트를 돌려보니 다음과 같이 성공한다.

 

자 일단 그럼 여기까지는 성공이다.

 

지금까지 코드를 더 깔끔하게 만들기는 했지만, 몇가지 문제가 있다.

우선 Dao에서 사용할 메서드마다 StatementStrategy를 구현한 클래스를 만들어야 한다.

또한 add()와 같이 User로 무언가 정보를 넘기는 경우, 이를 해당 구현 클래스의 생성자에 넣어서 전달해야 한다.

 

이런 문제를 로컬 클래스로 해결가능 할 거 같다.

    public void add(User user) throws SQLException {
        class AddStatement implements StatementStrategy{

            @Override
            public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
                PreparedStatement ps = c.prepareStatement("insert into users(id, name, password) values(?, ?, ?)");

                ps.setString(1, user.getId());
                ps.setString(2, user.getName());
                ps.setString(3, user.getPassword());

                return ps;
            }
        }
        StatementStrategy statementStrategy = new AddStatement();
        jdbcContextWithStatementStrategy(statementStrategy);
    }

 

이렇게 add() 메서드 안에 클래스를 만들어서 별도의 클래스 파일을 만들지 않도록 했다.

user 변수에 접근도 가능하기 때문에 추가적으로 생성자에 User를 넘겨줄 필요가 없다.

 

하지만 이 방법은 진짜 클래스파일을 추가적으로 만들지 않는 것이지, 코드의 작성량은 비슷하다.

그래도 더 간결하게는 만들어보자.

지금 AddStatement는 어차피 내부에서만 사용하기에 이름도 필요 없다.

그냥 익명 내부 클래스로 만들어보자.

 

    public void add(User user) throws SQLException {
        StatementStrategy statementStrategy = new StatementStrategy() {
            @Override
            public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
                PreparedStatement ps = c.prepareStatement("insert into users(id, name, password) values(?, ?, ?)");

                ps.setString(1, user.getId());
                ps.setString(2, user.getName());
                ps.setString(3, user.getPassword());

                return ps;
            }
        };
        jdbcContextWithStatementStrategy(statementStrategy);
    }

이렇게 만들면 더욱 간결하게 코드를 작성할 수 있다.

728x90

저번에는 UserDao에 try-catch-finally를 추가했다.

이제 이걸 적용하려면 이 예외처리를 기존의 코드에 하나하나 추가해줘야 한다.

 

당연히 말도 안된다.

코드를 빼먹을 수도 있고, 수정할 때마다 모두 찾아서 수정을 해줘야한다.

 

그렇기에 반드시 개선해야 하는 부분이다.

    public void deleteAll() throws SQLException {
        Connection connection = null;
        PreparedStatement preparedStatement;
        try{
            connection = dataSource.getConnection();

            preparedStatement = connection.prepareStatement("delete from users");

            preparedStatement.executeUpdate();
            preparedStatement.close();
        } finally{
            if(connection != null){
                connection.close();
            }
        }
    }

 

코드를 보면 Connection, PreparedStatement를 준비하는 부분과 finally 부분은 모두 공통으로 변하지 않는 것을 볼 수 있다.

중간 부분만 바꾸면 되는것이다.

 

메서드로 추출하는 방법도 있지만 큰 이점은 없어보이기에, 템플릿 메서드 패턴을 적용해보려고 한다.

템플릿 메서드 패턴은 상속을 통해 기능을 확장해서 사용하는 방법이다.

변하지 않는 부분은 슈퍼클래스에 두고, 변하는 부분은 추상 메서드로 정의해서 서브클레스에서 오버라이드해서 새롭게 정의해 쓰도록 만드는 것이다.

 

public class UserDaoDeleteAll extends UserDao {

    public UserDaoDeleteAll(DataSource dataSource) {
        super(dataSource);
    }

    @Override
    protected PreparedStatement makeStatement(Connection c) throws SQLException {
        return c.prepareStatement("delete from users");
    }
}

 

하지만 여기서 템플릿 메서드의 단점은 사용하고 싶은 sql문마다 dao 클래스를 만들어줘야 한다는 것이다.

 

이거보다 더 확장적인 방법은 오브젝트를 둘로 분리하고 클래스 레벨에서는 인터페이스를 통해서만 의존하도록 만드는 전략 패턴이다.

좌측의 Context의 contextMethod()에서 일정한 구조를 가지고 동작하다가 특정 확장 기능은 Strategy 인터페이스를 통해 외부의 독립된 전략 클래스에 위임하는 것이다.

 

deleteAll()에서 변하지 않는 부분이라고 한 것이 바로 contextMethod()가 되는 것이다.

 

변하는 부분을 StatementStrategy로 하고 인터페이스를 만든다.

public interface StatementStrategy {

    PreparedStatement makePreparedStatement(Connection c) throws SQLException;
}

 

그리고 이거를 상속받아, 변하는 부분을 작성한다.

여기에서는 deleteAll이다.

public class DeleteAllStatement implements StatementStrategy {

    @Override
    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
        return c.prepareStatement("DELETE FROM users");
    }
}

 

그리고 삭제할 때마다 해당 클래스를 생성해서 함수를 호출하는 것이다.

    public void deleteAll() throws SQLException {
        Connection connection = null;
        PreparedStatement preparedStatement;
        try{
            connection = dataSource.getConnection();

            StatementStrategy statementStrategy = new DeleteAllStatement();

            preparedStatement = statementStrategy.makePreparedStatement(connection);

            preparedStatement.executeUpdate();
            preparedStatement.close();
        } finally{
            if(connection != null){
                connection.close();
            }
        }
    }

 

이렇게하면 뭐..가능은 하지만 항상 클래스를 생성해줘야 하고, 구체적인 클래스인 DeleteAllStatement에 의존하기 때문에 OCP에 잘 맞는다고 할 수는 없다.

 

더 발전시키기 위해 전략패턴을 더 생각해보자.

Context가 어떤 전략을 사용할지는 Context를 사용하는 Client가 결정하도록 한다.

Client가 구체적인 전략을 선택해서 Context에 전달하는 것이다.

    public void jdbcContextWithStatementStrategy(StatementStrategy statementStrategy) throws SQLException {
        Connection connection = null;
        PreparedStatement preparedStatement;
        try{
            connection = dataSource.getConnection();

            preparedStatement = statementStrategy.makePreparedStatement(connection);

            preparedStatement.executeUpdate();
            preparedStatement.close();
        } finally{
            if(connection != null){
                connection.close();
            }
        }
    }

 

이렇게 공통적으로 동작하는 부분을 분리하고

    public void deleteAll() throws SQLException {
        StatementStrategy strategy = new DeleteAllStatement();
        jdbcContextWithStatementStrategy(strategy);
    }

deleteAll()을 클라이언트로 전략 오브젝트를 만들어서 컨텍스트를 호출하도록 한다.

사용할 전략 클래스는 DeleteAllStatement이고 컨텍스트로 분리한 jdbcContextWithStatementStrategy() 메서드를 호출한다.

 

 

728x90

지난번에 작성한 UserDao를 다시보자.

    public void deleteAll() throws SQLException {
        Connection connection = dataSource.getConnection();

        PreparedStatement preparedStatement = connection.prepareStatement("delete from users");

        preparedStatement.executeUpdate();

        preparedStatement.close();
        connection.close();
    }

 

바로 알겠지만 executeUpdate()에서 에러가 발생하면 connection등의 리소스가 닫히지 않고 메서드가 종료된다.

이렇게 반환되지 않고 connection이 계속 열리게 된다면 서버의 운영 중에 큰 문제가 발생한다.(일반적으로는 커넥션 풀을 사용하지만)

 

그렇기 때문에 어떤 상황에서도 connection이 닫힐 수 있도록 try/catch/finally 구문을 사용해야 한다.

 

    public void deleteAll() throws SQLException {
        Connection connection = null;
        PreparedStatement preparedStatement;
        try{
            connection = dataSource.getConnection();

            preparedStatement = connection.prepareStatement("delete from users");

            preparedStatement.executeUpdate();
            preparedStatement.close();
        } finally{
            if(connection != null){
                connection.close();
            }
        }
    }

 

 

이렇게 변경해보았다.

 

만약 connection을 생성하는 도중에 에러가 발생할 수 있고, 그런 connection에서 close()를 호출하면 NPE가 발생하기 때문에 null 체크를 해줘야 한다.

 

JDBC를 통해서 조회를 하는 경우에는 예외처리가 더 어려워진다.

ResultSet이 추가되기 때문이다.

 

그렇기 때문에 ResultSet까지 체크하며 닫아주어야 한다.

    public int getCount() throws SQLException {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;

        try{
            connection = dataSource.getConnection();
            preparedStatement = connection.prepareStatement("select count(*) from users");
            resultSet = preparedStatement.executeQuery();
            resultSet.next();
            return resultSet.getInt(1);
        }finally {
            if(resultSet != null){
                try{
                    resultSet.close();
                }catch (SQLException ignored){

                }
            }

            if(preparedStatement != null){
                try{
                    preparedStatement.close();
                }catch (SQLException ignored){

                }
            }

            if(connection != null){
                try {
                    connection.close();
                }
                catch (SQLException ignored){

                }
            }
        }
    }
728x90

개발자는 보통 자신이 작성한 코드에 대하여 테스트를 작성하지만, 다른 사람이 작성하거나 프레임워크에 대해서도 테스트를 작성한다고 한다.

진짜 테스트하려는 목적보다 사용방법에 대해 배우려고 하는 것이다.

 

이렇게 작성한 학습테스트의 장점은 다음이 있다고 한다.

  • 다양한 조건에 따른 기능을 손쉽게 확인해볼 수 있다.
  • 학습 테스트 코드를 개발 중에 참고할 수 있다.
  • 프레임워크나 제품을 업그레이드 할 때 호환성 검증을 도와준다.
  • 테스트 작성에 대한 좋은 훈련이 된다.
  • 새로운 기술을 공부하는 과정이 즐거워진다??

그렇기에 몇가지의 예시 테스트로 테스트에 대해 좀 더 알아보자.

public class JUnitTest {

    static JUnitTest testObject;

    @Test
    public void test1(){
        System.out.println(this);
        System.out.println(testObject);
        Assertions.assertNotEquals(this, testObject);
        System.out.println(this);
        System.out.println(testObject);
        System.out.println();
    }

    @Test
    public void test2(){
        System.out.println(this);
        System.out.println(testObject);
        Assertions.assertNotEquals(this, testObject);
        testObject = this;
        System.out.println(this);
        System.out.println(testObject);
        System.out.println();
    }

    @Test
    public void test3(){
        System.out.println(this);
        System.out.println(testObject);
        Assertions.assertNotEquals(this, testObject);
        testObject = this;
        System.out.println(this);
        System.out.println(testObject);
        System.out.println();
    }
}

 

일단 위는 성공하는 테스트이다.

 

근데 이상하지 않나?

분명 test2, test3에서 testObject를 this로 해주었기에, 둘 중 하나는 testObject가 this로 테스트는 실패해야 한다.

 

성공하는 이유는 각 메서드가 실행될 때마다 새로운 테스트 오브젝트가 생성이 되기에 일치하지 않는 것이다.

실제로 실행하면 다음과 같이 로그가 작성된다.

 

testObject가 바뀌는 것이 아니라, 테스트 클래스 자체가 새로운 오브젝트로 변경된다.

 

이렇게 테스트를 작성하다보면 스프링에 대해 깊게 알 수 있다고 한다...

728x90

@Before annotation을 사용해서 모든 메서드에 미리 데이터베이스에 대한 정보를 넣었었다.

하지만 여기서의 문제는 애플리케이션 컨텍스트가 계속 초기화된다.

 

애플리케이션 컨텍스트가 만들어질 때는 모든 싱글톤 빈 오브젝트를 초기화하기 때문에 테스트에 많은 비용이 소모될 수 있다.

테스트마다 로그를 찍어보니

모든 다른 애플리케이션 오브젝트로 생성이 되고, 거기서 생긴 UserDao도 싱글톤이라고 하기에는 다른 인스턴스인 것을 볼 수 있다.

 

그렇기에 테스트마다 애플리케이션 컨텍스트를 생성하는 방법이 아닌, 스프링에서 직접 제공하는 애플리케이션 컨텍스트 테스트 지원 기능을 사용하는 것이 더 좋다고 한다.

 

package seungkyu;

import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.sql.SQLException;
import java.sql.SQLIntegrityConstraintViolationException;
import java.util.UUID;

import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = {"/applicationContext.xml"})
class UserDaoTest {

    private UserDao userDao;

    @Autowired
    private ApplicationContext applicationContext;

    @BeforeEach
    public void setUp() throws SQLException, ClassNotFoundException {
        this.userDao = applicationContext.getBean("userDao", UserDao.class);
        userDao.deleteAll();
    }
}

이렇게 수정해주었다.

applicationContext.xml에서 빈의 정보를 읽어서 주입해달라는 코드이다.

그렇게 만든 애플리케이션 오브젝트와 빈은 모두 싱글톤으로 하나만 만들어서 사용하는 것을 볼 수 있다.

이렇게 해서 하나의 테스트 클래스 내의 테스트 메서드는 같은 애플리케이션 컨텍스트를 공유해서 사용할 수 있음을 확인했다.

 

만약 다른 테스트까리 하나의 설정파일을 읽어와서 테스트를 진행한다면, 해당 테스트는 애플리케이션 컨텍스트를 하나만 만들어서 공유하도록 해준다.

이 덕분에 애플리케이션 컨텍스트를 하나만 만들 수 있게 되어 테스트의 속도가 대폭 빨라진다.

 

그리고 여기서 주입을 위해 @Autowired를 사용했는데, 간단하게 말하자면 @Autowired가 붙은 인스턴스 변수가 있으면 테스트 컨텍스트 프레임워크는 변수 타입과 일치하는 컨텍스트 내의 빈을 찾는다. 그리고 타입이 일치한다면 인스턴스 변수에 주입해준다. setter와 생성자가 없어도 스프링이 리플랙션을 통해 주입해준다고 한다. 만약 같은 타입이 2개 이상이라면 변수의 이름과 같은 이름의 빈이 있는지 확인한 후 있다면 주입한다.

 

@Autowired
private ApplicationContext applicationContext;

그리고 해당 코드를 보면 알겠지만, applicationContext 또한 자기 자신을 빈으로 등록한다.

 

이제 테스트를 진행해볼텐데, 이걸 실제 데이터베이스에 연결하면 안된다는 것을 알것이다.

만약 실제 데이터베이스에 연결하고 deleteAll을 해버린다면, 모든 사용자의 정보가 삭제되기 때문이다.

그렇기에 테스트를 위한 별도의 데이터베이스를 설정해야 한다.

 

지금 작성한 applicationContext.xml 말고, test-applicationContext.xml을 만들어서 테스트 환경에서만 해당 설정을 사용하도록 해보자.

 

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd">


    <bean id="dataSource"
          class = "org.springframework.jdbc.datasource.SimpleDriverDataSource">
        <property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/test"/>
        <property name="username" value="root"/>
        <property name="password" value="1204"/>
    </bean>

    <bean id="userDao" class="seungkyu.UserDao">
        <constructor-arg ref="dataSource" />
    </bean>


</beans>

 

이렇게 스키마를 변경하고

UserDaoTest의 ContextConfiguration을 다음과 같이 변경한다.

@ContextConfiguration(locations = {"/test-applicationContext.xml"})

 

해당 스키마를 사용하는지 확인해보기 위해, @AfterEach로 데이터를 남기며 테스트해보았다.

이렇게 테스트 데이터베이스를 사용해 테스트를 진행하는 것을 볼 수 있다.

 

테스트를 진행하면서 별도의 테스트 데이터베이스를 통해 더 완벽하게 테스트를 진행하도록 해야겠다.

728x90

지금까지 테스트를 실행하면서 가장 불편한 점은, 매번 UserDaoTest 태스트를 실행하기 위해 직접 데이터베이스를 모두 비워줬던 것이다.

그냥 실행하면 기본키가 충돌하기 때문이다.

 

테스트가 외부 상태에 따라 성공하기도 하고 실패하기도 하는 것이 좋은 방법일까?

당연히 테스트는 별도의 준비 작업 없이 반복적으로 수행해도 일관되게 성공해야 한다.

 

그렇기에 테스트를 마치고 나면 테스트에서 삽입한 데이터를 삭제하여, 테스트를 수행하기 이전 상태로 만들어줘야 한다.

일단 UserDao에 다음과 같은 메서드를 추가해보자.

 

    public void deleteAll() throws SQLException, ClassNotFoundException {
        Connection connection = connectionHelper.makeConnection();

        PreparedStatement preparedStatement = connection.prepareStatement("delete from users");

        preparedStatement.executeUpdate();

        preparedStatement.close();
        connection.close();
    }

    public int getCount() throws ClassNotFoundException, SQLException {
        Connection connection = connectionHelper.makeConnection();

        PreparedStatement preparedStatement = connection.prepareStatement("select count(*) from users");

        var resultSet = preparedStatement.executeQuery();

        resultSet.next();

        int count = resultSet.getInt(1);

        resultSet.close();
        preparedStatement.close();
        connection.close();

        return count;
    }

 

그리고는 테스트에 다음과 같이 추가해본다.

class UserDaoTest {

    @Test
    public void addAndGet() throws SQLException, ClassNotFoundException {

        ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");

        UserDao userDao = context.getBean("userDao", UserDao.class);

        userDao.deleteAll();
        Assertions.assertEquals(0, userDao.getCount());

        User createdUser = new User();
        createdUser.setId("seungkyu");;
        createdUser.setName("승규");
        createdUser.setPassword("password");

        userDao.add(createdUser);
        Assertions.assertEquals(1, userDao.getCount());

        User readUser = userDao.get("seungkyu");

        Assertions.assertEquals(createdUser.getId(), readUser.getId());
        Assertions.assertEquals(createdUser.getName(), readUser.getName());
        Assertions.assertEquals(createdUser.getPassword(), readUser.getPassword());
    }

}

 

이 테스트가 좋은 테스트는 아니라고 생각한다.

하나의 테스트에서 너무 많은 메서드를 테스트한다.

만약 테스트를 작성하면 이렇게 너무 많은 메서드를 하나의 테스트에서 진행하지는 말도록 하자.

이런 경우에는 어디서 문제가 생겼는지 파악하기 힘들어진다.

 

이제 테스트를 반복 실행해보아도, 동일하게 성공하는 것을 볼 수 있다.

 

getCount() 메서드를 테스트에 적용했지만, 기존의 테스트에서 확인할 수 있었던 것은 deleteAll()을 실행했을 때 테이블이 비어있는 경우와 add()를 한번 호출한 뒤의 결과만 테스트했다.

2개 이상의 add()도 물론 잘 작동하겠지만, 그래도 테스트 해보는 것이 더 좋은 자세이다.

 

그렇기에 다른 메서드부터 차례로 꼼꼼하게 테스트 해보도록 하자.

여러개의 User를 등록하며 getCount()를 호출하는 것이다.

    @Test
    public void count() throws SQLException, ClassNotFoundException {
        ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");

        UserDao userDao = context.getBean("userDao", UserDao.class);

        userDao.deleteAll();

        for(int i = 0; i < 3; i++){
            User user = new User();

            user.setId(UUID.randomUUID().toString().substring(0, 6));
            user.setName(UUID.randomUUID().toString().substring(0, 6));
            user.setPassword(UUID.randomUUID().toString().substring(0, 6));

            userDao.add(user);

            Assertions.assertEquals(i + 1, userDao.getCount());
        }
    }

데이터를 모두 지우고 하나씩 추가하며 개수를 세어보니, 잘 동작하는 것을 볼 수 있다.

이렇게 get() 메서드가 제대로 동작한다고 믿을 수 있게 되었다.

 

만약 아이디가 충돌해서 에러가 생긴다면 어떨까?

이렇게 말이다.

당연히 

 

에러가 발생해도 테스트를 통과시킬 수 있도록 하는 방법이 있다.

    @Test
    public void countWithDuplicate() throws SQLException, ClassNotFoundException {
        ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");

        UserDao userDao = context.getBean("userDao", UserDao.class);

        userDao.deleteAll();

        String name = UUID.randomUUID().toString().substring(0, 6);

        User user = new User();

        user.setId(name);
        user.setName(UUID.randomUUID().toString().substring(0, 6));
        user.setPassword(UUID.randomUUID().toString().substring(0, 6));
        userDao.add(user);

        for(int i = 0; i < 2; i++){

            user.setId(name);
            user.setName(UUID.randomUUID().toString().substring(0, 6));
            user.setPassword(UUID.randomUUID().toString().substring(0, 6));

            Assertions.assertThrows(SQLIntegrityConstraintViolationException.class,
                    () -> userDao.add(user));

            Assertions.assertEquals(1, userDao.getCount());
        }
    }

대신 기대했던 에러가 발생하지 않는다면, 해당 테스트는 실패한다.

예외가 반드시 발생하는 경우에 사용할 수 있다.

 

이제 마지막으로 할 일은 get() 메서드의 테스트를 완료하는 것이다.

우선 get()에서 유저가 존재하지 않는다면 NPE를 발생하도록 수정했다.

public User get(String id) throws ClassNotFoundException, SQLException {

        Connection connection = connectionHelper.makeConnection();

        PreparedStatement preparedStatement = connection.prepareStatement(
                "select * from users where id = ?"
        );

        preparedStatement.setString(1, id);

        var resultSet = preparedStatement.executeQuery();

        User user = null;

        if(resultSet.next()){

            user = new User();

            user.setId(resultSet.getString("id"));
            user.setName(resultSet.getString("name"));
            user.setPassword(resultSet.getString("password"));
        }

        resultSet.close();
        preparedStatement.close();
        connection.close();

        if(user == null){
            throw new NullPointerException("user is null");
        }

        return user;
    }

그리고 없는 계정으로 조회할 때 에러가 잘 발생하는지 테스트를 해보았다.

 

    @Test
    public void get() throws SQLException, ClassNotFoundException {
        ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");

        UserDao userDao = context.getBean("userDao", UserDao.class);

        Assertions.assertThrows(NullPointerException.class, () -> userDao.get(UUID.randomUUID().toString().substring(0, 6)));
    }

 

자 이제 테스트코드를 리펙토링 해보자.

지금 테스트 코드에서는 데이터베이스에 연결하는 공통적인 코드를 가지고 있다.

 

항상 테스트의 시작 부분에서 작성된 코드이다.

ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
UserDao userDao = context.getBean("userDao", UserDao.class);
userDao.deleteAll();

 

이 부분을 추출할 수는 없을까?

당연히 가능하다.

JUnit에서는 공통 시작 부분을 추출할 수 있도록 @BeforeEach annotation을 지원한다.

해당 클래스의 모든 테스트에서 시작 전에 수행되어야 할 명령어를 명시해두면 된다.

 

    @BeforeEach
    public void setUp() throws SQLException, ClassNotFoundException {
        ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
        this.userDao = context.getBean("userDao", UserDao.class);
        userDao.deleteAll();
    }

 

당연히 메서드의 종료 후 동작할 명령어도 명시할 수 있다.

    @AfterEach
    public void tearDown(){
        System.out.println("테스트 종료");
    }

 

이렇게 테스트를 수행할 때에는 JUnit에서 각각의 테스트 메서드에 대하여, 각각의 오브젝트가 생성된다고 한다. 그렇기 때문에 각 테스트끼리는 정보를 인스턴스를 제외하고는 주고받을 수 없다.

 

 

728x90

우선 테스트 결과를 검증하는 부분을 자동화로 만들어보자.

이 테스트에서는 add()로 전달한 사용자의 정보와 get()으로 조회하는 사용자의 정보가 일치하는지 검증한다.

 

기존에는 단순하게 로그를 확인해 테스트의 결과를 확인했다면, 이것을 코드로 성공과 실패여부를 출력하도록 만들어보자.

 

public class Main {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {

        ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");

        UserDao userDao = context.getBean("userDao", UserDao.class);


        User createdUser = new User();
        createdUser.setId("seungkyu");;
        createdUser.setName("승규");
        createdUser.setPassword("password");

        userDao.add(createdUser);

        System.out.println(createdUser.getId() + "계정을 등록했습니다.");

        User readUser = userDao.get("seungkyu");

        if(!readUser.getName().equals(createdUser.getName()))
        {
            System.out.println("테스트 실패 - name");
        }
        else if(!readUser.getPassword().equals(createdUser.getPassword()))
        {
            System.out.println("테스트 실패 - password");
        }
        else
        {
            System.out.println("조회 테스트 성공");
        }
    }
}

 

이렇게 결과를 비교하여 테스트의 결과를 확인하고, 그에 맞는 결과를 출력한다.

이러면 출력된 로그에 따라 적절한 대응만 하면 된다.

 

굉장히 좋은 테스트이다.

하지만 main() 메서드를 이용한 테스트 작성법으로는 규모가 커지고 테스트의 수가 많아지면, 부담이 될 것이다.

 

그렇기에 JUnit을 사용해본다.

JUnit은 프레임워크로 IoC를 사용해 main() 메서드에 직접 제어권을 작성해 작성할 필요가 없도록 한다.

 

class UserDaoTest {

    @Test
    public void addAndGet() throws SQLException, ClassNotFoundException {
    	//테스트 코드
    }
}

 

이렇게 test 패키지에 test관련 종속성을 추가하고, 해당 메서드에 @Test annotation을 추가한 후 코드를 옮겨적어주면 된다.

그러면 이렇게 초록색으로 결과가 나오게된다.

 

다른 방법으로 검증 할 수는 없을까?

지금 이 방법도 결국 우리가 눈으로 로그를 확인해야 하는 것이다.

JUnit에서는 이런 검증을 할 수 있도록 matcher를 제공한다.

첫번째와 두번째의 파라미터를 비교하여 결과를 만들어주는 기능이다.

 

    @Test
    public void addAndGet() throws SQLException, ClassNotFoundException {

        ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");

        UserDao userDao = context.getBean("userDao", UserDao.class);

        User createdUser = new User();
        createdUser.setId("seungkyu");;
        createdUser.setName("승규");
        createdUser.setPassword("password");

        userDao.add(createdUser);

        System.out.println(createdUser.getId() + "계정을 등록했습니다.");

        User readUser = userDao.get("seungkyu");

        Assertions.assertEquals(createdUser.getId(), readUser.getId());
        Assertions.assertEquals(createdUser.getName(), readUser.getName());
        Assertions.assertEquals(createdUser.getPassword(), readUser.getPassword());
    }

 

이렇게 Assertions에서 제공하는 메서드들로 값을 검증하면 된다.

 

만약 실패한다면

이런 식으로 결과가 나오게 된다.

 

일단 간단하게 테스트를 알아보았고, 다음부터 JUnit에 대해서 더 깊게 공부해보도록 하자

728x90

이번 챕터의 주제는 테스트이다.

개발을 하고 1년이 지난 시점부터 굉장히 중요하다고 생각했던 주제이다.

 

기존에 작정한 UserDao는 데이터를 메인 메서드에서 데이터를 삽입해보고 정상적으로 들어갔는지 확인하는 방법이다.

 

  • 웹을 통한 DAO 테스트 방법의 문제점

기존에 웹 프로그램에서 사용하는 테스트 방법은 모든 입출력 기능을 대충이라도 만들고 테스트 웹 애플리케이션을 배치한 뒤, 다시 데이터를 조회하는 URL을 만들어서 제대로 들어갔는지 확인하는 방법이라고 한다.

당연히 이러한 방법은 큰 문제들이 있다.

JSP, API까지 모두 만든 후에나 테스트가 가능하다는 단점이 가장 치명적이라고 한다. 또한, 버그가 발생하면 어느 부분에서 버그가 발생했는지 찾아야 한다고 한다.

 

  • 작은 단위의 테스트

그렇기 때문에 테스트를 작은 단위로 나누고, 해당 대상에만 집중해서 테스트를 해야한다고 한다. 너무 많은 로직들이 모여있으면 정확한 원인을 찾기 힘들기 때문이다. 그렇기에 테스트의 관심이 다르다면 테스트 할 대상을 분리하고 집중해서 접근해야 한다. 그리고 이렇게 작은 단위테스트를 진행하는 이유는 개발자가 작성한 코드를 최대한 빠르게 검증받기 위해서다.

 

  • 자동 수행 테스트 코드

기존에 사용하던 UserDaoTest는 테스트가 코드를 통해 자동으로 실행되고 있었다. 웹을 통해 데이터를 계속 넣어줄 필요가 없다는 말이다.

이렇게 하나하나 데이터를 넣으면서 테스트 하도록 만드는 것이 아니라, 코드를 통해서 자동으로 테스트가 진행되도록 만들어야 한다. 빠르고 지속적으로 기존에 만들었던 코드들을 검증해야 하기 때문이다.

 

  • 지속적인 개선과 점진적인 개발을 위한 테스트

UserDaoTest는 처음에 무식하고 단순한 방법으로 정상적으로 동작하는 코드를 만들고, 지속적으로 테스트를 진행하며 코드를 검증했다.

그렇기에 코드를 리펙토링하면서 발생하는 에러들을 빠르게 검증 할 수 있었다. 이렇게 테스트를 이용하면 새로운 기능도 기대한 대로 동작하는지 확인할 수 있을 뿐 아니라, 기존에 만든 기능을 검증할 수 있는 것이다.

 

하지만 기존에 작성한 UserDaoTest가 완벽한 것은 아니다. 테스트는 자동으로 동작하지만, 검증은 데이터베이스와 로그를 확인해야 한다는 것이다. 그리고 main() 메서드라고 하더라도 매번 그것을 실행해야 하는 것이다. 이런 방법보다 더 편리하고 체계적인 테스트가 필요하다.

+ Recent posts