2. 테스트 - 개발자를 위한 테스팅 프레임워크 JUnit

이유석·2022년 5월 3일
0

Book - Toby's Spring

목록 보기
11/20
post-thumbnail

개발자를 위한 테스팅 프레임워크 JUnit

JUnit

  • 자바의 표준 테스팅 프레임워크
  • 단순하고 빠르다.
    main() 메소드와 System.out.println()으로 만들어져 있기 때문이다.

2.3.1 JUnit 테스트 실행 방법

가장 좋은 JUnit 테스트 실행 방법은 자바 IDE에 내장된 JUnit 테스트 지원 도구를 사용하는 것 이다.

IDE

IDE에서 지원하는 JUnit 테스트 도구를 통해 얻을 수 있는 정보

  • 테스트 수행 시간, 실행한 테스트의 수, 테스트 에러의 수, 테스트 실패의 수, 테스트 클래스의 이름

테스트 실패 시 얻을 수 있는 정보

  • 실패한 이유
    ex) Expected : "박성철", Got : null

빌드 툴
프로젝트의 빌드를 위해 ANT 또는 Maven 과 같은 빌드 툴을 사용시, 빌드 툴에서 제공하는 JUnit 플러그인 또는 태스크를 이용해 JUnit 테스트를 실행할 수 있다.

테스트 실행 결과는 옵션에 따라 보기 좋게만들어진다.
ex) HTML, 텍스트 파일

빌드 툴을 이용하여 테스트하는 경우

  • 여러 개발자가 만든 코드를 모두 통합해서 테스트를 수행해야 할 때,
    빌드 스크립트를 이용해 JUnit 테스트를 수행 후, 결과를 메일 등으로 통보받는 방법을 사용한다.

2.3.2 테스트 결과의 일관성

지금까지의 테스트 실행 과정에서의 불편한 점이 있다.

  • UserDaoTest를 실행하기 전에, DB의 User테이블 데이터를 모두 삭제 해줘야 한다는 것 이다.

해당 사항에서 생각해볼 문제가 존재한다.

  • 테스트가 외부 상태에 따라 결과가 바뀔 수 있다는 점 이다.

가장 좋은 해결 방법은 테스트 종료 후, 자동으로 테스트가 등록한 데이터를 삭제하여 테스트 이전 상태로 만들어주는 것 이다.


deleteAll()의 getCount()추가
UserDao 클래스에 아래의 함수를 추가해준다.

  • deleteAll() : User 테이블의 모든 데이터를 삭제해주는 함수이다.
public void deleteAll() throws SQLException {
    Connection c = this.dataSource.getConnection();
        
    PreparedStatement ps = c.prepareStatement("delete from users");

    ps.executeUpdate();

    ps.close();
    c.close();
}
  • getCount() : User 테이블의 데이터 개수를 반환한다.
public int getCount() throws SQLException {
    Connection c = this.dataSource.getConnection();

    PreparedStatement ps = c.prepareStatement("select count(*) from users");

    ResultSet rs = ps.executeQuery();
    rs.next();

    int count = rs.getInt(1);

    rs.close();
    ps.close();
    c.close();

    return count;
}

deleteAll() 과 getCount()의 테스트
추가된 기능의 테스트도 만들어주어야 한다.

그런데, 해당 기능들은 독립적인 테스트가 불가능하다.
(User 테이블에 데이터가 존재하여야 가능하기 때문이다.)

그러므로 기존 addAndGet() 테스트를 확장하는 방법을 사용하자.

addAndGet() {
	deleteAll() // DB의 모든 데이터 지움
    getCount() == 0 ? // 모든 데이터를 지웠으니, 현재 User 테이블의 데이터 개수 : 0
    
    add(user) // 새로운 데이터를 DB에 추가
    getCount() == 1? // 새로운 데이터를 추가했으니, 현재 User 테이블의 데이터 개수 : 1
    
    get(user.getId()) // get 테스트 수행
}
@Test
public void addAndGet() throws SQLException {
	ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");

	UserDao userDao = context.getBean("userDao", UserDao.class);
        
    // delete All test 
    userDao.deleteAll();
    assertThat(userDao.getCount(), is(0));
        
    // insert test
    User user = new User();
    user.setId("Id");
    user.setName("test_name");
    user.setPassword("Password");

    userDao.add(user);
    assertThat(userDao.getCount(), is(1));

	// select test
    User searchedUser = userDao.get(user.getId());

	assertThat(searchedUser.getName(), is(user.getName()));
    assertThat(searchedUser.getPassword(), is(user.getPassword()));

    }

동일한 결과를 보장하는 테스트
단위 테스트는 항상 일관성 있는 결과가 보장돼야 한다.

  • 외부 환경에 영향을 받지 말아야 한다.
  • 테스트를 실행하는 순서를 바꿔도 동일한 결과가 보장되어야 한다.
    (JUnit은 특정 테스트 메소드읜 실행 순서를 보장해주지 않는다.)

2.3.3 포괄적인 테스트

테스트 메소드는 한 번에 한 가지 검증 목적에만 충실하는 것이 좋다.

  • 이전의 getCount()에 대한 테스트를 addAndGet()테스트를 통하여 진행하였다.
    미처 생각하지 못한 문제가 숨어 있을지도 모르니 더 꼼꼼하게 테스트해보는 것이 좋다.

getCount() 테스트
테스트 시나리오

  1. User 테이블의 모든 데이터를 지운다.
  2. getCount() 의 결과가 0인지 확인한다.
  3. 새로운 User 데이터를 등록한다.
  4. getCount() 의 결과가 1 증가하였는지 확인한다.
  5. 3~4 과정을 3번 반복한다.
@Test
public void count() throws SQLException {
	ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
    
    UserDao userDao = context.getBean("userDao", UserDao.class);
    
    User user1 = new User("gyumee", "박성철", "springno1");
    User user2 = new User("leegw700", "이길원", "springno2");
    User user3 = new User("bumjin", "박범진", "springno3");
        
    userDao.deleteAll();
    assertThat(userDao.getCount(), is(0));

	userDao.add(user1);
    assertThat(userDao.getCount(), is(1));

	userDao.add(user2);
    assertThat(userDao.getCount(), is(2));

	userDao.add(user3);
    assertThat(userDao.getCount(), is(3));
}

addAndGet() 테스트 보완
get()메소드에 대한 테스트 기능을 좀 더 보완해보자.

  • get()이 파라미터로 주어진 Id에 해당하는 사용자를 가져온 것인지, 그냥 아무거나 가져온 것인지 테스트에서 검증하지 못한다.

테스트 시나리오

  1. User 테이블의 모든 데이터를 지운다.
  2. getCount() 의 결과가 0인지 확인한다.
  3. 새로운 2개의 User 데이터를 등록한다.
  4. getCount() 의 결과가 2인지 확인한다.
  5. 서로 다른 id에 대하여 get() 테스트를 진행한다.
@Test
public void addAndGet() throws SQLException {
    ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");

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

	// delete All test
    userDao.deleteAll();
    assertThat(userDao.getCount(), is(0));

	// insert test
    User user1 = new User("gyumee", "박성철", "springno1");
    User user2 = new User("leegw700", "이길원", "springno2");
	
    userDao.add(user1);
    userDao.add(user2);
    assertThat(userDao.getCount(), is(2));

	// select test
    User searchedUser1 = userDao.get(user1.getId());

    assertThat(searchedUser1.getName(), is(user1.getName()));
    assertThat(searchedUser1.getPassword(), is(user1.getPassword()));

    User searchedUser2 = userDao.get(user2.getId());

    assertThat(searchedUser2.getName(), is(user2.getName()));
    assertThat(searchedUser2.getPassword(), is(user2.getPassword()));

}

get() 예외조건에 대한 테스트
get() 메소드에 전달된 id값에 해당사는 user 정보가 없을때를 고려해보자.

해결 방법

  1. null과 같은 특별한 값을 반환한다.
  2. 예외를 던진다.

2번의 방법을 사용해서 예외처리를 해보자.

예외처리에 대한 테스트는 이전 테스트 방법과는 다르다.

  • 테스트 진행 중에 특정 예외가 던져지면 테스트가 성공한 것이다.
  • 예외가 던져지지 않고 정상적으로 작업을 마치면 테스트가 실패했다고 판단해야 한다.

이를 위해 JUnit은 예외조건 테스트를 위한 특별한 방법을 제공해준다.
@Test()의 속성 중 expected 에 해당 예외 클래스를 설정해주면 된다.

@Test(expected = EmptyResultDataAccessException.class)
public void getUserFailure() throws SQLException {
    ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");

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

    userDao.deleteAll();
    assertThat(userDao.getCount(), is(0));

	userDao.get("unknown_id");
        
}

EmptyResultDataAccessException 을 위해, spring-dao.jar 파일을 import 해주자

해당 테스트 실행시, 당연히 테스트는 실패한다.
get() 메소드에서 쿼리 결과의 첫 번째 로우를 가져오게 하는 rs.next()를 실행할 때,
가져올 로우가 없다는 SQLException이 발생한다.


테스트를 성공시키기 위한 코드의 수정
get() 메소드에 id에 해당하는 데이터가 없으면 EmptyResultDataAccessException 예외를 던지도록 수정해주자.

public User get(String id) throws SQLException {
   ...

	User user = null;
    if (rs.next()) {
    	user = new User();
    	user.setId(rs.getString("id"));
        user.setName(rs.getString("name"));
        user.setPassword(rs.getString("password"));
    }

	rs.close();
    ps.close();
    c.close();

	if (user == null) {
    	throw new EmptyResultDataAccessException(1);
    }
    
    return user;
}

포괄적인 테스트

테스트를 만들때 생각해야할 점

  • DAO의 메소드에 대한 포괄적인 테스트를 만들어두는 편이 훨씬 안전하고 유용하다.
    즉, 사소한 부분에 대해서도 테스트를 만들어두자!

  • 테스트를 작성할 때 부정적인 케이스를 먼저 만드는 습관을 들이는 게 좋다.

2.3.4 테스트가 이끄는 개발 (TDD)

  • 테스트 케이스를 먼저 만들고, 해당 기능을 위한 코드를 개발한다.

테스트 개발 을 위한 설계 문서

  • 조건(given) : 어떤 조건을 가지고
  • 행위(when) : 무엇을 할 때
  • 결과(then) : 어떤 결과가 나온다.

TDD 의 특징

  • 테스트를 빼먹지 않고 코드를 작성할 수 있다.
  • 테스트를 작성하고, 이를 성공시키기 위한 코드를 작성하는 주기를 가능한 짧게 하는 것이 좋다.
  • 빠른 테스트-개발 주기 덕분에, 오류가 나는 부분을 쉽게 찾을 수 있다.

2.3.5 테스트 코드 개선 (리팩토링)

테스트 코드 클래스를 살펴보면, 모든 테스트에 반복적인 부분이 존재한다.

  • 스프링의 애플리케이션 컨텍스트를 만드는 부분
  • 컨텍스트에서 UserDao를 가져오는 부분
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");

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

중복된 코드는 별도의 메소드로 뽑아내는 것이 가장 손쉬운 방법이다.
하지만, 이번에는 JUnit이 제공하는 기능을 활용해보겠다.

JUnit 은 테스트를 실행할 때마다 반복되는 준비 작업을 병도의 메소드에 넣게 해주고, 이를 매번 테스트 메소드를 실행하기 전에 먼저 실행시켜 주는 기능이 있다.

먼저 세 개의 테스트 메소드에서 반복적으로 등장하는 앞의 코드를 제거한다.


@Before
중복됐던 코드를 setUp()이라는 메소드를 생성하여 넣어준다.

userDao 변수가 로컬 변수였기 때문에, 테스트 메소드에서 접근 가능하도록 인스턴스 변수로 변경한다.

public class main {

    private UserDao userDao;

    @Before
    public void setUp() {
        ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");

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


    @Test
    public void addAndGet() throws SQLException {...}

    @Test
    public void count() throws SQLException {...}

    @Test(expected = EmptyResultDataAccessException.class)
    public void getUserFailure() throws SQLException {...}

}

JUnit이 하나의 테스트 클래스를 가져와 테스트를 수행하는 방식은 다음과 같다.

  1. 테스트 클래스에서 @Test가 붙은 public이고 void형이며 파라미터가 없는 테스트 메소드를 모두 찾는다.
  2. 테스트 클래스의 오브젝트를 하나 만든다.
  3. @Before가 붙은 메소드가 있으면 실행한다.
  4. @Test가 붙은 메소드를 하나 호출하고 테스트 결과를 저장해둔다.
  5. @After가 붙은 메소드가 있으면 실행한다.
  6. 나머지 테스트 메소드에 대해 2~5번을 반복한다.
  7. 모든 테스트의 결과를 종합해서 돌려준다.

실제로는 이보다 더 복잡하다.

보통 하나의 테스트 클래스 안에 있는 테스트 메소드들은 공통적인 준비 작업 - @Before공통적인 정리 작업 - @After가 필요하다.

대신 테스트 메소드와 서로 주고받을 정보나 오브젝트가 있다면, 인스턴스 변수를 이용해야 한다.

각 테스트 메소드를 실행할 때마다 테스트 클래스의 오브젝트를 새로 만든다.

  • 각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 확실히 보장해주기 위함이다.
  • 테스트 메소드의 일부에서만 공통적으로 사용되는 코드가 있다면
    • 일반적인 메소드 추출 방법을 사용하여 메소드를 분리한다.
    • 공통적인 특징을 지닌 테스트 메소드를 모아 별도의 테스트 클래스로 만든다.

fixture (픽스처)

  • 테스트를 수행하는 데 필요한 정보나 오브젝트

  • 일반적으로 픽스처는 여러 테스트에서 반복적으로 사용된다. 그러므로 @Before를 통해 메소드를 생성하여 사용하면 편리하다.

User 오브젝트의 생성 과정을 @Before 메소드와 인스턴스 변수를 이용해 분리해보자.

public vlass main {
	private UserDao userdao;
    
    private User user1;
    private User user2;
    private USer user3;
    
	@Before
	public void setUp() {
		...
		this.user1 = new User("gyumee", "박성철", "springno1");
    	this.user2 = new User("leegw700", "이길원", "springno2");
    	this.user3 = new User("bumjin", "박범진", "springno3");
	}
}

소스코드 : github

profile
https://github.com/yuseogi0218

0개의 댓글