소개
테스트 주도 개발 (Test-Driven Development) 로 라이브러리 기능 개발하기에 오신 것을 환영합니다. 이 랩은 Rust Book의 일부입니다. LabEx 에서 Rust 기술을 연습할 수 있습니다.
이 랩에서는 테스트 주도 개발을 사용하여 라이브러리의 기능을 개발하여 프로그램에 검색 로직을 추가할 것입니다.
This tutorial is from open-source community. Access the source code
테스트 주도 개발 (Test-Driven Development) 로 라이브러리 기능 개발하기에 오신 것을 환영합니다. 이 랩은 Rust Book의 일부입니다. LabEx 에서 Rust 기술을 연습할 수 있습니다.
이 랩에서는 테스트 주도 개발을 사용하여 라이브러리의 기능을 개발하여 프로그램에 검색 로직을 추가할 것입니다.
이제 로직을 src/lib.rs로 추출하고 인수 수집 및 오류 처리를 src/main.rs에 남겨두었으므로, 코드의 핵심 기능에 대한 테스트를 훨씬 쉽게 작성할 수 있습니다. 명령줄에서 바이너리를 호출하지 않고도 다양한 인수로 함수를 직접 호출하고 반환 값을 확인할 수 있습니다.
이 섹션에서는 다음 단계를 사용하여 테스트 주도 개발 (TDD) 프로세스를 통해 minigrep 프로그램에 검색 로직을 추가합니다.
소프트웨어를 작성하는 여러 방법 중 하나일 뿐이지만, TDD 는 코드 설계를 추진하는 데 도움이 될 수 있습니다. 테스트를 통과시키는 코드를 작성하기 전에 테스트를 작성하면 프로세스 전반에 걸쳐 높은 테스트 커버리지를 유지하는 데 도움이 됩니다.
파일 내용에서 쿼리 문자열을 실제로 검색하고 쿼리와 일치하는 줄 목록을 생성하는 기능의 구현을 테스트 주도 방식으로 개발할 것입니다. 이 기능을 search라는 함수에 추가할 것입니다.
더 이상 필요하지 않으므로 프로그램의 동작을 확인하는 데 사용했던 src/lib.rs 및 src/main.rs에서 println! 문을 제거하겠습니다. 그런 다음, 11 장에서 했던 것처럼 src/lib.rs에 테스트 함수가 있는 tests 모듈을 추가합니다. 테스트 함수는 search 함수가 가져야 할 동작을 지정합니다. 즉, 쿼리와 검색할 텍스트를 가져와 쿼리를 포함하는 텍스트의 줄만 반환합니다. 목록 12-15 는 아직 컴파일되지 않는 이 테스트를 보여줍니다.
파일 이름: src/lib.rs
#[cfg(test)]
mod tests {
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)
);
}
}
목록 12-15: 우리가 원했던 search 함수에 대한 실패하는 테스트 생성
이 테스트는 문자열 "duct"를 검색합니다. 검색할 텍스트는 세 줄이며, 그 중 하나만 "duct"를 포함합니다 (여는 큰따옴표 뒤의 백슬래시는 Rust 에게 이 문자열 리터럴의 내용 시작 부분에 줄 바꿈 문자를 넣지 않도록 지시합니다). search 함수에서 반환된 값에 우리가 예상하는 줄만 포함되어 있는지 확인합니다.
테스트가 컴파일되지 않으므로 이 테스트를 실행하고 실패하는 것을 아직 볼 수 없습니다. search 함수가 아직 존재하지 않기 때문입니다! TDD 원칙에 따라, 목록 12-16 과 같이 항상 빈 벡터를 반환하는 search 함수 정의를 추가하여 테스트가 컴파일되고 실행되도록 충분한 코드를 추가합니다. 그러면 빈 벡터가 "safe, fast, productive." 줄을 포함하는 벡터와 일치하지 않으므로 테스트가 컴파일되고 실패해야 합니다.
파일 이름: src/lib.rs
pub fn search<'a>(
query: &str,
contents: &'a str,
) -> Vec<&'a str> {
vec![]
}
목록 12-16: 테스트가 컴파일되도록 search 함수를 충분히 정의
search의 시그니처에서 명시적인 수명 'a를 정의하고 해당 수명을 contents 인수 및 반환 값과 함께 사용해야 합니다. 10 장에서 수명 매개변수가 어떤 인수 수명이 반환 값의 수명에 연결되는지 지정한다는 것을 기억하십시오. 이 경우 반환된 벡터에 인수 contents (인수 query가 아님) 의 슬라이스를 참조하는 문자열 슬라이스가 포함되어야 함을 나타냅니다.
다시 말해, search 함수에서 반환된 데이터가 contents 인수의 search 함수에 전달된 데이터만큼 오래 지속되도록 Rust 에 알려줍니다. 이것이 중요합니다! 슬라이스 에 의해 참조된 데이터는 참조가 유효하려면 유효해야 합니다. 컴파일러가 query가 아닌 contents의 문자열 슬라이스를 만들고 있다고 가정하면 안전성 검사를 잘못 수행합니다.
수명 주석을 잊고 이 함수를 컴파일하려고 하면 다음과 같은 오류가 발생합니다.
error[E0106]: missing lifetime specifier
--> src/lib.rs:31:10
|
29 | query: &str,
| ----
30 | contents: &str,
| ----
31 | ) -> Vec<&str> {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the
signature does not say whether it is borrowed from `query` or `contents`
help: consider introducing a named lifetime parameter
|
28 ~ pub fn search<'a>(
29 ~ query: &'a str,
30 ~ contents: &'a str,
31 ~ ) -> Vec<&'a str> {
|
Rust 는 두 인수 중 어느 것이 필요한지 알 수 없으므로 명시적으로 알려줘야 합니다. contents가 모든 텍스트를 포함하는 인수이고 일치하는 해당 텍스트의 일부를 반환하려는 경우, 수명 구문을 사용하여 반환 값에 연결해야 하는 인수가 contents임을 알 수 있습니다.
다른 프로그래밍 언어에서는 시그니처에서 인수를 반환 값에 연결할 필요가 없지만, 이 관행은 시간이 지남에 따라 더 쉬워질 것입니다. "수명으로 참조 유효성 검사"의 예와 이 예를 비교해 볼 수 있습니다.
이제 테스트를 실행해 보겠습니다.
[object Object]
훌륭합니다. 예상대로 테스트가 실패했습니다. 테스트를 통과시켜 봅시다!
현재 테스트가 실패하는 이유는 항상 빈 벡터를 반환하기 때문입니다. 이를 수정하고 search를 구현하려면 프로그램이 다음 단계를 따라야 합니다.
각 단계를 살펴보겠습니다. 먼저 줄을 반복하는 것부터 시작합니다.
lines 메서드를 사용하여 줄 반복하기Rust 에는 문자열의 줄별 반복을 처리하는 데 유용한 메서드인 lines가 있습니다. 이는 목록 12-17 과 같이 작동합니다. 아직 컴파일되지 않는다는 점에 유의하십시오.
파일 이름: src/lib.rs
pub fn search<'a>(
query: &str,
contents: &'a str,
) -> Vec<&'a str> {
for line in contents.lines() {
// do something with line
}
}
목록 12-17: contents의 각 줄을 반복
lines 메서드는 이터레이터 (iterator) 를 반환합니다. 13 장에서 이터레이터에 대해 자세히 이야기하겠지만, 목록 3-5 에서 이터레이터를 사용하는 방식을 보았다는 것을 기억하십시오. 여기서는 이터레이터가 있는 for 루프를 사용하여 컬렉션의 각 항목에 대해 일부 코드를 실행했습니다.
다음으로, 현재 줄에 쿼리 문자열이 포함되어 있는지 확인합니다. 다행히 문자열에는 이를 수행하는 데 도움이 되는 contains라는 메서드가 있습니다! 목록 12-18 과 같이 search 함수에 contains 메서드 호출을 추가합니다. 이것 역시 아직 컴파일되지 않는다는 점에 유의하십시오.
파일 이름: src/lib.rs
pub fn search<'a>(
query: &str,
contents: &'a str,
) -> Vec<&'a str> {
for line in contents.lines() {
if line.contains(query) {
// do something with line
}
}
}
목록 12-18: 줄에 query 문자열이 포함되어 있는지 확인하는 기능 추가
현재 기능을 구축하고 있습니다. 코드가 컴파일되도록 하려면 함수 시그니처에서 표시했듯이 본문에서 값을 반환해야 합니다.
이 함수를 완성하려면 반환하려는 일치하는 줄을 저장하는 방법이 필요합니다. 이를 위해 for 루프 전에 가변 벡터 (mutable vector) 를 만들고 push 메서드를 호출하여 벡터에 line을 저장할 수 있습니다. for 루프 후, 목록 12-19 와 같이 벡터를 반환합니다.
파일 이름: src/lib.rs
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
}
목록 12-19: 일치하는 줄을 저장하여 반환할 수 있도록 함
이제 search 함수는 query를 포함하는 줄만 반환해야 하며, 테스트가 통과해야 합니다. 테스트를 실행해 보겠습니다.
$ cargo test
--snip--
running 1 test
test tests::one_result ... ok
test result: ok. 1 passed
0 failed
0 ignored
0 measured
0
filtered out
finished in 0.00s
테스트가 통과했으므로 작동하는 것을 알 수 있습니다!
이 시점에서 동일한 기능을 유지하면서 테스트를 통과시키는 동안 search 함수의 구현을 리팩터링할 기회를 고려할 수 있습니다. search 함수의 코드는 그다지 나쁘지 않지만 이터레이터의 몇 가지 유용한 기능을 활용하지 않습니다. 13 장에서 이 예제로 돌아가 이터레이터를 자세히 살펴보고 개선 방법을 살펴보겠습니다.
run 함수에서 search 함수 사용하기이제 search 함수가 작동하고 테스트되었으므로 run 함수에서 search를 호출해야 합니다. config.query 값과 run이 파일에서 읽은 contents를 search 함수에 전달해야 합니다. 그런 다음 run은 search에서 반환된 각 줄을 출력합니다.
파일 이름: src/lib.rs
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
for line in search(&config.query, &contents) {
println!("{line}");
}
Ok(())
}
여전히 search에서 각 줄을 반환하고 출력하기 위해 for 루프를 사용하고 있습니다.
이제 전체 프로그램이 작동해야 합니다! 먼저 Emily Dickinson 의 시에서 정확히 한 줄을 반환해야 하는 단어인 frog로 시도해 보겠습니다.
$ cargo run -- frog poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.38s
Running `target/debug/minigrep frog poem.txt`
How public, like a frog
멋지네요! 이제 body와 같이 여러 줄과 일치하는 단어를 시도해 보겠습니다.
$ cargo run -- body poem.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep body poem.txt`
I'm nobody! Who are you?
Are you nobody, too?
How dreary to be somebody!
마지막으로, monomorphization과 같이 시에 없는 단어를 검색할 때 어떤 줄도 얻지 못하는지 확인해 보겠습니다.
$ cargo run -- monomorphization poem.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep monomorphization poem.txt`
훌륭합니다! 우리는 고전적인 도구의 자체 미니 버전을 구축했으며 애플리케이션을 구조화하는 방법에 대해 많은 것을 배웠습니다. 또한 파일 입출력, 라이프타임 (lifetimes), 테스트 및 명령줄 구문 분석에 대해서도 조금 배웠습니다.
이 프로젝트를 마무리하기 위해 환경 변수를 사용하는 방법과 명령줄 프로그램을 작성할 때 유용한 표준 오류로 출력하는 방법을 간략하게 시연하겠습니다.
축하합니다! 테스트 주도 개발 (Test-Driven Development) 을 통해 라이브러리 기능을 개발하는 랩을 완료했습니다. LabEx 에서 더 많은 랩을 연습하여 기술을 향상시킬 수 있습니다.