테스트-2

정훈희·2022년 10월 15일
0

Spring

목록 보기
8/24

테스트-2

참조

  • 토비의 스프링 3.1 vol.1 168p~196p

이전에 getCount 메소드를 테스트에 적용하긴 했지만, 테이블이 비어 있는 경우와 add()를 한번 호출한 뒤의 결과 뿐이다.

더 꼼꼼하게 getCount를 테스트 하려면 사용자 정보를 하나씩 추가하며 매번 getCount의 값이 1씩 증가하는지 확인해봐야 한다.

일단 편의를 위해 파라미터가 있는 User클래스의 생성자를 추가한 상태에서 진행한다.

@Test
public void count() throws SQLException {
	// ...
	User user1 = new User("gyumee" , "박성철", "springno1");
	User user2 = new User("leegw700", "이길원", "springno2");
	User user3 = new User("bumjin", "박범진", "springno3");
	dao.deleteAll();
	assertThat(dao.getCount(), is(0));
	dao.add(user1);
	assertThat (dao .getCount(), is(1));
	dao .add(user2);
	assertThat(dao.getCount(), is(2));
	dao.add(user3);
	assertThat(dao.getCount(), is(3));
}

getCount 테스트와 이전에 있던 테스트중에 어떤 테스트가 먼저 실행될지 알 수 없으므로 각 테스트는 실행 순서에 상관없이 독립적으로 같은 결과를 낼 수 있도록 해야 한다.


이번에는 addAndGet() 테스트를 보완해보자. add는 이미 검증이 된 것 같지만, get의 경우는 get이 파라미터로 주어진 id에 해당하는 사용자를 가져온 것인지 그냥 아무거나 가져온 것인지 테스트에서 검증하지는 못했다.

→ User를 하나 더 추가해서 두 개의 User를 add( ) 하고, 각 User의 id를 파라미터로 전달해서 get()을 실행하도록 해보자.

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

	UserDao dao = context.getBean("userDao", UserDao.class);
	User user1 = new User("gyumee", "박성철", "springno1")
	User user2 = new User("leegw700", "이길원", "springno2");

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

	dao.add(user1);
	dao.add(user2);
	assertThat(dao.getCount(), iS(2));

	User userget1 = dao.get(user1.getld());
	assertThat(userget1.getName(), is(user1.getName()));
	assertThat(userget1.getPassword(), is(user1.getPassword()));

	User userget2 = dao.get(user2.getld());
	assertThat(userget2.getName(), is(user2.getName()));
	assertThat(userget2.getPassword(), is(user2.getPassword()));
}

이리하여 get 메소드에 대한 검증이 더 탄탄해졌다.

하지만, 만약 get 메소드가 받은 id에 대한 사용자 정보가 없다면 어떻게 될까?

null을 리턴시키는 방법도 있지만, 여기선 id에 해당하는 정보가 없다면 error가 발생하도록 해보자.

@Test(expected=EmptyResultDataAccessException.class)
public void getUserFailure() throws SQLException {
	// ...
	dao.deleteAll();
	assertThat(dao.getCount(), is(0));
	dao.get("unknown_id");
}

@Test(expected=EmptyResultDataAccessException.class) 이부분은 테스트 코드 실행 결과로 예상되는 값을 넣은 것이다. 즉 이 테스트에서는 EmptyResultDataAccessException이 발생해야 테스트가 성공했다고 인식하는 것이다.

즉 dao.get("unknown_id"); 이 부분에서 에러가 발생하지 않으면 테스트가 실패되는 것이다.

하지만 우리가 get 메소드에 해당하는 id를 찾지 못했을 때 에러를 반환하도록 하지 않았으므로 테스트는 실패한다. 그러므로 get에 id에 해당하는 데이터가 없으면 에러를 던지도록 수정해야한다.


테스트 주도 개발(TDD)

만들고자 하는 기능의 내용을 담고 있으면서, 만들어진 코드를 검증도 해줄 수 있도록 테스트 코드를 먼저 만들고, 테스트를 성공하게 해주는 코드를 작성하는 방식의 개발 방법

즉 만들 기능에 대한 테스트 코드를 먼저 작성한 뒤 테스트 코드가 성공하도록 개발을 하는 방식을 테스트 주도 개발이라고 한다.

TDD는 테스트를 먼저 만들고 그 테스트가 성공하도록 하는 코드만 만드는 식으로 진행하기 때문에

  1. 테스트를 빼먹지 않고 꼼꼼하게 만들어낼 수 있다.
  2. 테스트를 작성하는 시간과 애플리케이션 코드를 작성하는 시간의 간격이 짧아진다.
  3. 코드를 작성하면서 바로바로 테스트를 실행해볼 수 있다.
  4. 코드에대한 피드백을 매우 빠르게 받을 수 있게 된다.
  5. 매번 테스트가 성공하는 것을 보면서 작성한 코드에 대한 확신을 가질 수 있다.

TDD에서는 테스트를 작성하고, 성공시키는 코드를 만드는 작업주기를 가능한 짧게 가져가도록 권장한다.


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

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

→ 테스트를 수행하는 데 필요한 오브젝트나 정보(픽스처, fixture)들은 @Before에 넣고, 일부 메소드에서만 반복적으로 호출되는 코드는 일반 메서드로 빼두는 것이 좋다.


스프링 테스트 적용

기존 테스트 코드는 테스트 메소드 수 만큼 애플리케이션 컨텍스트가 만들어지고 있다.

스프링은 이를 위해 애플리케이션 컨텍스트 테스트 지원 기능을 제공한다.

아래의 코드는 스프링 테스트 컨텍스트를 적용한 UserDaoTest이다.

// 스프링의 테스트 컨텍스트 프레임 워크의 JUnit 확장기능 지정
@RunWith(SpringlUnit4ClassRunner.class)
// 테스트 컨텍스트가 자동으로 만들어줄 애플리케이션 컨텍스트의 경로 지정
@ContextConfiguration(locations="/applicationContext.xm1")
public class UserDaoTest {
	// 테스트 오브젝트가 만들어지고 나면 스프링 테스트 컨텍스트에 의해 자동으로 값이 주입된다.
	@Autowired
	private ApplicationContext context;
	// ...
	@Before
	public void setUp() {
		this.dao = this.context.getBean("userDao", UserDao.class);
		// ...
	}
}

여기서 각 테스트 메소드 실행 전에 this.context와 this를 출력해보면 this는 매번 다르게 나오는 반면에 this.context는 매번 같게 나오는 것을 알 수 있다.

또한 다른 테스트 클래스를 만들더라도 @ContextConfiguration 어노테이션에서 같은 경로를 지정해주면 설정파일을 공유할 수 있다.

@Autowired가 붙은 인스턴스 변수가 있으면 태스트 컨텍스트 프레임워크는 변수 타입 과 일치하는 컨텍스트 내의 빈을 찾는다. 타입이 일치하는 빈이 있으면 인스턴스 변수 에 주입해준다.

public class UserDaoTest {
	// 테스트 오브젝트가 만들어지고 나면 스프링 테스트 컨텍스트에 의해 자동으로 값이 주입된다.
	@Autowired
	private UserDao dao;

ApplicationContext DI 받고 DL로 UserDao를 가져오는 방식대신에 바로 UserDao를 DI 받도록 할 수도 있다.

@Autowired는 변수에 할당 가능한 타입을 가진 빈을 자동으로 찾는다. 따라서 SimpleDriverDataSource 클래스 타입은 물론이고, 인터페이스인 DataSource 타입으로 변수를 선언해도 된다.

단, @Autowired는 같은 타입의 빈이 두 개 이상 있는 경우에는 타입만으로는 어떤 빈을 가져올지 결정할 수 없다. 만약 같은 타입의 빈이 여러개 있다면 변수의 이름과 같은 빈이 있다면 해당 빈을 주입하고, 변수 이름으로도 빈을 찾을 수 없다면 예외가 발생한다.


근데 굳이 왜 DataSource 인터페이스 를 사용하고 DI를 통해 주입해주는 방식을 이용해야 할까? 그냥 UserDao에서 직접 SimpleDriverDataSource를 생성하고 사용하면 안 될까? 라는 의문이 든다.

그럼에도 불구하고 인터페이스를 두고 DI를 적용해야 하는 이유는

  1. 소프트웨어 개발에서 절대로 바뀌지 않는 것은 없기 때문이다.

  2. 클래스의 구현 방식은 바뀌지 않는다고 하더라도 인터페이스를 두고 DI 를 적용하게 해두면 다른 차원의 서비스 기능을 도입할 수 있기 때문이다.

    예시로 1장에서 DB 커넥션의 개수를 카운팅하는 부가기능을 추가했던 것이 있다.

  3. 테스트

    테스트할 대상의 범위가 넓어지면 테스트를 작성하기가 어려워진다. DI는 테스트가 작은 단위의 대상에 대해 독립적으로 만들어지고 실행되게 하 는 데 중요한 역할을 한다.

테스트 코드에 의한 DI

만약 애플리케이션이 사용할 설정파일에 정의된 DataSource 빈은 서버의 DB 풀 서비스와 연결해서 운영용 DB 커넥션을 돌려주도록 만들어져 있다고 하자.

그렇다면 테스트할 때 이 DataSource를 이용하다가 deleteAll() 메소드로 모든 데이터가 삭제된다면 정말 큰일이 난다. 그런 경우에는 테스트 코드에 의한 DI를 이용해서 테스트 중에 DAO가 사용할 DataSource를 바꿔주는 방법을 이용하면 된다.

	
// 테스트 메소드에서 애플리케이션 컨텍스트의 구성이나 상태를 변경한다는 것을 알려준다
@DirtiesContext
public class UserDaoTest {
	@Autowired
	UserDao dao;
	@Before
	public void setUp() {
		// ...

		// 테스트에서 UserDao가 사용할 DataSource오브젝트를 직접 생성한다.
		DataSource dataSource = new SingleConnectionDataSource(
			"jdbc:mysql://localhost/testdb", "spring", "book", true);
		dao.setDataSource(dataSource); // 코드에 의한 수동 DI

테스트를 위한 별도의 DI 설정

위의 방법보다는 사실 별도의 DI 설정 파일을 만들어서 하는편이 낫다.

test-applicationContext.xml

...
	<property name="url" value="jdbc:mysql:lllocalhost/testdb" />
...

그리고 경로를 새로 만든 설정파일로 바꿔주면 된다.

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

그 외에 컨테이너 없는 DI 테스트 방식도 있다.

profile
DB를 사랑하는 백엔드 개발자입니다. 열심히 공부하고 열심히 기록합니다.

0개의 댓글