TDD를 적용하지 않은체 서비스에 올렸는데 


온갔 컴파일에러에... 별별 일이 다터지네요. 


파이썬 같은 동적타이핑 언어의 경우에는 TDD가 필수인것으로 보입니다.


실제로 개념도 이쁘고 녹색바가 올라가는 것도 이쁘고...


스프링에서 공부한 개념들 파이썬 장고 관련 TDD서적보면서 다시한번 적용해보도록하겠습니다. 


정리하면서 여기에도 raw한 공부글이 올라오게 되겠네요. 당장 월요일 부터 시작 ~:~ 


다만 영어공부의 비중이 커짐에 따라서 절대적 공부시간, 코딩시간이 줄어들게 될 예정이라 어느정도 주기로 올릴지는 모르겠네요..


시무룩

'web > django' 카테고리의 다른 글

django시작하기  (0) 2016.10.26

공부하기 싫다. 





3.5 템플릿과 콜벡


 우리는 지금까지 전략패턴을 적용하고 익명 내부 클래스를 활용한 방식으로 코드를 작성해왔다. 이런 방식을 스프링에서는 템플릿/콜백 패턴이라고 부른다. 

이 때, 템플릿은 미리 만들어둔 모양이 있는 툴을 가리킨다. 

콜백은 실행되는 목적으로 다른 오브젝트의 메소드에 전달되는 오브젝트를 말한다. 이 때, 이 오브젝트는 파라미터로 전달되지만 값을 참조하기 위함이아니라 특정 로직을 담은 메소드를 실행시키기 위해 사용한다. 


템플릿/콜백의 특징

1) 일반적으로 단일 메소드 인터페이스를 사용한다. 다만, 여러가지 종류의 전략을 사용해야한다면 하나이상의 코백 오브젝트를 사용할 수 있다.

2) 콜백 인터페이스의 메소드에는 보통 파라미터가 있는데, 이 파라미터는 템플릿의 작업 흐름 중에 만들어지는 컨텍스트 정보를 전달 받을 때 사용한다.


콜백의 분리와 재활용

우리가 구현해온 탬플릿/콜백 패턴은 익명 내부클래스를 통해서 작성된 코드이다보니 적지 않게 중첩된 괄호가 만들어지게된다. 이 부분을 분리해 보도록하자.


1
2
3
4
5
6
7
8
9
10
11
public void deleteAll() throws SQLException{
    executeSql("delete from users");
}
public void executeSql(final String query) throws SQLException{
    this.jdbcContext.workWithStatementStrategy(new StatementStrategy() {
        @Override
        public PreparedStatement makePreParedStatement(Connection c) throws SQLException {
            return c.prepareStatement(query);
        }
    });
}
cs


애초에 필요한 것은 sql문장 뿐이었으니 위처럼 익명 클래스를 만드는 작업을 분리를 해서 재활용해보도록 하자. 또한, 이 executeSql은 UserDao뿐만 아니라 다른 Dao에서도 생성 될 수 있다. 따라서 우리가 이전에 만들어둔 JdbcContext클래스로 옮기도록하자


deleteAll 메소드가 더 간결하고 보기 편해졌다. 


 * * * 예제 * * *

 파일에서 라인별로 숫자를 입력받아 더한 후 출력하는 것을 템플릿 콜백으로 작성하라
 파일에서 라인별로 숫자를 입력받아 곱한 후 출력하는 것을 템플릿 콜백으로 작성하라





이제 JdbContext를 버리고 스프링이 제공하는 JDBC 코드용 기본 템플릿을 사용해보자. 클래스명은 JdbcTemplate이다


1
2
3
4
5
public void setDataSource(DataSource dataSource){
    this.dataSource = dataSource;
    
    jdbcTemplate = new JdbcTemplate(dataSource);
}
cs


다음과 같이 설정한 후 deleteAll에 적용해보도록 하자.

1
2
3
4
5
6
7
8
public void deleteAll() throws SQLException{
    jdbcTemplate.update(new PreparedStatementCreator() {
        @Override
        public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
            return connection.prepareStatement("delete from users");
        }
    });
}
cs


더 편하게 다음과 같이 사용할 수도 있다.

1
2
3
public void deleteAll() throws SQLException{
    jdbcTemplate.update("delete from users");
}
cs


add 메소드는 다음과 같이 편하게 구현할 수 있다.



1
2
3
4
public void add(final User user) throws ClassNotFoundException, SQLException{
    jdbcTemplate.update("insert into users(id, name, password) values(?,?,?)",
            user.getId(), user.getName(), user.getPassword());
}
cs



getCount는 다음과 같이 구현할 수 있다. 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public int getCount() throws SQLException{
    return this.jdbcTemplate.query(new PreparedStatementCreator() {
        @Override
        public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
 
            return connection.prepareStatement("select count(*) from users");
        }
    }, new ResultSetExtractor<Integer>() {
        @Override
        public Integer extractData(ResultSet resultSet) throws SQLException, DataAccessException {
            resultSet.next();
            return resultSet.getInt(1);
        }
    });
}
cs


JdbcTemplate은 이보다 더 간단한 방식으로 처리하는 메소드로 queryForInt()라는 친구를 제공한다고 써저 있으나 spring의 3.2.2버전에서 deprecated됐다. 

 


1
2
3
public int getCount() throws SQLException{
    return this.jdbcTemplate.queryForObject("select count(*) from users",Integer.class);
}
cs


대신 위와 같이 구현이 가능하다.


get 메소드는 이보다 좀더 복잡한데, qureyForObject를 통해서 구현이 가능하다



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public User get(String id) throws ClassNotFoundException, SQLException{
    return this.jdbcTemplate.queryForObject(
            "select * from users where id = ?",
            new Object[]{id},
            new RowMapper<User>() {
                @Override
                public User mapRow(ResultSet resultSet, int i) throws SQLException {
                    User user= new User();
                    user.setId(resultSet.getString("id"));
                    user.setName(resultSet.getString("name"));
                    user.setPassword(resultSet.getString("password"));
                    return user;
                }
            });
}
cs



원래 get 메소드는 EmptyResultDataAccessException을 던지게 만들어져 있었는데, 위의 queryForObject가 조회결과가없을 경우에는 해당 Exception을 발생시켜준다.


여기까지 테스트하고 확인!



 * * * 예제 * * *

 getAll을 구현해보자!

 TDD할 것 (즉 테스트 먼저 생성 후,  


이 이후에 네거티브 테스트에 관하여 나온다. 요약해보자면, 테스트를 만들 때, 부정적인 테스트를 먼저 만들라는 것이다. 예를 들자면, 나이에 음수값을 넣어보거나 생일에 문자를 넣어보는 등에 관련 된 테스트에 관하여 중요하게 설명하고 있다. 



소스코드 : https://github.com/choiking10/spring_test/tree/vol1_3.5_3.7/lifeignite  



'web > spring' 카테고리의 다른 글

스프링 공부 일시 중지  (0) 2017.08.04
[토비 vol1] 4.1~4.3 예외  (0) 2017.07.26
[토비 vol1] 3.1~3.4 템플릿(1)  (0) 2017.07.26
[토비 vol1] 2.4~2.6 테스트(2)  (0) 2017.07.25
[토비 vol1] 2.1~2.3 테스트(1)  (0) 2017.07.25


하루에 장 하나씩하는 것도 벅차네




junit의 작동방식의 특성상 테스트는 before -> test -> after 식으로 테스트 개수만큼 실행된다. 이게 spring의 경우 문제가 있는데 applicationContext가 테스트 개수만큼 생성됐다 소멸됐다 하는 것이다. 테스트 규모가 적을 경우 이는 문제가 없지만, 프로그램의 규모가 커지면 테스트하는 속도를 늦출 수 있다.  따라서 이 어플리케이션 컨텍스트를 단 한번만 생성할 수 있도록 스프링은 테스트를 지원한다.



1
2
3
4
5
6
 
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/applicationContext.xml")
public class UserDaoTest {
    @Autowired
    private ApplicationContext context;
cs


@RunWith는 Junit 프레임워크의 테스트 실행방법을 확장할 때 사용하는 애노테이션인데 SpringJUnit~ 는 이를 스프링용으로 확장하여 JUnit이 테스트를 진행하는 중에 테스트가 사용할 애플리케이션 컨텍스트를 만들고 관리하는 작업을 진행해준다. 

@ContextConfiguration은 설정파일의 위치를 지정한 것이다. 


이렇게 만들어진 어플리케이션 컨텍스트는 각 테스트가 여러개 진행되어도 단 하나만 존재하게 되며, 뿐만 아니라 다른 클래스에서 똑같은 어플리케이션 컨텍스트를 만들어서 테스트를 하더라도 단 하나만 존재하게 된다. 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/applicationContext.xml")
public class UserDaoTest {
    @Autowired
    private ApplicationContext context;
}
 
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/applicationContext.xml")
public class GroupDaoTest {
    @Autowired
    private ApplicationContext context;
}
 
cs


즉 위와같이 클래스가 다른 테스트 에서도 애플리케이션 컨텍스트는 단 하나만 생성된다.


이때 사용되는 어노테이션 @Autowired라는 친구를 변수에 붙이면 테스트 컨텍스트 프레임워크는 변수타입과 일치하는 컨텍스트 내의 빈을 찾아 해당 변수에 넣어준다. 애플리케이션 컨텍스트는 최초 초기화시에 자기자신을 빈으로 등록하므로 위와 같은 코드가 정상 작동하는 것이다.

 위처럼 UserDao도 @Autowired를 통해서 자동으로 빈을 DI받을수있는데 단, 같은 타입의 빈이 두 개 이상 있는 경우에는 어떤 빈을 가져올지 결정할 수 없다.이럴 경우 예외가 발생하게된다. 

 테스트의 경우에도 되도록 느슨하게 결합하기 위해 인터페이스를 활용하는게 좋지만, 필요에 따라 특정 타입의 구체화된 인스턴스를 의도적으로 테스트할 필요성이 있을 수 있기에 밀접한 관계를 가져도 상관없다.


 테스트에서 때로는 의존관계를 수동으로 DI해줘야 할 경우가 있다. 예를 들자면, 우리가 작성한 테스트가 실 서비스상에서 제공된다고 가정해보자. 그렇다면 deleteAll을 실행하는 순간 모든 데이터가 날아가 버릴 것이다. 이건 너무 위험천만한 행위이므로 우리가 임의적으로 testdb에서 실험을 진행할 수 있다. 이 때, 우리가 수동으로 DataSource를 만들어서 테스트디비에 접속하게 만들 수 있다. 다만, 이런 방식의 테스트는 애플리케이션 컨텍스트를 직접 수정하는 것이므로 위험할 수 있다. 애플리케이션은 한번 생성되어 다른 테스트들에게도 영향을 줄 수 있기 때문이다. 

 이럴 때 사용하는 어노테이션이 있는데 바로 @DirtiesConext이다. 간단하게 설명하자면 해당 테스트에서 컨텍스트를 더럽힐 것이므로 테스트 종료 후 새로운 어플리케이션컨텍스트를 생성하라는 어노테이션이다. 이는 클래스에도, 메소드에도 붙일 수있다. 

 하지만 이러는 것보다 그냥 테스트용 설정 파일(xml)을 만드는 것을 추천한다.(아니 왜설명한거야... )



2.5 학습 테스트

 학습 테스트는 자신이 만든 코드에 관한 테스트가 아니라 앞으로 사용하고자하는 api나 프레임워크의 기능을 테스트하는 것을 말한다. 이런 테스트는 검증에 목적이 있지 않고, 해당 기술의 학습에 목적이 있다. 

 

 길게 써놨는데 간략히 요약하자면, 레퍼런스가 될 수도 있고, 이미 작성되어있는 학습 테스트로 부터 레퍼런스 이상의 사용법을 배울 수 있으며, 해당 기술의 테스트방법을 공부할 수 있다는 의견인것 같다. 


 테스트 방법으로 동등분할과 경계값 분석이 있다. 동등 분할은 같은 결과를 내는 값의 범위를 구분해서 대표값으로 테스트 하는 것을 말하고, 경계값 분석은 0이나 그 주변 값 또는 최대값, 최소값등으로 테스트하는 것을 의미한다.


소스코드 : https://github.com/choiking10/spring_test/tree/vol1_2.4_2.6/lifeignite

블로그 쓰면서 공부하려니 넘나 힘든 것... 


이게 나중에 도움이 되겠지?... 


딱히 누구 보라고 작성하는 건 아니긴 한데... 흠... 




2.1 <- 테스트의 필요성이다. 

당욘한거죠!

넘어가


2.2 UserDaoTest

기존의 방식은 테스트의 결과 확인을 직접해야하는 번거로움이 있었다. 이것을 이제 테스트의 결과를 확인하기 쉽도록 UserDaoTest를 수정해보도록 하자



1
2
3
System.out.println(user2.getName());
System.out.println(user2.getPassword());
System.out.println(user2.getId()+" 조회 성공");
cs


이게 원래 수정 전 테스트 코드이다.


이 경우에는 실제 조회가 성공했는지 눈으로 확인해야한다. 이걸 자동화 시켜보도록 하자.



1
2
3
4
5
6
7
8
if(!user.getName().equals(user2.getName())){
    System.out.println("테스트 실패 (name)");
}
if(!user.getPassword().equals(user2.getPassword())){
    System.out.println("테스트 실패 (password)");
}else {
    System.out.println("조회 테스트 성공");
}
cs


자동화 성공!

...

별거없다.


2.3 JUnit 테스트로 전환하기

일단 junit을 사용하기위해 dependency를 pom.xml에 추가해주도록 하자.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.11</version>
    <exclusions>
        <exclusion>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest-all</artifactId>
    <version>1.3</version>
</dependency>
cs

그리고 다음을 static import한다 


1
2
import static org.junit.Assert.assertThat;
import static org.hamcrest.CoreMatchers.is;
cs


자 그러면  다음과 같은 모습이 되는데


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package lifeignite;
 
import static org.junit.Assert.assertThat;
import static org.hamcrest.CoreMatchers.is;
 
import org.junit.Test;
import org.junit.runner.JUnitCore;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;
import user.User;
import user.UserDao;
 
import java.sql.SQLException;
 
public class UserDaoTest {
    public static void main(String[] args) {
        JUnitCore.main("lifeignite.UserDaoTest");
    }
 
    @Test
    public void addAndGet() throws ClassNotFoundException, SQLException{
        ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
 
        UserDao dao = context.getBean("userDao", UserDao.class);
 
        User user = new User();
        user.setId("lifeignite");
        user.setName("광전사");
        user.setPassword("for");
 
        dao.add(user);
 
        User user2 = dao.get(user.getId());
 
        assertThat(user2.getName(),is(user.getName()));
        assertThat(user2.getPassword(),is(user.getPassword()));
    }
}
 
cs


조작을해보면서 성공시와 실패시 어떤 방식으로 뜨는지 체크할 수 있다. 


기존의 방식은 디비에서 직접 추가된 user를 삭제하고, 테스트를 돌리는 방식이었는데 넘나 귀찮다. 따라서 이제 삭제하는 메소드도 만드려고한다. 또 user테이플의 개수를 가져오는 것도 만들고 뚝딱뚝딱



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class UserDaoTest {
    @Test
    public void addAndGet() throws ClassNotFoundException, SQLException{
        ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
 
        UserDao dao = context.getBean("userDao", UserDao.class);
 
        dao.deleteAll();
        assertThat(dao.getCount(), is(0));
 
        User user = new User();
        user.setId("lifeignite");
        user.setName("광전사");
        user.setPassword("for");
 
        dao.add(user);
        assertThat(dao.getCount(),is(1));
 
        User user2 = dao.get(user.getId());
 
        assertThat(user2.getName(),is(user.getName()));
        assertThat(user2.getPassword(),is(user.getPassword()));
    }
}
 
cs


중요한 내용은 아니었고...


junit에서 테스트 중에 발생될 것으로 기대하는 예외 클래스를 지정해 줄 수 있다.

@Test(expected=somethingException.class)

이런방식으로 설정해둔 테스트에서는 예외가 발생하면 성공이고 아니라면 실패가 된다.

@Before 어노테이션은 테스트를 시작하기전에 먼저 실행해야하는 메소드를 정의하는데 사용된다.

@After 어노테이션은 테스트가 끝나고 난 후에 실행해야하는 메소드를 정의하는데 사용된다. 


JUNIT에서 테스트를 진행하는 과정을 간단히 정리하자면 아래와 같다.

1) 테스트 클래스에서 @Test가 붙은 public이고 void형이며 파라미터가 없는 테스트 메소드를 찾는다.

2) 테스트 클래스의 오브젝트를 하나 만든다.

3) @Before가 붙은 메소드가 있으면 실행한다.

4) @Test가 붙은 메소드를 하나 호출하고 테스트 결과를 저장해둔다

5) @After가 붙은 메소드가 있으면 실행한다.

6) 나머지 테스트 메소드에 대해 2~5번을 반복한다

7. 모든 테스트의 결과를 종합해서 돌려준다.



TDD에 관한 설명이 나오는데 너무 아름다우니까 두번 외치자


테스트를 만들고 테스트에 맞춰서 구현을 한다. 


알고리즘 사이트에서 테스트 케이스 만들고 구현하는 느낌인데 되게 새롭고 참신하고 매력적인 방법인것 같다.

이런방식으로 개발해야지


소스코드 : https://github.com/choiking10/spring_test/tree/vol1_2.1_2.3/lifeignite




+ Recent posts