이 시리즈는 Rust 공식문서를 통해 공부한 흔적임을 밝힙니다.
지난 시간 우리는 grep
을 단순화한 minigrep
프로젝트를 시작하였다.
이번 시간에는 테스트 주도 개발 방법을 통해 라이브러리 기능을 구현배도록 하겠다.
우리는 테스트 코드를 먼저 작성하고 그것을 성공시키는 코드를 구현하는
테스트 주도 개발 방법으로 기능을 구현하기로 하였다.
테스트 주도 개발 방법은 테스트 적용 범위를 충분히 확보하기 위한 개발 방법으로
다음과 같은 과정으로 이루어진다.
- 실패하는 테스트를 작성하고 실행하여 의도한 원인으로 인해 실패하는지 확인한다.
Write a test that fails and run it to make sure it fails for the reason you expect.- 작성한 테스트를 성공시킬 수 있도록 코드를 작성하거나 수정한다.
Write or modify just enough code to make the new test pass.- 추가 또는 수정한 코드를 리팩토링하고 그것이 여전히 성공하는지 확인한다.
Refactor the code you just added or changed and make sure the tests continue to pass.- 1단계부터 반복한다.
Repeat from step 1!
자, 그럼 우선 지난 시간에 작성한 lib.rs
에 테스트 모듈과 테스트 함수를 추가해보자.
테스트의 대상이 될, 아직 정상적으로 작동하지 않는 함수 search
를 정의하고
이에 대한 테스트를 작성한다.
peter@hp-laptop:~/rust-practice/chapter12/minigrep$ vi src/lib.rs
src/lib.rs
// snip pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { vec![] } #[cfg(test)] mod test { use super::*; #[test] fn one_result() { let query = "duct"; let contents = "\ Rust: safe, fast, productive. Pick three."; assert_eq!( vec!["safe, fast, productive."], search(query, contents) ); } }
search
함수가 반환할 것은 contents
의 일부이므로
반환값의 수명은 그것과 같도록 명시한다.
우린 아직 search
함수의 기능을 구현하지 않았기 때문에 이 테스트는 당연히 실패한다.
peter@hp-laptop:~/rust-practice/chapter12/minigrep$ cargo test
Compiling minigrep v0.1.0 (/home/peter/rust-practice/chapter12/minigrep)
# snip warnings
Finished test [unoptimized + debuginfo] target(s) in 0.37s
Running target/debug/deps/minigrep-bff0512f5e40b6c8
running 1 test
test test::one_result ... FAILED
failures:
---- test::one_result stdout ----
thread 'test::one_result' panicked at 'assertion failed: `(left == right)`
left: `["safe, fast, productive."]`,
right: `[]`', src/lib.rs:45:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
test::one_result
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
error: test failed, to rerun pass '--lib'
peter@hp-laptop:~/rust-practice/chapter12/minigrep$
테스트 코드를 작성했으니 이제 이를 성공시킬 수 있는 함수 본문을 작성해보자.
반복문을 통해 contesnts
를 한 줄씩 살펴보고 query
가 포함된 줄은
contain
메서드를 통해 검사하여 반환할 벡터에 추가하도록 한다.
peter@hp-laptop:~/rust-practice/chapter12/minigrep$ vi src/lib.rs
src/lib.rs
// snip pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { let mut results = Vec::new(); for line in contents.lines() { if line.contains(query) { results.push(line); } } results } // snip
이제 다시 테스트를 수행해보자.
peter@hp-laptop:~/rust-practice/chapter12/minigrep$ cargo test
Compiling minigrep v0.1.0 (/home/peter/rust-practice/chapter12/minigrep)
Finished test [unoptimized + debuginfo] target(s) in 0.44s
Running target/debug/deps/minigrep-bff0512f5e40b6c8
running 1 test
test test::one_result ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Running target/debug/deps/minigrep-4f288ac76df25b3e
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests minigrep
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
peter@hp-laptop:~/rust-practice/chapter12/minigrep$
테스트가 성공했다.
즉, 우리가 작성한 search
함수는 우리가 의도한 대로 작동한다는 걸 확인할 수 있다.
사실 search
함수는 보완할 수 있는 부분이 있지만
그건 추후에 반복자에 대해 더 자세히 다루면서 다시 이야기하도록 하겠다.
이제 run
함수가 search
함수를 호출하고
그 결과값을 출력하도록 수정해보자.
peter@hp-laptop:~/rust-practice/chapter12/minigrep$ vi src/lib.rs
src/lib.rs
pub fn run(config: Config) -> Result<(), Box<dyn Error>> { let contents = fs::read_to_string(config.filename)?; for line in search(&config.query, &contents) { println!("{}", line); } Ok(()) }
그리고 main
에 존재하는 불필요한 출력문은 지우거나 주석처리 한다.
peter@hp-laptop:~/rust-practice/chapter12/minigrep$ vi src/main.rs
src/main.rs
// println!("Search for {}", config.query); // println!("In file {}", config.filename);
이제 의도한 대로 실행되는 것을 확인할 수 있다.
peter@hp-laptop:~/rust-practice/chapter12/minigrep$ cargo run frog poem.txt
Compiling minigrep v0.1.0 (/home/peter/rust-practice/chapter12/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.23s
Running `target/debug/minigrep frog poem.txt`
How public, like a frog
peter@hp-laptop:~/rust-practice/chapter12/minigrep$
peter@hp-laptop:~/rust-practice/chapter12/minigrep$ cargo run body poem.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/minigrep body poem.txt`
I’m nobody! Who are you?
Are you nobody, too?
How dreary to be somebody!
peter@hp-laptop:~/rust-practice/chapter12/minigrep$
peter@hp-laptop:~/rust-practice/chapter12/minigrep$ cargo run 'to be' poem.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/minigrep 'to be' poem.txt`
How dreary to be somebody!
peter@hp-laptop:~/rust-practice/chapter12/minigrep$
peter@hp-laptop:~/rust-practice/chapter12/minigrep$ cargo run 'not exist' poem.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/minigrep 'not exist' poem.txt`
peter@hp-laptop:~/rust-practice/chapter12/minigrep$
이걸로 기본적인 기능을 하는 것은 확인했다.
이제 minigrep
에 한 가지 기능을 추가해보자.
환경 변수는 실행 환경을 설정하기 위한 변수로, 필요에 따라 동적으로 적용할 수 있다.
한 번 환경 변수를 등록하면 그 터미널 세션에서는 그것이 유지되어 다시 설정할 필요가 없다.
우리는 이걸 이용하여 minigrep
이 대소문자를 구분할 것인지
사용자가 선택하여 사용할 수 있도록 하겟다.
이번에도 역시 테스트 주도 개발 방법을 사용할 것이다.
대소문자를 구분하지 않고 탐색하기 위한 search_case_insensitive
함수를
그 기능은 구현하지 않은 채 정의한다.
기존의 one_result
테스트 함수를 case_sensitive
로 변경하고
새 테스트 함수 case_insensitive
를 추가한다.
그리고 테스트에 사용되는 contents
도 각각의 테스트에 적합하게 수정하겠다.
peter@hp-laptop:~/rust-practice/chapter12/minigrep$ vi src/lib.rs
src/lib.rs
// snip pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { vec![] } #[cfg(test)] mod test { use super::*; #[test] fn case_sensitive() { let query = "duct"; let contents = "\ Rust: safe, fast, productive. Pick three. Duct tape."; assert_eq!( vec!["safe, fast, productive."], search(query, contents) ); } #[test] fn case_insensitive() { let query = "rUsT"; let contents = "\ Rust: safe, fast, productive. Pick three. Trust me."; assert_eq!( vec!["Rust:", "Trust me."], search_case_insensitive(query, contents) ); } }
peter@hp-laptop:~/rust-practice/chapter12/minigrep$ cargo test
Compiling minigrep v0.1.0 (/home/peter/rust-practice/chapter12/minigrep)
# snip warnings
Finished test [unoptimized + debuginfo] target(s) in 0.47s
Running target/debug/deps/minigrep-bff0512f5e40b6c8
running 2 tests
test test::case_sensitive ... ok
test test::case_insensitive ... FAILED
failures:
---- test::case_insensitive stdout ----
thread 'test::case_insensitive' panicked at 'assertion failed: `(left == right)`
left: `["Rust:", "Trust me."]`,
right: `[]`', src/lib.rs:74:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
test::case_insensitive
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
error: test failed, to rerun pass '--lib'
peter@hp-laptop:~/rust-practice/chapter12/minigrep$
일단 case_sensitive
는 성공했지만 case_insensitive
는 실패한 걸 확인할 수 있다.
그럼 이제 테스트를 성공시키기 위한 search_case_insensitive
함수를 구현해보자.
search_case_insensitive
함수는 기본적으로 search
함수와 비슷하다.
다만 비교할 때 contents
와 query
를 모두 소문자 또는 대문자로 변환하여 비교함으로써
대소문자 구분 없이 비교할 수 있도록 한다.
우리는 모두 소문자로 변환하는 방식을 사용하겠다.
peter@hp-laptop:~/rust-practice/chapter12/minigrep$ vi src/lib.rs
src/lib.rs
// snip pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { let query = query.to_lowercase(); let mut results = Vec::new(); for line in contents.lines() { if line.to_lowercase().contains(&query) { results.push(line); } } results } // snip
여기서 주의할 건, to_lowercase
는 문자열 슬라이스가 아닌 문자열을 반환한다는 것이다.
따라서 이 녀석을 line.to_lowercase().contains
에 전달할 땐 &
를 붙여야 한다.
그렇지 않으면 소유권이 넘어가 다음 반복 때 오류가 발생할 것이다.
자, 이제 다시 테스트를 해보자.
peter@hp-laptop:~/rust-practice/chapter12/minigrep$ cargo test
Compiling minigrep v0.1.0 (/home/peter/rust-practice/chapter12/minigrep)
Finished test [unoptimized + debuginfo] target(s) in 0.44s
Running target/debug/deps/minigrep-bff0512f5e40b6c8
running 2 tests
test test::case_insensitive ... ok
test test::case_sensitive ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Running target/debug/deps/minigrep-4f288ac76df25b3e
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests minigrep
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
peter@hp-laptop:~/rust-practice/chapter12/minigrep$
테스트를 성공했으니 이제 이 함수를 우리 프로그램에 반영해주자.
Config
구조체에 대소문자 구분 여부를 나타내는 bool 값을 추가하고
run
에서 이 값에 따라 적절한 함수를 호출하도록 작성해준다.
그리고 이 bool 값은 환경 변수로 가져오도록 한다.
peter@hp-laptop:~/rust-practice/chapter12/minigrep$ vi src/lib.rs
src/lib.rs
use std::env; // snip pub fn run(config: Config) -> Result<(), Box<dyn Error>> { let contents = fs::read_to_string(config.filename)?; let results = if config.case_sensitive { search(&config.query, &contents) } else { search_case_insensitive(&config.query, &contents) }; for line in results { println!("{}", line); } Ok(()) } pub struct Config { pub query: String, pub filename: String, pub case_sensitive: bool, } impl Config { pub fn new(args: &[String]) -> Result<Config, &'static str> { if args.len() < 3 { return Err("not enough arguments"); } let query = args[1].clone(); let filename = args[2].clone(); let case_sensitive = env::var("CASE_INSENSITIVE").is_err(); Ok(Config { query, filename, case_sensitive }) } } // snip
CASE_INSENSITIVE
에 어떤 값이 설정되어 있든,
그것이 설정되어 있다면 대소문자를 구분하지 않고 탐색한다.
이 때, export
를 통해 환경 변수를 등록하면 해당 터미널 세션에서는 환경 변수가 유지되며
따로 등록하지 않고 실행할 때 cargo run
앞에 붙이면 그 때만 적용된다.
이제 그냥 실행하면 대소문자를 구분하여 실행되고
peter@hp-laptop:~/rust-practice/chapter12/minigrep$ cargo run to poem.txt
Compiling minigrep v0.1.0 (/home/peter/rust-practice/chapter12/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.40s
Running `target/debug/minigrep to poem.txt`
Are you nobody, too?
How dreary to be somebody!
peter@hp-laptop:~/rust-practice/chapter12/minigrep$
CASE_INSENSITIVE
를 설정하면 구분하지 않고 실행되는 것을 확인할 수 있다.
peter@hp-laptop:~/rust-practice/chapter12/minigrep$ CASE_INSENSITIVE=1 cargo run to poem.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/minigrep to poem.txt`
Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!
peter@hp-laptop:~/rust-practice/chapter12/minigrep$
그리고 export
를 해두면 이 터미널 세션에서는 환경변수가 유지되며
다른 터미널을 열고 실행했을 땐 적용되지 않는 것을 알 수 있다.
peter@hp-laptop:~/rust-practice/chapter12/minigrep$ export CASE_INSENSITIVE=1
peter@hp-laptop:~/rust-practice/chapter12/minigrep$ cargo run to poem.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/minigrep to poem.txt`
Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!
peter@hp-laptop:~/rust-practice/chapter12/minigrep$
마지막으로 한 가지만 더 적용해보자.
현재 우리 프로그램은 모든 출력을 표준 출력stdout을 사용한다.
그러나 오류 메시지는 표준 오류 출력을 사용하여 출력하는 것이 좋다.
이를 통해 출력 스트림을 어떤 파일로 지정하여 그곳에 실행 결과를 저장하면서도
오류 메시지는 터미널에 띄우거나 다른 로그 파일에 저장할 수 있다.
표준 출력으로 출력되던 것을 표준 오류 출력으로 변경하는 건 어렵지 않다.
println!
대신 eprintln!
을 사용하면 된다.
peter@hp-laptop:~/rust-practice/chapter12/minigrep$ vi src/main.rs
src/main.rs
use std::env; use std::process; use minigrep::Config; fn main() { let args: Vec<String> = env::args().collect(); let config = Config::new(&args).unwrap_or_else(|err| { eprintln!("Problem parsing arguments: {}", err); process::exit(1); }); // println!("Search for {}", config.query); // println!("In file {}", config.filename); if let Err(e) = minigrep::run(config) { eprintln!("Application error: {}", e); process::exit(1); } }
이제 리다이렉션을 통해 출력 스트림을 변경하여 파일에 저장을 해도
오류 메시지는 터미널에 뜨는 것을 확인할 수 있다.
peter@hp-laptop:~/rust-practice/chapter12/minigrep$ cargo run > output.txt
Compiling minigrep v0.1.0 (/home/peter/rust-practice/chapter12/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.23s
Running `target/debug/minigrep`
Problem parsing arguments: not enough arguments
peter@hp-laptop:~/rust-practice/chapter12/minigrep$
위와 같이 실행하면 output.txt
파일이 생성되는데
표준 출력으로 출력된 것이 없으므로 빈 파일이다.
output.txt
오류가 발생하지 않고 정상적으로 실행되는 경우에는 터미널에 출력되는 내용이 없다.
peter@hp-laptop:~/rust-practice/chapter12/minigrep$ cargo run body poem.txt > output.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/minigrep body poem.txt`
peter@hp-laptop:~/rust-practice/chapter12/minigrep$
그리고 이번엔 출력 내용이 파일에 들어있는 걸 확인할 수 있다.
output.txt
I’m nobody! Who are you? Are you nobody, too? How dreary to be somebody!
이 포스트의 내용은 공식문서의 12장 4절 Developing the Library’s Functionality with Test-Driven Development & 12장 5절 Working with Environment Variables & 12장 6절 Writing Error Messages to Standard Error Instead of Standard Output에 해당합니다.