Skip to content
isdnetworks
Go back

100%가 안 되면 어디까지 되는지

시험 코드 적용률을 100%로 하자는 얘기가 나왔다. 해 보니 안 되는 부분이 있었다.

그대로 두면 못 하겠다는 결론으로 끝날 상황이었다.

Table of contents

Open Table of contents

안 되는 것을 목록으로 만들었다

무엇 때문에 안 되는지를 먼저 적었다.

1. 하드웨어에 직접 접근하는 코드      장치가 있어야 함
2. 시간에 의존하는 코드              특정 시각에만 도는 분기
3. 외부 연동                        상대 서버가 필요
4. 시작·종료 처리                   프로세스 수명과 얽힘
5. 오류 처리 중 일부                재현이 어려움

다섯 가지였는데 막연히 안 된다고만 하면 정말 안 되는 것인지 방법을 모르는 것인지가 안 갈린다.

적어 놓고 각각 정말 안 되는지 다시 봤다. 이유별로 묶으니 덩어리마다 다시 볼 여지가 생겼다.

나눠 보니 되는 것이 있었다

하드웨어 접근부터 봤다. 읽고 쓰는 함수를 감싸면 그 감싼 층은 대체할 수 있고 실제로 장치를 만지는 부분만 남는다.

/* 원래 */
int read_temp(float *out) {
    uint8_t buf[2];
    if (i2c_read(ADDR, REG, buf, 2) != 0) return -1;
    *out = convert(buf);
    return 0;
}

/* 나눔 */
int read_temp_raw(uint8_t *buf);          /* 하드웨어 — 시험 불가 */
float convert_temp(const uint8_t *buf);   /* 계산 — 시험 가능 */

i2c_read 를 부르는 read_temp_raw 만 시험이 안 되고 convert_temp 는 값만 넣으면 된다. 실제로 이 변환식에서 계수 오류가 나왔다.

시간 의존도 같은 방식으로 풀렸다.

int should_report(time_t now);   /* time(NULL) 을 안에서 안 부른다 */

time(NULL) 을 함수 안에서 부르지 않고 인자로 받게 하면 어느 시각이든 넣어 볼 수 있다. 외부 연동은 응답을 파일로 두고 그것을 읽어 처리하는 부분만 시험했다.

다섯 중 셋이 나눠서 시험 가능해졌고 안 된다고 적힌 것의 절반 넘게가 방법을 몰랐던 쪽이었다.

목표 — 부분별 도달선

나머지 둘은 실제로 어려웠으므로 목표를 하나로 두지 않고 부분별로 나눴다.

전체            100%  → 불가
계산·변환 로직    100%  → 가능
분기·조건        90%   → 가능
하드웨어 접근     0%    → 대신 실장비 확인
시작·종료        0%    → 대신 실행 확인

전체 100%가 안 되면 어느 부분이 얼마나 되는지로 나누고 목표가 부분마다 다르면 어디가 모자란지도 그 단위로 나온다.

전체 하나의 숫자로 두면 어느 쪽을 올려도 같은 숫자가 오른다. 그러면 올리기 쉬운 쪽만 올리고 중요한 자리는 남는다.

안 되는 것을 어떻게 확인할지 정했다

시험 코드로 못 하는 것은 다른 방법을 붙였다.

하드웨어 접근   실장비에서 확인 (확인 목록 작성)
시작·종료      실행해서 확인 (기동·정상종료·강제종료 3가지)

확인 목록을 만들어 두고 배포 전에 사람이 훑는데 종료는 정상 종료와 강제 종료를 나눠서 각각 해 본다.

자동으로 못 하는 것을 확인 안 함으로 두지 않았다. 확인하는 방법이 다를 뿐 사람이 해 보는 것도 확인이다.

검증 — 적용률을 세는 방식

숫자가 어떻게 나오는지도 봤다. 줄 기준으로 세면 시험 불가인 하드웨어 코드가 분모에 그대로 들어간다.

그래서 제외 표시를 붙였다.

/* LCOV_EXCL_START */
int i2c_read(uint8_t addr, uint8_t reg, uint8_t *buf, size_t len) {
    ...
}
/* LCOV_EXCL_STOP */

LCOV_EXCL_STARTLCOV_EXCL_STOP 사이를 분모에서 뺀다. 제외한 뒤 숫자가 62%에서 91%가 됐다.

제외를 남용하면 숫자만 예뻐진다. 그래서 무엇을 왜 뺐는지를 목록으로 남겼다.

docs/coverage-exclusions.md

i2c_read()       하드웨어 직접 접근. 실장비 확인 항목 3-1
main()           프로세스 시작. 실행 확인 항목 1-1
signal_handler   신호 처리. 실행 확인 항목 1-3

각 항목이 다른 확인 방법과 번호로 연결돼 있다. 뺀 것이 어디서 확인되는지가 같이 적혀 있어야 뺀 것이 사라지지 않는다.

숫자보다 못 본 것이 중요했다

91%라는 숫자보다 나머지 9%가 무엇인지가 중요했다. 목록을 보니 오류 처리 분기가 대부분이었다.

정상 흐름은 다 돌아가는데 오류 경로가 안 돌아갔고 실제로 문제가 나는 자리는 대개 그쪽이다.

숫자가 높다는 것과 위험한 자리를 봤다는 것이 다른 말이었다. 91%가 좋아 보이지만 안 본 9%가 하필 그 자리면 그 숫자는 안심할 근거가 못 된다.

조치 — 오류 경로를 돌리는 장치

오류를 일부러 만드는 장치를 넣었다.

#ifdef TEST_BUILD
int g_force_i2c_fail = 0;
#endif

int read_temp_raw(uint8_t *buf) {
#ifdef TEST_BUILD
    if (g_force_i2c_fail) return -1;
#endif
    return i2c_read(...);
}

TEST_BUILD 일 때만 g_force_i2c_fail 을 보고 실패를 돌려준다. 운영 빌드에는 이 줄이 아예 안 들어간다.

이걸로 오류 경로를 돌려 보다가 결함이 나왔는데 실패했을 때 buf 를 초기화하지 않고 있었다.

일부러 실패시키지 않았으면 못 찾았을 것이다. 현장에서 i2c_read 가 실패한 뒤에야 드러났을 결함이다.

정리


Share this post on:

Previous Post
같은 쓰기를 두 방식으로 했다
Next Post
마커를 맨 마지막에 쓴다