본문 바로가기
SPA/React

[React] Jest로 테스트 커버리지 확인하기 (2) - 커버리지 비율 한계점

by J4J 2023. 11. 20.
300x250
반응형

안녕하세요. J4J입니다.

 

이번 포스팅은 Jest로 테스트 커버리지 확인하는 방법 마지막인 커버리지 비율 한계점에 대해 적어보는 시간을 가져보려고 합니다.

 

 

 

이전 글

 

[React] Jest로 테스트 커버리지 확인하기 (1) - 개념과 설정 방법

 

 

반응형

 

 

커버리지 비율 한계점이란?

 

커버리지 비율 한계점은 커버리지 동작을 위해 수집된 코드들에서 테스트 코드로 동작되는 코드들의 비율이 충족되어야만 하는 기준치를 의미합니다.

 

이전 글을 확인해 보시면 커버리지 비율 한계점을 설정하기 위해서는 jest.config.js에서 coverageThreshold 값을 활용하면 가능했었고, 설정되는 값들에는 statements, branches, functions, lines와 같이 총 4가지가 있는 것을 볼 수 있었습니다.

 

이번 글에서는 statements, braches, functions, lines 등이 정확히 무엇을 의미하는지 확인해 보고 테스트 코드를 활용하여 비율을 점점 높여보는 예제 소스를 같이 보도록 하겠습니다.

 

 

 

 

Statements

 

statements는 구문을 의미합니다.

 

statements가 비율 측정을 위해 수집되는 코드들 중 1개의 갯수로 판단되는 것은 상황에 따라 다르지만 이해하기 쉬운 것은 ";" 기준이라고 생각하면 조금 더 이해하기 좋을 것 같습니다.

 

예를 들어 다음의 코드들이 예시가 될 수 있습니다.

 

export function increaseValue(value: number) { // 1 count (선언형)
    return value + 1; // 1 count
}

export const decrease = () => { // 2 count (표현형)
    const decreaseValue = (value: number) => { // 1 count (내부함수)
        if (value <= 0) { // 1 count (if-else)
            return 0; // 1 count
        } else {
            return value - 1; // 1 count
        }
    };

    return { // 1 count
        decreaseValue,
    };
};

export const multiplyValue = (value: number) => { // 2 count (표현형)
    switch (value) { // 1 count (switch-case)
        case 1: {
            return 1 * 1; // 1 count
        }
        case 2: {
            return 2 * 2; // 1 count
        }
        default: {
            return value * value; // 1 count
        }
    }
};

 

 

 

그래서 위의 코드들이 담겨있는 js 파일인 경우 총 15개의 statements가 존재하는 것이고 얼마나 실행되었냐에 따라 비율이 계산됩니다.

 

또한 혹시나 헷갈려하시는 분들을 위해 다음과 같이 한 줄에 코드들을 모두 작성하더라도 ";" 기준으로 구분을 한다고 생각하면 statements가 총 몇 개인지, 어떻게 비율을 높일 수 있는지를 생각해 볼 수 있습니다.

 

export const multiplyValue = (value: number) => { // 2 count (표현형)
    switch (value) { case 1: { return 1 * 1; } case 2: { return 2 * 2; } default: { return value * value; } } // 4 count
};

 

 

 

 

Branches

 

branches는 분기를 의미합니다.

 

분기라고 하는 것은 if-else, switch-case 문과 같이 조건 값이 무엇인지에 따라 서로 다른 코드를 수행하는 것을 의미합니다.

 

그래서 branches 비율 측정을 위해 1개의 개수로 판단되는 것은 조건에 따라 다른 결과가 수행되는 곳들을 뜻하고 다음과 같은 예시 코드를 볼 수 있습니다.

 

export function increaseValue(value: number) {
    return value + 1;
}

export const decrease = () => {
    const decreaseValue = (value: number) => {
        if (value <= 0) { // 1 count (if)
            return 0;
        } else { // 1 count (else)
            return value - 1;
        }
    };

    return {
        decreaseValue,
    };
};

export const multiplyValue = (value: number) => {
    switch (value) {
        case 1: { // 1 count (case)
            return 1 * 1;
        }
        case 2: { // 1 count (case)
            return 2 * 2;
        }
        default: { // 1 count (case)
            return value * value;
        }
    }
};

 

 

 

위의 코드들이 담겨 있는 js 파일인 경우 총 5개의 branches가 존재하게 됩니다.

 

그리고 테스트 코드들에서 얼마나 분기 처리가 수행될 수 있는 파라미터 값이 넘어온 지에 따라 비율이 올라가게 됩니다.

 

 

 

 

Functions

 

functions는 함수를 의미합니다.

 

함수는 말 그대로 1개의 개수로 판단하는 것은 각각의 함수를 뜻하며 더 이상 설명이 없어도 많은 분들이 이해하실 거라고 생각됩니다.

 

functions에 대해서도 예시 코드를 작성해 보면 다음과 같습니다.

 

export function increaseValue(value: number) { // 1 count
    return value + 1;
}

export const decrease = () => { // 1 count
    const decreaseValue = (value: number) => { // 1 count
        if (value <= 0) {
            return 0;
        } else {
            return value - 1;
        }
    };

    return {
        decreaseValue,
    };
};

export const multiplyValue = (value: number) => { // 1 count
    switch (value) {
        case 1: {
            return 1 * 1;
        }
        case 2: {
            return 2 * 2;
        }
        default: {
            return value * value;
        }
    }
};

 

 

 

그래서 위의 코드들이 담겨 있는 js 파일에는 총 4개의 functions가 존재합니다.

 

그리고 함수가 얼마나 테스트 코드에서 실행되었느냐에 따라 비율이 올라가게 됩니다.

 

 

 

 

Lines

 

lines는 줄을 의미합니다.

 

말 그대로 작성된 코드들의 줄을 의미합니다.

 

하지만 1개의 개수로 판단할 땐 작성된 모든 줄의 코드들이 해당되지는 않습니다.

 

일반적인 경우에는 모두 1개로 측정이 되지만 if-else의 else와 switch-case의 case, default 등과 같은 것들은 1개로 측정되지 않습니다.

 

그래서 lines에 대해 예시 코드를 작성해 보면 다음과 같이 확인해 볼 수 있습니다.

 

export function increaseValue(value: number) { // 1 count
    return value + 1; // 1 count
}

export const decrease = () => { // 1 count
    const decreaseValue = (value: number) => { // 1 count
        if (value <= 0) { // 1 count
            return 0; // 1 count
        } else {
            return value - 1; // 1 count
        }
    };

    return { // 1 count
        decreaseValue,
    };
};

export const multiplyValue = (value: number) => { // 1 count
    switch (value) { // 1 count
        case 1: {
            return 1 * 1; // 1 count
        }
        case 2: {
            return 2 * 2; // 1 count
        }
        default: {
            return value * value; // 1 count
        }
    }
};

 

 

 

위의 코드들이 담겨있는 js 파일에서 lines는 총 13개입니다.

 

그리고 다른 값들과 마찬가지로 lines에 해당하는 줄이 테스트 코드에서 얼마나 실행되느냐에 따라 비율을 높일 수 있습니다.

 

 

 

 

비율 한계점 높이기 예제 코드

 

이번엔 예제 코드를 활용하여 비율 한계점을 순서대로 높여보도록 하겠습니다.

 

간단하게 코드를 작성해 보고 커버리지 결과도 확인해 보겠습니다.

 

 

 

[ 1. 렌더링 추가 ]

 

먼저 코드를 작성하고 테스트 코드에는 단순히 렌더링만 하는 테스트입니다.

 

렌더링만 할 경우 다음과 같이 비율에 대한 개수가 포함되는 것을 볼 수 있습니다.

 

// app.module.ts
export function increaseValue(value: number) { // statements (1/1), functions (0/1), lines (1/1)
    return value + 1; // statements (0/1), lines (0/1)
}

export const decrease = () => { // statements (2/2), functions (0/1), lines (1/1)
    const decreaseValue = (value: number) => { // statements (0/1), functions (0/1), lines (0/1)
        if (value <= 0) { // statements (0/1), branches (0/1), lines (0/1)
            return 0; // statements (0/1), lines (0/1)
        } else { // branches (0/1)
            return value - 1; // statements (0/1), lines (0/1)
        }
    };

    return { // statements (0/1), lines (0/1)
        decreaseValue,
    };
};

export const multiplyValue = (value: number) => { // statements (2/2), functions (1/1), lines (1/1)
    switch (value) { // statements (1/1), lines (1/1)
        case 1: { // branches (0/1)
            return 1 * 1; // statements (0/1), lines (0/1)
        }
        case 2: { // branches (0/1)
            return 2 * 2; // statements (0/1), lines (0/1)
        }
        default: { // branches (1/1)
            return value * value; // statements (1/1), lines (1/1)
        }
    }
};


// App.tsx
import { useState } from 'react'; // statements (1/1), lines (1/1)
import { decrease, increaseValue, multiplyValue } from './app.module'; // statements (1/1), lines (1/1)

export default function App() { // statements (1/1), functions (1/1), lines (1/1)
    const [value, setValue] = useState<number>(0); // statements (1/1), lines (1/1)

    const handleIncrease = () => { // statements (1/1), functions (0/1), lines (1/1)
        setValue(increaseValue(value)); // statements (0/1), lines (0/1)
    };

    const handleDecrease = () => { // statements (1/1), functions (0/1), lines (1/1)
        setValue(decrease().decreaseValue(value)); // statements (0/1), lines (0/1)
    };

    return ( // statements (1/1), lines (1/1)
        <main>
            {value > 3 ? ( // branches (0/1)
                <h2 data-testid="value-greater-three-text">
                    value가 3보다 크네요?, value끼리 곱한 값은 {multiplyValue(value)}
                </h2>
            ) : ( // branches (1/1)
                <h2 data-testid="value-lower-three-text">
                    value가 3보다 작거나 같네요?, value끼리 곱한 값은 {multiplyValue(value)}
                </h2>
            )}

            <button data-testid="increase-button" onClick={handleIncrease}>
                증가
            </button>

            <button data-testid="decrease-button" onClick={handleDecrease}>
                감소
            </button>
        </main>
    );
}


// app.test.tsx
import { render } from '@testing-library/react';
import App from './App';

describe('app test', () => {
    test('render test', () => {
        render(<App />);
    });
});

 

렌더링 추가 커버리지 결과

 

 

 

 

[ 2. 증가 버튼 한번 클릭 추가 ]

 

이번엔 증가 버튼 한 번을 클릭하는 테스트를 추가해 보겠습니다.

 

증가 버튼을 클릭하는 케이스가 추가된다면 increase와 관련된 기능들이 수행되면서 다음과 같이 비율이 변경되는 것을 볼 수 있습니다.

 

// app.module.ts
export function increaseValue(value: number) { // statements (1/1), functions (1/1), lines (1/1)
    return value + 1; // statements (1/1), lines (1/1)
}

export const decrease = () => { // statements (2/2), functions (0/1), lines (1/1)
    const decreaseValue = (value: number) => { // statements (0/1), functions (0/1), lines (0/1)
        if (value <= 0) { // statements (0/1), branches (0/1), lines (0/1)
            return 0; // statements (0/1), lines (0/1)
        } else { // branches (0/1)
            return value - 1; // statements (0/1), lines (0/1)
        }
    };

    return { // statements (0/1), lines (0/1)
        decreaseValue,
    };
};

export const multiplyValue = (value: number) => { // statements (2/2), functions (1/1), lines (1/1)
    switch (value) { // statements (1/1), lines (1/1)
        case 1: { // branches (1/1)
            return 1 * 1; // statements (1/1), lines (1/1)
        }
        case 2: { // branches (0/1)
            return 2 * 2; // statements (0/1), lines (0/1)
        }
        default: { // branches (1/1)
            return value * value; // statements (1/1), lines (1/1)
        }
    }
};


// App.tsx
import { useState } from 'react'; // statements (1/1), lines (1/1)
import { decrease, increaseValue, multiplyValue } from './app.module'; // statements (1/1), lines (1/1)

export default function App() { // statements (1/1), functions (1/1), lines (1/1)
    const [value, setValue] = useState<number>(0); // statements (1/1), lines (1/1)

    const handleIncrease = () => { // statements (1/1), functions (1/1), lines (1/1)
        setValue(increaseValue(value)); // statements (1/1), lines (1/1)
    };

    const handleDecrease = () => { // statements (1/1), functions (0/1), lines (1/1)
        setValue(decrease().decreaseValue(value)); // statements (0/1), lines (0/1)
    };

    return ( // statements (1/1), lines (1/1)
        <main>
            {value > 3 ? ( // branches (0/1)
                <h2 data-testid="value-greater-three-text">
                    value가 3보다 크네요?, value끼리 곱한 값은 {multiplyValue(value)}
                </h2>
            ) : ( // branches (1/1)
                <h2 data-testid="value-lower-three-text">
                    value가 3보다 작거나 같네요?, value끼리 곱한 값은 {multiplyValue(value)}
                </h2>
            )}

            <button data-testid="increase-button" onClick={handleIncrease}>
                증가
            </button>

            <button data-testid="decrease-button" onClick={handleDecrease}>
                감소
            </button>
        </main>
    );
}


// app.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import App from './App';

describe('app test', () => {
    test('render test', () => {
        render(<App />);
    });

    test('increase button one time test', async () => {
        render(<App />);
        await userEvent.click(screen.getByTestId('increase-button'));
    });
});

 

증가 버튼 한번 클릭 추가 커버리지 결과

 

 

 

 

[ 3. 감소 버튼 한번 클릭 추가 ]

 

다음으로 감소 버튼 한 번을 클릭하는 테스트를 추가해 보겠습니다.

 

증가 버튼 한 번을 클릭한 경우와 유사하게 decrease와 관련된 기능들이 수행되면서 비율이 올라가는 것을 볼 수 있습니다.

 

// app.module.ts
export function increaseValue(value: number) { // statements (1/1), functions (1/1), lines (1/1)
    return value + 1; // statements (1/1), lines (1/1)
}

export const decrease = () => { // statements (2/2), functions (1/1), lines (1/1)
    const decreaseValue = (value: number) => { // statements (1/1), functions (1/1), lines (1/1)
        if (value <= 0) { // statements (1/1), branches (1/1), lines (1/1)
            return 0; // statements (1/1), lines (1/1)
        } else { // branches (0/1)
            return value - 1; // statements (0/1), lines (0/1)
        }
    };

    return { // statements (1/1), lines (1/1)
        decreaseValue,
    };
};

export const multiplyValue = (value: number) => { // statements (2/2), functions (1/1), lines (1/1)
    switch (value) { // statements (1/1), lines (1/1)
        case 1: { // branches (1/1)
            return 1 * 1; // statements (1/1), lines (1/1)
        }
        case 2: { // branches (0/1)
            return 2 * 2; // statements (0/1), lines (0/1)
        }
        default: { // branches (1/1)
            return value * value; // statements (1/1), lines (1/1)
        }
    }
};


// App.tsx
import { useState } from 'react'; // statements (1/1), lines (1/1)
import { decrease, increaseValue, multiplyValue } from './app.module'; // statements (1/1), lines (1/1)

export default function App() { // statements (1/1), functions (1/1), lines (1/1)
    const [value, setValue] = useState<number>(0); // statements (1/1), lines (1/1)

    const handleIncrease = () => { // statements (1/1), functions (1/1), lines (1/1)
        setValue(increaseValue(value)); // statements (1/1), lines (1/1)
    };

    const handleDecrease = () => { // statements (1/1), functions (1/1), lines (1/1)
        setValue(decrease().decreaseValue(value)); // statements (1/1), lines (1/1)
    };

    return ( // statements (1/1), lines (1/1)
        <main>
            {value > 3 ? ( // branches (0/1)
                <h2 data-testid="value-greater-three-text">
                    value가 3보다 크네요?, value끼리 곱한 값은 {multiplyValue(value)}
                </h2>
            ) : ( // branches (1/1)
                <h2 data-testid="value-lower-three-text">
                    value가 3보다 작거나 같네요?, value끼리 곱한 값은 {multiplyValue(value)}
                </h2>
            )}

            <button data-testid="increase-button" onClick={handleIncrease}>
                증가
            </button>

            <button data-testid="decrease-button" onClick={handleDecrease}>
                감소
            </button>
        </main>
    );
}


// app.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import App from './App';

describe('app test', () => {
    test('render test', () => {
        render(<App />);
    });

    test('increase button one time test', async () => {
        render(<App />);
        await userEvent.click(screen.getByTestId('increase-button'));
    });

    test('decrease button one time test', async () => {
        render(<App />);
        await userEvent.click(screen.getByTestId('decrease-button'));
    });
});

 

감소 버튼 한번 클릭 추가 커버리지 결과

 

 

 

 

[ 4. 증가 네 번, 감소 한번 추가 ]

 

이제 마지막으로 분기 처리가 테스트되고 있지 않은 경우까지 모두 포함시키기 위해 버튼을 여러 번 클릭하는 경우를 추가해 보겠습니다.

 

해당 테스트까지 넣게 되면 파일에 존재하는 모든 코드들이 동작이 이루어지기 때문에 커버리지 비율이 모두 100으로 보이는 것을 확인할 수 있습니다.

 

// app.module.ts
export function increaseValue(value: number) { // statements (1/1), functions (1/1), lines (1/1)
    return value + 1; // statements (1/1), lines (1/1)
}

export const decrease = () => { // statements (2/2), functions (1/1), lines (1/1)
    const decreaseValue = (value: number) => { // statements (1/1), functions (1/1), lines (1/1)
        if (value <= 0) { // statements (1/1), branches (1/1), lines (1/1)
            return 0; // statements (1/1), lines (1/1)
        } else { // branches (1/1)
            return value - 1; // statements (1/1), lines (0/1)
        }
    };

    return { // statements (1/1), lines (1/1)
        decreaseValue,
    };
};

export const multiplyValue = (value: number) => { // statements (2/2), functions (1/1), lines (1/1)
    switch (value) { // statements (1/1), lines (1/1)
        case 1: { // branches (1/1)
            return 1 * 1; // statements (1/1), lines (1/1)
        }
        case 2: { // branches (1/1)
            return 2 * 2; // statements (1/1), lines (1/1)
        }
        default: { // branches (1/1)
            return value * value; // statements (1/1), lines (1/1)
        }
    }
};


// App.tsx
import { useState } from 'react'; // statements (1/1), lines (1/1)
import { decrease, increaseValue, multiplyValue } from './app.module'; // statements (1/1), lines (1/1)

export default function App() { // statements (1/1), functions (1/1), lines (1/1)
    const [value, setValue] = useState<number>(0); // statements (1/1), lines (1/1)

    const handleIncrease = () => { // statements (1/1), functions (1/1), lines (1/1)
        setValue(increaseValue(value)); // statements (1/1), lines (1/1)
    };

    const handleDecrease = () => { // statements (1/1), functions (1/1), lines (1/1)
        setValue(decrease().decreaseValue(value)); // statements (1/1), lines (1/1)
    };

    return ( // statements (1/1), lines (1/1)
        <main>
            {value > 3 ? ( // branches (1/1)
                <h2 data-testid="value-greater-three-text">
                    value가 3보다 크네요?, value끼리 곱한 값은 {multiplyValue(value)}
                </h2>
            ) : ( // branches (1/1)
                <h2 data-testid="value-lower-three-text">
                    value가 3보다 작거나 같네요?, value끼리 곱한 값은 {multiplyValue(value)}
                </h2>
            )}

            <button data-testid="increase-button" onClick={handleIncrease}>
                증가
            </button>

            <button data-testid="decrease-button" onClick={handleDecrease}>
                감소
            </button>
        </main>
    );
}


// app.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import App from './App';

describe('app test', () => {
    test('render test', () => {
        render(<App />);
    });

    test('increase button one time test', async () => {
        render(<App />);
        await userEvent.click(screen.getByTestId('increase-button'));
    });

    test('decrease button one time test', async () => {
        render(<App />);
        await userEvent.click(screen.getByTestId('decrease-button'));
    });

    test('increase button three time, decrease button one time test', async () => {
        render(<App />);
        await userEvent.click(screen.getByTestId('increase-button'));
        await userEvent.click(screen.getByTestId('increase-button'));
        await userEvent.click(screen.getByTestId('increase-button'));
        await userEvent.click(screen.getByTestId('increase-button'));

        await userEvent.click(screen.getByTestId('decrease-button'));
        await userEvent.click(screen.getByTestId('decrease-button'));
    });
});

 

증가 네번, 감소 한번 추가 커버리지 결과

 

 

 

 

CI 파이프라인 연계

 

마지막으로 확인할 수 있는 부분은 CI 파이프라인 연계 부분입니다.

 

일반적으로 많이 사용되는 CI 파이프라인은 github, gitlab, aws 등이 존재합니다.

 

이런 파이프라인들을 구축할 때 커버리지 비율에 대한 한계점을 설정하는 것은 일정 비율이 넘지 않은 소스 코드에 대한 파이프라인이 동작될 경우 동작을 멈추도록 도와줍니다.

 

 

 

저는 회사 업무 때문에 gitlab을 활용한 파이프라인 구축하여 사용하고 있기 때문에 gitlab으로 예시를 들면 다음과 같이 "패키지 설치 → 커버리지 → 로그 출력" 순서대로 동작되도록 .gitlab-ci.yml 파일을 작성해 볼 수 있습니다.

 

# 실행될 stage 지정 (위에서 아래로 차례대로 실행)
stages:
  - install
  - coverage
  - log

install:   # JOB 이름
  # 사용될 이미지 설정
  image: node:18.16.0
  # stage 설정
  stage: install
  # 실행될 script 설정
  script:
    - npm install
  artifacts:
    # 보관이 이루어질 경로 설정
    paths:
      - node_modules
    # 유효기간 설정
    expire_in: 1 days
  # JOB이 수행될 branch 설정 (설정된 branch에 push가 발생될 시 JOB 수행)
  only:
    - master

coverage:   # JOB 이름
  # 사용될 이미지 설정
  image: node:18.16.0
  # stage 설정
  stage: coverage
  # 실행될 script 설정
  script:
    - npm run coverage
  # JOB이 수행될 branch 설정 (설정된 branch에 push가 발생될 시 JOB 수행)
  only:
    - master

log:   # JOB 이름
  # 사용될 이미지 설정
  image: node:18.16.0
  # stage 설정
  stage: log
  # 실행될 script 설정
  script:
    - echo coverage pass
  # JOB이 수행될 branch 설정 (설정된 branch에 push가 발생될 시 JOB 수행)
  only:
    - master

 

 

 

그리고 소스 코드에서는 커버리지 비율을 설정하고 비율이 넘지 않도록 테스트 케이스를 구성한 뒤 gitlab에 push를 해보면 다음과 같은 결과를 확인할 수 있습니다.

 

파이프라인 수행 중지

 

gitlab-ci log

 

 

 

이처럼 파이프라인을 동작할 때에도 커버리지 비율 설정의 영향을 줄 수 있습니다.

 

만약 개발하고 있는 서비스에 파이프라인이 구축되어 있고 우리가 설정한 커버리지 비율이 넘을 때만 파이프라인을 동작시키고 싶을 때 활용하신다면 많은 도움이 될 수 있습니다.

 

 

 

 

 

 

 

 

 

이상으로 Jest로 테스트 커버리지 확인하는 방법 마지막인 커버리지 비율 한계점에 대해 간단하게 알아보는 시간이었습니다.

 

읽어주셔서 감사합니다.

 

 

 

728x90
반응형

댓글