본문 바로가기

Dev Diary

TestCode ? 왜 작성하는거야?

시작하기에 앞서

 

프로젝트를 하다 보면 하나의 기능을 완성하고 실제로 프로젝트의 요구사항에 맞게 잘 동작하는지 테스트를 하는 과정을 필수적으로 거치게 됩니다.

 

 저의 경우, 이번 SSAFY 공통 프로젝트에서는 JPA , Docker , Redis … 등 프로젝트 기간 안에 새로운 기술들을 빠르게 익히고 적용해야 했습니다. 때문에 기능 구현에만 초점을 두고 개발을 진행하게 되었는데요. 내가 구현한 기능이 올바르게 동작하는지 , 버그는 없는지 , 잘 작성이 되었는지 문득 궁금해졌습니다.

 

하지만 프로젝트 기간 안에 기능을 정확하게 테스트하는 과정에 많은 시간을 쏟지 못했습니다. 결국 다음과 같은 방법으로 해당 기능이 제대로 작동된는지 안되는지 확인하게 되었는데요..!  어떤 문제점이 있었을까요?

 

예를들어 댓글을 조회하는 API를 테스트한다고 가정해 보겠습니다.

 

1. 코드 작성 및 수정
2. 서버 실행
3. 필요에 따라 테스트하기 위한 테스트 댓글 데이터를 DataBase에 저장
4. 브라우저를 통해 프로젝트 서버에 접속하고 , 테스트를 수행할 대상 메소드를 동작시키는 요청
5. 테스트를 마치고 , DataBase에 저장했던 테스트 데이터 정리
6. 1~5 무한 반복…

 

 처음에는 이러한 과정으로 테스트를 진행해도 부담이 없었지만 , 기능들이 수정되거나 코드가 길어지고 다른 기능들이 많아지면서 굉장히 번거로운 작업이 되었습니다. 반복적으로 테스트를 수행해야 하는 경우에도 효율적이지 못했고 , 테스트를 수행하면서 발생하는 오류를 찾기에도 어려웠습니다.

 

따라서 저는 프로젝트 끝나고 결심했습니다... 비록 개발은 끝났지만 테스트 코드를 작성함으로써 기존 코드들의 신뢰성과 유지보수성을 향상하도록 말이죠!

 

이번 포스팅에서는 TestCode는 무엇인지, 왜 필요한지, Test 종류, 테스트 코드 기반 TDD에 대해 알아보도록 하겠습니다.

 


Test Code 란?

💡 Test Code는 소프트웨어 개발에서 테스트를 수행하기 위해 작성되는 코드를 의미합니다.

일반적으로 Test Code는 소프트웨어 시스템의 기능을 테스트하고 , 버그를 찾거나 , 코드를 검증하고 , 성능을 평가하는 등 , 기능들이 예상하는 대로 동작하는지 수행하는 작업을 진행하게 됩니다.

Test Code는 왜 작성해야 될까?

 

그렇다면 Test Code는 왜 작성할까요? 어떠한 장점들이 있는지 살펴봅시다.

 

  1. 코드의 신뢰성을 확보
    ⇒ Test Code를 작성하여 예상치 못한 버그나 예외 상황에 미리 대비할 수 있게 됩니다. 따라서 이를 통해 코드의 품질을 향상하며 도시에 코드의 신뢰성을 확보할 수 있게 됩니다.
  2. 리팩토링을 위한 안정망을 제공
    ⇒ 리펙토링은 코드를 개선하고 유지보수하기 쉽도록 하는 작업입니다. 리펙토링을 하게 될 경우 기존 코드의 동작이 변경될 수 있습니다. 따라서 이때 TestCode를 작성해 두었다면 리펙토링 이후에도 코드가 제대로 동작했는지 검증할 수 있게 됩니다.
  3. 코드의 문서화 역할
    ⇒ Test Code를 작성하면 해당 코드가 어떻게 동작하는지 쉽게 이해할 수 있게 됩니다. ( 동작시작 → 동작 → 동작 결과)
  4. 개발 생산성 향상
    ⇒ 또한 Test Code를 작성하면 개발자 자신이 작성한 코드에 대한 자신감을 갖게 되며 코드를 더욱 적극적으로 개발할 수 있습니다.
    ⇒ 버그나 예외 상황을 발견하고 이를 미리 대처할 수 있기 때문에 개발 생산성이 향상됩니다.
  5. 테스트 주도 개발(TDD)을 위한 기반을 마련
    ⇒ TestCode는 이러한 TDD(테스트 주도 개발)의 기반을 방식을 마련하여 코드의 품질을 높일 수 있습니다.
    ⇒ TDD는 테스트 코드를 먼저 작성하고 , 그에 맞는 코드를 작성하는 방식으로 개발을 진행하는 방법을 의미합니다.

단위  (Unit) / 통합  (Integration) / 인수  (Appceptance) 테스트

 

소프트웨어를 테스트하는 데 있어 여러 유형의 테스트 방법들이 존재합니다. 각 테스트는 목적과 방법 등에 따라 차이점을 가지고 있는데요! 이번 글에서는 Unit(단위) , Integration (통합) , Acceptance (인수) 테스트에 대하여 알아봅시다.

단위 테스트 (Unit Test)

💡 단위 테스트는 하나의 모듈을 기준으로 독립적으로 진행되는 가장 작은 단위 테스트를 의미합니다.

여기서의 모듈은 애플리케이션에서 작동하는 하나의 메서드 혹은 기능을 의미합니다. 해당 단위의 기능을 검증하고 예상되는 결과를 확인하는 것이 목적이죠.

예를 들어 다음과 같은 코드가 있다고 가정해 보겠습니다.

Calculator 클래스에 a와 b의 곱을 연산하는 mutiply함수가 있습니다.

public class Calculator {
    public int multiply(int a, int b) {
        return a + b;
    }
}

이는 다음과 같은 단위 테스트로 기능을 검증할 수 있습니다.

import org.junit.Test;
import static org.junit.Assert.assertEquals;

public class CalculatorTest {

    @Test
    public void testMultiply() {
        Calculator calculator = new Calculator();
        int result = calculator.add(2, 3);
        assertEquals(6, result);
    }
}

assertEquals 메서드는 두 개의 인자를 받게 됩니다. 첫 번째 인자는 예상한 값 (expected value)을 의미하고 , 두 번쨰 인자는 실제 반환값( actual value)를 의합니다. assertEquals는 두 값이 같은지를 비교하고 값이 다를 경우에는 테스트에 실패하게 됩니다.

 

즉 6과 result가 같다면 Test에 통과하게 되고 그렇지 않은 경우는 실패하게 되는 것이죠. 실패한 경우는 해당 테스트 메서드의 실행이 중단되고 테스트 결과는 실패로 기록되게 됩니다. Junit과 같은 테스트 프레임워크에서 자동으로 성공과 실패를 감지하고 결과를 요약하여 제공합니다. 이를 통해서 개발자가 자동화된 단위 테스트를 수행하면서 코드의 정확성을 보장할 수 있게 됩니다!

 

통합 테스트(Integration Test)

💡 통합 테스트는 여러 개의 모듈이나 시스템 구성요소가 함께 동작할 때, 이들 간의 상호작용을 테스트하여 전체적인 시스템의 동작이 예상대로 이루어지는지를 검증하는 테스트 방법입니다 . DB에 접근하거나 전체 코드와 다양한 환경이 제대로 작동하는지 확인하는데 필요한 모든 작업을 수행할 수 있습니다.

 

 

댓글 등록 기능을 예시로 들어서 설명해 보겠습니다.

 

댓글을 등록하기 위해서는 사용자가 필요하고 댓글을 등록할 게시물이 필요합니다. 따라서 이들을 결합하여 전체 시스템을 구성하여 통합 테스트를 하게 됩니다.

@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class CommentIntegrationTest {

    @Autowired
    private UserService userService; //사용자

    @Autowired
    private PostService postService; //게시물

    @Autowired
    private CommentService commentService; //댓글

    private User testUser; //테스트할 유저 Object
    private Post testPost; //테스트할 게시판 Object

    @Before
   // 테스트에 필요한 사용자와 게시물 등록합니다.
    public void setup() {

        testUser = new User("사용자아이디", "사용자비밀번호");
        userService.registerUser(testUser);

        testPost = new Post("test post title", "test post content", testUser);
        postService.addPost(testPost);
    }
    //댓글 등록을 테스트 합니다.
    @Test
    public void testAddComment() {
        // 댓글 등록
        Comment comment = new Comment("댓글 테스트 내용 1", testUser, testPost);
        commentService.addComment(comment);

        // 댓글 잘 등록이 되었는지 확인합니다. 
        List<Comment> comments = commentService.getComments(testPost);
        assertEquals(1, comments.size());

        Comment savedComment = comments.get(0);
                //등록한 댓글이 조회한 댓글과 일치하는지 확인합니다.
        assertEquals(comment.getContent(), savedComment.getContent());
        assertEquals(comment.getUser(), savedComment.getUser());
        assertEquals(comment.getPost(), savedComment.getPost());
    }
}

위 코드에서는 댓글 등록 기능이 있는 <CommentService>를 테스트하게 됩니다. PostService와 UserService는 게시물과 사용자 관련된 기능을 제공합니다. 각각의 클래스는 서로 독립적으로 개발되었지만 이들을 결합하여 통합 테스트를 하게 됩니다.

 

CommentIntegrationTest클래스는 @RunWith(SpringRunner.class@SpringBootTest을 사용하여 스프링 컨테이너를 로드하고, @Transactional을 사용하여 각각의 테스트 메서드가 실행될 때마다 트랜잭션을 시작하고 테스트가 완료되면 롤백합니다.

 

결론적으로 위의 예시 코드에서는 새로운 사용자와 게시물을 등록하고 댓글을 등록하는 기능을 테스트합니다. 이를 통해서 Comment 클래스가 PostService와 UserService 클래스와 잘 통합되어 전체 시스템이 올바르게 동작하는지를 검증하게 됩니다.

장단점

이러한 통합 테스트는 장점도 있지만 단점도 존재합니다.

 

장점

  • 시스템의 전반적인 동작을 확인 할 수 있다.
    =>위에서 단위 테스트 경우 메서드 하나, 작은 단위로만 테스트가 가능했지만 통합 테스트에서는 다른 모듈끼리 상호작용하는 시스템의 전반인 동작을 확인 할 수 있습니다. 이는 개별적으로 테스트했을 때는 발견하지 못하는 문제점, 버그를 발견할 수 있습니다.
  • 테스트 비용이 많이 든다.
    =>통합 테스트의 경우는 다양한 모듈을 연결하고 , 복잡한 상호작용을 검증해야 되기 때문에 테스트 비용이 많이 들 수 있습니다. 이에 따라 테스트를 자주 실행하기 어렵고 커버리지가 낮아질 수 있습니다.

단점

  • 디버깅이 어려워진다.
    • 다양한 모듈을 연결하고 , 복잡한 상호작용을 연결하다 보니 디버깅이 단위 테스트에 비해 어려워질 수 있습니다.
  • 신뢰성이 떨어질 수 있다.
    • 단위테스트 보다 더 많은 코드를 테스트하기 때문에 신뢰성이 떨어질 수 있습니다.

인수 테스트(Acceptance Test)

💡 인수 테스트는 소프트웨어가 최종 사용자의 요구사항을 만족시키는지 검증하는 테스트 입니다.

즉 ,사용자 스토리(시나리오)에 맞춰 시스템의 동작을 확인하여 요구사항을 총족시키는지 검증하는 것이죠

인수 테스트의 경우는 사용자 스토리 (User Stort)를 기반으로 작성하게 됩니다. 사용자 스토리는 소프트웨어가 제공해야 할 기능이나 사용자가 어떠한 목적으로 소프트웨어를 사용하는지에 대한 내용입니다.

 

인수 테스트는 일반적으로 기능 테스트 이후에 수행됩니다. 즉 , 기능적인 측면에서 문제가 발견되지 않은 경우, 최종 사용자가 소프트웨어를 사용했을 때 요구사항에 충족한 결과를 제공하는지 검증하는 것이죠.

 

인수 테스트는 사용자 관점에서 검증하기 때문에 , 실제 사용한다고 했을 때 발생 할 수 있는 문제를 예측하여 사전에 방지할 수 있습니다.

 

 

Test Code 작성이 어려운 이유

 

하지만 이러한 TestCode를 작성하는 것은 쉽지 않은 작업이며 시간과 비용이 많이 들게 됩니다.

 

  1. 개발자들은 주로 코드 작성에 집중한다.
  2. 단위 테스트의 경우 테스트 케이스 수가 많아질 수 있고 작성하고 관리하는데 쉽지 않다.
  3. 코드 수정 및 리팩토링이 발생할 때마다 테스트 코드도 같이 변경해 주어야 한다.
  4. 테스트 코드를 작성하면서 발생하는 문제점도 해결해야 한다.

 

마치며

 

 저는 이번 글을 작성해 보면서 시간과 비용이 들더라고 TestCode를 작성하는 것은 개발자에게 있어 반드시 필요한 과정이라고 생각이 들었습니다.

 

 그 이유는 간단한 프로젝트에서는 영향을 미치지는 않겠지만 회사에서 서비스를 하는 정도의 규모에서는 사용자들이 서비스를 이용하며 사용자들의 자산이나 , 제공하는 데이터들이 올바르게 반영되야 하고 서비스를 제공하는 회사는 이를 올바르게 수행해야 되어야 하기 때문입니다.

 

 이를 위해서는 신뢰성 있는 코드가 작성이 돼야 하고 이를 가장 도울 수 있는 방법은 Test Code를 작성하는 것이라고 생각합니다. 혼자 개발하는 것이 아닌 팀원끼리 함께 개발을 할 때 TestCode를 작성한다면 보다 코드를 이해하기 더 쉽게 되며 이는 곧 코드의 생산성과 개발의 단축을 시킬 수 있는 중요한 작업과정이라 생각합니다.

 

다음 진행하는 프로젝트에 TestCode를 작성해 보며 몸소 느껴보고 다음 글을 작성해 보겠습니다.

 

 

 

참고

https://tecoble.techcourse.co.kr/post/2021-05-25-unit-test-vs-integration-test-vs-acceptance-test/

 

단위 테스트 vs 통합 테스트 vs 인수 테스트

소프트웨어 테스트에는 여러 유형들이 있다. 각 테스트는 목적, 방법 등에 따라 차이점을 가진다. 이번 글에서는 그 중 단위 테스트, 통합 테스트, 인수 테스트에 대해 개념을 정리하려 한다.

tecoble.techcourse.co.kr

https://betterprogramming.pub/the-test-pyramid-80d77535573

 

The Test Pyramid

Thoughts on the test pyramid, end-to-end tests, and achieving high test coverage from nothing.

betterprogramming.pub