티스토리 뷰

이전 글: https://gojs.tistory.com/70

 

테스트의 종류와 필요성

여러가지 개발 서적을 접하다보면 단위 테스트의 필요성을 자주 강조하고 있다.그렇다면 여러 가지 종류의 테스트 중에서 단위 테스트는 무엇이며, 단위 테스트가 필요한 이유는 무엇일까? 테

gojs.tistory.com

 

테스트 코드를 작성하는 것은 단순하게 구현된 기능을 테스트한다는 의미 이상의 효과를 발휘한다.

여러 긍정적인 효과를 발휘하고 테스트 자체로도 품질 지표가 되기도 한다.

그렇다면 테스트 코드를 작성함으로써 프로젝트 전반에 끼치게 되는 긍정적인 효과를 알아보자.

 

테스트 커버리지

테스트 커버리지가 높으면 안정적으로 리팩토링이 가능하기 때문에, 이상적으로는 테스트 커버리지를 100% 달성하는 것이 좋다.

테스트 커버리지를 높히기 위해서는 모든 public API를 테스트하는 것만으로는 부족하다. (블랙박스 테스트)

우선 테스트 커버리지를 측정하고 이에 대해 커버되지 않는 영역에 대해 화이트박스 테스트를 진행해야한다.

그렇다면 테스트 커버리지는 어떻게 측정할 수 있을까?

 

IntelliJ의 내장 기능 사용

Run ... with Coverage 기능

IntelliJ에서 테스트 코드를 실행할 때 "Run ... with Coverage" 기능을 활용하여 테스트 커버리지를 확인할 수 있다.

 

Test Coverage 결과

위와 같은 Test Coverage 결과를 받을 수 있으며, 특정 라인이 테스트 중에 실행되지 않았음을 알 수있다.

그리고 위 그림 상단에 하이라이트된 "Generate Coverage Report" 기능을 사용하며 HTML 형식의 리포트도 받을 수 있다.

 

패키지 단위의 테스트 커버리지

 

클래스 단위의 테스트 커버리지

 

코드 라인 단위의 테스트 커버리지

위의 3개의 그림과 같이 패지키, 클래스, 라인 단위의 테스트 커버리지를 확인할 수 있다.

특히 라인 단위의 테스트 커버리지 리포트는 테스트되지 않는 라인을 붉은 색으로 표현해준다.

 

JaCoCo 활용하기

인텔리제이의 내장 기능을 활용하더라도 간단하게 테스트 커버리지를 확인하고 리포트를 생성할 수 있다.

그러나 명령줄 프롬프트에서 테스트를 실행하고 리포트를 생성하도록 구현되어야 CI/CD에 활용할 수 있다.

이러한 경우에 JaCoCo 플러그인을 사용해서 구현할 수 있다.

 

https://docs.gradle.org/current/userguide/jacoco_plugin.html

 

The JaCoCo Plugin

The JaCoCo plugin adds the following dependency configurations: Table 2. JaCoCo plugin - dependency configurations Name Meaning jacocoAnt The JaCoCo Ant library used for running the JacocoReport and JacocoCoverageVerification tasks. jacocoAgent The JaCoCo

docs.gradle.org

위 링크를 참고하여 JaCoCo 플러그인을 추가해보도록 하자.

 

plugins {
    id 'java'
    id 'jacoco'
}

group = 'jaeseok.sample'
version = '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    testImplementation platform('org.junit:junit-bom:5.9.1')
    testImplementation 'org.junit.jupiter:junit-jupiter'
}

test {
    useJUnitPlatform()
    finalizedBy jacocoTestReport // report is always generated after tests run
}

jacoco {
    toolVersion = "0.8.12"
}

jacocoTestReport {
    dependsOn test // tests are required to run before generating the report
    reports {
        xml.required = false
        csv.required = false
        html.outputLocation = layout.buildDirectory.dir('jacocoHtml')
    }
}

위와 같이 jacoco 플러그인 설정을 마치고 "./gradlew test" 명령어를 실행하면 build/jacocoHtml 하위에 리포트 파일이 생성된다.

 

jacoco html report

위와 같이 정상적으로 리포트 파일이 생성되었음을 확인할 수 있다.

 

gradle.build 파일에 JacocoCoverageVerification task를 설정하여 verification rule들을 설정할 수도 있고 이 룰에 위반되는 경우 빌드에 실패하도록 처리할 수 있다.

jacocoTestCoverageVerification {
    violationRules {
        rule {
            limit {
                minimum = 0.9
            }
        }

        rule {
            enabled = true
            element = 'CLASS'
            includes = ['org.gradle.*']

            limit {
                counter = 'LINE'
                value = 'TOTALCOUNT'
                maximum = 0.9
            }
        }
    }
}

위와 같이 task를 추가한 후 "./gradlew test jacocoTestCoverageVerification" 명령어를 실행하면 빌드가 실패한다.

 

빌드 실패

설정된 값에 의하면 instructions 커버리지가 최소 0.9여야 하나 실제 0.6이기 때문이다.

 

 

테스트하기 쉬운 코드 작성하기

기본적으로 가독성이 높고 간단한 구조의 코드가 테스트하기 용이하다.

 

public API 잘 설계하기

코드를 작성하다보면 public API를 생산하여 상용 솔루션이나 오픈 소스로 제공하게 된다.

이 public API의 사용자가 생기게되면 쉽게 수정할 수가 없다.

이러한 경우에 테스트 코드가 작성되어있다면 어느정도 리팩토링이 가능하겠지만, API 시그니처를 수정할 수는 없다.

그렇기 때문에 public API는 잘 설계해서 변경이 일어나서는 안된다.

 

의존성 줄이기

단위 테스트는 코드를 격리한 상태에서 실행된다.

그렇기 때문에 의존성 관리가 제대로되지 않는다면 테스트하기 까다로워진다.

public class Car {

    private Driver driver;
    
    public Car {
        this.driver = new TaxiDriver();
    }
    
    public String drive() {
        return "Driving by " + this.driver.getName();
    }
}

위 코드와 같이 Car.drive() 메서드를 테스트한다고 가정해보자.

Car는 TaxiDriver 클래스에 의존하고 있다. 그러나 Car 클래스를 사용하는 클라이언트는 이 사실을 알기 어렵다.

 

만약 Car 클래스가 더 이상 TaxiDriver가 아닌 BusDriver에 의존성을 가지도록 수정되어야한다고 가정해보자.

drive() 메서드가 "Driving by taxi driver" 대신 "Driving by bus driver"를 반환하기 때문에 기존 작성된 테스트가 깨질 것이다. 

그리고 이 시점부터 drive() 메서드를 사용하던 모든 클라이언트를 고려해서 새로운 메서드를 만들어야할 수도 있다.

 

public class Car {

    private Driver driver;
    
    public Car(Driver driver) {
        this.driver = driver;
    }
    
    public String drive() {
        return "Driving by " + this.driver.getName();
    }
}

위와 같이 Car의 생성자에서 Driver 클래스의 인스턴스를 주입받도록 수정하게 되면, 각 클라이언트에서 Driver의 구현 클래스를 결정하게 될 것이며 BusDriver가 추가된다고 하더라도 필요한 부분에서 주입시켜 사용하게 될 것이다.

또한 테스트 코드 역시 BusDriver 의존성을 주입하는 테스트 코드를 추가하기만 하면 된다.

 

제네릭 메서드 사용하기

제네릭 메서드를 사용한다는 것은 컴파일 타임이 아닌 런타임에 타입을 정하기 때문에 재사용을 고려한 설계라고 볼 수 있다.

코드를 재사용하게 되면 재사용될 코드에만 테스트 코드를 작성하면 되기 때문에 테스트 커버리지를 올리기 훨씬 수월하다.

 

public static Set<String> union(Set<String> set1, Set<String> set2) {
    Set<String> unionSet = new HashSet(set1);
    unionSet.addAll(set2);
    return unionSet;
}

위와 같이 String 타입을 요소로 가지는 두 개의 Set을 합치는 메서드가 있다고 가정하자.

이 때 만에 Integer 타입과 Double 타입에 대해 동일한 요구사항이 발생해 두 개의 메서드가 더 생기게 되면 두 개의 테스트 코드를 추가로 작성해야한다.

 

public static <E> Set<E> union(Set<E> set1, Set<E> set2) {
    Set<E> unionSet = new HashSet(set1);
    unionSet.addAll(set2);
    return unionSet;
}

위와 같이 코드를 수정하게되면 하나의 테스트 코드로 여러 타입을 커버할 수 있게 된다.

또한 하나의 메서드로 중복 없이 사용할 수 있어 소스 코드의 전반적인 복잡도도 줄일 수 있다.

 

다음 글: https://gojs.tistory.com/73

 

격리하여 단위 테스트 작성하기 (Stub, Mock)

`단위 테스트는 코드 내의 단위(클래스, 메서드) 범위를 테스트하기 때문에 다른 객체나 외부 리소스에 의존성을 가지는 경우 테스트를 작성하기 까다롭다.예를 들면 데이터베이스에 의존하는

gojs.tistory.com

'공부 > 테스트' 카테고리의 다른 글

격리하여 단위 테스트 작성하기 (Stub, Mock)  (0) 2025.04.20
테스트의 종류와 필요성  (0) 2025.04.13
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/08   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
글 보관함