테스팅 가이드
Vais 프로그램의 품질을 보장하기 위한 테스트 작성 방법을 안내합니다.
개요
Vais는 다양한 수준의 테스트를 지원합니다:
- 단위 테스트: 개별 함수와 모듈의 동작 검증
- 통합 테스트: 여러 모듈 간 상호작용 검증
- 속성 기반 테스트: 무작위 입력으로 속성 검증
- E2E 테스트: 컴파일부터 실행까지 전체 파이프라인 검증
- 벤치마킹: 성능 측정 및 회귀 감지
단위 테스트
기본 테스트 함수
Vais에서 테스트 함수는 test_ 접두사로 시작하는 함수로 작성합니다:
# math.vais
F add(a: i64, b: i64) -> i64 {
a + b
}
F test_add_positive() {
assert_eq(add(2, 3), 5)
}
F test_add_negative() {
assert_eq(add(-5, 3), -2)
}
F test_add_zero() {
assert_eq(add(0, 0), 0)
}
Assert 빌트인
Vais는 다음 assert 빌트인을 제공합니다:
# 조건 검증
assert(condition)
assert(x > 0)
# 값 동등성 검증
assert_eq(actual, expected)
assert_eq(result, 42)
# 부동소수점 근사 비교 (epsilon: 1e-10)
assert_approx(3.14159, pi(), 0.0001)
구조체 테스트
S Point {
x: i64,
y: i64
}
F Point::distance_squared(self) -> i64 {
self.x * self.x + self.y * self.y
}
F test_point_distance() {
p := Point { x: 3, y: 4 }
assert_eq(p.distance_squared(), 25)
}
F test_point_origin() {
origin := Point { x: 0, y: 0 }
assert_eq(origin.distance_squared(), 0)
}
Enum 및 패턴 매칭 테스트
E Result<T, E> {
Ok(T),
Err(E)
}
F divide(a: i64, b: i64) -> Result<i64, str> {
I b == 0 {
R Result::Err("division by zero")
}
Result::Ok(a / b)
}
F test_divide_success() {
result := divide(10, 2)
M result {
Result::Ok(val) => assert_eq(val, 5),
Result::Err(_) => assert(false)
}
}
F test_divide_by_zero() {
result := divide(10, 0)
M result {
Result::Ok(_) => assert(false),
Result::Err(msg) => assert_eq(msg, "division by zero")
}
}
통합 테스트
모듈 간 테스트
여러 모듈을 조합하여 테스트합니다:
# tests/integration_test.vais
U std/vec
U std/string
F test_vec_operations() {
v := Vec::new()
v.push(10)
v.push(20)
v.push(30)
assert_eq(v.len(), 3)
assert_eq(v.get(0), 10)
assert_eq(v.get(2), 30)
}
F test_string_manipulation() {
s := String::from("hello")
s.push_str(" world")
assert_eq(s.len(), 11)
assert_eq(s.as_str(), "hello world")
}
파일 I/O 테스트
U std/fs
F test_file_roundtrip() {
path := "/tmp/test.txt"
content := "test data"
# 파일 쓰기
fs::write_file(path, content)
# 파일 읽기
read_content := fs::read_file(path)
assert_eq(read_content, content)
# 정리
fs::remove_file(path)
}
속성 기반 테스트
vais-testgen 크레이트를 사용하여 속성 기반 테스트를 작성할 수 있습니다:
U vais_testgen/property
# 속성: 리스트를 정렬한 후 뒤집으면 내림차순 정렬과 같다
F prop_sort_reverse() {
gen := property::list_i64(100)
property::check(gen, |xs| {
sorted := xs.sort()
reversed := sorted.reverse()
descending := xs.sort_by(|a, b| b - a)
assert_eq(reversed, descending)
})
}
# 속성: 문자열 연결은 결합법칙을 만족한다
F prop_string_concat_associative() {
gen := property::string_gen(50)
property::check_triple(gen, |a, b, c| {
left := a.concat(b).concat(c)
right := a.concat(b.concat(c))
assert_eq(left, right)
})
}
E2E 테스트
컴파일 및 실행 테스트
# 컴파일 성공 테스트
vaisc examples/hello.vais -o hello
./hello
# 출력 검증
output=$(./hello)
test "$output" = "Hello, Vais!"
# 종료 코드 검증
./hello
test $? -eq 0
에러 케이스 테스트
# 컴파일 에러 테스트
vaisc examples/invalid.vais 2>&1 | grep "E032: Type inference failed"
# 런타임 에러 테스트 (예상된 panic)
vaisc examples/bounds_check.vais -o bounds_check
! ./bounds_check # 실패가 예상되므로 ! 사용
Rust 기반 E2E 테스트
// crates/vaisc/tests/e2e_tests.rs
#[test]
fn test_fibonacci() {
let output = std::process::Command::new("cargo")
.args(&["run", "--bin", "vaisc", "--", "examples/fibonacci.vais"])
.output()
.expect("Failed to execute vaisc");
assert!(output.status.success());
// 컴파일된 바이너리 실행
let result = std::process::Command::new("./fibonacci")
.output()
.expect("Failed to execute fibonacci");
assert_eq!(String::from_utf8_lossy(&result.stdout), "55\n");
}
벤치마킹
Criterion 기반 벤치마크 (Rust)
// benches/my_benchmark.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use vais_parser::Parser;
use vais_lexer::tokenize;
fn bench_parser(c: &mut Criterion) {
let source = std::fs::read_to_string("examples/large.vais").unwrap();
let tokens = tokenize(&source).unwrap();
c.bench_function("parse_large_file", |b| {
b.iter(|| {
let mut parser = Parser::new(black_box(&tokens));
parser.parse_module()
})
});
}
criterion_group!(benches, bench_parser);
criterion_main!(benches);
런타임 벤치마크 (Vais)
# benches/bench_sorting.vais
U std/vec
U std/time
F benchmark_bubble_sort(size: i64) -> i64 {
v := Vec::with_capacity(size)
# 무작위 데이터 생성
L i:0..size {
v.push(size - i)
}
start := time::now_micros()
bubble_sort(&v)
end := time::now_micros()
end - start # 마이크로초 단위 반환
}
F bubble_sort(arr: &mut Vec<i64>) {
n := arr.len()
L i:0..n {
L j:0..(n - i - 1) {
I arr.get(j) > arr.get(j + 1) {
swap(arr, j, j + 1)
}
}
}
}
F main() -> i64 {
time_1k := benchmark_bubble_sort(1000)
time_5k := benchmark_bubble_sort(5000)
print_i64(time_1k)
print_i64(time_5k)
0
}
컴파일 후 실행:
vaisc benches/bench_sorting.vais --emit-ir -o bench_sorting.ll
clang bench_sorting.ll -o bench_sorting -lm
./bench_sorting
코드 커버리지
LLVM Source-Based Coverage
Vais 컴파일러는 --coverage 플래그로 LLVM 커버리지를 지원합니다:
# 커버리지 활성화하여 컴파일
vaisc --coverage src/main.vais -o main
# 실행하여 프로파일 생성
./main # default.profraw 생성됨
# HTML 리포트 생성
llvm-profdata merge -sparse default.profraw -o default.profdata
llvm-cov show ./main -instr-profile=default.profdata -format=html > coverage.html
Rust 컴파일러 커버리지
# Rust 컴파일러 테스트 커버리지
cargo install cargo-llvm-cov
cargo llvm-cov --html --open
CI 통합
GitHub Actions 워크플로우
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install LLVM 17
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 17
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Run tests
run: cargo test --workspace
- name: Run clippy
run: cargo clippy --workspace -- -D warnings
- name: E2E tests
run: |
cargo build --release
./target/release/vaisc examples/hello.vais -o hello
./hello
커버리지 리포팅
# codecov 통합
- name: Generate coverage
run: cargo llvm-cov --lcov --output-path lcov.info
- name: Upload to codecov
uses: codecov/codecov-action@v3
with:
files: lcov.info
테스트 조직화
디렉토리 구조
my_project/
├── src/
│ ├── main.vais
│ ├── lib.vais
│ └── utils.vais
├── tests/
│ ├── unit/
│ │ ├── test_lib.vais
│ │ └── test_utils.vais
│ ├── integration/
│ │ └── test_full_pipeline.vais
│ └── e2e/
│ └── test_cli.sh
└── benches/
└── bench_performance.vais
테스트 명명 규칙
- 단위 테스트:
test_<function_name>_<scenario> - 통합 테스트:
test_<module>_<interaction> - E2E 테스트:
test_e2e_<feature> - 벤치마크:
bench_<operation>_<size>
모범 사례
- 독립성: 각 테스트는 독립적으로 실행 가능해야 합니다
- 결정성: 테스트 결과는 항상 동일해야 합니다
- 빠른 실행: 단위 테스트는 밀리초 단위로 실행되어야 합니다
- 명확한 실패 메시지: assert 시 의미 있는 메시지를 포함합니다
- 경계 조건: 0, 음수, 최대값 등 경계 케이스를 테스트합니다
- 에러 경로: 정상 경로뿐만 아니라 에러 경로도 테스트합니다