Today I Learned_230505

5 분 소요

토비의 스프링 3.1

2장. 테스트

스링이 개발자에게 제공하는 가장 중요한 가치

→ 객체지향과 테스트

DI는 스프링의 핵심으로, 오브젝트의 설계, 생성, 관계, 사용에 관한 기술

스프링은 IoC/DI를 이용해 객체지향 프로그래밍 언어의 근본과 가치를 개발자가 손쉽게 적용하고 사용할 수 있게 도와주는 기술

또한, 복잡한 엔터프라이즈 애플리케이션을 효과적으로 개발하기 위한 기술

→ 이걸 개발하기 위해서는 객체지향 필요

→ 테스트도 필요

테스트 기술 : 만들어진 코드를 확신할 수 있게 하고, 변화에 유연하게 대처할 수 있다.

테스트의 유용성

테스트는 내가 예상하고 의도했던 대로 코드가 정확히 동작하는지를 확인해서 만든 코드를 확신할 수 있게 해주는 작업

테스트의 결과가 원하는 대로 나오지 않는 경우에는 코드나 설계에 결함이 있음을 알 수 있다.

웹을 통한 테스트의 문제점 → 모든 레이어의 기능을 다 만들어야지 테스트가 가능

뷰, 컨트롤러, 서비스 클래스를 다 만들어야 테스트가 가능하다.

테스트 하는 중에 에러가 나거나 테스트가 실패했다면 문제가 생긴 곳을 찾기 어렵다.

단위 테스트

  • 작은 단위의 코드에 대해 테스트를 수행한 것
  • 테스트의 관심이 다르다면 테스트할 대상을 분리하고 집중해서 접근해야 한다.
  • 단위의 정의는 명확하지 않지만, 일반적으로 단위는 작을수록 좋다.
  • 통제할 수 없는 외부의 리소스에 의존하는 테스트는 단위 테스트가 아니라고 보기도 한다.
    • 테스트하는데 외부의 DB 상태에 영향을 받는 등..
    • 멱등성이 보장되지 않는 상황..
  • 개발자 테스트 또는 프로그래머 테스트라고도 한다.

단위 테스트가 필요한 이유

  • 개발자가 설계하고 만든 코드가 원래 의도한 대로 동작하는지를 개발자 스스로 빨리 확인받기 위함
  • 단위별로 충분한 검증을 한 뒤에 긴 테스트를 한다면 오류가 덜 할 것

자동수행 테스트 코드

  • 테스트는 자동으로 수행되도록 코드로 만들어지는 것이 중요하다.
    • 테스트를 자주 수행하는데 부담이 들면 안 된다.
  • 자동수행 테스트 코드의 장점은 자주 반복할 수 있다는 것이다.

기존 테스트와 그 문제점

public class UserDaoTest {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
        UserDao dao = context.getBean("userDao", UserDao.class);

        User user = new User();
        user.setId("user");
        user.setName("강희정");
        user.setPassword("123");

        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() + " 조회 성공");
    }
}
  • main 메소드 사용
  • 테스트 대상인 UserDao 직접 호출
  • 수동 확인 작업의 번거로움
    • 결과를 사람의 눈으로 확인해서 기록하고 정리해야 한다.

테스트 검증의 자동화

모든 테스트는 성공과 실패의 두 가지 결과를 가질 수 있다.

테스트의 실패는 두 가지로 나뉠 수 있음

  • 테스트 에러 : 테스트가 진행되는 동안 에러가 발생해서 실패하는 경우
  • 테스트 실패 : 에러는 나지 않았지만 결과가 기대한 것과 다르게 나오는 경우

테스트 에러는 즉각적인 확인이 가능하지만, 테스트 실패는 눈으로 직접 확인해야 함 → 자동화 필요

테스트란 개발자가 마음 편하게 잠자리에 들 수 있게 해 주는 것!

JUnit을 사용한 테스트의 효율적인 수행과 결과 관리

JUnit : 프로그래머를 위한 자바 테스팅 프레임워크

자바로 단위 테스트를 만들 때 유용하게 쓸 수 있다.

  • 일정한 패턴을 가진 테스트를 만들 수 있다.
  • 많은 테스트를 간단히 실행시킬 수 있다.
  • 테스트 결과를 종합해서 볼 수 있다.
  • 테스트가 실패한 곳을 빠르게 찾을 수 있다.
  • 위 기능들을 지원하는 테스트 작성 방법이 별도로 필요.

프레임워크의 기본 동작원리는 제어의 역전!

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

JUnit 프레임워크가 요구하는 조건

  1. 메소드가 public으로 선언되어야 함
  2. 메소드에 @Test 어노테이션을 붙여 줘야 함
  3. retrun값이 void형이고 파라미터가 없어야 한다.

JUnit을 사용하여 테스트코드 수정

package com.example.demo.dao;

import org.junit.Test;
import org.junit.runner.JUnitCore;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;
import java.sql.SQLException;

import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;

public class UserDaoTest {
    
    @Test   // JUnit에게 테스트용 메소드임을 알려줌
    // 반드시 public으로 선언되어야 한다.
    public void addAndGet() throws ClassNotFoundException, SQLException {
        ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");

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

        User user = new User();
        user.setId("user");
        user.setName("강희정");
        user.setPassword("123");

        dao.add(user);

        User user2 = dao.get(user.getId());

        // org.junit.Assert.assertThat은 deprecated되었지만 우선은 예제에 따르기로 한다.
        assertThat(user2.getName(), is(user.getName()));
        assertThat(user2.getPassword(), is(user.getPassword()));
    }

    // JUnit 시작부분
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        JUnitCore.main("com.example.demo.dao.UserDaoTest");
    }
}
  • assertThat()
    • 첫 번째 파라미터를 매처로 비교해서 일치하면 다음으로 넘어감. 아니면 테스트가 실패
    • is()는 매처의 일종으로 equals로 비교해주는 기능을 가졌다.
  • JUnit은 assertThat()을 이용해 검증을 했을 때 기대한 결과가 아니면 AssertionError를 던진다.

JUnit 테스트 실행 방법

  • IDE의 기능 이용
  • JUnit은 한 번에 여러 테스트 클래스를 동시에 실행할 수 있다.
  • 빌드 툴에서 제공하는 JUnit 플러그인이나 태스크를 이용할 수 있다.
  • 여러 개발자가 만든 코드를 모두 통합해서 테스트를 해야 할 경우
    • 서버에서 모든 코드를 가져와 통합하고 빌드한 뒤에 테스트 수행
    • 빌드 스크립트를 이용해 JUnit 테스트를 실행하고 결과 통보받기

테스트 결과의 일관성

테스트가 외부 상태에 따라 성공하기도 하고 실패하기도 한다.

때에 따라서는 성공해야 하는 테스트가 실패하기도 함

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

또한 테스트를 실행하는 순서를 바꿔도 동일한 결과가 보장되도록 만들어야 한다.

→ JUnit은 특정한 테스트 메소드의 실행 순서를 보장해주지 않는다.

예외조건에 대한 테스트

일반적으로는 테스트 중에 예외가 던져지면 테스트 메소드의 실행은 중단되고 테스트는 실패한다.

→ 테스트 진행 중에 특정한 예외가 던져져야 하는 상황

→ 예외 발생 여부는 assertThat()으로는 검증이 불가능

→ 별도 처리 필요

스프링에서는 데이터 액세스 예외 클래스를 별도로 제공하고 있음 (예 : EmptyResultDataAccessException)

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

        UserDao dao = context.getBean("userDao", UserDao.class);
        dao.deleteAll();
        assertThat(dao.getCount(), is(0));

        dao.get("unknown_id");  // 여기서 예외가 발생해야 한다. 예외가 발생하지 않으면 테스트가 실패한다.
    }
  • expected : 테스트 메소드 실행 중에 발생하리라 기대하는 예외 클래스 넣어주기
  • 예외가 반드시 발생해야 하는 경우를 테스트하고 싶을 때 유용하게 사용 가능

항상 네거티브 테스트를 먼저 만들라.

  • 부정적인 케이스 테스트부터 먼저 만드는 습관을 들이자.
  • 다양한 상황과 입력값을 고려하는 포괄적인 테스트를 만들자.
  • 긍정적으로 작성하면 테스트가 넘어가기 쉽다.

테스트가 이끄는 개발

테스트를 구성 → 추가하고 싶은 기능을 코드로 표현하는 과정

테스트 코드는 잘 작성된 하나의 기능정의서와도 같다.

일반적인 개발 흐름의 기능설계에 해당하는 부분을 테스트 코드가 일부분 담당할 수 있다

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

  • 테스트 코드를 먼저 만들고, 테스트를 성공하게 해주는 코드를 작성하는 방식의 개발 방법
  • 테스트 우선 개발(Test First Development)이라고도 한다.
  • 개발자가 테스트를 만들어가며 개발하는 방법이 주는 장점을 극대화한 방법
  • 실패한 테스트를 성공시키기 위한 목적이 아닌 코드는 만들지 않는다.
  • 이 원칙을 따라서 개발된 코드는 빠짐없이 테스트로 검증된 코드라 할 수 있음

TDD의 장점

  • 테스트를 빼먹지 않고 꼼꼼하게 만들어낼 수 있다.
  • 테스트를 작성하는 시간과 애플리케이션 코드를 작성하는 시간의 간격이 짧아진다.
  • 코드에 대한 피드백을 빠르게 받아 작성한 코드에 대한 확신을 가질 수 있다.
  • 자연스럽게 단위 테스트를 만들 수 있다.
  • 코드를 만들어 테스트를 실행하는 사이의 간격이 매우 짧다.
    • 코드의 오류가 빨리 발견되어 수정하기 좋다.

단점? 테스트를 잘 만들지 않는 이유

→ 과거에는 엔터프라이즈 애플리케이션의 테스트를 만들기가 어려웠다.

반복 작업 개선

JUnit은 테스트를 실행할 때 마다 반복되는 준비 작업을 별도에 메소드에 넣게 해 주고, 매번 테스트 메소드 실행 전에 실행하게 해 주는 기능이 있다. → @Before

@Before // @Test 메소드가 실행되기 전에 먼저 실행되어야 하는 메소드 정의
    public void setUp() {
        ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
        this.dao = context.getBean("userDao", UserDao.class);
    }

JUnit이 테스트 클래스를 가져와 테스트를 수행하는 방식

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

@Before, @After 메소드는 테스트 메소드에서 직접 호출하지 않는다

→ 서로 주고받을 정보나 오브젝트가 있으면 인스턴스 변수를 이용해야 한다.

각 테스트 메소드를 실행할 때마다 테스트 클래스의 오브젝트를 새로 만든다.

한번 만들어진 테스트 클래스의 오브젝트는 하나의 테스트 메소드를 사용하고 나면 버려진다.

→ @Test 어노테이션이 붙은 메소드의 개수만큼 인스턴스가 생성된다.

링크 alt

테스트 메소드를 실행할 때 마다 새로운 오브젝트를 만드는 이유

→ 각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 보장해주기 때문

픽스처

  • 테스트를 수행하는 데 필요한 정보나 오브젝트
  • 여러 테스트에서 반복적으로 사용되므로 @Before 메소드를 이용해 생성

카테고리:

업데이트: