유닛 테스트

소스코드 살펴보기

파이썬에서 유닛테스트를 위해서 사용하는 라이브러리로, 내장 라이브러리인 unittest 가 있습니다. 하지만 실제 현업에서는 일반적으로 pytest 패키지를 많이 사용합니다. 아래 명령어로 패키지를 설치합니다.

pip install pytest

먼저 파이썬 코드를 살펴보겠습니다. logic.py 모듈은 다음과 같습니다.

from typing import Optional

CARDS = ["Rock", "Scissors", "Paper"]


def play(card1: str, card2: str) -> Optional[bool]:
    assert card1 in CARDS, "Invalid card1"
    assert card2 in CARDS, "Invalid card2"

    if card1 == card2:
        return None

    if (
        (card1 == "Rock" and card2 == "Scissors")
        or (card1 == "Scissors" and card2 == "Paper")
        or (card1 == "Paper" and card2 == "Rock")
    ):
        return True
    else:
        return False


def stop():
    raise Exception("stop!")

play() 함수는 입력받은 두 카드의 값을 비교해 첫 번째 카드의 승패 유무를 리턴하는 함수입니다. 가위바위보에서 이기면 True, 지면 False , 비기면 None 을 리턴합니다. stop() 함수는 무조건 에러를 발생시켜 프로그램을 종료시킵니다.

이제 test.py 모듈을 보겠습니다. @pytest.mark.parametrize 는 테스트의 각 파라미터를 테스트 수행 중에 동적으로 넣을 수 있는 데코레이터입니다. 여기서 승, 무, 패 3가지를 테스트하는 함수 test_win, test_draw, test_lose 와 함께 함수 stop() 이 에러를 발생시키는지를 검사하는 test_stop 까지 총 4개의 테스트가 존재합니다.

import pytest

from logic import play, stop


@pytest.mark.parametrize(
    "card1, card2",
    [("Rock", "Scissors"), ("Scissors", "Paper"), ("Paper", "Rock")],
)
def test_win(card1, card2):
    assert play(card1, card2) == True


@pytest.mark.parametrize(
    "card1, card2",
    [("Rock", "Rock"), ("Scissors", "Scissors"), ("Paper", "Paper")],
)
def test_draw(card1, card2):
    assert play(card1, card2) == None


@pytest.mark.parametrize(
    "card1, card2",
    [("Scissors", "Rock"), ("Rock", "Paper"), ("Paper", "Scissors")],
)
def test_lose(card1, card2):
    assert play(card1, card2) == False


def test_stop():
    with pytest.raises(Exception) as exc:
        stop()

여기에 승무패 테스트에는 파라미터가 3종류씩 들어가므로 총 10개의 테스트가 수행되게 됩니다.

pytest 에는 정말 다양한 사용법이 존재하지만, 여기서는 기본적으로 테스트 모듈을 실행하는 것만 해보겠습니다. 현재 파이썬 폴더 밑에 test.py 파일에 정의된 테스트들을 수행해줍니다.

pytest test.py

실행 결과

============================= test session starts ==============================
platform darwin -- Python 3.8.2, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /ch10/python
plugins: dash-2.0.0, anyio-3.3.4
collected 10 items                                                             

test.py ..........                                                       [100%]

============================== 10 passed in 0.05s ==============================

러스트 코드도 살펴봅시다. 여기서는 크레이트 루트로 라이브러리 크레이트를 사용합니다.

#[derive(PartialEq)]
pub enum Cards {
    Rock,
    Scissors,
    Paper,
}

/// Demonstrate Rock, Scissors, Paper
///
/// ```
/// use rust_part::{play, Cards};
/// 
/// let result = play(Cards::Rock, Cards::Scissors);
/// assert_eq!(result, Some(true));
/// ```
pub fn play(card1: Cards, card2: Cards) -> Option<bool> {
    if card1 == card2 {
        return None;
    }
    match (card1, card2) {
        (Cards::Rock, Cards::Scissors) => Some(true),
        (Cards::Scissors, Cards::Paper) => Some(true),
        (Cards::Paper, Cards::Rock) => Some(true),
        _ => Some(false),
    }
}

pub fn stop() {
    panic!("stop!");
}

#[cfg(test)]
pub mod test {
    // import everything in this module
    use super::*;

    // No parametrized tests out of the box in Rust.
    #[test]
    fn test_win() {
        assert_eq!(play(Cards::Paper, Cards::Rock), Some(true));
        assert_eq!(play(Cards::Scissors, Cards::Paper), Some(true));
        assert_eq!(play(Cards::Paper, Cards::Rock), Some(true));
    }
    #[test]
    fn test_draw() {
        assert_eq!(play(Cards::Rock, Cards::Rock), None);
        assert_eq!(play(Cards::Scissors, Cards::Scissors), None);
        assert_eq!(play(Cards::Paper, Cards::Paper), None);
    }
    #[test]
    fn test_lose() {
        assert_eq!(play(Cards::Rock, Cards::Paper), Some(false));
        assert_eq!(play(Cards::Paper, Cards::Scissors), Some(false));
        assert_eq!(play(Cards::Scissors, Cards::Rock), Some(false));
    }

    #[test]
    #[should_panic(expected="stop!")]
    fn test_stop(){
        stop();
    }
}

cargo 에 내장된 test 러너로 유닛 테스트 실행이 가능합니다. 러스트는 테스트 파일을 별도로 만들지 않고, 같은 파일 안에 test 모듈을 넣어서 작성합니다. 이렇게 하면 테스트 모듈에서 대상 모듈에 대한 접근이 쉬워집니다. 다시 말해, private으로 선언된 함수에도 접근할 수 있기 때문에 테스트가 용이해집니다.

아래 명령어로 테스트 모듈의 테스트들을 수행합니다.

cargo test

실행 결과

   Compiling rust_part v0.1.0 (/ch10/rust_part)
    Finished test [unoptimized + debuginfo] target(s) in 2.02s
     Running unittests (target/debug/deps/rust_part-e26a6c1814367b2a)

running 4 tests
test test::test_draw ... ok
test test::test_lose ... ok
test test::test_stop - should panic ... ok
test test::test_win ... ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests rust_part

running 1 test
test src/lib.rs - play (line 10) ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.59s

러스트는 테스트를 위해 바이너리를 빌드하는 과정이 먼저 수행됩니다. 그리고 유닛 테스트를 수행합니다. 마지막에 "Doc-tests"라는게 추가로 수행되는데 이는 밑에서 더 자세히 설명하겠습니다.

비슷하게 클래스도 테스트할 수 있습니다. 먼저 파이썬에서 다음과 같은 클래스를 정의합니다.

class Person:
    def __init__(self, name, age):
        self.name = name
        self._age = age

    @property
    def age(self):
        return self._age

    def hi(self):
        return f"Hi, I'm {self.name}, I'm {self._age} years old."

테스트 모듈에서는 객체화를 한 다음 프로퍼티와 메소드가 잘 적용되는지를 테스트합니다.

def test_hi():
    name = "John"
    age = 30
    person = Person(name, age)
    assert person.hi() == f"Hi, I'm {name}, I'm {age} years old."
    assert person.hi() == f"Hi, I'm {person.name}, I'm {person.age} years old."

러스트에서는 다음과 같이 구조체를 선언했습니다. 먼저 person 모듈을 선언하고 그 다음 Person 구조체와 메소드를 정의했습니다. 여기서 별도로 모듈을 만들지 않아도 상관없습니다.

pub mod person {

    pub struct Person {
        pub name: String,
        age: u8,
    }

    impl Person {
        pub fn new(name: &str, age: u8) -> Person {
            Person {
                name: name.to_string(),
                age: age,
            }
        }

        pub fn hi(&self) -> String {
            format!("Hi, I'm {}, I am {} years old.", self.name, self.age())
        }

        pub fn age(&self) -> u8 {
            self.age
        }
    }
}

그 다음 테스트 모듈에 아래 함수를 추가합니다.

#[test]
fn test_hi() {
  let name = "John";
  let age: u8 = 30;
  let person = person::Person::new(name, age);
  assert_eq!(
    person.hi(),
    format!("Hi, I'm {}, I am {} years old.", name, age)
  );
  assert_eq!(
    person.hi(),
    format!("Hi, I'm {}, I am {} years old.", person.name, person.age())
  );
}

파이썬과 마찬가지로 프로퍼티와 메소드가 잘 적용되는지를 테스트합니다. 아래 명령어로 테스트들을 수행합니다.

cargo test

비동기 함수 테스트

#[tokio::test]를 함수에 붙여주면 됩니다.

async fn give_order(order: u64) -> u64 {
    println!("Processing {order}...");
    tokio::time::sleep(std::time::Duration::from_secs(3 - order)).await;
    println!("Finished {order}");
    order
}

#[tokio::main]
async fn main() {
    let result = tokio::join!(give_order(1), give_order(2), give_order(3));

    println!("{:?}", result);
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_give_order() {
        let result = give_order(1).await;
        assert_eq!(result, 1);
    }
}

실행 결과

running 1 test
test tests::test_give_order ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.00s