[토비의 스프링] 2장 테스트

susu·2022년 10월 10일
0
post-thumbnail

<토비의 스프링 3.1 vol.1>을 읽고 공부한 내용을 개인적으로 정리한 글입니다.

📌 테스트?

  • 내가 예상하고 의도했던 대로 코드가 정확히 동작하는 지를 확인.
  • 만들어진 코드를 확신할 수 있게 해주고, 변화에 유연하게 대처할 수 있다는 자신감을 준다.
  • 코드의 결함을 탐지해 수정하는 (=디버깅) 과정을 통해 코드의 무결성을 확보할 수 있다.

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

DAO 테스트 과정을 생각해보자.

  1. 필요한 모든 웹 계층을 대강이라도 만들어두고,
  2. 웹 화면을 띄워 폼에 값을 입력한 뒤 등록 버튼을 누른다.
  3. 폼에 입력된 값은 파싱되어 User 객체가 되어 UserDao를 호출한다.

이처럼 단순 웹 기능과는 달리 DAO 테스트를 위해서는 DAO뿐만 아니라 서비스 클래스, 컨트롤러, 뷰 계층 등
모든 레이어의 기능을 다 만들고 나서야 테스트가 가능하다는 문제가 있다.

이 경우 테스트하고 싶은 기능은 DAO에 관한 기능 뿐인데,
DAO를 테스트하기 위해 다른 계층들을 깔아주는 과정에서 생긴 오류들이 복합적으로 얽혀버리면
문제의 원인을 찾기 힘들어 정작 DAO에 대한 테스트에 집중하기 어려울 가능성이 크다.

→ 테스트하고자 하는 대상이 명확하다면, 그 대상에만 집중해서 테스트하자!

단위 테스트

앞에서 정리한 관심사의 분리를 떠올려보면 된다.
테스트를 통해 확인하고 싶은 한 가지 관심사를 정해 접근해보자는 것.

앞에서 만든 UserDao의 기능을 확인해보고 싶다.
그렇다면 지금부터 우리의 관심사는 UserDao 단 하나다.
UserDao를 테스트하기 위한 테스트 코드를 UserDaoTest라 하자.
이때, UserDaoTestUserDao에 대한 단위 테스트라고 한다.

  • 일반적으로 단위는 작을수록 좋다. (외부의 리소스에 의존하지 않음)
  • 확인의 대상이 간단하고 명확할수록 좋다.

자동수행 테스트 코드

매 테스트마다 개발자가 매번 웹 화면에 폼을 띄우고 값을 입력할 필요 없이,
테스트가 자동으로 수행되도록 코드를 짜는 것이 중요하다.
또한 테스트 코드를 메인 애플리케이션에 포함시키는 것보다 별도로 테스트 클래스를 만드는 것이 좋다.

테스트 검증 자동화

테스트 코드의 도입으로 테스트 과정이 한결 편해졌지만,
테스트 결과 확인을 매번 수동으로 해야 하는 번거로움을 해결하기 위해 검증 과정을 자동화할 필요가 있다.

개발 과정에서, 또는 유지보수 과정에서 기존 애플리케이션 코드를 수정해야 할 때,
빠르게 실행 가능하고 스스로 테스트 수행과 기대하는 결과에 대한 확인까지 해주는 테스팅 프레임워크의 도움을 받을 수 있다.

xUnit !

📌 JUnit

🚨 테스트 역시 자바 코드이므로 main() 함수에서 한 번은 호출되어야 하지만,
JUnit을 사용하면 알아서 내부에서 적절한 main 함수를 생성해 실행시켜준다.
테스트 객체는 public으로 선언되어야 하며 @Test 어노테이션을 붙여주어야 한다.
test 패키지 하에 테스트를 수행할 내용에 대해서만 테스트 코드를 작성해주고 실행해보면 된다.

JUnit 특징

  • 텍스트와 GUI 기반 : JUnit 실행시 테스트 결과를 나타내는 View가 제공된다.
  • 단정문(assert)을 이용해 테스트 케이스의 수행 결과를 판별한다.
  • 결과는 성공시 녹색, 실패시 붉은색으로 표시된다.
  • 테스트 결과를 확인하는 것 외에도 최적화된 코드를 유추해내는 기능도 제공하고 있다.

Assert() 메소드

🗣 책에는 없는 내용이지만, 같이 보면 좋을 것 같아 추가했습니다.
JUnit API docs에서 더 자세한 내용을 확인할 수 있습니다.

JUnit에서 가장 많이 사용되는 단정(assert) 메소드에 대해 알아보도록 하겠다.
단정 메소드는 테스트 케이스의 수행 결과를 판별하는 메소드이다.

  • `assertThat(T actual, Matcher<? super T> matcher)`
    • actual 자리에 검증을 원하는 결과 객체를 넣고, 비교 로직(Matcher)을 주입받아 검증을 수행
    • cf. JUnit에서의 assertThat보다 Assertj의 assertThat이 사용하기 더 편하다고 한다.
  • **assertEquals**(x, y)
    • 객체 x와 y가 일치함을 확인
    • x (예상값) 와 y (실제값) 가 같으면 테스트 통과
  • **assetArrayEquals**(a, b)
    • 배열 a와 b가 일치함을 확인
  • **assertFalse**(x), **assertTrue**(x)
    • x가 False인지 True인지 확인
  • **assertTrue**(메세지, 조건문)
    • 조건문이 True면 메세지를 표시
  • **assertNull**(x), **assertNotNull**(x)
    • 객체 x가 null인지, null이 아닌지를 확인
  • **assertSame**(ox, oy), **assertNotSame**(ox, oy)
    • 객체 ox와 객체 oy가 같은 객체임을 확인
    • ox와 oy가 같은 객체를 참조하고 있으면 테스트 통과
    • assertEquals()는 두 객체의 이 같은지를 확인하고, assertSame()은 두 객체의 레퍼런스가 동일한지를 확인
  • **assertfail**()
    • 테스트를 바로 실패처리

예외처리 테스트

1이라는 값을 가질 것으로 기대되는 객체 A가 있다고 생각해보자.
만약 A가 1이 아닌 다른 값을 가지는 경우에 대해 어떻게 처리할 것인가?

이에 대해 두 가지 방법을 생각해볼 수 있다.

  • null과 같은 특별한 값을 리턴
  • 잘못된 값이라는 의미의 예외를 throw → 스프링이 가지고 있는 예외를 사용하는 방법과, 직접 예외 클래스를 커스텀하는 방법이 있다.

여기서는 예외를 던지는 방식으로 문제를 해결한다고 가정하겠다.
이떄, 예외의 발생 여부는 assertThat()과 같은 단정 메소드로 확인할 수 없다.
이러한 상황에 대비해 JUnit은 예외조건 테스트 기능을 제공한다.

get()이라는 메소드의 예외상황을 테스트하기 위한 메소드를 추가한다.
존재하지 않는 id로 user를 검색하는 상황이며,
user가 존재하지 않아 CustomException이 던져져야 테스트가 성공한다.

@Test(expected=CustomException.class) // 테스트 중에 발생할 것으로 기대되는 예외 클래스 지정
public void getUserFailure() throws SQLException {
	ApplicationContext context = new GenerivXmlApplicationContext("applicationContext.xml");

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

/* ... */

dao.get("존재하지 않는 id"); // 여기서 CustomException 예외가 발생해야 한다.
													// 예외가 발생하지 않으면 테스트는 실패한다.

}

이때, get 메소드는 존재하지 않는 id로 user를 검색하는 상황에 대해 CustomException을 발생시키도록 설계해야 한다.

그렇지 않으면 테스트가 실패한다.

🚨 네거티브 테스트를 먼저 수행하자

개발자가 테스트를 직접 만들 때 자주 하는 실수 중 하나가 성공하는 테스트만 골라서 만드는 것이다.
예외적인 상황이나 문제가 될 만한 케이스를 무의식적으로 피해가며 테스트를 만들게 되면,
개발자의 PC를 벗어났을 때 문제를 일으킬 수 있다.

따라서 테스트를 작성할 때, 부정적인 케이스를 먼저 만드는 습관을 들이자.

테스트 주도 개발 (TDD)

테스트 코드를 먼저 만들고,
테스트를 성공하게 해주는 코드를 작성하는 방식의 개발 방법.

만들고자 하는 기능의 내용을 담고 있으면서, 만들어진 코드를 검증도 해줄 수 있도록.
TDD의 기본 원칙은 “실패한 테스트를 성공시키기 이한 목적이 아닌 코드는 만들지 않는다” 이다.

TDD의 장점으로는

  • 테스트를 빼먹지 않고 꼼꼼하게 만들어낼 수 있다.
  • 테스트를 작성하는 시간과 애플리케이션 코드를 작성하는 시간의 간격이 짧아진다. → 오류를 빨리 발견할 수 있다.
  • 성공한 테스트에 대한 코드를 짜기 때문에, 작성한 코드에 대해 확신을 가질 수 있다.

@Before

JUnit에서 제공하는 어노테이션.
@Test 어노테이션이 붙은 메소드가 실행되기 전에 먼저 실행돼야 하는 메소드를 정의한다.
이 과정을 잘 이해하기 위해선 JUnit이 하나의 테스트 클래스를 수행하는 흐름을 이해해야 한다.

  1. 테스트 클래스에서 @Test가 붙었고 / public void형이며 / 파라미터가 없는 테스트 메소드를 모두 찾는다.
  2. 테스트 클래스 객체를 하나 만든다.
    → 모든 테스트가 독립적으로 이루어질 수 있도록.
  3. @Before가 붙은 메소드가 있으면 먼저 실행한다.
  4. @Test가 붙은 메소드를 하나 호출하고 테스트 결과를 저장해둔다.
  5. @After가 붙은 메소드가 있으면 실행한다.
  6. 나머지 테스트 메소드에 대해 2~5번을 반복한다.
  7. 모든 테스트의 결과를 종합해 돌려준다.

📌 스프링 테스트 적용

애플리케이션 컨텍스트가 만들어질 때는 모든 싱글톤 빈 오브젝트가 초기화된다.
이 과정에서 초기화 자체가 오랜 시간을 소요하기도 하고,
어떤 빈은 독자적으로 많은 리소스를 할당하거나 독립적인 스레드를 띄우기도 한다.

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

스프링은 JUnit을 이용하는 테스트 컨텍스트 프레임워크를 제공한다.
테스트 컨텍스트의 지원을 받으면 어노테이션을 붙여주는 것만으로도 테스트에서 필요로 하는 어플리케이션 컨텍스트를 만들어서 모든 테스트가 공유하게 할 수 있다.

@RunWith(SpringJUnit4ClassRunner.class)

스프링의 테스트 컨텍스트 프레임워크의 JUnit 확장기능 지정
테스트를 진행하는 중에 테스트가 사용할 애플리케이션 컨택스트를 자동으로 만들고 관리하는 작업을 진행

@ContextConfiguration(locations=”/applicationContext.xml”)

테스트 컨텍스트가 자동으로 만들어줄 애플리케이션 컨텍스트의 위치를 지정

@Autowired

변수에 할당 가능한 타입을 가진 빈을 자동으로 찾아 주입받음

테스트를 위한 DI 설정

테스트 과정에서 테스트에 의해 실제 운영용 DataSource가 변경되는 것은 문제를 야기할 수 있다.
따라서 DAO가 테스트에서만 다른 Datasource를 사용하게 하는 방법에 대해 생각해보자.

  1. 테스트 코드에서 빈 객체에 수동으로 다른 DataSource를 DI (@DirtiesContext)
    → 애플리케이션 컨텍스트를 매번 새로 만들어야 한다는 부담감이 있다.

  2. 아예 테스트에서 사용될 DataSource 클래스를 빈으로 정의해둔 테스트 전용 설정파일을 따로 만들어두기
    → 번거롭게 수동으로 DI하지 않아도 되고, @DirtiesContext 메소드도 필요없다.

    <bean id="dataSource">
    			class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
    	...
    	<property name="url" value="jdbc:mysql//localhost/testdb" />
    	...
    </bean>
    // Test Class
    @RunWith(SpringJUnit4ClassRunner.class)
    @CintextConfiguration(locations="/test-applicationContext.xml") // 설정파일 따로 적용하기
    public class UserDaoTest {
    	...
    }
  1. 컨테이너 없는 DI 테스트 해보기
    → 아예 스프링 컨테이너를 사용하지 않고 테스트를 만드는 방식이다.

    public class UserDaoTest {
    	
    	UserDao dao; // @Autowired 를 사용하지 않았다.
    	...
    
    	@Before
    	public void setUp() {
    		...
    		dao = new UserDao();
    		DataSource dataSource = new SingleConnectionDataSource("jdbc:mysql://localhost/testdb", "spring", "book", true);
    		dao.setDataSource(dataSource);
    		// 위 세 줄의 코드에서 객체 생성, 관계 설정 등을 모두 직접 해준다.
    	}
    }

    애플리케이션 컨텍스트를 아예 사용하지 않으니 코드는 더 단순해진다.

위의 예제를 통해, 컨테이너가 DI를 가능하게 해주는 역할은 아니라는 것을 알 수 있다.
즉, DI 컨테이너는 DI를 편하게 적용하도록 도움을 줄 뿐, 컨테이너가 DI를 가능하게 하지는 않는다.

스프링은 비침투적인 기술의 대표적 예이다.
침투적 기술이란 기술을 적용했을 때 코드에 기술 관련 API가 등장하거나, 특정 인터페이스나 클래스를 사용하도록 강제하는 기술을 말한다. 침투적 기술을 사용하면 애플리케이션 코드가 해당 기술에 종속되는 결과를 가져온다.
반면 비침투적인 기술은 기술이 애플리케이션 코드에 영향을 주지 않는다. 기술과 코드 사이에 종속성을 가지지 않는다. 따라서 위의 3번과 같은 DI 테스트 방식이 가능했던 것이다.

보통 실제 개발 환경에서는 2번의 방법을 많이 사용하고,
때에 따라 예외적인 의존관계를 강제로 구성해서 테스트해야 할 경우 1번의 방식을 사용한다.

📌 학습 테스트

개발자가 자신이 만든 코드가 아닌 다른 사람이 만든 코드와 기능에 대한 테스트를 작성하는 과정.
학습 테스트의 목적은 자신이 사용할 API나 프레임워크의 기능을 테스트로 보면서 사용 방법을 익히는 것이다.
학습 테스트를 작성하는 과정은 다음과 같은 장점을 가진다.

  • 다양한 조건에 따른 기능을 손쉽게 확인해볼 수 있다.

  • 학습 테스트 코드를 개발 중에 참고할 수 있다.

  • 프레임워크나 제품을 업그레이드할 때 호환성 검증을 도와준다.

    🗣 버전업에 따라 Depreciated 된 어노테이션이나 메소드들을 어떻게 사용해야 할지 늘 답답했는데 학습 테스트 과정을 거치면 훨씬 편하게 사용할 수 있을 것 같습니다.

  • 테스트 작성에 대한 좋은 훈련이 된다.

  • 동작하는 코드를 보며 새로운 기술을 공부하는 과정이 즐거워진다.

버그 테스트

코드에 오류가 있을 때 그 오류를 가장 잘 드러내줄 수 있는 테스트.
코드에서 오류가 발견되었을 때, 무턱대고 코드를 수정하기보단 먼저 버그 테스트를 만드는 편이 좋다.

  • 버그 테스트는 일단 실패하도록 만들어야 한다.
    버그가 원인이 되어 테스트가 실패하는 것이기에,
    코드 수정 후 테스트가 성공하면 버그가 해결된 것을 확인할 수 있다.
  • 버그 테스트는 테스트의 완성도를 높여준다.
  • 버그 테스트는 버그의 내용을 명확하게 분석하게 해준다.
  • 기술적인 문제를 해결하는 데에도 도움이 된다.
    → 외부 커뮤니티나 전문가의 도움을 받을 때에도 유용하게 사용할 수 있다.

동등분할
같은 결과를 내는 값의 범위를 구분해서 각 대표 값으로 테스트를 하는 방법.

경계값 분석
에러는 동등분할 범위의 경계에서 주로 발생한다는 특징을 이용한 테스트 방법.
경계의 근처값을 이용해 테스트하는 방법.

profile
블로그 이사했습니다 ✈ https://jennairlines.tistory.com

0개의 댓글