해당 내용은 Datacamp의 Data engineering track을 정리했습니다.
Unit Testing for Data Science in Python의 chapter 2에 대한 내용입니다.
해당 포스팅에는 아래의 내용을 포함하고 있습니다.
- assert문에서 message 출력하기
- assert할 때 소수점 문제 해결하기
- unit test 효율적으로 사용하기
- TDD
1. Mastering assert statements
assert boolean_expression, message
이전 강의에서는 assert문으로 boolean 표현식만 사용했습니다. 추가적으로 boolean 표현식 뒤에 추가적으로 인수를 사용할 수 있습니다. message는 boolean_expression이 에러가 발생했을 때, AssertionError가 발생한 이유에 대한 정보를 포함해야 합니다.
보시는 것처럼 AssertionError가 발생하면 뒤의 message가 출력되고, AssertionError가 발생하지 않으면 message를 출력하지 않습니다.
# test_row_to_list.py
# without message
import pytest
def test_for_missing_area():
assert row_to_list("\t293,410\n") is None
# with message
import pytest
def test_for_missing_area_with_message():
actual = row_to_list("\t293,410\n")
expected = None
message = ("row_to_list('\t293,410\n') returned {0} instead of {1}".format(actual, expected))
assert actual is expected, message
기존에 없었던 message를 추가하여 아래의 함수처럼 변경할 수 있습니다.
message가 없는 경우에는 자동 출력으로 return이 되지만, message 포함된 경우에는 사람이 편하게 읽을 수 있도록 표시됩니다. message는 주로 assert statements를 포함하는 것을 추천하며, 디버깅과 관련 될 수 있는 변수를 포함하여 출력하기도 합니다.
다음으로 함수가 float를 반환할 때 발생할 수 있는 문제들을 다룹니다. 0.1 + 0.1 + 0.1 == 0.3을 계산하면, False가 나옵니다. 그 이유는 파이썬에서 float를 나타내는 방식 때문입니다.
위의 그림처럼 직접 파이썬에서 실행해보면 원하는 값과는 다릅니다. 이 강의에서는 왜 이런 일이 발생하는 지는 다루지 않습니다. 그래서 float를 비교하는 assert문은 기존의 방법대로 사용해서는 안됩니다. 대신 pytest.approx()를 사용하면 가능합니다.
assert 0.1 + 0.1 + 0.1 == pytest.approx(0.3)
pytest.approx는 numpy array에서도 동일하게 동작합니다.
assert np.array([0.1 + 0.1, 0.1 + 0.1 + 0.1]) == pytest.approx(np.array([0.2, 0.3]))
지금까지는 1개의 단위 테스트 당 1개의 assert문이 존재한 경우만 봤습니다.
# one assertion in one unit test
import pytest
def test_on_string_with_one_comma():
assert convert_to_int("2,081") == 2081
# multiple assertions in one unit test
import pytest
def test_on_string_with_one_comma():
return_value = convert_to_int("2,081")
assert isinstance(return_value, int)
assert return_value == 2081
위의 보시는 예시는 하나의 단위 테스트 내부에 여러 개의 assert 문을 사용할 수 있음을 보여주고 있습니다. 모든 assert를 통과해야 해당 단위테스트를 통과한 것으로 간주합니다.
2. Testing for exceptions instead of return values (추후 보강)
3. The well tested function
함수를 테스트하기 위해 수많은 경우를 모두 대입해볼 수는 없습니다. 그래서 테스트를 위해 시간을 효율적으로 사용하기 위해 몇 가지 argument를 포함하기를 권장합니다. Bad arguments, special arguments, normal arguments들을 반드시 포함시켜야 합니다. 위의 argument들을 모두 pass 했다면, 함수는 잘 동작한다고 판단할 수 있습니다.
Bad arguments는 함수가 값을 반환하지 않고 예외를 발생시키는 argument입니다. 예를 들어, 입력으로 2차원 배열이 들어와야 하지만, 1차원 배열이 들어오게 되면 ValueError가 발생합니다. 이러한 경우를 Bad arguments로 판단합니다.
Special arguments는 경계선에 위치한 값이나 특별한 로직을 사용하는 경우를 말합니다. 어떤 경우에는 동일하지 않은 로직으로 계산되길 원할 때가 존재합니다. 이런 경우에도 Special arguments로 포함할 수 있으며, 해당 arguments를 기준으로 좌우의 값도 boundary value가 되어 Special arguments에 포함시킵니다. 이외에 동일 로직에 의해 계산되는 것들을 Normal arguments라고 부릅니다.
물론, 모든 함수가 Bad arguments나 Special arguments를 가지지는 않습니다. 그런 경우에는 해당 arguments를 포함시키지 않아도 괜찮습니다.
4. Test Driven Development(TDD)
현실에서는 unit test를 작성하는 것을 건너 뛸 때가 많습니다. 빠르게 기능을 구현하기 원하기 때문입니다. 하지만 이렇게 unit test를 작성하지 않는 것은 추후 발생할 수 있는 문제에 대해 부채를 쌓는 것과 같습니다. 그래서 Test Driven Development는 구현하기 전에 unit test에 대해 작성하는 시간을 따로 만들어 놓는 것입니다.
먼저, unit test를 위한 요구조건들을 설정하고 test파일을 미리 작성합니다.
# test_convert_to_int.py
import pytest
def test_with_no_comma():
...
def test_with_one_comma():
...
def test_with_two_commas():
...
이렇게 작성되지 않은 test파일을 가지고 테스트합니다. 당연히 함수를 아직 만들지 않았기 때문에 테스트는 통과되지 않습니다. 마지막으로 함수를 구현하고 테스트를 다시 실행합니다. 올바르게 함수를 작성했다면 test를 통과해야 합니다. 그렇지 않다면, 다시 수정하고 테스트하는 과정을 반복해야합니다.