본문 바로가기
Spring/Toby's Spring Reading Club

1-2장. 테스트

by Soono991 2022. 9. 19.

애플리케이션은 계속 변하고 복잡해져 갑니다. 그 변화에 대응하는 첫 번째 전략이 확장과 변화를 고려한 객체지향적 설계와 그것을 효과적으로 담아낼 수 있는 IoC/DI 같은 기술이라면, 두 번째 전략은 만들어진 코드를 확신할 수 있게 해 주고, 변화에 유연하게 대처할 수 있는 자신감을 주는 테스트 기술입니다. (p145)

 

2장에서 테스트를 설명하면서 토비님은 "스프링으로 개발을 하면서 테스트를 만들지 않는다면 이는 스프링이 지닌 가치의 절반을 포기하는 셈"이라고 하실 정도로 테스트의 중요성을 강조하고 있습니다.

 

2장은 전반적으로 테스트의 중요성, 그리고 테스트를 작성해야 하는 이유에 대해 친절히 설명하고 있습니다.

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

    UserDao dao = new UserDao();
    User user = new User();

    user.setId(UUID.randomUUID().toString());
    user.setName("soonho");
    user.setPassword("1234");

    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() + " 조회 성공");
}

 

1장에서 작성했던 UserDaoTest 코드입니다.

이 테스트 방법의 특징은 main() 메서드를 이용해 쉽게 테스트를 수행했다는 점과, UserDao를 직접 호출해서 사용한다는 점입니다.

 

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

 

보통 웹 프로그램에서 DAO를 테스트하는 방법은 다음과 같습니다.

  1. DAO를 작성합니다.
  2. 서비스 계층, MVC 프레젠테이션 계층까지 모든 입출력 기능을 만듭니다.
  3. 웹 애플리케이션을 서버에 배치합니다.
  4. 웹 화면을 띄워 폼을 열고, 값을 입력한 뒤 버튼을 눌러 등록해 봅니다.

 

이런 식으로 DAO를 테스트하기 위해 웹 화면까지 모두 개발한 후에야 비로소 DAO를 테스트하게 되는데,

이렇게 수직적인 테스트는 단점이 너무 많습니다.

 

테스트를 하는 중에 에러가 발생했을 때 정확히 어디에서 에러가 발생했는지를 알 수가 없기 때문입니다.

그리고 등록이 성공했다고 해서 과연 의도한 대로 코드가 동작했다고 확신할 수 있을지도 의문입니다.

따라서 DAO를 테스트하기 위해서는 정확히 DAO만 테스트를 해야 문제가 생겼을 때도 DAO에서 발생했다고 확신할 수 있기 때문에 오류를 해결하기도 쉽고, 코드가 제대로 동작했는지 확인도 쉬울 것입니다.

 

작은 단위의 테스트

테스트는 가능하면 작은 단위로 쪼개서 집중해서 할 수 있어야 합니다. 이렇게 작은 단위의 코드에 대해 테스트를 수행한 것을 단위 테스트라고 합니다.

단위 테스트에서 중요한 것은 테스트를 실행한 결과가 매번 같아야 한다는 점입니다.
가령 DB의 상태가 매번 달라지고, 테스트를 위해 DB를 매번 특정 상태로 만들어줄 수 없다면 이 때는 단위 테스트라고 부를 수 없습니다.

 

자동수행 테스트 코드

 

테스트는 자동으로 수행되도록 코드로 만들어지는 것이 중요합니다. 그런데 main() 메서드에 테스트 코드를 포함시키는 것보다 별도로 테스트용 클래스를 만들어서 테스트 코드를 넣는 편이 낫습니다.

 

2.1.3 UserDaoTest의 문제점

UserDaoTest가 불필요하게 웹 화면까지 개발할 필요가 없이 UserDao를 테스트할 수 있다는 점은 장점이지만, 이 외에 몇 가지 단점이 존재합니다.

  • 수동 확인 작업의 번거로움
    • UserDao 테스트를 실행한 결과를 개발자가 직접 확인해야 한다는 점입니다. 입력한 값이 개발자가 의도한 값인지 비교하는 작업을 테스트 코드가 해주지는 않습니다.
  • 실행 작업의 번거로움
    • 아무리 간단한 main() 메서드라도 매번 실행하는 것은 부담스럽습니다. 만약 DAO가 수백 개가 되고 그에 대한 main() 메서드가 있을 경우 개발자가 직접 main() 메서드를 수백 번 실행해야 하며, 그 결과를 다시 눈으로 확인해야 합니다.

2.2.1 테스트 검증의 자동화

    public static void main(String[] args) throws SQLException, ClassNotFoundException {
        UserDao dao = new UserDao();
        User user = new User();

        user.setId(UUID.randomUUID().toString());
        user.setName("soonho");
        user.setPassword("1234");

        dao.add(user);

        System.out.println(user.getId() + " 등록 성공");

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

        if (!Objects.equals(user.getName(), user2.getName())) {
            System.out.println("테스트 실패 (name)");
        } else if (!Objects.equals(user.getPassword(), user2.getPassword())) {
            System.out.println("테스트 실패 (password)");
        } else {
            System.out.println("조회 테스트 성공");
        }

    }

 

간단하게 위와 같이 if문을 사용해 테스트 성공, 테스트 실패를 구분할 수 있습니다.

하지만 이 방법도 결국 개발자가 작성한 조건에 부합하지 않는 오류가 발생할 경우 또 디버깅을 통해 확인해야 합니다.

 

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

자바에는 자바 테스팅 프레임워크라고 불리는 JUnit이 있는데, 이를 이용하면 main() 메서드로 만든 테스트를 더 편리하고 효율적으로 만들 수 있습니다.

 

테스트 메소드 전환

 

JUnit 프레임워크로 테스트 코드를 작성할 때는 두 가지 규칙이 있습니다.

  1. 메서드가 public으로 선언돼야 합니다.
  2. @Test 어노테이션을 메서드에 붙여줘야 합니다.

 

지금에 와서는 1번은 굳이 public으로 선언하지 않아도 된다고 합니다.

이유를 간단하게 설명하자면 JUnit은 리플렉션을 사용하는데 JUnit이 처음 만들어지던 때 사용한 JDK 1.1은 public 메서드만 리플렉션이 가능했기 때문에 반드시 public으로 선언해야 했지만,

1.2부터는 private 메서드도 테스트가 가능해졌다고 합니다. 하지만 처음부터 public으로 작성하던 습관, 관례가 계속 이어져 지금도 public으로 작성한다고 합니다.

 

관련 링크: https://groups.google.com/g/ksug/c/xpJpy8SCrEE?pli=1

@SpringBootTest
@TestPropertySource(locations = "classpath:application.yml")
public class UserDaoTest {

    @Autowired
    private UserDao dao;

    @Test
    public void addAndGet() throws SQLException, ClassNotFoundException {
        User user = new User();

        user.setId(UUID.randomUUID().toString());
        user.setName("soonho");
        user.setPassword("1234");

        dao.add(user);

        System.out.println(user.getId() + " 등록 성공");

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

        assertThat(user2.getName()).isEqualTo(user.getName());
        assertThat(user2.getPassword()).isEqualTo(user.getPassword());
    }
}

 

JUnit을 사용하면 좋은 점이 위와 같이 테스트를 실행 후 테스트 결과를 코드에서 검증하여 성공/실패를 알려줍니다.

따라서 개발자가 직접 눈으로 확인할 필요가 없으며, 성공한 코드의 신뢰성이 높아집니다.

 

2.3.2 테스트 결과의 일관성

앞서 설명했듯이 작성된 테스트 코드의 결과는 항상 일정해야 합니다.

코드를 실행할 때마다 결과가 달라진다면 그 코드가 항상 성공한다고 가정해도 잘 작성된 테스트 코드라 할 수 없다고 생각합니다.

 

addAndGet() 테스트 보완

addAndGet 테스트를 조금 더 보완해 보겠습니다. 한 가지 아쉬운 점은 user를 등록하고 등록한 user의 id로 user를 조회하면 등록한 user의 값을 반환하는지는 아직 검증하지 못했습니다.

    @Test
    public void addAndGet() throws SQLException, ClassNotFoundException {
        User user1 = new User(UUID.randomUUID().toString(), "soonho", "1234");
        User user2 = new User(UUID.randomUUID().toString(), "zooni", "1234");

        dao.deleteAll();
        assertThat(dao.getCount()).isEqualTo(0);

        dao.add(user1);
        dao.add(user2);
        assertThat(dao.getCount()).isEqualTo(2);

        User userget1 = dao.get(user1.getId());
        assertThat(userget1.getName()).isEqualTo(user1.getName());
        assertThat(userget1.getPassword()).isEqualTo(user1.getPassword());

        User userget2 = dao.get(user2.getId());
        assertThat(userget2.getName()).isEqualTo(user2.getName());
        assertThat(userget2.getPassword()).isEqualTo(user2.getPassword());
    }

 

포괄적인 테스트

개발자가 테스트를 직접 만들 때 자주 하는 실수가 하나 있습니다. 바로 성공하는 테스트만 골라서 만드는 것입니다.

스프링의 창시자인 로드 존슨은 "항상 네거티브 테스트를 먼저 만들라"라고 했습니다.

로드 존슨의 말처럼 개발자는 테스트를 작성할 때 항상 부정적인 케이스를 먼저 만드는 습관을 들여야 합니다.

 

학습 테스트

학습 테스트란, 자신이 만들지 않은 프레임워크나 다른 개발팀에서 만들어서 제공한 라이브러리 등에 대해서도 테스트를 작성하는 것을 말합니다.

학습 테스트의 목적은 자신이 사용할 API, 프레임워크의 기능을 테스트로 보면서 사용 방법을 익히려는 것입니다.

따라서 이전의 테스트와 달리 프레임워크, 기능에 대한 검증이 목적이 아닙니다.

결국 기능을 테스트해보면서 사용 방법을 익히는 것이기 때문에, 흔히 말하는 "클론 코딩" 또한 학습 테스트라고 할 수 있지 않을까?라는 생각이 듭니다.

 

2.5.1 학습 테스트의 장점

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

 

2.5.3 버그 테스트

버그 테스트란 코드에 오류가 있을 때 그 오류를 가장 잘 드러내줄 수 있는 테스트를 말합니다. 버그 테스트는 일단 실패하도록 만들어야 합니다.

 

버그 테스트의 필요성 / 장점

  • 테스트의 완성도를 높여줍니다.
  • 버그의 내용을 명확하게 분석하게 해 줍니다.
  • 기술적인 문제를 해결하는 데 도움이 됩니다.

 

그리고 마지막에 언급된 동등분할, 경곗값 분석 개념도 중요한 것 같습니다.

무분별하게 테스트 케이스를 많이 작성하는 것보다 어떤 작업의 결과가 반환하는 종류를 파악하여 해당 종류별로 테스트 케이스를 작성하는 것이 중요한 것 같습니다.

예를 들어 어떤 코드의 결과가 true, false, exception 세 가지라면 각각의 결과를 내는 테스트 케이스를 작성하는 것입니다.

만약 무분별하게 랜덤으로 100번 테스트 케이스를 실행한다고 해도 우연히 true, false만 반환한다고 하면 결국 exception에 대한 테스트는 검증되지 않았기 때문에 잘 작성된 테스트라고 보기는 어렵습니다.

'Spring > Toby's Spring Reading Club' 카테고리의 다른 글

1-5장. 서비스 추상화  (0) 2023.01.08
1-4장. 예외  (0) 2023.01.08
1-3장. 템플릿  (0) 2023.01.07
1-1장. 오브젝트와 의존관계  (0) 2022.09.05
토비의 스프링 3.1 시작하기  (0) 2022.09.04

댓글