#18 유사 grep 작성하기 下 테스트 주도 방법

Pt J·2020년 8월 30일
0

[完] Rust Programming

목록 보기
21/41
post-thumbnail

이 시리즈는 Rust 공식문서를 통해 공부한 흔적임을 밝힙니다.

지난 시간 우리는 grep을 단순화한 minigrep 프로젝트를 시작하였다.
이번 시간에는 테스트 주도 개발 방법을 통해 라이브러리 기능을 구현배도록 하겠다.

테스트 주도 개발 Test Driven Development

우리는 테스트 코드를 먼저 작성하고 그것을 성공시키는 코드를 구현하는
테스트 주도 개발 방법으로 기능을 구현하기로 하였다.
테스트 주도 개발 방법은 테스트 적용 범위를 충분히 확보하기 위한 개발 방법으로
다음과 같은 과정으로 이루어진다.

  1. 실패하는 테스트를 작성하고 실행하여 의도한 원인으로 인해 실패하는지 확인한다.
    Write a test that fails and run it to make sure it fails for the reason you expect.
  2. 작성한 테스트를 성공시킬 수 있도록 코드를 작성하거나 수정한다.
    Write or modify just enough code to make the new test pass.
  3. 추가 또는 수정한 코드를 리팩토링하고 그것이 여전히 성공하는지 확인한다.
    Refactor the code you just added or changed and make sure the tests continue to pass.
  4. 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에 한 가지 기능을 추가해보자.

환경 변수 Environment Variable

환경 변수는 실행 환경을 설정하기 위한 변수로, 필요에 따라 동적으로 적용할 수 있다.
한 번 환경 변수를 등록하면 그 터미널 세션에서는 그것이 유지되어 다시 설정할 필요가 없다.
우리는 이걸 이용하여 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 함수와 비슷하다.
다만 비교할 때 contentsquery를 모두 소문자 또는 대문자로 변환하여 비교함으로써
대소문자 구분 없이 비교할 수 있도록 한다.

우리는 모두 소문자로 변환하는 방식을 사용하겠다.

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$

표준 오류 출력 stderr

마지막으로 한 가지만 더 적용해보자.
현재 우리 프로그램은 모든 출력을 표준 출력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에 해당합니다.

profile
Peter J Online Space - since July 2020

0개의 댓글