Group Study (2024-2025)/Spring 심화

[ Spring 심화 ] 2주차 - 테스트

y00nsj 2024. 10. 10. 20:33

1. 테스트

UI 테스트의 문제점

  • 하나의 테스트를 수행하는 데 참여하는 클래스와 코드가 너무 많음
  • → 다른 계층의 코드/컴포넌트/서버 설정 등 많은 요소가 테스트에 영향

Unit Test

관심사의 분리

테스트는 가능한 한 최소 단위로 쪼개서 집중해야 함

단위

  • 한 관심에 집중해 효율적으로 테스트할 만한 범위
  • 작을수록 용이
  • 단위를 넘어서는 코드는 신경 X 참여 X
  • 확인의 대상과 조건이 간단하고 명확할수록 좋음

외부 리소스 의존 금지

  • 리소스(DB, ..)의 상태를 테스트가 관장하면 가능
  • 의존 : 리소스의 상태가 매번 변화, 테스트를 위해 DB를 특정 상태로 만들 수 없는 상태

선 단위테스트 → 후 플로우 테스트

  • 단위별로 검증해 오류 잡은 후 긴 테스트를 하면 디버깅이 용이할 것

자동수행 테스트 코드

  • 테스트의 전 과정을 자동으로 진행 ⇒ 자주 반복 가능!
  • 테스트용 클래스는 별도로 만들기

지속적 개선과 점진적 개발

2. UserDaoTest 개선

UserDaoTest의 문제점

수동 확인 작업

  • 콘솔은 값만 출력 ⇒ 일치 여부 확인은 사람의 책임

실행 작업의 번거로움

  • main() 메소드 이용하는 방법보다 좀 더 편리하고 체계적인 대안 필요

Fix: 테스트 검증의 자동화

실패의 종류

  • 테스트 에러 : 테스트 진행 중 에러 발생
  • 테스트 실패 : 에러는 없지만 결과가 기대한 바와 불일치
 if (!user.getName().equals(user2.getName())) {
     System.out.println("테스트 실패 : name 불일치");
}

Comprehensive Test

만들어진 코드의 기능을 모두 점검

좋은 테스트

  • 빠르게 실행 가능
  • 코드가 스스로 테스트 수행 + 기대하는 결과에 대한 확인 진행

Fix: 테스트의 효율적인 실행과 결과 관리

테스트 지원 도구 + 작성법

  • 일정한 패턴 가진 테스트 생성
  • 테스트 간단히 실행
  • 결과 종합해서 열람
  • 실패한 곳을 빠르게 찾음

JUnit : 자바 테스팅 프레임워크

  • 프레임워크 : 개발자가 만든 클래스에 대해 IoC 수행 → 주도적으로 app 흐름 제어
    • 오브젝트 생성, 실행
    • main() 불필요(일반 메소드 사용), 오브젝트 생성 후 실행하는 코드 불필요
  • 요구 조건
    • 메소드 public 으로 선언
    • 메소드에 @Test 어노테이션 붙이기
    • 리턴 값 void
    • 매개변수 X
  • 실패 여부 검증 기능
  • assertThat(user2.getName(), is(user.getName()));
  • 성공 여부 직접 알려줌
  • 실행
    • 어디에든 main 메소드 추가
    • JUnitCore 클래스의 main메소드 호출
      • 인자 : @Test 메소드 가진 클래스명
  • inport org.junit.runner.JUnitCore; ... public static void main(String[] args) { JUnitCore.main("springbook.user.dao.UserDaoTest"); }
  • 결과
    • 성공 시: 실행 소요 시간, 테스트 결과, 실행한 테스트 메소드 갯수
    • 실패 시
      • OK 대신 FAILURES!!! 출력
      • 실패한 테스트 개수
      • 기대한 값, 실제 얻은 값
      • 호출 스택
      • assertThat() 실행한 경우 : AssertionError

3. JUnit

실행 방법

  1. JUnitCore 이용: main() 메소드 필요
  2. Run As JUnit 옵션 클릭 : 개별 클래스 단위 / 패키지 단위 / 프로젝트 단위 실행 가능

빌드 툴

  • Maven : 빌드 툴에서 제공하는 JUnit 플러그인/태스크 이용 → HTML or TXT로 결과 제공
  • 여러 명이 작성한 코드 통합 수행 시
    1. 서버에서 모든 코드 가져와 통합 및 빌드 : 빌드 스크립트 이용
    2. JUnit 테스트 수행
    3. 결과 메일로 통보받기

결과의 일관성

코드에 변경사항이 없다면 테스트는 항상 동일한 결과를 내야 함!

  • 여러 번 돌렸을 때 성공하기도 하고 실패하기도 하면 좋은 테스트라 볼 수 없음

테스트 전 테스트 실행에 문제 되지 않는 상태 만들기

  • DB 잔류 데이터 등 외부 환경에 영향 받지 말아야 함

실행 순서를 바꿔도 동일한 결과가 보장되어야 함

  • 독립적으로 항상 동일한 결과 도출해야

포괄 테스트

  • JUnit 은 한 클래스 안에 여러 개 테스트 메소드 들어가도록 허용
  • 코드 수정 후에는 그 수정에 영향 받을 만한 테스트 실행해야 함

예외 테스트

네거티브 테스트를 먼저 만들기

  • 부정적인 케이스를 먼저 만드는 습관 들여야 예외적인 상황 누락 방지 가능

TDD

  1. 만들어야 할 기능 결정
  2. 테스트 개발
  3. 추가하고 싶은 기능을 코드로 표현

테스트 = 기능 정의서

  • 조건 : 어떤 조건을 가지고
  • 행위 : 무엇을 할 때
  • 결과 : 어떤 결과가 나온다

테스트 주도 개발

  1. 테스트 코드 먼저 작성 : 만들고자 하는 기능의 내용 + 검증 코드 포함
  2. 테스트 성공하게 해주는 코드 작성
  • 테스트 작성 - 성공 코드 작성 주기를 가능한 한 짧게
    • 단위 테스트 방식을 통해 작은 단위로 빠르고 간편하게 개발 진행

장점

  • 코드에 대한 피드백 신속
  • 코드 개발 착수 시 머릿속에서 일어나는 프로세스를 그대로 코드에 반영
    • 머릿속으로 구상할 때보다 더 자유로운 조건에서 오류를 제어하고, 간편하게 반복 가능
  • 테스트 코드 작성 시간이 적고 작성 방법 또한 간편 → 개발 속도 향상

테스트 코드 리팩토링

  • 결과가 일정하게 유지되는 한 얼마든지 리팩토링 가능!

중복 코드 추출

  • JUnit : 반복되는 준비 작업을 별도의 메소드에 넣고, 이를 매번 테스트 메소드 실행 전 먼저 실행시켜줌
  • 일부 테스트 메소드만 공통적으로 사용하는 코드
    • 메소드 추출 기법을 통해 분리한 후 테스트 메소드에서 직접 호출
    • 공통적인 특징 지닌 테스트 메소드들 모아 별도의 테스트 클래스로 분리

JUnit의 테스트 메소드 실행 흐름

  1. 테스트 클래스에서 조건을 만족하는 테스트 메소드 모두 찾기
    • @Test 어노테이션
    • public 메소드
    • void
    • 매개변수 X
  2. 테스트 클래스 오브젝트 생성
    • 하나의 테스트 메소드 사용 후 버려짐
    • ∵ 독립 실행 보장
  3. @Before 붙은 메소드 존재 시 실행
  4. @Test 붙은 메소드 한 개 실행 → 결과 저장
  5. @After 붙은 메소드 존재 시 실행
  6. 나머지 테스트 메소드에 대해 2~5번 반복
  7. 모든 테스트의 결과 종합해 보고

Fixture

테스트를 수행하는 데 필요한 정보나 오브젝트
e.g. UserDaoTest의 dao, add() 메소드에 전달하는 User 오브젝트들

  • @Before 메소드 이용해 생성해두면 편리
  • 매번 새로운 테스트 오브젝트가 생성되므로 인스턴스 변수 선언 시 바로 초기화해도 상관 없으나,
    픽스처 생성 로직을 모아두는 것이 더 깔끔

4. 스프링 테스트 적용

테스트 전체가 공유하는 오브젝트

  • @Before 메소드가 테스트 메소드 개수만큼 반복 ⇒ 어플리케이션 컨텍스트도 그만큼 생성
    • 애플리케이션 컨텍스트 생성 시 모든 싱글톤 빈 오브젝트 초기화
    • 생성 시 자체적으로 초기화 작업까지 하는 빈도 있음
    • 독자적으로 많은 리소스 할당하거나 독립적 스레드 띄우는 빈도 존재
    • 테스트 마칠 때마다 컨텍스트 내 빈이 할당한 리소스 정리 필요
  • ⇒ 빈이 많아지고 복잡해질 경우 생성 시간이 많이 소모됨!
  • 생성에 많은 시간/자원 소모되는 오브젝트는 테스트 전체가 공유하도록 생성
    • 최초 1회만 만들고 여러 테스트가 공유해서 사용
    • 애플리케이션 컨텍스트 : 초기화 후 내부 상태가 바뀌는 일이 거의 없음
    • 빈 : 싱글톤이므로 상태 X
  • 스태틱 필드에 애플리케이션 컨텍스트 저장
    • 매번 테스트 클래스 오브젝트 새로 만드는 문제 해결 (↔ 오브젝트 레벨 저장)
    • 💡@BeforeClass : JUnit 제공
    • 클래스전체에 걸쳐 딱 한 번 실행
    • 스프링에서 제공하는 애플리케이션 컨텍스트 테스트 지원 기능이 더 편리

테스트를 위한 애플리케이션 컨텍스트 관리: 테스트 컨텍스트 프레임워크

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

    @Before
    public void setUp() {
        this.dao = this.context.getBean("userDao", UserDao.class);
    }
}

RunWith : 한 클래스 內 테스트 메소드의 컨텍스트 공유

  • 스프링의 JUnit 확장 기능 : 일종의 DI 원리
    1. 테스트 실행 전 단 1회 애플리케이션 컨텍스트 생성
    2. 테스트 오브젝트 생성 때마다, 애플리케이션 컨텍스트 자신을 테스트 오브젝트의 특정 필드에 주입
  • 하나의 애플리케이션 컨텍스트가 만들어진 뒤, 모든 테스트 메소드에서 해당 컨텍스트를 공유해 사용
    • 첫 번째 테스트의 실행 시간이 가장 긺

↔ JUnit : 테스트 메소드 실행할 때마다 새로운 테스트 오브젝트 생성

@ContextConfiguration : 테스트 클래스의 컨텍스트 공유

여러 테스트 클래스가 모두 같은 설정파일 가진 애플리케이션 컨텍스트 사용 시
→ 클래스 간 애플리케이션 컨텍스트 공유!

@Autowired

스프링 DI에 사용

  • 인스턴스 변수의 타입과 일치하는 빈을 컨텍스트에서 찾음
    ⇒ 존재 시 인스턴스 변수에 주입
  • 생성자/수정자 메소드 없이도 주입 가능
  • 타입에 의한 오토 와이어링 : 별도 DI 설정 없이 필드의 타입정보 이용해 빈을 자동으로 가져옴
  • ApplicationContext 타입 빈
    • 스프링 애플리케이션 컨텍스트는 초기화 시 자기 자신도 빈으로 등록
      ⇒ 애플리케이션 컨텍스트에 해당 타입의 빈이 존재하게 됨
    • ∴ xml 등에 정의된 빈이 아님에도 DI 가능!
  • 사용 시, DI받은 컨텍스트에서 다시 DL 방식으로 빈을 가져오지 않아도 됨!
    애플리케이션 컨텍스트가 갖고 있는 빈을 직접 DI
  • 같은 타입 빈이 2개 이상인 경우 : 변수명과 동명의 빈이 있는지 확인 → 못 찾을 시 예외 발생
  • 인터페이스 타입 vs 클래스 타입
    • 가능한 한 인터페이스 사용해 애플리케이션 코드와 느슨하게 연결!
      • 구현 클래스를 변경해도 테스트 코드는 유지할 수 있기 때문
    • 단순히 인터페이스에 정의된 메소드 사용하고 싶을 때 - 인터페이스 타입
    • 구현 클래스 타입의 오브젝트 자체에 관심이 있을 때 - 클래스 타입
      • e.g. XML에서 프로퍼티로 설정한 DB 연결 정보 확인, 클래스의 메소드를 직접 이용해 테스트, ..

테스트에 DI 이용

인터페이스로 DI 적용해야 하는 이유

  • 절대로 바뀌지 않는 개발은 없음
  • 다른 차원의 서비스 기능 도입 가능
    • 새로운 기능 추가 시 기존 코드 수정 X, 삭제 시에도 설정 파일만 간단히 수정하면 됨
    • AOP
  • 효율적인 테스트의 손쉬운 제작
    • 작은 단위로 테스트 생성 및 실행을 가능케 함

DI 적용된 코드는 테스트도 다양한 방식 활용 가능할만큼 유연함

테스트 중 DAO가 사용할 DataSource 오브젝트를 별도로 지정하기
(운영용 DB 사용 방지를 위함)

테스트 코드에 의한 DI

Sol 1) 테스트 코드에서 빈 오브젝트에 수동 DI

  • 장점 : XML 수정 없이, 테스트 코드 통해 오브젝트 관계 재구성
  • XML의 설정 정보를 따라 구성한 오브젝트를 가져와 의존관계 수동(강제) 변경
    • 나머지 모든 테스트 수행하는 동안 변경된 컨텍스트를 사용해야 함
      • @DirtiesContext 이용으로 해결
    • 테스트 내에서 애플리케이션 컨텍스트의 구성이나 상태를 변경하지 않는다는 원칙 위배

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

Sol 2) 테스트 전용 설정 파일 별도 제작 → @ContextConfiguration 의 location 엘리먼트 변경

  • 장점 : 수동 DI 코드 불필요, @DirtiesContext 불필요
  • 애플리케이션 컨텍스트 1개만 생성 후 모든 테스트에서 공유 가능

컨테이너 없는 DI 테스트

Sol 3) 테스트 코드에서 직접 오브젝트 생성 후 DI

public class UserDaoTest {
    UserDao dao;
    . . .

    @Before
    public void setUp() {
        . . .
        dao = new UserDao();
        DataSource dataSource = new SingleconnectionDataSource(
            "jdbc:mysql://localhost/testdb", "spring", "book", true);
        dao.setDataSource(dataSource);
    }
}
  • 스프링 테스트 컨텍스트 프레임워크, Autowired X
    @Before 메소드에서 직접 UserDao 오브젝트 생성 → 테스트용 DataSource 오브젝트 생성 → 직접 DI
  • 장점 : 컨텍스트 미사용 ⇒ 코드 단순, 테스트 시간 절약
  • 단점 : DataSource 직접 생성하는 번거로움, JUnit에 의해 매번 새로운 오브젝트 생성
  • 컨테이너/프레임워크는 DI의 편리한 적용을 도우기만 할 뿐, 가능하게 해주는 것은 아님!

invasive 기술: 기술 적용 시 코드에 기술 관련 API 등장하거나, 특정 인터페이스/클래스 사용 강제

→ 코드가 해당 기술에 종속

non-invasive 기술 : 코드에 영향 X → 기술 독립적인 순수한 코드 유지 가능

e.g. 스프링

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

  1. 스프링 컨테이너 없이 테스트할 수 있는 방법을 최우선 고려
    • 장점 : 수행 속도 신속, 테스트 간결
    • 테스트 위해 필요한 오브젝트 생성 & 초기화 단순할 경우
  2. 스프링의 설정을 이용한 DI
    • 여러 오브젝트와 복잡한 의존 관계를 갖고 있는 오브젝트 테스트할 경우
    • 테스트에서 컨텍스트 사용 시 테스트 전용 XML 별도 생성
      • 개발 환경(개발자 테스트 시 이 환경) / 테스트 환경 / 운영 환경
  3. 수동 DI
    • 예외적인 의존 관계를 강제로 구성해서 테스트해야 할 경우

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

Learning Test : 자신이 만들지 않은 프레임워크나 다른 팀에서 제공한 라이브러리에 대한 테스트 작성

  • 목적 : 자신이 사용할 API / 프레임워크를 테스트로 보면서 사용법 습득
    • 기능 검증이 목적이 아님!
    • 기능/기술 이해도, 사용법 숙지 여부 검토가 목적
    • 테스트 코드를 작성하며 신속정확하게 사용법 숙지 가능

장점

  • 자동 테스트 통해 다양한 조건에 따른 기능 확인 용이
  • 만든 코드를 개발 중에도 참고 가능
  • 제품 업그레이드 시 호환성 검증 기능
  • 테스트 작성 훈련

Bug Test

코드에 오류가 있을 때 그 오류를 가장 잘 드러내주는 테스트

  • 코드를 뒤지며 트러블슈팅하는 것보다 유용

방법

  1. 테스트가 실패하도록 코드 생성 : 버그가 원인이 되도록 함
  2. 버그 테스트가 성공하도록 애플리케이션 코드 수정

필요성

  • 테스트의 완성도 보완, 유사 문제 발생 시 추적 용이
  • 버그 내용 명확히 분석
  • 기술적 문제 해결 시 도움

Equivalence Partitioning

같은 결과를 내는 값의 범위를 구분 → 각 대표값으로 테스트 진행

e.g. {true, false, exception} : 각 결과를 내는 입력값 or 상황조합의 범위 만들기

Boundary Value Analysis

경계의 근사값 이용해 테스트

  • 에러는 동등분할 범위의 경계에서 주로 발생한다는 특징 반영