C 언어 테스트 기반 개발 환경 구축기 (CLion & VSCode) Mac
안녕하세요! 크래프톤 정글 8기 진행 중인 고웅입니다. 크래프톤 정글 5주 차가 시작되며 본격적으로 C 언어 학습 및 자료구조 구현이 진행되고 있는데 정글에서 제공해 준 과제를 구현하고 있는데 테스트 케이스를 직접 터미널 입력으로 테스트하는 것이 불편하기도 했고 자료구조를 처음부터 전부 구현하는 것이 아니라 특정 함수의 기능만 구현하다 보니 오히려 구현 의도나 방법이 잘 생각나지 않고 이해가 되지 않는 것 같아 처음부터 구현을 할 수 있었으면 하는 바람이 있었습니다. 그래서 이 글에서는 제가 C 언어로 구축했던 테스트 기반 개발 환경을 소개합니다. Clion과 VSCode 두가지 IDE에서 테스트를 진행했으며 맥북에서는 테스트를 했으나 윈도우의 경우는 다를 수 있습니다. C에서도 테스트 주도 개발(TDD) 방식으로 검증할 수 있는 구조로 만들었습니다.
TDD 란 무엇이고 왜 필요할까?
테스트 주도 개발(TDD)를 들어보셨나요? 테스트 주도 개발 즉 어떠한 기능을 구현하기 전에 테스트 코드를 작성하고 해당 테스트를 통과하는 실제 구현 코드를 작성하며 기능을 개발하는 것입니다.
테스트 코드를 작성해야 한다는 이야기는 많이 들었지만 이를 몸소 느끼기는 쉽지 않습니다. 왜냐하면 실무에서는 자기 하는 일도 바쁜데 테스트 코드까지 작성한다는 것은 쉬운 일이 아니기 때문입니다.
제가 과거 스타트업 근무하던 당시 개발자 커리어를 가진지 얼마 되지 않았었고 사수도 없었고 회사의 요구에 맞게 기능을 구현은 해야 했던 때가 있었고 당시에는 하루하루 바쁘게 기능 구현과 들어오는 일을 끝내는데 집중을 했습니다. 그러다 시간이 조금 지나 새로운 기능을 추가하거나 기존 기능을 수정해야 하는데 분명 내가 다 짠 코드인데 왜 나도 뭔지 모르겠지? 수정을 했는데 왜 저기서 코드가 망가진 거지? 이런 의문들이 생겨났습니다. 그때부터 테스트 코드를 작성하기 위해 준비를 했고 폴더 구조를 갈아엎고 새로 구성하며 특정 기능, 특정 프로젝트에 맞는 코드들만 포함하도록 구성하려 노력했습니다.
그렇게 테스트 코드를 작성하고 더 나아가 기능 개발 전 테스트를 작성해야 겠다는 필요성을 인식하게 되었습니다. 물론 이를 매번 지키며 개발하는 것은 이상에 가깝다고 느낍니다. 스타트업 특성상 그런 거 걱정할 시간 없이 일단 문제부터 해결해야 하는 상황이 생기는데 그때는 일단 기능을 개발하되 추 후 테스트 코드들을 많이 추가해 검증을 하는 방식으로 진행하고는 했습니다.
무엇을 개선하고 싶었나?
그렇다면 크래프톤 정글에서 무엇을 개선하고 싶었던 것일까? 정글에서 준 과제가 생각보다 어려웠습니다. 예를 들어 링크드 리스트를 C로 구현해야 하는데 방금 까지 파이썬으로 알고리즘 공부를 하던 사람들이 한 순간에 C라는 언어로 태세를 전환하고 처음부터가 아니라 링크드 리스트, 큐, 이진트리 등을 구현해야 하는 것이 어렵다고 생각이 들었습니다. 정글의 방식에 적응했다 생각하는 본인도 첫날에 환경 설정을 한 이후에는 무엇을 해야 하는지 방향이나 목표를 잡지 못해 몇 시간이나 복잡한 생각이 들었습니다. 테스트 케이스가 있다는데 그건 어디 있는 거지? 내가 구현해야 하는 것은 무엇이지? 과제를 하는 게 좋을까? C언어 공부를 해야 하는 것이 맞을까? 이런 생각이 들었고 결국 과제를 해보며 부딪치자라고 생각했습니다. 하지만 결국 GPT의 힘을 빌리고 있던 저를 발견했습니다. 이래서는 도움이 되지는 않을 것 같다는 생각이 들었고 처음부터 내가 구현을 해보고 싶다는 생각이 들어 새로 환경을 구성하기 시작했습니다.
그러는 중 내가 구현한 기능을 잘 수행했는지 테스트 해볼 수 있는 방법이 있으면 좋겠다고 생각이 들었고 그래서 C언어 전용 테스트 프레임워크를 찾기 시작했습니다.
C 언어에서 사용 가능한 테스트 프레임워크 비교
C 언어는 다른 고급 언어(Python, Java 등)에 비해 테스트 도구가 많지는 않지만, 아래와 같은 대표적인 유닛 테스트 프레임워크들이 존재합니다.
프레임워크 | 특징 | 장점 | 단점 |
---|---|---|---|
MinUnit | 가장 작고 단순한 헤더 파일 기반 테스트 프레임워크 (함수 매크로만 사용) | 매우 가볍고 의존성 없음 | 기능이 단순함 |
Unity | 임베디드 시스템에서도 널리 사용됨 | 다양한 단언문, 출력 메시지 지원 | 파일 구조가 복잡하고 셋업 필요 |
Check | POSIX 기반의 유닛 테스트 프레임워크 | setup/teardown, 테스트 케이스 분리 지원 | 외부 의존성 있고 학습 비용 있음 |
CMocka | Check의 경량 버전, 테스트 isolation 제공 | 다양한 기능 제공, 리눅스 테스트에 적합 | MinUnit보다는 무거움 |
CuTest | MinUnit보다 살짝 발전된 구조, 테스트 그룹/러너 존재 | 경량 + 구조적 테스트 가능 | 설정 필요 |
위의 여러 테스트 프레임워크가 있지만 저는 MinUnit을 선택했습니다.
왜 MinUnit 을 선택했는가?
제가 왜 MinUnit을 선택했는가? 라면 단순합니다. 이 과제들이 서비스나 기능 개발 용이라면 더 좋은 테스트 도구를 사용하겠지만 현재 집중해야 하는 것은 결국 학습이니 학습에 큰 영향을 주면 안 된다는 것이 주된 생각이었습니다.
기준 | 이유 |
---|---|
✅ 간단한 사용법 | 테스트 프레임워크 자체를 배우는 데 시간을 쓰고 싶지 않았음 |
✅ 외부 빌드 도구 없음 | 복잡한 Makefile 없이 바로 사용할 수 있어야 함 |
✅ CMake와의 호환성 | CLion과 VSCode 모두에서 쉽게 연동 가능해야 했음 |
✅ 소스 코드 분석 학습 목적 | 직접 테스트 흐름을 이해하고 싶었기 때문에 매크로 기반이 유리했음 |
선택 결과
- MinUnit은 단 하나의 헤더파일(minunit.h)만 포함시키면 바로 테스트를 작성할 수 있었고,
- 단순한 매크로 기반 구조 덕분에 테스트 실행 흐름도 눈으로 따라가기 쉬웠습니다.
- 저는 C 프로젝트를 학습 목적으로 진행하고 있기 때문에, 테스트가 무거워지지 않고 직관적인 것이 가장 큰 장점이었습니다.
참고용 예시: MinUnit 테스트
mu_assert("head->data != 10", head->data == 10);
mu_run_test(test_insert_front);
이처럼 mu_assert()
로 직접 값을 비교하고, mu_run_test()
로 개별 테스트를 실행하면 전체 흐름을 쉽게 제어할 수 있습니다.
프로젝트 구조
project/
├── include/ # 헤더 파일
│ └── linkedlist.h
| └── minunit.h # 초경량 테스트 프레임워크
├── src/ # 구현 파일
│ └── linkedlist.c
├── test/ # 테스트 코드
│ └── test_linkedlist.c
├── main.c # 수동 테스트용 main
├── CMakeLists.txt
└── .vscode/ # VSCode 전용 설정 (선택)
프로젝트 구조를 보면 루트 폴더를 기준으로 사용자 지정 헤더 파일이 있는 include 폴더가 있고 그 아래 minunit.h 라는 헤더 파일이 있습니다. 이 외에도 실제 구현파일이 있는 src폴더가 있고 test 코드들이 있는 test 폴더가 있고 루트에 main.c 파일이 있는 구조입니다.
✅ 테스트 프레임워크: MinUnit
// minunit.h
#ifndef MINUNIT_H
#define MINUNIT_H
#define mu_assert(message, test) do { if (!(test)) return message; } while (0)
#define mu_run_test(test) do { char *message = test(); tests_run++; \
if (message) return message; } while (0)
extern int tests_run;
#endif
이 것이 제가 선택한 테스트 프레임 워크인 MinUnit 입니다. 이 코드를 헤더 파일로 하나 만들면 다른 코드에서 테스트를 진행할 수 있습니다.
예제 테스트: Linked List
static char * test_insert_front() {
Node* head = NULL;
insert_front(&head, 10);
insert_front(&head, 20);
mu_assert("insert_front failed", head->data == 20);
free_list(head);
return 0;
}
int main(void) {
char *result = all_tests();
if (result != 0) printf("%s\n", result);
else printf("ALL TESTS PASSED\n");
printf("Tests run: %d\n", tests_run);
return result != 0;
}
위와 같은 코드 방식으로 코드를 작성하는 것인데 이것도 복잡하다고 느끼신다면 지금은 대 AI 시대 니 MinUnit을 사용한 테스트 코드를 생성해 달라고 해도 될 것 같습니다. 우리가 중요하게 해보고 싶은 것은 구현이지 테스트 코드에 시간을 쏟으면 아깝기 때문입니다.
CMakeList.txt 만들기
저는 초기 clion을 사용하며 자연스럽게 CMake를 사용하게 되었는데 이 것이 무엇인지는 자세히 모릅니다. 느낌 상 빌드 하는 것을 도와주는 도구? 마치 자바의 Gradle 같은 도구라고 판단했습니다.
CMake는 C/C++ 프로젝트를 빌드하기 위한 오픈소스 크로스 플랫폼 빌드 자동화 도구로 컴파일과 링크 과정을 자동화 해주는 도구라고 합니다.
왜 필요한가?
예시: gcc 직접 컴파일
gcc -o myapp main.c module.c helper.c
정글 나침반 페이지에서 제공하는 내용을 보면 gcc컴파일 하기 이런 내용이 있는데 학습을 하는 것이니 직접 해 보는 것도 좋지만 매번 이렇게 컴파일을 하고 실행을 하기는 불편합니다.
CMake가 해주는 일
- 소스 파일이 많아도 한 번에 빌드하게 해줌
- 디버그/릴리즈 설정을 쉽게 구분 가능
- 라이브러리, 실행 파일, 테스트 파일 등 타깃을 나눠 빌드 가능
- CLion, VSCode, Ninja, Make 등 IDE나 빌드 시스템과 연동이 쉬움
모든 CMake 프로젝트는 CMakeLists.txt라는 설정 파일로 관리됩니다.
cmake_minimum_required(VERSION 3.30)
project(c_linkedlist C)
set(CMAKE_C_STANDARD 11)
add_library(linkedlist STATIC src/linkedlist.c)
add_executable(main_exec main.c)
target_link_libraries(main_exec linkedlist)
이렇게 하면 CMake가 자동으로:
- linkedlist.c → 라이브러리 빌드
- main.c → 실행 파일 빌드
- 연결도 자동으로 처리
# CMake의 최소 요구 버전 설정
cmake_minimum_required(VERSION 3.30)
# 프로젝트 이름과 사용 언어 지정
project(c_programing C)
# 사용할 C 표준 설정 (C11)
set(CMAKE_C_STANDARD 11)
# 실행 파일이 생성될 디렉토리를 지정 (기본은 루트이지만 여기선 bin/으로 분리)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
# include 디렉토리 추가 - 헤더 파일(.h)을 이 경로에서 찾게 해줌
include_directories(${CMAKE_SOURCE_DIR}/include)
# 🔹 라이브러리 빌드 설정
# src/linkedlist.c 안에 구현된 함수들을 정적 라이브러리로 빌드함
# 이후 여러 실행 파일에서 재사용 가능
add_library(linkedlist STATIC
src/linkedlist.c
)
# 🔹 main 실행용 실행 파일 생성
# main.c는 수동 테스트나 샘플 실행 용도로 사용됨
add_executable(main_exec
main.c
)
# 🔹 테스트 실행용 실행 파일 생성
# test_linkedlist.c 안에는 minunit을 기반으로 테스트 코드들이 있음
# minunit.h는 헤더 파일이지만 테스트 타겟에 직접 포함시켜도 무방
add_executable(test_linkedlist
test/test_linkedlist.c
include/minunit.h
)
# 🔗 실행 파일들이 linkedlist 라이브러리를 사용할 수 있도록 연결
# 이렇게 하면 main_exec나 test_linkedlist에서도 insert_end(), free_list() 등을 호출 가능
target_link_libraries(main_exec linkedlist)
target_link_libraries(test_linkedlist linkedlist)
이것은 제가 설정한 파일 예시이고 혹시 사용하실 예정이라면 본인에 맞게 수정을 해야 합니다. 이런 작업은 GPT도 잘 처리해 줍니다.
CLion 실행 방법
- Run > Edit Configurations (실행 -> 구성편집)
- main_exec와 test_linkedlist 실행 타깃 등록
- cmake-build-debug/bin/에 실행파일 생성되도록 CMake 설정
저는 main_exec의 실행파일이 cmake-build-debug 폴더 아래 bin이라는 폴더애서 실행되도록 설정을 했습니다. 이렇게 설정했던 이유는 프로그램을 실행할 때마다 루트 폴더에 실행파일들이 생성되는 것이 매우 꼴 보기? 싫었습니다.
이렇게 bin 폴더 안에 무엇인가 생겨나면 성공일 것입니다. 만약 이 생성 파일들과 폴더들이 보기 싫으시다면 gitignore에 등록하거나 프로젝트에서 제외하는 방식으로 안 보이게 처리할 수 있습니다.
맥 기준으로 cmd + , 를 눌러 설정을 열고
모양 및 동작 -> 범위 : 에서 새로운 범위를 만들고 해당 폴더를 제외하면 됩니다.
이후 좌측 상단에 프로젝트 부분에서 커스텀으로 만든 범위를 설정하면
이렇게 빌드 폴더 등이 보이지 않게 될 겁니다. 이제 실행을 해봐야겠죠?
// test_stack.c
#include <stdio.h>
#include "minunit.h"
int tests_run = 0;
// 예제 함수
int add(int a, int b) {
return a + b;
}
static char * test_add() {
mu_assert("error, add(2, 3) != 5", add(2, 3) == 5);
mu_assert("error, add(-1, 1) != 0", add(-1, 1) == 0);
return 0;
}
static char * all_tests() {
mu_run_test(test_add);
return 0;
}
int main(void) {
char *result = all_tests();
if (result != 0) {
printf("%s\n", result);
} else {
printf("ALL TESTS PASSED\n");
}
printf("Tests run: %d\n", tests_run);
return result != 0;
}
이 코드를 main.c 에 붙여 넣고 실행을 눌러봅시다.
이런 출력이 나왔다면 성공입니다. 이제 복잡한 테스트 코드를 GPT에게 요구한 뒤 해당 코드를 하나씩 구현하고 테스트하며 같이 학습을 진행해 보는 것은 어떨까요? 물론 malloc 사용 시 주의하면서 꼭 free 잘하세요!!
추가 - Vscode 환경 설정
- 확장 설치: C/C++, CMake Tools
- .vscode/launch.json 생성:
- .vscode/ 폴더 생성 후 settings.json & launch.json & tasks.json 추가
.vscode/settings.json
{
"cmake.sourceDirectory": "${workspaceFolder}",
"cmake.buildDirectory": "${workspaceFolder}/build",
"cmake.generator": "Ninja"
}
.vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Run main_exec",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build/bin/main_exec",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": true,
"MIMode": "lldb", // Mac일 경우
"preLaunchTask": "build"
}
]
}
.vscode/tasks.json
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"type": "shell",
"command": "cmake",
"args": [
"--build",
"build"
],
"group": {
"kind": "build",
"isDefault": true
}
}
]
}
- CMake 명령 실행 (VSCode 명령 팔레트)
- Cmd + Shift + P → CMake: Configure
- 다시 Cmd + Shift + P → CMake: Build
- Ctrl + (터미널) → cd build/bin → ./main_exec 실행도 가능
이때 만약 CMake가 보이지 않는 경우 CMake를 brew를 통해 다운로드하여야 합니다.
cmake 가 없는 경우
which cmake
결과 예시:
/opt/homebrew/bin/cmake
또는
/usr/local/bin/cmake
📌 아무것도 출력되지 않으면 CMake가 설치되지 않은 것이므로 아래 중 하나로 설치:
💻 macOS 기준 설치:
brew install cmake
설치 후 다시 which cmake로 확인
Cmd + Shift + P → Preferences: Open Settings (JSON) 선택하고 아래 추가:
"cmake.cmakePath": "/opt/homebrew/bin/cmake"
위 경로는 which cmake 결과와 맞게 수정해야 한다
예:
- 인텔 맥: /usr/local/bin/cmake
- 애플 실리콘: /opt/homebrew/bin/cmake
다시 VSCode에서 CMake Configure 시도
- Cmd + Shift + P → CMake: Configure 실행
- 그 후 CMake: Build 도 정상 작동해야 함
이후 빌드 시 실패하는 경우 Ninja 빌드 도구가 없어서 그런 것 일 수 없다.
방법 1: Ninja 설치하기 (추천)
Ninja는 빠르고 효율적인 빌드 시스템이라 VSCode + CMake 환경에서 자주 사용된다.
brew install ninja
방법 2: Ninja 대신 Makefiles 사용
만약 Ninja 설치를 원하지 않는다면, CMake 생성기를 Makefiles로 변경할 수도 있다.
{
"cmake.generator": "Unix Makefiles"
}
저장 후 다시 CMake: Configure 실행
추가 사항
vscode에서 무시된 폴더 숨기기
앞에서 Clion에서 보고 싶지 않은 빌드 폴더 등을 숨기는 작업을 했다. vscode에서도 같은 작업이 가능하다.
.vscode/settings.json추가
{
"files.exclude": {
"**/cmake-build-debug": true,
"**/build": true,
"**/.vscode": false // false면 `.vscode/`는 보이게 설정
},
"search.exclude": {
"**/cmake-build-debug": true,
"**/build": true
}
}
💡 files.exclude: 파일 탐색기에서 숨기기
💡 search.exclude: 검색 결과에서 제외