Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
05-17 00:00
관리 메뉴

nomad-programmer

[Programming/Python] 단위 테스트(unittest) - 보다 견고한 코드 만들기 본문

Programming/Python

[Programming/Python] 단위 테스트(unittest) - 보다 견고한 코드 만들기

scii 2021. 5. 26. 18:32

단위 테스트(unittest)

파이썬에서는 PyUnit이라고도 불리는 unittest 모듈이 있다. 이 모듈은 일반적으로 어던 모듈이나 함수를 작성할 때 정상 동작 여부를 테스트하는 과정을 거치게된다. 이러한 테스트케이스들은 한 번 사용하고 버리는 경우가 대부분이다. 하지만 다음에 코드가 수정되어도 기존에 작성한 테스트들을 수행할 수 있다면 더욱 편리하겠다. 

이러한 기능을 제공해주는 라이브러리가 바로 unittest 이다.

TestCase

기본적으로 unittest 모듈은 다음과 같이 간단하게 사용할 수 있다.

# _*_ coding: utf-8 _*_

import unittest


def sum(a, b):
    return a + b


class Module1Test(unittest.TestCase):
    def test_sum1(self):
        self.assertEqual(sum(1, 2), 3)

    def test_sum2(self):
        self.assertEqual(sum(1, -1), 0)


if __name__ == '__main__':
    unittest.main()

코드를 보면 unittest 모듈을 임포트하고 나서 테스트의 대상이 되는 sum()함수를 정의한다. 그 아래에는 테스트를 수행할 클래스를 만드는데, 반드시 unittest.TestCase 클래스를 상속받아야만 한다. 그 다음에는 실제로 테스트 결과를 비교할 멤버 함수인 test_sum1()과 test_sum2()를 구현한다. 마지막으로 unittest.main()을 호출하여 테스트를 수행한다.

이렇게 정해진 형식으로, 즉 unittest.TestCase 클래스를 상속 받는 클래스를 만들고 test로 시작하는 멤버 메서드 안에 assert로 시작하는 함수들로 테스트 결과를 확인하는 작업을 해야만, unittest.main()이 호출되었을 때 제대로 테스트를 수행한다.

또한 멤버 메서드 이름은 test로 시작해야 따로 지정을 하지 않아도 테스트 대상으로 인식한다.

# 실행 결과

Testing started at 오후 7:20 ...
Launching unittests with arguments python -m unittest main.Module1Test in D:\workspace\python\json_parser



Ran 2 tests in 0.002s

OK

실제로는 저렇게 테스트를 빈약하게 하지도 않을 것이고, 테스트할 대상이 저렇게 간단하지도 않겠다. 그러나 위와 같이 모듈을 만들 때 테스트를 같이 넣는다면, 모듈 코드의 수정이 있을 경우에도 기존 기능이 제대로 동작하는지 쉽게 확인할 수 있을 것이다.

위의 예제 코드에서 assertEqual() 함수가 나왔는데, 이 함수는 입력인자로 오는 두 값이 동일한지 검사하는 함수이다.

다음은 테스트 값을 좀 더 쉽게 비교하도록 제공되는 assert 관련 함수들 중 자주 사용되는 함수들을 나열한 것이다.

함수명 설명
assertEqual(first, second, [msg]) first와 second가 같은지 테스트한다. 같지 않은 경우 테스트가 실패하며 msg를 출력한다.
assertNotEqual(first, second, [msg]) first와 second가 다른지 테스트한다. 같은 경우 테스트가 실패하며 msg를 출력한다.
assertTrue(expr, [msg]) expr이 True인지 테스트한다. False인 경우 테스트가 실패하며 msg를 출력한다.
assertFalse(expr, [msg]) expr이 False인지 테스트한다. True인 경우 테스트가 실패하며 msg를 출력한다.

이 외에도 setUp()과 tearDown()도 자주 쓰이는데, setUp() 함수는 TestCase 클래스의 테스트가 수행되기 전에 테스트 환경을 설정하는 역할을 하며, tearDown() 함수는 테스트 수행 후 테스트 환경을 정리하는 역할을 한다.

함수명 설명
setUp() 테스트가 수행되지 전 테스트 환경 설정
tearDown() 테스트 수행 후 테스트 환경 정리

다음 예제 코드는 setUp() 함수와 tearDown() 함수의 사용 예이다.

# _*_ coding: utf-8 _*_

import unittest


class Module2Test(unittest.TestCase):
    def setUp(self) -> None:
        self.bag = [True, True]

    def tearDown(self) -> None:
        del self.bag

    def test_true(self) -> None:
        for element in self.bag:
            self.assertTrue(element)


if __name__ == '__main__':
    unittest.main()

위의 코드에서 bag이라는 멤버 변수를 설정해 놓고, test_true() 메서드 내에서 bag에 들어있는 값들이 True인지 검사하는 작업을 한다. 위의 예제에서 setUp() 메서드에서는 초기값 설정을, tearDown() 메서드에서는 멤버 변수 삭제라는 다소 의미 없는 작업을 했지만, test_true() 메서드를 비롯하여 다른 테스트에서도 bag이 사용되거나, 보다 복잡한 환경을 설정/해제할 때는 의미 있게 사용될 수 있다.


TestSuite

테스트 슈트(Test Suite)란 테스트 케이스(Test Case)나 테스트 슈트의 집합을 의미한다. 즉, 이번에는 테스트가 여러 개인 경우에 대해서 생각해 보자. 위에서 예로든 Module1Test와 Module2Test 클래스를 각각 테스트 슈트로 지정하고, 이를 모두 테스트하는 코드를 작성해 보자.

# _*_ coding: utf-8 _*_

import unittest


def sum(a: int, b: int) -> int:
    return a + b


class Module1Test(unittest.TestCase):
    def test_sum1(self) -> None:
        self.assertEqual(sum(1, 2), 3)

    def test_sum2(self) -> None:
        self.assertEqual(sum(1, -1), 0)


class Module2Test(unittest.TestCase):
    def setUp(self) -> None:
        self.bag = [True, True]

    def tearDown(self) -> None:
        del self.bag

    def test_true(self) -> None:
        for element in self.bag:
            self.assertTrue(element)


# testcase를 이용해서 testsuite 생성
def make_suite(testcase: unittest.TestCase, tests: list) -> unittest.TestSuite:
    return unittest.TestSuite(map(testcase, tests))


if __name__ == '__main__':
    suite1 = make_suite(Module1Test, ['test_sum1', 'test_sum2'])
    suite2 = make_suite(Module2Test, ['test_true'])

    # 모든 test suite를 묶음
    allsuites = unittest.TestSuite([suite1, suite2])
    # 모든 test suite를 수행
    unittest.TextTestRunner(verbosity=2).run(allsuites)

이 코드의 핵심은 TestSuite 클래스를 생성하는 부분이다. 원래는 unittest.TestSuite()를 이용하여 TestSuite 클래스를 생성해야 하는데 중복되는 부분이므로 따로 함수로 정리하였다.

TestSuite 클래스는 TestCase나 TestSuite 클래스들로 이루어지며, unittest.TestSuite 클래스의 생성자에 TestCase들을 리스트로 입력해서 간단하게 만들 수도 있다. 위에서는 각각 함수를 지정하고 있다.

마지막으로 이렇게 만들어진 TestSuite를 TextTestRunner 클래스의 run() 메서드를 실행한다. (일반적으로 TextTestRunner와 같은 클래스들을 'Test runner' 라고 한다.)
verbosity는 아래 실행 결과와 같이 test 결과에서 출력 레벨을 조정하는 인자이다.

# 실행 결과

Testing started at 오후 7:51 ...


Launching unittests with arguments python -m unittest main.Module1Test in D:\workspace\python\json_parser
Ran 2 tests in 0.002s


OK

FunctionTestCase

기존에 작성된 함수를 이용해서 TestCase를 만드는 방법을 알아보자. 이러한 기능은 unittest.FunctionTestCase 클래스를 이용하면 쉽게 작성할 수 있다. 또한 기존 메서드 중에서 SetUp()과 TearDown()에 매핑할 수도 있다.

다음 예제코드를 보면 기존의 간단한 함수들(test, init, fin)이 있을 경우, 어떻게 TestCase가 생성되고 실행될 수 있는지 알 수 있을 것이다.

# _*_ coding: utf-8 _*_

import unittest


def test() -> None:
    print('\tthis function is to test old functions')
    assert 1 is not None


def init() -> None:
    print('\n\tinitialized')


def fin() -> None:
    print('\tfinalized')


if __name__ == '__main__':
    testcase = unittest.FunctionTestCase(test, setUp=init, tearDown=fin)
    suite = unittest.TestSuite([testcase])
    unittest.TextTestRunner(verbosity=2).run(suite)

test() 함수 내의 assert문에서 발생하는 예외를 잡아서 테스트의 성공 여부를 확인하는 것을 알 수 있다. FunctionTestCase 클래스 생성시 setUp으로 init() 함수를, tearDown으로 fin() 함수를 할당하는 것을 볼 수 있다. 다음과 같이 init, test, fin이 정상적으로 수행되는 것을 확인할 수 있다.

Testing started at 오후 8:00 ...


Ran 0 tests in 0.000s

OK
Comments