✅ 테스트 코드를 작성 해봐야 하는 이유

이상현·2024년 8월 14일

Swift

목록 보기
9/10
post-thumbnail

Youtube - 테스트 코드 작성을 해봐야 하는 이유
의 발표 내용과 동알합니다.

테스트 코드, 작성하고 계신가요?

테스트 코드를 잘 작성하지 않거나, 제대로 작성해본 적이 없나요? 왜 그런가요?

아마도 어떻게 작성해야 할지 모르거나, 많은 품이 들어가는 작업이라고 생각해서 그랬을 것입니다.

저도 "테스트 코드" 하면, 작성할 수록 좋다는 것은 알지만, 귀찮고 오래 걸리는 작업이라는 생각이 듭니다. 하지만 언젠가는 능숙해져야 할 테스트 코드. 작성할 가치가 있다는 것은 우리 모두 알고 있습니다.

이 글의 목적은 제가 테스트 코드를 학습하고 작성해본 경험과 고찰을 공유함으로써, 테스트 코드를 작성해본 적이 없던 분들도 흥미를 가지고 작성을 해볼 수 있도록 하는 것입니다.

테스트 코드란 무엇인가?

테스트 코드는 소프트웨어 개발에서 작성된 코드가 의도한 대로 작동하는지 검증하기 위해 작성된 코드입니다. 이는 소프트웨어의 각 구성 요소가 올바르게 동작하는지 확인하는 데 사용되며, 다양한 종류가 있습니다.

테스트의 종류

iOS 에서의 단위 테스트에는 다음과 같은 테스트들이 있습니다.

  • 단위 테스트: 코드의 작은 단위(함수나 메서드)가 올바르게 동작하는지 테스트
    ex) 사용자가 입력한 이메일 형식의 유효성을 검증하는 로직이 잘 작동하는지 테스트

  • 통합 테스트: 여러 단위가 함께 작동할 때 문제가 없는지 테스트
    ex) 사진들이 서버에서 잘 받아와지는지 테스트

  • UI 테스트: 사용자가 인터페이스를 통해 상호작용할 때 예상한 대로 동작하는지 테스트
    ex) 로그인된 유저일 경우 메인 화면이 보이고, 비로그인 유저는 회원가입 화면이 보이는지 테스트

이러한 테스트 코드를 작성해두면, 소프트웨어의 수정이나 기능 추가 시 기존 기능이 정상적으로 동작하는지 자동으로 검증할 수 있어 매우 유용합니다.

이 글에서는 단위 테스트(유닛 테스트) 에 대해서만 말씀드리겠습니다.

Xcode 에서의 테스트는 어떻게 생겼나

Xcode(Swift) 에서는 XCTest 모듈을 사용해서 테스트를 할 수 있습니다.
(WWDC 2024에서 새로운 테스트 모듈인 @Test 가 공개되긴 했습니다)

Unit Test , UI Test 를 생성 가능합니다.

Swift 테스트 코드 예시

이는 간단한 유닛 테스트 (단위 테스트) 코드입니다.
이렇게 생겼구나~ 하고 넘어가면 됩니다.

테스트 성공 사진

테스트 작동시 이처럼 성공/실패 표시가 보입니다.

테스트 코드 작성의 이점

그래서 테스트 코드를 왜 작성하냐? 하면 다음과 같은 이점들이 있습니다.

  • 버그가 발생하기 이전에 미리 발견 가능
  • 코드 변경 및 리팩토링 할 때 안정성 확보
  • 의도와 동일하게 작동하는지 검증 가능
  • QA 역할 어느정도 대체 가능

테스트 코드 작성하는게 과연 효율적일까

이점들에도 불구하고 테스트 코드가 효율적일까? 이야기를 나눠보면 다음과 같은 의견이 많습니다.

- 같은 로직을 왜 두번 작성하냐
- 구현 코드 작성할 시간도 촉박하다
- 어차피 사람이 버그를 다 잡지 못한다. 테스트 코드에도 빈틈이 생긴다.
- 막상 잘 안쓰는 회사 많다.

틀린 말 없습니다. 이런 이유들로 단위 테스트 또는 TDD 는 이상론 취급을 받고, 많은 개발자들이 철저하게 작성하지는 않습니다.

그럼에도 불구하고 시도해야 하는 이유

테스트 가능한 코드는
품질이 좋은 코드의 중요한 특징 중 하나이다.

왜 그럴까요?

테스트 가능한 코드의 특징

  • 모듈화가 잘 되어있다.
  • 리팩토링 안정성이 높다.
  • 상태와 로직이 분리되어있다.
  • 확장성이 좋다.

-> 테스트 "가능" 하게만 만들어도 위에 조건들이 충족됩니다.

테스트 가능한 코드 작성하기

그래서 테스트 코드 써보자! 하고 막상 프로젝트를 열면, 뭘 테스트 해야하는지 감이 잡히지 않습니다.

주로 테스트 하는 것들은 다음과 같습니다.

  1. 중요 로직 테스트
  2. 경계값 테스트
  3. 예외 테스트
  4. 최대한 많은 코드 테스트

로직이 내가 의도한대로 작동하는지 경계값을 유의하며 테스트하고, 예외가 의도한 대로 발생하고 처리되는지 테스트하면 됩니다. 물론 많은 코드를 테스트 할수록 좋긴 합니다. Covarage 라고 메소드 별로 테스트 도중 실행된 코드의 비율을 보여주는 기능도 있습니다.

추가로, 어떤 클래스의 private 메서드는 테스트 하지 않아도 됩니다. 클래스의 내부 로직은, 외부에서 알 필요가 없다는 뜻이므로, public 메서드만 테스트하면 됩니다.

이 의문은 하도 유명해서 다음과 같은 사이트도 있습니다.

https://shoulditestprivatemethods.com/

한번 클릭해보시길 추천드립니다.

테스트 불가능한 코드 예시

테스트 가능한 코드가 어떤 코드인지 알아보기 위해, 먼저 테스트가 어려운 코드 예시를 보여드리겠습니다

struct Article {
    let id: Int
    let content: String
    var isFavorite: Bool = false
}
class Database {
    func loadArticles() -> [Article] {
         return // DB에서 불러온 실제 데이터 반환 한다고 가정
    }
}
class ArticleService {
    private var articles: [Article]
    private var articleScore: Int
    private var database: Database // 데이터베이스 의존성
    
    init(database: Database) {
        self.articles = repository.loadArticles()
        self.articleScore = 0
        self.database = database
    }
    
    // 아티클의 상태에 따라 점수 계산
    func calcArticleScoreStatus() {
        var score = 0
        for article in articles {
            score += article.isFavorite ? 20 : 10
        }

        // 내부 상태 변경
        articleScore = score
    }
}

예시 코드를 잘 보면, 실제 DB에 서비스 클래스가 의존하고 있어서, DB 서버가 켜져있어야만 테스트가 가능합니다.

좋은 테스트의 조건중에는, 어떤 환경에서도 같은 값을 반환해야한다는 조건이 있습니다. 무인도에서 테스트를 돌려도 집에서 돌린 것과 같은 결과가 나와야 한다는 뜻입니다.

정리하자면, 현재 코드는 외부 환경에 따라 결과값이 변경되고있고, 메소드가 값 반환이 아닌 클래스의 상태를 변경하고 있으므로 테스트가 어렵습니다.

테스트 가능한 구조 예시

방금 그 구조를 테스트 가능하도록 변경해보겠습니다.

ArticleRepository 라는 프로토콜을 추가했습니다.

struct Article {
    let id: Int
    let content: String
    var isFavorite: Bool = false
}
protocol ArticleRepository {
    func loadArticles() -> [Article]
}
class Database: ArticleRepository {
    func loadArticles() -> [Article] {
         return // DB에서 불러온 실제 데이터 반환
    }
}
class ArticleService {
    private var articles: [Article]
    private let repository: ArticleRepository // 의존성 주입
    
    init(repository: ArticleRepository) {
        self.articles = repository.loadArticles()
        self.repository = repository
    }
    
    // 테스트 용이하도록 값 반환하게 변경
    func calculateScore() -> Int {
        var score = 0
        for article in articles {
            score += article.isFavorite ? 20 : 10
        }

        return score
    }
}

  1. 서비스 클래스가 Database 클래스가 아닌 Repository 프로토콜에 의존하게 만들었습니다.

  2. 테스트하기 위한 Repository 구현체를 DB 서버에 관계 없이 같은 결과를 반환하게, 그리고 Repotisory 프로토콜을 준수하도록 작성해서 외부에서 주입해주면 테스트 가능합니다.

  3. 메서드가 클래스의 상태값을 변경하는게 아닌 값을 반환하도록 했습니다.

정리하자면, 의존성 주입 받아서 외부 환경에 의존하지 않게 되었고, 메소드가 상태를 변경하는 것이 아니라 값을 반환하고 있으므로 테스트가 가능합니다.

테스트 코드 예시

위에서 작성한 메소드의 테스트 코드입니다.

import XCTest
@testable import TechBlogNotifications

class MockRepository: ArticleRepository {
    func loadArticles() -> [Article] {
        return [
            Article(id: 1, content: "테스트1", isFavorite: true),
            Article(id: 2, content: "테스트2")
        ]
    }
}

final class ArticleServiceTests: XCTestCase {
    func test점수_계산() {
        let mockRepository = MockRepository()
        let articleService = ArticleService(repository: mockRepository)
        
        let score = articleService.calculateScore()
        
        XCTAssertEqual(score, 30, "점수 계산 테스트 통과 실패")
    }
}

ArticleRepository 프로토콜을 준수하는 MockRepositoryloadArticles 메서드가 고정된 값을 반환하도록 구현했습니다.

이러고 서비스에 해당 클래스를 주입해주면 내부 동작을 외부에서 정해줄 수 있습니다. 그래서 결과값은 어떤 환경에서도 변하지 않는 값이 나오도록 되어, 테스트에 성공하였습니다.

테스트 가능한 구조

테스트 가능한 코드로 만들다 보니 다음과 같은 점들이 개선되어 코드 품질이 좋아졌습니다.

  1. 결합도 감소, 응집도 증가
  2. 코드 단순화
  3. 확장성 증가

위 규칙들을 지키기 위에 코드를 작성한것이 아니라, 그냥 테스트가 가능하게끔만 코드를 수정했을 뿐인데, 그동안 공부해오던 객체지향 원칙, 클린코드 원칙들의 일부가 알아서 지켜지고 있었습니다.

수정한 코드는 테스트가 가능한것을 떠나서 가독성도 좋아졌고, 다른 데이터베이스 전략이 생기더라도 최소한의 변경으로 대응 가능하도록 개선되었습니다.

결론

이렇듯, 테스트 코드 작성은 테스트가 가능하도록 구현에 신경쓰게 되면서 자연스럽게 품질 높은 코드를 작성하게 됩니다.

저는 테스트 코드를 철저하게 작성하는 경험을 해보면서, 그동안 이론으로만 공부하고 체감되지 않았던 많은 이론들의 목적을 몸소 느끼고 실천하게 되었습니다.

그 경험을 많이 쌓으면 쌓을 수록 테스트 코드를 작성하지 않고 구현만 한다고 하더라도, 품질 높은 코드를 작성하는 실력이 많이 향상될것 이라고 장담합니다.

더 나아가기

AI

테스트 코드 작성이 그래도 부담이 된다면, AI를 활용해도 좋다고 생각합니다.
저는 ChatGPT에 테스트 코드 작성 로봇을 만들어서 사용하기도 했었습니다. 입맛에 맞게 잘 작성해줍니다.
물론 AI 가 작성하는 모든 코드는 한줄 한줄 읽어가며 내 의도와 맞는지 확인해봐야 합니다.

GPT로 테스트 코드 작성하는 방법은 유명합니다. 요즘은 더 나아가서 요구사항을 보고 테스트 코드를 먼저 작성한다음, 그것을 GPT 한테 전달해서 본 구현을 해본다고도 합니다.

FIRST

본문에서, 좋은 테스트의 조건 같은 말이 언급되었는데, 그것은 단위 테스트의 FIRST 원칙을 기반으로 설명한 것이였습니다.
해당 이론에 대해 한번 읽어봐도 좋은것 같습니다.

TDD

TDD (Test Driven Development) 라는 개발 방법론이 있습니다. 그것은 요구사항을 보고 테스트 코드를 모두 작성한 다음 테스트가 통과하도록 구현을 하는 것인데요, TDD 는 실제 프로젝트의 데드라인에 맞춰서 유효성 있게 적용하기에는 정말 쉽지 않은것 같습니다. 그렇지만 TDD 까지는 아니더라도 테스트 코드 작성을 시도해보는것은 큰 가치를 가져다 준다고 생각합니다.

그러므로 TDD 도 학습해서 한번쯤 빡세게 적용해보는 경험을 쌓아보면 좋을것 같습니다.

FIRST 와 TDD 에 관해 간단하게 정리해놓은 게시글입니다.
[Java] 테스트 주도 개발 (TDD)

0개의 댓글