Home [토비의 스프링] 토비의 스프링 3.1 2장 테스트
Post
Cancel

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

게시글에는 책의 설명과 무관한 내용이 섞여있습니다.

테스트

계속 변화하는 애플리케이션에 대응하는 첫번째 전략이 확장과 변화를 고려한 객체지향 설계와 그것을 효과적으로 담아낼 수 있는 IoC/DI 같은 기술이라면

두번째 전략은 만들어진 코드를 확신할 수 있게 해주고 변화에 유연하게 대처할 수 있는 테스트 기술이다. 스프링으로 개발하면서 테스트를 만들지 않는다면 이는 스프링이 지는 가치의 절반을 포기하는 셈이다.

UserDaoTest 다시보기

테스트의 유용성

  • 처음과 동일한 기능을 수행함을 보장해줄 수 있는 방법은 바로 테스트이다.
  • 테스트란 결국 내가 예상하고 의도했던 대로 코드가 정확히 동작하는지 검증하기 위한 작업
  • 테스트가 정확히 동작하지 않는다는 것은 코드나 설계에 결함이 있음을 알 수 있음. 즉 개발자는 테스트를 통해 코드의 결함을 제거할 수 있음

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

보통 웹 프로그램에서 DAO를 테스트하려면 웹 프로그램을 실행하기 위한 모든 계층의 코드를 전부 작성하고 이를 서버에 배치한뒤 실행시켜서 직접 사용자가 사용하는 방식처럼 테스트 해야한다.

이러한 테스트는 비효율적이다. 첫번째 문제점은 DAO를 테스트하기 위한 부가작업이 너무 많다는 점이고 두번째는 목적은 DAO 테스트지만 다른 계층에서 문제가 발생하면 그 문제를 찾아내야한다는 수고가 필요하다.

목적은 DAO 테스트 였는데 다른 계층의 코드와 컴포넌트, 그리고 서버 설정 상태까지 모두 테스트에 영향을 줄 수 있기 때문에 직접 웹 프로그램을 실행시켜서 테스트 하는 방법은 비효율적이다.

작은 단위의 테스트

  • 테스트하고자 하는 대상이 명확하다면 그 대상에만 집중해서 테스트하는 것이 바람직함
  • 테스트는 가능하면 작은 단위로 쪼개서 집중해서 할 수 있어야 한다.
  • 통제할 수 없는 외부의 리소스에 의존하는 테스트는 단위 테스트라고 보기 어려울 수 있음
  • 외부 리소스에 의존하는 경우 통합 테스트나 인수 테스트를 이용하여 검증할 필요가 있음
  • 많은 단위가 참여하는 테스트는 문제의 원인을 찾는게 어려울 수 있으니 단위 테스트로 먼저 검증을 하고 난 뒤에 단위를 묶어 테스트 하는 것이 바람직

지속적인 개선과 점직전인 개발을 위한 테스트

  • 처음 만든 초난감 DAO 코드를 객체지향적 코드로 발전시키는 과정의 일등 공신은 테스트였음
  • 테스트가 없었다면 다양한 방법을 동원해서 코드를 수정하고 설계를 개선해나가는 과정이 미덥지 않을 수 있고 마음이 불편해지면서 개선을 멈췄을 수도 있음
  • 결론은 DAO 기능을 검증해주는 테스트가 있었기 때문에 점직적으로 코드를 개선할 수 있었던 것. 코드의 결함도 테스트를 통해 빠르게 알아낼 수 있었음
  • 테스트를 이용하면 새로운 기능의 정상동작을 확인할 뿐만 아니라 기존에 만들어뒀던 코드가 새로운 기능을 위해 수정한 코드로 인해 영향을 받지 않는지 확인할 수 있음

main()을 이용한 테스트의 문제점

수동 확인 작업의 번거로움

UserDaoTest는 수행하는 과정과 입력 데이터의 준비를 모두 자동으로 진행하지만 값이 올바른지는 출력된 데이터를 보고 개발자가 판단해야 한다. 즉 테스트 수행은 코드에 의해 자동적으로 진행되지만 결과를 확인하는 일은 사람의 책임이므로 완전히 자동으로 테스트되는 방법이라고 말할 수 없다. 이러한 방법은 검증할 단위가 적을때는 문제가 되지 않지만 테스트가 커질수록 불편하다. 또한 작은 차이는 발견하지 못하고 지나칠 수 있다.

실행 작업의 번거로움

간단히 실행 가능한 main() 메소드라고 해도 매번 실행하는 것은 번거로울 수 있다. 테스트할 클래스가 수백개가 되고 main() 메소드도 그만큼 만들어진다면 전체 기능을 테스트하기 위해 main() 메소드를 수백번 실행해야하는 수고가 필요하다. 그리고 결과를 확인하는 것도 사람의 몫이기 때문에 수백개의 실행결과를 보고 정상수행인지 직접 확인해야한다. 결론은 main() 메소드를 실행하는 방법보다 좀 더 편리하고 체계적으로 테스트를 실행하고 그 결과를 확인하는 방법이 필요하다.

테스트의 효율적인 수행과 결과 관리

main 메소드로는 편리한 수행과 편리한 결과 확인이 어렵다. 때문에 일정한 패턴을 가진 테스트를 만들 수 있고, 많은 테스트를 간단하게 실행시키고, 테스트 결과를 간단하게 확인하고 실패한 곳을 빠르게 찾아갈 수 있는 기능을 갖춘 테스트 지원 도구와 그에 맞는 테스트 작성 방법이 필요하다.

Junit 테스트로 전환

Junit은 프레임워크이다. 프레임워크의 기본 동작원리는 제어의 역전이다. 프레임워크는 개발자가 만든 클래스의 제어 권한을 넘겨받아서 주도적으로 애플리케이션 흐름을 제어한다. Junit도 개발자가 만든 클래스의 오브젝트를 생성하고 실행하는 일을 대신한다. 따라서 프레임워크에서 동작하는 코드는 main() 메소드도 필요없고 오브젝트를 만들어서 실행시키는 코드를 만들 필요도 없다.

테스트 메소드 전환

새로 만들 테스트 메소드는 Junit 프레임워크가 요구하는 두가지 조건을 따라야한다.

  1. 메소드가 public으로 선언되어야 한다.
  2. 테스트 메소드의 리턴값은 void여야 한다.
  3. 메소드에 @Test라는 애노테이션을 붙여줘야 한다.

빌드 툴

개발자 개인별로 IDE에서 Junit 도구를 사용해 테스트를 실행하는 것이 가장 편리하지만 여러 개발자가 만든 테스트 코드를 통합해서 테스트를 수행해야 할 때는 서버에서 모든 코드를 가져와 통합하고 빌드한 뒤애 테스트를 수행하는 것이 좋다. 이떄는 빌드 스크립트를 이용해 Junit 테스트를 실행하고 결과를 메일로 통보받는 방법을 사용하면 된다.

(개인적인 생각) 위와같은 상황 뿐만 아니라 테스트가 너무 많아 실행시간이 오래 소요되는 경우도 빌드 스크립트를 이용하면 좋은 해결 방법이 될 수 있을 것 같다.

테스트 결과의 일관성

  • 테스트의 결과가 외부 상태에 따라 성공하기도 실패하기도 한다는 것은 큰 문제이다. 테스트는 코드가 변경되지 않으면 항상 동일한 결과를 내야한다.
  • 기존 UserDaoTest의 문제는 이전 테스트 때문에 DB에 등록된 중복 데이터가 있을 수 있다는 점이다.
  • 가장 좋은 해결책은 addAndGet() 테스트를 마치고 나면 테스트를 위해 등록된 데이터를 삭제해서 테스트를 수행하기 이전 상태로 만들어주는 것이다.
  • 이러한 방식을 사용하면 테스트를 여러번 반복해서 실행하더라도 항상 동일한 결과를 얻을 수 있다.

포괄적인 테스트

  • 테스트를 만들지 않는 것도 위험한 일이지만 성의없이 테스트를 만드는 바람에 문제가 있는 코드인데도 테스트가 성공하게 만드는 것은 더 위험하다.
  • 특히 한 가지 결과만 검증하고 마는 것은 상당히 위험하다. 이런 테스트는 마치 하루에 두 번은 정확히 맞는다는 시계와 같을 수 있다.
  • 모든 코드의 수정 후에는 그 수정에 영향을 받을만한 테스트를 실행하는 것을 잊지 말아야한다. (사이드 이펙트 체크)
  • 모든 테스트는 실행 순서에 상관없이 독립적으로 항상 동일한 결과를 낼 수 있도록 해야한다.
  • 성공하는 테스트 뿐만 아니라 실패하는 경우의 테스트도 작성해야 한다. 실패하는 테스트를 작성하면 예외적인 상황을 빠뜨리지 않는 꼼꼼한 개발이 가능하다.

테스트 주도 개발(TDD)

테스트 코드를 먼저 작성하고 이후 테스트 코드에서 정의된 기능을 프로덕션 코드로 작성하여 테스트를 성공하게 하는 방식의 개발 방법을 테스트 주도 개발(TDD) 이라고 한다.

테스트에 만들고 싶은 기능의 대한 조건과 행위 결과에 대한 내용이 잘 표현되어있으면 테스트 코드가 기능 정의서의 역할을 대신할 수 있다.

  • TDD는 테스트를 만들고 그 테스트가 성공하도록 하는 코드만 만드는 식으로 진행하기 때문에 테스트를 빼먹지 않고 꼼꼼하게 만들어낼 수 있다.
  • 또한 테스트를 작성하는 시간과 애플리케이션 코드를 작성하는 시간의 간격이 짧아진다.
  • 테스트를 작성하고 이를 성공시키는 코드를 만드는 작업의 주기는 짧게 가져가는 것이 좋다.
  • 사실 개발자는 TDD를 몰라도 이미 테스트가 개발을 이끄는 방식으로 개발을 하고 있다고 생각한다. 머릿속으로 기능 정의서를 그려놓고 정의서에 맞춰서 개발하기 때문이다.
    • 문제는 이렇게 머릿속으로 진행하는 테스트는 제약이 심하고, 오류가 많고, 다시 반복하기 힘들다는 점이다.
    • 차라리 머릿속으로 복잡하게 진행하던 테스트를 실제 코드로 끄집어 내놓으면 이게 바로 TDD가 된다.

(개인적인 생각) 간단하게 성공시키는 코드를 작성하고 테스트를 수행하면서 개선하는 방법이 이미 완성된 프로덕션 코드를 작성하는 것보다 효율적이고 문제점을 빠르게 찾을 수 있다고 생각한다.

Junit의 테스트 독립성 보장

junit은 각 테스트 메소드를 실행할 때마다 테스트 클래스의 인스턴스를 새로 생성한다. 한번 만들어진 테스트 클래스의 오브젝트는 하나의 테스트 메소드를 사용하고나면 버려진다. 테스트 클래스가 @Test 테스트 메소드를 2개 가지고 있다면 테스트 클래스의 인스턴스는 두 번 생성된다. 이러한 방법을 사용하는 이유는 각 테스트의 독립성을 보장하기 위해서이다.

그러나 ApplicationContext 처럼 생성에 많은 시간과 자원이 소모되는 경우에는 테스트 전체가 공유하는 오브젝트를 만들기도 한다. 이때도 테스트는 일관성있는 실행 결과를 보장해야하고, 테스트의 실행 순서과 결과에 영향을 미치지 않아야 한다.

픽스처?

테스트를 수행하는 데 필요한 정보나 오브젝트를 픽스처(fixture)라고 한다. 일반적으로 픽스처는 여러 테스트에서 반복적으로 사용되기 때문에 @Before 메소드를 이용해서 생성해두면 편리하다.

DI와 테스트

구현체가 절대 변하지 않는다고 해도 DI를 사용해야 하는 세가지 이유가 있다.

첫째, 소프트웨어 개발에서 절대 바뀌지 않는 것은 없다. 클래스 대신 인터페이스를 사용하고 new를 이용해 생성하게 하는 대신 DI를 통해 주입받게 하는 것은 아주 단순하고 쉬운 작업이다.

둘째, 구현체가 변경되지 않는다고 해도 인터페이스를 두고 DI를 적용하게 해두면 기존의 서비스 기능 자체를 확장할 수 있기 때문이다.

셋째, 세번째 이유는 테스트 때문이다. 단지 효율적인 테스트를 손쉽개 만들기 위해서라도 DI를 적용해야 한다. 테스트를 잘 활용하려면 자동으로 실행 가능하며 빠르게 동작하도록 테스트 고드를 만들어야 한다. DI는 테스트가 작은 단위의 대상에 대해 독립적으로 만들어지고 실행되게 하는데 중요한 역할을 한다.

DirtiesContext

스프링 테스트 컨텍스트 프레임워크를 적용하면 애플리케이션 컨텍스트는 테스트 중에 딱 하나만 만들어지고 모든 테스트에서 공유한다. 이러한 상황에서 애플리케이션 컨텍스트의 구성이나 상태를 하나의 테스트를 위해 변경하면 나머지 모든 테스트에도 영향을 끼칠 수 있어 위험하다.

이러한 문제는 @DirtiesContext를 사용하면 어느정도 해결될 수 있다. 이 어노테이션이 붙은 테스트 클래스에는 애플리케이션 컨텍스트 공유를 허용하지 않는다. 하지만 클래스 위에 @DirtiesContext를 선언할 경우 해당 클래스의 테스트를 실행할 때마다 애플리케이션 컨텍스트를 새로 만들기 때문에 완벽한 해결 방법이라고 보기엔 어렵다.

1
2
하나의 메소드에서만 컨텍스트 상태를 변경한다면 메소드 레벨에 @DirtiesContext 를 붙여주는 것이 더 낫다. 
해당 메소드의 실행이 끝나고 나면 이후에 진행되는 테스트를 위해 변경된 애플리케이션 컨텍스트는 폐기되고 새로운 애플리케이션 컨텍스트가 만들어진다.

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

  • 테스트에서 사용될 applicationContext를 따로 정의해두면 테스트에 필요한 설정 정보만 사용할 수 있으므로 효율적으로 테스트 할 수 있다.
  • 테스트하기 불편하게 설계된 코드가 좋은 코드일 확률은 매우 낮다.

DI를 이용한 테스트 방법 선택

  • 스프링 컨테이너 없이 테스트할 수 있는 방법을 가장 우선적으로 고려
    • 이 방법이 테스트 수행속도가 가장 빠르다
  • 여러 오브젝트와 복잡한 의존관계를 맺을 경우 스프링의 설정을 이용한 DI 방식의 테스트를 이용하면 편리함
  • 예외적인 의존관계를 강제로 구성해야할 경우 수동 DI 해서 테스트하는 방법을 고려할 수 있음.

학습 테스트로 배우는 스프링

  • 보통 개발자는 자신이 작성한 기능에 대해서만 테스트를 작성하면 됨
  • 그러나 때로는 자신이 만들지않은 프레임워크나 다른 개발팀에서 만들어서 제공한 라이브러리 등에서도 테스트를 작성할 수도 있음
    • 이러한 테스트를 학습 테스트 라고 함
  • 학습 테스트의 목적은 자신이 사용할 API나 프레임워크의 기능을 테스트로 보면서 사용 방법을 익히려는 것
    • 프레임워크나 라이브러리에 대한 검증이 아닌 자신이 얼마나 해당 기술이나 기능을 제대로 이해하고 있는지 확인하는 목적

학습 테스트의 장점

  • 다양한 조건에 따른 기능을 손쉽게 확인해볼 수 있음
  • 학습 테스트 코드를 개발 중에 참고할 수 있음
  • 프레임워크나 제품을 업그레이드할 떄 호환성 검증을 도와줌
  • 테스트 작성에 대한 좋은 훈련이 됨
  • 새로운 기술을 공부하는 과정이 즐거워짐

버그테스트

  • QA팀의 테스트 중 기능에서 오류를 발견했다고 가정했을 때 무턱대고 코드를 수정하는 것보다 버그 테스트를 만들어보는 편이 유용하다.
  • 버그 테스트는 처음에 버그가 원인이되어 실패하게 만들고 이후에 버그 테스트가 성공할 수 있도록 애플리케이션 코드를 수정한다.
    • 이떄 테스트가 성공하면 버그는 수정된 것

장점

  1. 테스트의 완성도를 높여준다.
    • 기존 테스트에서 발견하지 못한 이슈를 버그 테스트를 통해 완성도 향상
  2. 버그의 내용을 명확하게 분석하게 해준다
    • 버그를 테스트로 만들어서 실패하게 하려면 버그의 원인을 명확히 알아야한다.
    • 때문에 버그를 효과적으로 분석 가능
  3. 기술적인 문제를 해결하는 데 도움이 된다.
    • 버그가 있는 것은 알지만 원인을 파악하기 힘들 때 도움이 됨
    • 코드 분석으로 파악하기 힘들경우 동일한 문제가 발생하는 가장 단순한 코드와 그에 대한 버그 테스트를 만들어보면 도움이 됨
1
2
3
4
5
6
7
8
9
- 동등분할
같은 결과를 내는 값의 범위를 구분해서 각 대표 값으로 테스트하는 방법
어떤 작업의 결과 종류가 true, false, 또는 예외발생 세 가지라면 각 결과를 내는 입력 값이나 상황의 조합을 
만들어서 모든 경우에 대한 테스트를 해보는 것이 좋음

- 경계값 분석
에러는 동등분할 범위의 경계에서 주로 발생한다는 특징을 이용해서 경계의 근처에 있는 값을 이용해 테스트하는 방법
보통 숫자의 입력 값인 경우 0이나 그 주변 값 또는 정수의 최대값, 최소값 등으로 테스트 해보면 도움이 될 때가 많음

정리

  • 테스트는 자동화 되어야 하고 빠르게 실행되어야 함
  • main() 테스트보다 junit 프레임워크를 이용한 테스트가 생산적임
  • 테스트를 작성할 때 스프링에 의존하지 않는 테스트 작성 방법을 첫번째로 고려해보기
  • 테스트 작성이 힘든 코드는 잘 설계된 코드라고 할 수 없음
  • 테스트코드 작성 주기와 실제 기능 구현 주기는 짧게 가져가야 함
  • 성의없이 테스트를 작성하지 말 것 (문제가 있는 코드인데 테스트를 통과하게 되면 곤란)
  • 오류가 발견될 경우 그에 대한 버그 테스트를 만들어두면 유용
  • 학습 테스트를 통해 프레임워크나 라이브러리에 대한 이해도를 검증할 수 있음
  • 스프링을 사용하지 않더라도 DI는 유연한 테스트를 가능하게 해줌
  • 기능을 먼저 구현하고 테스트를 작성하는 것이 아닌, 테스트를 먼저 작성하고 기능을 구현하는 TDD 방식을 고려해볼 수도 있음.
This post is licensed under CC BY 4.0 by the author.

[토비의 스프링] 토비의 스프링 3.1 1장 오브젝트와 의존관계

HTTP/2