Rust로 작성해 보는 컴퓨터 구조 (8) - 외전: 유닛테스트
앞 장까지 모델을 하나 만들고 테스트할 때 화면에 결과를 출력하여 값을 확인하는 방식을 사용했다. 단순하고 간단하여 가장 빠르게 테스트 할 수 있는 방식이다. 그러나 모델이 계속 늘어서 몇 십개, 몇 백개가 된다고 가정했을 때도 유용한 테스트 방법은 분명히 아니다. 나중에 모델링 코드가 계속 커지는 것을 대비해서 지금쯤에 자동화 유닛 테스트를 만드는 것이 좋다.
로직 코드를 라이브러리로 리펙토링
유닛 테스트를 코드 본체와 분리해서 작성할 계획이다. 이렇게 하려면 유닛 테스트 코드에서 앞에 만든 여러 모델들에 접근할 수 있어야 한다. 현재는 러스트로 작성한 모델 객체를 main.rs에서 모듈로 선언해서 쓰고 있다. main.rs에 있는 모듈 선언을 main.rs 파일 밖으로 빼야만 외부에서 크레이트 라이브러리 형태로 접근할 수 있다.
러스트는 프로그래밍 언어 수준에서 라이브러리 파일 이름을 강제한다. main.rs에 선언한 모듈은 외부에서 접근 불가능하다. src/lib.rs 파일을 만들어서 모듈을 선언해야만 외부에서 크레이트 이름을 통해 접근이 가능하다. 이것은 프로그램의 로직과 실행을 분리하는 컨셉이다. main.rs에서는 실행만 하고 로직은 lib.rs를 통해서 구현하라는 뜻이다.
main.rs에 있는 모듈 선언을 lib.rs 파일을 만들어서 옮긴다.
{caption="lib.rs | 모듈 선언을 옮김"} pub mod transistor; pub mod gates;
위 코드 세 줄이 lib.rs 파일의 전체 내용이다. 선언을 lib.rs 파일에 만들어서 외부에 모듈 이름을 공개하는 것이다. 모듈 선언 코드가 이사했으니 main.rs 파일의 윗 부분 코드가 바뀌어야 한다.
{caption="main.rs | use 지시어로 모듈 이름 가져오기"} use programming_coms::transistor; use programming_coms::gates; use gates::GateInterfaces; fn main() { // ... 생략 ...
기존에 mod 지시어를 썼던 코드를 use 지시어로 바꿨다. programming_coms는 처음에 프로젝트를 생성할 때 쓴 프로젝트 이름이다. cargo는 프로젝트를 생성할 때 입력한 이름으로 기본 크레이트를 만든다. 그래서 위 코드에서 programming_coms는 프로젝트 이름이자 transistor, gates 모듈이 있는 크레이트 이름이다.
여기까지 작업하고 빌드 후 실행하면 기존과 같은 결과가 나온다. 코드를 변경했음에도 프로그램의 실행 결과가 같다는 말은 리펙토링을 성공적으로 잘 했다는 뜻이다.
테스트 생성
러스트는 cargo를 이용해 유닛 테스트를 자동으로 만든다. 우리는 정해진 형식에 맞춰 유닛 테스트 내용을 만들기만 하면 된다. cargo로 프로젝트를 새로 만들면 src 폴더가 생긴다. 지금까지 작업한 소스 코드 파일은 모두 src 폴더에 있다. 이 상태에서 src 폴더와 같은 레벨에 tests 폴더를 만든다. 그리고 일단 빈 파일로 gates_test.rs 파일을 만든다. 그러면 현재 파일과 폴더 구조는 다음과 같다.
$ ls -R
.:
Cargo.lock Cargo.toml src tests
./src:
gates.rs lib.rs main.rs transistor.rs
./tests:
gates_test.rs
일단 가장 간단한 인버터의 유닛테스트를 만든다. gates_test.rs 파일에 다음 코드를 작성한다.
{caption="gate_test.rs | 인버터 유닛 테스트"} extern crate programming_coms; use programming_coms::gates; #[test] fn test_inverter() { let mut inv = gates::NotGate::new(); assert_eq!(false, inv.proc(true)); assert_eq!(true, inv.proc(false)); }
첫 번째 줄에 extern crate 지시어로 src 폴더에 있는 모듈의 크레이트에 대한 접근을 선언한다. 그리고 3번째 줄에서 use 지시어로 gates 모듈 이름을 가져온다. 6번째 줄에서 11번째 줄이 유닛 테스트 코드다. 6번째 줄처럼 #[test] 매크로로 지정하면 cargo가 알아서 해당 함수를 유닛 테스트로 만든다. 테스트를 수행하는 코드는 9, 10번째 줄에 있는 assert_eq!() 함수다. inv.proc(true)면 결과는 false여야하고 inv.proc(false)면 결과는 true여야 한다는 뜻이다.
사실 이름 끝에 느낌표(!)가 붙은 함수는 매크로다. 러스트에서는 function-like macro라고 한다. 함수처럼 생긴 매크로라는 뜻이다. 매크로지만 사용법은 함수와 같기 때문에 함수로 이해해도 된다.
유닛 테스트가 잘 동작하는지 확인한다. 확인 방법은 cargo로 유닛 테스트를 실행해 보는 것이다. cargo test라는 명령을 사용한다.
$ cargo test
Compiling programming_coms v0.1.0 (/mnt/c/Users/testpc/Documents/Practice/ComAnato/src/programming_coms)
Finished dev [unoptimized + debuginfo] target(s) in 0.54s
Running target/debug/deps/programming_coms-93ef530c2127dc40
... 생략 ...
running 1 test
test test_inverter ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
... 생략 ...
test_inverter라는 테스를 실행해서 결과가 passed라고 화면에 출력된다. 유닛 테스트를 통과했다는 뜻이다. 이제부터 같은 방식으로 유닛 테스트를 만든다.
테스트 옮기기
간단한 인버터 유닛 테스트를 만들고 실행해서 잘 동작하는 것을 봤으므로 main() 함수에 있는 나머지 테스트 코드도 모두 유닛 테스트로 옮긴다.
{caption="gates_test.rs | 테스트를 모두 옮김"} extern crate programming_coms; use programming_coms::transistor; use programming_coms::gates; use gates::GateInterfaces; #[test] fn test_transistor() { let mut tr = transistor::Transistor::new(); tr.conn_c = false; assert_eq!(false, tr.proc(true)); assert_eq!(false, tr.proc(false)); tr.conn_c = true; assert_eq!(true, tr.proc(true)); assert_eq!(false, tr.proc(false)); } #[test] fn test_inverter() { let mut inv = gates::NotGate::new(); assert_eq!(false, inv.proc(true)); assert_eq!(true, inv.proc(false)); } #[test] fn test_and_gate() { let mut and_gate = gates::AndGate::new(); assert_eq!(false, and_gate.proc(false, false)); assert_eq!(false, and_gate.proc(false, true)); assert_eq!(false, and_gate.proc(true, false)); assert_eq!(true, and_gate.proc(true, true)); } #[test] fn test_or_gate() { let mut or_gate = gates::OrGate::new(); assert_eq!(false, or_gate.proc(false, false)); assert_eq!(true, or_gate.proc(false, true)); assert_eq!(true, or_gate.proc(true, false)); assert_eq!(true, or_gate.proc(true, true)); } #[test] fn test_nand_gate() { let mut nand_gate = gates::NandGate::new(); assert_eq!(true, nand_gate.proc(false, false)); assert_eq!(true, nand_gate.proc(false, true)); assert_eq!(true, nand_gate.proc(true, false)); assert_eq!(false, nand_gate.proc(true, true)); } #[test] fn test_nor_gate() { let mut nor_gate = gates::NorGate::new(); assert_eq!(true, nor_gate.proc(false, false)); assert_eq!(false, nor_gate.proc(false, true)); assert_eq!(false, nor_gate.proc(true, false)); assert_eq!(false, nor_gate.proc(true, true)); } #[test] fn test_xor_gate() { let mut xor_gate = gates::XorGate::new(); assert_eq!(false, xor_gate.proc(false, false)); assert_eq!(true, xor_gate.proc(false, true)); assert_eq!(true, xor_gate.proc(true, false)); assert_eq!(false, xor_gate.proc(true, true)); }
위 코드는 완성한 gates_test.rs 파일 전체 내용이다. 모두 같은 패턴으로 작성한 코드다. main() 함수서 결과값을 출력해 확인하던것을 assert_eq!() 함수를 통해 기대하는 값과 비교하여 테스트한다.
cargo test로 실행하면 위 코드에 새로 작성한 유닛 테스트의 결과를 볼 수 있다.
$ cargo test
... 생략 ...
running 7 tests
test test_and_gate ... ok
test test_inverter ... ok
test test_nand_gate ... ok
test test_nor_gate ... ok
test test_or_gate ... ok
test test_transistor ... ok
test test_xor_gate ... ok
test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
유닛 테스트 작업을 완료했다. 앞으로 추가하는 모델에 대한 테스트는 main() 함수에서 출력하는 방법 대신 유닛 테스트를 만들어서 할 것이다.
댓글
러스트는 빌트인 유닛 테스트 프레임워크가 매우 잘
러스트는 빌트인 유닛 테스트 프레임워크가 매우 잘 되어 있습니다. 서드파티 프레임워크를 쓸 필요를 느끼지 못할 정도입니다.
----------------------
얇은 사 하이얀 고깔은 고이 접어서 나빌레라
댓글 달기