[Python] Testing Framework - pytest, unittest

Jaehyeong Kwon·2023년 2월 27일
0

파이썬

목록 보기
2/2

1. Installation


1) Unittest

unittest는 python에 내장되어 있는 표준 라이브러리 입니다. 그래서 바로 import 하여 사용하는 것이 가능합니다.

import unittest

2) Pytest

pytest는 설치하고 import 하여 사용합니다.

$ pip install pytest
import pytest

2. Naming Conventions (unittest/ pytest)


1) Unittest

  • 파일명: test로 시작
  • Unittest에서는 지정된 파일은 반드시 모듈로 import 가능하여야 합니다.
  • 메소드명: test로 시작
  • 단위 테스트의 기본 구성 블록인 TestCase를 base class로 하는 클래스를 작성하고, 테스트를 수행하는 로직을 메서드로 추가하여 테스트 케이스를 생성합니다. 이때 특정한 테스트 코드를 수행하도록 test로 시작하는 이름의 테스트 메소드를 작성합니다.
  • 실행 명령: 실행하는 방법은 2가지로, 코드를 추가하고 python으로 직접 실행할 수 있으며 conventions에 맞게 파일명을 다 적어준다면 2번 명령어로 test 탐색을 통해 unittest가 일괄적으로 진행되게 할 수도 있습니다.
    • python [테스트 파일명]
    • python -m unittest [option] [테스트 파일명]

2) Pytest

pytest에서는 아래와 같은 규칙으로 작성만해주면 pytest라는 명령어로 테스트를 탐색하여 테스트를 진행합니다.

  • 파일명: pytest will run all files of the form test_ or _test.py inthe current directory and its subdirectories

  • 함수명: test로 시작 (pytest discovers all tests following its conventions for python test discovery, so it finds both test prefixed functions)

  • Class 명: Test로 시작 (prefix your class with Test otherwise the class will be skipped)

  • 실행명령:

    • 1) $ pytest
    • 2) $ pytest [테스트 파일 명]

3. An Example of a Simple Test (unittest / pytest)

1) Unittest

unittest의 경우 unittest.TestCase를 상속받아서 class 형태로 작성해야 한다.

from tests.unittest1.exl import add 

import unittest

class Add_testing(unittest.TestCase):
	def test1(self):
    	self.assertEqual(add(1,2), 3)
        
    def test_str(self):
    	self.assertEqual(add("a", "b"), "ab")
        
if __name__ == '__main__':
	unittest.main()

2) Pytest

pytest는 convention을 지켜서 함수형으로 작성할 수도 있고, unittest와 달리 TestCase처럼 상속받지 않아도 됩니다.

def validate_age(age):
	if age<0:
    	raise ValueError("Age cannot be less than 0"

Raise Error를 작성하였다면 이 역시 해당 상황에서 기대대로 실행되는 지 테스트를 하는 것이 필요합니다. 이때 unittest에서는 assertRaise, pytest에서는 pytest.raises를 사용할 수 있습니다.
unittest에서 컨텍스트 관리자로 사용되면, 컨텍스트 관리자는 잡은 예외 객체를 exception attribute에 저장하는데, 이를 예외에 대해서 추가적인 검사를 수행하려는 경우 위의 예시 코드처럼 사용할 수 있습니다.

1) Unittest

from tests.unittest2.ex2 import validate_age

import unittest

class Age_testing(unittest.TestCase):
	def test_validate_age_valid_age(self):
    	validate_age(10)
    
    def test_validate_age_invalid_age(self):
    	validate_age(-1)
        
    def test_validate_age_invalid_age2(self):
    	with self.assertRaises(ValueError) as exc_info:
        	validate_age(-1)
        the_exception = exc_info.exception
      	self.assertEqual(str(the_exception), "Age cannot be less than 0")
        
if __name__ == '__main__':
	unittest.main()

2) Pytest

import pytest

from tests.pytest2.ex2 import validate_age

def test_validate_age_valid_age():
	validate_age(10)
    
def test_validate_age_invalid_age():
	with pytest.raises(ValueError, match="Age cannot be less than 0"):
    	validate_age(-1)
        

5. Skipping Tests (unittest / pytest)

제목 그대로 test를 skip할 수 있는 방법입니다.

1) Unittest

import sys
import unittest

from tests.unittest6.ex6 import add 


class Skip_testing(unittest.TestCase):
	@unittest.skip(reason="just wanna skip it"
    def test_add_num(self):
    	self.assertEqual(add(1, 2), 3)
    
    @unittest.skipIf(sys.version_info > (3, 7), reason="use python 3.7 or less")
    def test_add_srt(self):
    	self.assertEqual(add("a", "b"), "ab")
        

2) Pytest

pytest에서 skip할 때 사용한 mark는 skip하는 역할 외에도 테스트에 대한 metadata를 지정해주는 역할을 합니다. 예를 들어 pytest-django에서 테스트할 때 DB에 접근이 필요한 경우
@pytest.mark.django_db 이런 식으로 적엉줍니다.

import pytest

from tests.pytest6.ex6 import add 

@pytest.mark.skip(reason="just wanna skip it")
def test_add_num():
	assert add(1, 2) == 3
    
@pytest.mark.skipif(sys.version_info > (3, 7), reason="use python 3.7 or less")
def test_add_str():
	assert add("a", "b") == "ab"

6. Test Fixture

테스트가 많아지는 경우 사전 설정이 반복되어야 하는 경우가 있다. 이때 Test Fixture가 필요합니다. Fixture는 여러 개의 테스트를 수행할 때 필요한 준비 도구와 재료 및 그에 관련된 정리 동작에 해당합니다.

1) Unittest

반복되는 사전 설정을 setUp()을 사용하여 사전 설정 코드를 밖으로 분리할 수 있습니다. 그리고 테스트 메소드가 실행되고 나서 정리를 위해 tearDown()이 사용됩니다. 정리하면 각각의 테스트 메소드가 실행되기 전에 항상 setUp() 메소드가 실행되어 setUp 내용을 생성하고, 테스트 실행 후 tearDown() 메소드가 실행됩니다.

class Student_testing(unittest.TestCase):
	def setUp(self):
    	print("!setUp making dummy student!")
		self.dummy_student = Student('nik', datetime(2000, 1, 1), "coe")
	
    def test_student_get_age(self):
		self.dummy_student_age = (datetime.now() - self.dummy_student.dob).days
    	self.assertEqual(self.dummy_student.get_age(), self.dummy_student_age)

	def test_student_add_credits(self):
    	self.dummy_student.add_credits(5)
		self.assertEqual(self.dummy_student.get_credits(), 5)

	def test_student_get_credits(self):
    	self.assertEqual(self.dummy_student.get_credits(), 0)

	def tearDown(self):
    	del self.dummy_student
        

테스트마다 setUp해주고 tearDown 했다가 다시 setUp 하는 것보다 모든 테스트가 끝난 이후에 tearDown 하는 것도 필요합니다. 이러한 경우에는 setUpClass, tearDownClass를 사용할 수 있습니다.

2) Pytest

pytest에서는 단순하게 pytest.fixture 데코레이터를 이용하여 구현해줄 수 있습니다.

pytest의 Fixture에는 4가지 scope가 있고, 각각의 scope마다 한 번씩 실행됩니다.

  • Session: pytest를 한 번 실행할 때마다 한 번
  • Module: 테스트 스크립트의 모듈마다 한 번
  • Class: 테스트 클래스마다 한 번
  • Function: 테스트 케이스마다 한 번
@pytest.fixture(scope="function")
def dummy_student(request):
	print("setUp making dummy student")
    dummy_student = Student("nik", datetime(2000, 1, 1), "coe")
    def teardown():
    	print("tearDown dummy student")
    request.addfinalizer(teardown)

	return dummy_student
	
def test_student_get_age(dummy_student):
	dummy_student_age = (datetime.now() - dummy_student.dob).days
	assert dummy_student.get_age() == dummy_student_age

def test_student_add_credits(dummy_student):
	dummy_student.add_credits(5)
	assert dummy_student.get_credits() == 5

def test_student_get_credits(dummy_student):
	assert dummy_student.get_credits() == 0
profile
나무와 같이 성장하는 사람

0개의 댓글