시험 코드 적용률을 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_START 와 LCOV_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 가 실패한 뒤에야 드러났을 결함이다.
정리
- 목표에 못 미치면 못 한다가 아니라 어디까지 되는지를 본다
- 안 되는 이유를 목록으로 적고 각각 정말 안 되는지 다시 본다
- 하드웨어·시간·외부 의존은 감싸서 나누면 일부가 시험 가능해진다
time(NULL)을 안에서 부르지 말고 인자로 받게 한다- 전체 하나의 숫자 대신 부분별로 목표를 다르게 둔다
- 자동으로 못 하는 것은 확인 목록으로 사람이 훑는다
LCOV_EXCL_START로 뺀 것은 왜 뺐고 어디서 확인하는지를 남긴다- 숫자보다 남은 9%가 무엇인지가 중요하다
TEST_BUILD에서만 도는 장치로 오류 경로를 일부러 돌린다