2장 테스트

soplia080 gyp·2022년 5월 7일
0

토비의 스프링

목록 보기
2/4
post-thumbnail

2.1 UserDaoTest 다시 보기

2.1.1 테스트의 유용성


이 정도는 알 것이다.

2.1.2 UserDaoTest의 특징

1장에서 했던 UserDaoTest.java 테스트 코드를 개선하는 것이 이번 장의 주제같다.
UserDaoTest.java

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

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

        User user = new User();
        user.setId("아이디1");
        user.setName("이름1");
        user.setPassword("비밀번호1");
        dao.add(user);
        System.out.println(user.getId() + " 등록 성공");


        User user2 = dao.get(user.getId());
        System.out.println(user2.getName());
        System.out.println(user2.getPassword());
        System.out.println(user2.getId() + "조회 성공");
    }
}

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

  • DAO에 대한 test를 하고자 할때 DAO뿐만 아니라 서비스 class, controller, jsp view 등 모든 레이어의 기능을 다 만들고 나서야 테스트가 가능하다

    어디가 문제인지 정확히 파악이 힘들다.

작은 단위의 테스트 - 단위 테스트(unit test)

  • 테스트의 관심이 다르다면 테스트할 대상을 분리하고 집중해서 접근해야 한다.
  • 단위라는 단어에 대해 정해진 크기는 없지만 통제할 수 없는 외부의 리소스에 의존하는 테스트는 단위 테스트가 아니라고 보기도 한다.
    • DB의 상태가 매번 달라지거나, 테스트를 위해 DB를 특정 상태로 만들수 없는 경우

2.1.3 UserDaoTest의 문제점

  • 수동 확인 작업의 번거로움
    - UserDaoTest는 단위 테스트 코드의 기능을 수행하지만 main() 메소드를 실행하기 때문에 결과를 일일히 콘솔로 확인해야한다. 테스트의 결과를 확인하는 작업이 자동화될 방법이 필요하다(검증해야 하는 양이 많아 질 경우 불편해진다.)
  • 실행 작업의 번거로움
    - 간단한 main()라도 매번 실행하는 것은 번거롭다. 몇 백개의 클래스마다 main()를 작성하고 테스트하는 모습을 상상해보자.

생략..


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

2.3.4 테스트가 이끄는 개발(p.175)

기능설계를 위한 테스트

  • given(조건), when(행위), then(결과)
  • 추가하고 싶은 기능을 테스트 코드로 표현한 뒤에 설계한대로 결과가 나오지 않으면 개선하라.

테스트 주도 개발(TDD, Test Driven Development)

  • 순서
  1. (만들고자 하는 기능의 내용 + 이미 만들어진 코드)를 검증 할 수 있도록 테스트 코드를 먼저 만듬
  2. 테스트를 성공하도록 코드 작성

Test First Development(테스트 우선 개발) 이라고도 한다.

  • 개발자가 테스트를 만들어가며 개발하는 방법이 주는 장점을 극대화한 방법이다.
  • "실패한 테스트를 성공시키기 위한 목적이 아닌 코드는 만들지 않는다." - TDD의 기본원칙

굳이 왜 이렇게 하는가?

  1. 개발자들이 개발을 하다 보면 테스트 코드를 만들 타이밍을 놓친다. 코드의 양이 많아질수록 테스트 작성은 미루어지고 점점 의미가 사라진다.
  2. TDD는 아예 테스트를 먼저 만들고 그 테스트가 성공하도록 하는 것을 목표로 하기에 테스트를 빼먹지 않고 꼼꼼하게 만들어낼 수 있다.
  3. 이미 테스트를 만들어두고 코드를 작성하면 바로바로 테스트를 실행해 빠른 피드백을 받을 수 있다.
    • 개발한 코드의 오류는 빨리 발견할수록 좋다.
    • 테스트 없이 오랜 시간 동안 코드를 만들고 나서 테스트를 하면, 오류 발생시 원인 찾기가 쉽지 않다. (예외는 A라는 곳에서 발생하고 사실 원인은 Z였다던지..)
  4. TDD를 하면 자연스럽게 단위 테스트를 만들 수 있다.(매번 서버를 띄울 필요가 없다.)

좋은건 알겠는데.. 개발이 지연되지 않을까?

  • 테스트는 애플리케이션 코드보다 상대적으로 작성이 쉬움
  • 각 테스트가 독립적이기 때문에 코드의 양에 비해 작성하는 시간은 짧다.
  • 테스트 덕분에 오류를 빨리 잡아낼 수 있어서 전체적인 개발 속도는 오히려 빨라진다.

2.3.5 테스트 코드 개선

ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
UserDao dao = context.getBean("userDao", UserDao.class);
  • 위와 같이 중복되는 코드가 UserDaoTest에 있는 경우 Junit에서 제공해주는 기능이 있다.

@Before

  • 테스트 메소드를 실행하기 전에 먼저 실행시켜주는 기능(주로 반복되는 준비 작업)
public class UserDaoTest {
		
        /**
        setUp() 메소드에서 만드는 오브젝트를 테스트 메소드에서 사용할 수 있도록 인스턴스 변수로 정의
        **/
		private UserDao dao;
        
        @Before -> (@Test 메소드가 실행되기 전에 먼저 실행해야 하는 메소드 정의)
        public void setUp(){
        	ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
        	this.dao = context.getBean("userDao", UserDao.class);
        }
        ...
        
        @Test
        public void addAndGet() throws SQLException{
        	(각 테스트 메소드에 반복적으로 나타났던 코드를 별도의 메소드(@Before)로 옮긴다.)
        	...
        }
        
        @Test
        public void count() throws SQLException{
        	...
            dao.get(...)
        }
        
        @Test(expected=EmptyResultDataAccessException.class)
        public void getUserFailure() throws SQLException{
        	...
        }
    }
}
  • JUnit이 하나의 test class를 가져와 test를 수행하는 방식
    1. test class에서 @Test가 붙은 public 이고 void형이며 파라미터가 없는 test method를 모두 찾는다.
    2. test class의 오브젝틀를 하나 만든다.
    3. @Before가 붙은 메소드를 실행한다.
    4. @Test가 붙은 메소드를 하나 호출하고 테스트 결과를 저장해둔다.
    5. @After가 붙은 메소드가 있으면 실행한다.
    6. 나머지 테스트 메소드에 대해 2-5를 반복한다.
    7. 모든 테스트 메소드의 결과를 종합해서 돌려준다.

주의점

  • 각 test method를 실행할 때마다 test class의 오브젝트를 새로 만든다.
  • 한번 만들어진 test class의 오브젝트는 하나의 test method를 사용하고 나면 버려진다.

왜? 클래스마다 하나의 오브젝트만 만드는 편이 더 효율적이지 않나?

  • JUnit 개발자는 각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 보장해주기 위해 매번 새로운 오브젝트를 만들게 했다.

테스트 코드 "일부"에서만 공통적으로 사용하는 코드가 있다면?

  • 일반적인 메소드 추출 방법을 써서 분리하고 직접 호출해서 쓰는 것이 낫다.
  • @Before, @After 는 클래스 전체 메소드에 적용되기 때문에 사용 ㄴㄴ

픽스처(fixture)

  • 테스트를 수행하는데 필요한 정보나 오브젝트(여기선 user1, user2, user3)
  • 일반적으로 여러 테스트에서 반복적으로 사용되므로 @Before 메소드를 이용해 생성해 두면 좋다.
public class UserDaoTest {
		
		private UserDao dao;
        private User user1;
        private User user2;
        private User user3;
        
        @Before -> 
        public void setUp(){
        	...
            this.user1 = new User("gyumee", "저팔계", "spring1");
            this.user2 = new User("leegw700", "이길원", "spring2");
            this.user3 = new User("son", "손오공", "spring3");
        }
        ...
    }
}

코드 링크 https://github.com/SpringFrameworkStudy/LeeJooHyun/tree/main/2week/problemDao/version2.3.5/src


2.4 스프링 테스트 적용

UserDaoTest.java

import org.junit.*;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;
import org.springframework.dao.EmptyResultDataAccessException;

import java.sql.SQLException;

import static org.hamcrest.CoreMatchers.is;


public class UserDaoTest {
    private UserDao dao;
    private User user1;
    private User user2;
    private User user3;
    
    @Before
    public void setup(){
        ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
        this.dao = context.getBean("userDao", UserDao.class);

        this.user1 = new User("아이디133", "이름1", "비밀번호1");
        this.user2 = new User("leegw700", "이름1", "spring2");
        this.user3 = new User("bunjin", "이름이다.", "spring203");
        
    }

    @Test
    public void addAndGet() throws SQLException, ClassNotFoundException {
        dao.deleteAll();
        Assert.assertThat(dao.getCount(), is(0));

        dao.add(user1);
        dao.add(user2);

        User userGet1 = dao.get(user1.getId());
        Assert.assertThat(userGet1.getName(), is(user1.getName()));
        Assert.assertThat(userGet1.getPassword(), is(user1.getPassword()));

        User userGet2 = dao.get(user2.getId());
        Assert.assertThat(userGet2.getName(), is(user2.getName()));
        Assert.assertThat(userGet2.getPassword(), is(user2.getPassword()));
    }

    @Test
    public void count() throws SQLException{
        dao.deleteAll();
        Assert.assertThat(dao.getCount(), is(0));

        dao.add(user1);
        Assert.assertThat(dao.getCount(), is(1));
        dao.add(user2);
        Assert.assertThat(dao.getCount(), is(2));
        dao.add(user3);
        Assert.assertThat(dao.getCount(), is(3));
    }

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

아직 개선해야될 부분이 남아있다.

  • 애플리케이션 컨텍스트 생성이 반복된다는 것이다.
  • @Before 메소드가 테스트 메소드 개수만큼 반복되면서 애플리케이션 컨텍스트 생성이 반복.
  • 애플리케이션 컨텍스트 내의 빈이 할당한 리소스 등을 깔끔하게 정리하지 않을 경우 문제 발생 우려
  • 애플리케이션 컨텍스트처럼 생성에 많은 시간과 자원이 소모되는 경우 테스트 전체가 공유하는 오브젝트를 만들기도 함.

JUnit은 매번 test class의 오브젝트를 새로 만들지만, test class 전체에 걸쳐 딱 1번만 실행되는 @BeforeClass static method를 지원한다.
-> 하지만 스프링이 직접 제공하는 애플리케이션 테스트 지원 기능이 더 편리하다.


2.4.1 테스트를 위한 애플리케이션 컨텍스트 관리

스프링은 JUnit을 이용하는 테스트 컨텍스트 프레임워크를 제공한다.

스프링 테스트 컨텍스트 프레임워크 적용

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext.xml")
public class UserDaoTest {
    @Autowired private ApplicationContext context;

    private UserDao dao;
    private User user1;
    private User user2;
    private User user3;

    @Before
    public void setup(){
        this.dao = context.getBean("userDao", UserDao.class);
        this.user1 = new User("아이디133", "이름1", "비밀번호1");
        this.user2 = new User("leegw700", "이름1", "spring2");
        this.user3 = new User("bunjin", "이름이다.", "spring203");

    }
    @Test
    ...
  • 스프링 컨텍스트 프레임워크의 JUnit 확장 기능이 context변수에 ApplicationContext를 주입해준다.
  • @RunWith(SpringJUnit4ClassRunner.class)
    - @RunWith는 JUnit 프레임워크의 테스트 실행 방법을 확장할 때 사용하는 애노테이션
    - SpringJUnit4ClassRunner(JUnit용 테스트 컨텍스트 프레임워크 확장 클래스)를 지정하면 JUnit이 테스트용 애플리케이션 컨텍스트를 관리한다.
  • @ContextConfiguration(locations = "/applicationContext.xml")
    - 자동으로 만들어줄 애플리케이션 컨텍스트의 설정파일 위치 지정

테스트 메소드의 컨텍스트 공유

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext.xml")
public class UserDaoTest {
    @Autowired private ApplicationContext context;
	...

    @Before
    public void setup(){
        this.dao = context.getBean("userDao", UserDao.class);
		...
        System.out.println(this.context);
        System.out.println(this);

    }

console :

org.springframework.context.support.GenericApplicationContext@2eafffde (똑같음)
UserDaoTest@29b5cd00
org.springframework.context.support.GenericApplicationContext@2eafffde (똑같음)
UserDaoTest@73ee04c8
org.springframework.context.support.GenericApplicationContext@2eafffde (똑같음)
UserDaoTest@2cd2a21f
  • context는 모두 똑같은 객체의 메모리 주소를 가지고 있다.
    - 하나의 애플리케이션이 만들어져 모든 테스트에서 공유된다.
  • UserDaoTest의 오브젝트는 매번 주소값이 다르다.
    - JUnit은 테스트 메소드를 실행할 때마다 새로운 테스트 오브젝트를 만들기 때문이다.

context 인스턴스 변수에 어떻게 애플리케이션 컨텍스트가 들어있는가?

  • 스프링의 JUnit확장기능은 테스트가 실행되기 전 딱 1번 애플리케이션 컨텍스트를 만든다.
  • 테스트 오브젝트가 만들어질 때마다 특별한 방법으로 애플리케이션 컨텍스트를 특정 필드(@Autowired가 있는 필드)에 주입한다.

테스트 클래스의 컨텍스트 공유

스프링 테스트 컨텍스트 프레임워크는 테스트 클래스 안에서 애플리케이션 컨텍스트를 공유해주는 것이 전부가 아니다. 테스트 클래스 사이에서도 애플리케이션 컨텍스트 공유가 가능하다!

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext.xml") <--
public class UserDaoTest {..}

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext.xml") <--
public class GroupDaoTest {..}
  • 두 클래스는 하나의 애플리케이션 컨텍스트가 공유된다.

@Autowired(간단히)

  • @Autowired는 스프링의 DI에 사용되는 애노테이션이다.
  • @Autowired가 붙은 인스턴스 변수가 있으면, 테스트 컨텍스트 프레임워크는 변수타입과 일치하는 컨텍스트 내의 빈을 찾는다.
  • 생성자나 수정자 메소드가 필요없다.
  • 같은 타입의 빈이 2개 이상 있는 경우 변수의 이름과 같은 빈을 확인 후, 주입한다. 같은 이름도 없는 경우 예외가 발생한다.

2.4.2 DI와 테스트

테스트 코드에 의한 DI

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

컨테이너 없는 DI 테스트

profile
천방지축 어리둥절 빙글빙글 돌아가는

1개의 댓글

comment-user-thumbnail
2022년 7월 5일

2022 우정테스트 하기 →→→ https://grandedesafio.com/ko

답글 달기