본문으로 바로가기
728x90
반응형

목차

     

    개요

    최근 SQLAlchemy의 Imperative Mapping을 자주 이용하고 여러 형태의 사용법을 점검하고 있다. 이 과정에서 Pytest를 이용해 어떻게 하면 Imperative Mapping 방식을 이용한 ORM을 테스트할 수 있을지 고민했다.

     

    이 포스팅에 기록하려고 하는 건 나름대로 구성한 프로젝트 구조에서 Pytest를 사용하면서 고민했던 부분과 이를 어떤 방식으로 해결했는지에 관한 것이다.

     

    무엇을 테스트하며 무엇을 고려했는가

    테스트를 하려고 한 대상 코드는 다음과 같은 구성이다.

     

    Service가 Repository의 Method를 사용하고 Repository의 Method는 DataBase에 Query를 날린다. 이를 코드로 표현하면 다음과 같은 구조이다.

    from sqlalchemy.orm.session import Session
    
    class Repository:
        session: Session
    
        def find_by_entity(self, entity_id=None):
            return self.session.query(Entity).filter(Entity.id == entity_id).one_or_none()
    
    class Service:
        repository = Repository()
    
        def retreieve(self):
            self.repository.find_by_entity(entity_id=1)
    

    Service, Repository Class 별로 테스트 코드를 작성할 수도 있겠지만 시스템 상에서 필요한 테스트는 사용자의 행동에 관련된 것이라 생각한다. 위와 같은 구조에서는 Service Class의 retrieve Method이며 이는 사용자가 어떤 데이터를 조회하는 행동이다.

     

    즉, 위와 같은 간단한 예제의 Service Class를 테스트하기 위해 필요한 목록들은 다음과 같다.

    1. DataBase Session 초기화
    2. 조회 기능 테스트에 사용할 Entity (만약 Entity가 없는 걸 가정한다면 테스트 케이스를 추가하자)

    그러나 위의 두 가지는 지극히도 코드에서 보이는 부분에서 필요한 사실일 뿐이다. 테스트를 하기 위한 환경을 Application에서 컨트롤하려면 다음과 같은 사항들이 추가된다.

    1. Test DataBase 생성
    2. 테스트 시작 전 Test DataBase에 Table 생성
    3. 테스트 시작 전 Test DataBase로 연결될 Session 초기화
    4. 테스트 종료 후 Test DataBase에 Table 제거 (혹은 truncate)

    이에 더해 Imperative Mapping 방식을 사용하니 테스트 시작 전 orm_mapper를 호출해줘야 하며 종료 후 orm_mapper도 정리해줘야 한다.

     

    Pytest를 이용한 Imperative Mapping Test

    이제 Imperative Mapping 방식을 이용해 Test 코드를 작성할 때 필요했던 부분을 기술해 보고자 한다. 앞서 언급한 내용에서 유추할 수 있듯 테스트용 데이터베이스가 따로 존재함을 가정한다.

     

    fixture를 이용한 Test DatBase Session 설정

    pytest에서는 conftest.py라는 파일에 테스트 코드에 사용될 fixuture를 미리 지정할 수 있다.

    ├── tests
    │   ├── __init__.py
    │   ├── conftest.py
    

    이 conftest.py를 활용해 Test DataBase를 바라보는 Session fixture를 작성하자.

    @pytest.fixture
    def get_test_db_session():
        db = get_test_db_engine()
        
        db_session = scoped_session(sessionmaker(
            bind=db,
            expire_on_commit=True,
            autocommit=False,
            autoflush=False
        ))
    
        yield db_session()
    
        db_session.close()
    

     

    테스트 시작과 종료 시 테이블 생성 및 삭제하기

    pytest에서 xUnit Style의 테스트 코드를 작성할 수 있는데 이를 이용하면 setup_method, teardown_method를 이용해 테스트 코드에 사용할 리소스를 다룰 수 있게 된다.

     

    이 글에서는 Table을 생성 및 삭제하는 방법에 해당하는데 대략 다음과 같은 형태로 구성할 수 있다.

    class TestService:
        db = get_test_db_engine()
    
        @pytest.fixture(autouse=True)
        def setup_method(self, get_test_db_session):
    		    """ 테스트 메서드 시작 전 테이블 생성 """ 
            Base.metadata.create_all(self.db)
    
    		
        def teardown_method(self):
    		    """ 테스트 메서드 종료 후 테이블 삭제 """ 
            Base.metadata.drop_all(self.db)
    

     

    Test Imperative Mapping

    Imperative Mapping은 명시적으로 orm mapper를 호출해야 한다. 이를 테스트 코드에서 사용할 때는 setup_method 부분에서 사용할 수 있다.

    class TestService:
        db = get_test_db_engine()
    
        @pytest.fixture(autouse=True)
        def setup_method(self, get_test_db_session):
    				start_mapper()		
    

    그러나 setup_method에만 위와 같이 mapper를 호출한다면 테스트 메서드가 제대로 실행되지 않는 현상이 존재한다. 이를 해결하려면 dispose를 호출해야 한다.

    class TestService:
        db = get_test_db_engine()
    
        @pytest.fixture(autouse=True)
        def setup_method(self, get_test_db_session):
    				self.mapper = start_mapper()		
    	
        def teardown_method(self):
    		    self.mapper.dispose()
    

    Imperative Mapping의 dispose관련 문서는 여기를 참조하자.

     

    마치며

    본 포스팅에 기재된 내용은 Pytest를 이용해 Imperative Mapping에 관련된 테스트 코드를 작성할 때 겪었던 부분에 대해 다뤘다.

     

    Project 구조에 따라 Mocking이 필요할 수도 있다. 이는 pytest-mock을 사용하기보다는 python에서 기본적으로 제공되는 unitest의 patch를 선호하는 편인데 이용 형태가 개인적으로는 더 깔끔한 느낌이기 때문이다.

     

    이 포스팅에 기재된 내용은 반드시 적용되어야 하기보다는 이런 이슈가 있었다는 내용이기에 Imperative Mapping 도입을 고려하고 있다면 본 포스팅에서 등장 내용에 유의해보도록 하자.

    728x90
    반응형