본문 바로가기

COMPUTER SCIENCE/PYTHON

[OSSCA] python-mysql-replication - 오픈소스는 어떻게 테스트를 진행하고 있을까?

개발을 하다보면 기능 구현에 집중하느라 정작 테스트 확인은 간과하게 되는 경우가 많았다.
이 정도면 괜찮겠지..! 하고 넘기면 며칠 뒤의 나에게 고통을 선사하게 되는 악순환을 겪게 되었다. 

ChatGPT에게 물어보니 안전벨트 없이 운전하는 거라는 엄청난 비유로 말해주었다

특히 오픈소스는 누구나 기여할 수 있다는 부분으로 인해 테스트가 더 중요하게 여겨질 것이라고 생각이 들었다.
그래서 이번 글에서는 python-mysql-replication 오픈소스에서 어떻게 테스트를 구현했는지 살펴보고자 한다.
또한 2023 오픈소스 컨트리뷰션을 통해 기존 테스트 구현 방식을 직접 개선한 부분에 대해서도 담아볼 예정이다.

 

지난 글 다시 보기

 

python-mysql-replication 테스트 과정

테스트는 pytest를 기반으로 Github Actions를 통해 진행되고 있었다.
해당 오픈소스 특성 상 Python과 MySQL에 대한 테스트가 필요했는데,
Python의 여러 구현체 및 버전에 대해서는 Github Actions의 matrix 기능을 이용하고 있었으며,
MySQL의 여러 버전에 대해서는 Docker Compose를 이용해 띄워주고 있었다.

한번 Github Actions 기반의 workflow yaml 파일을 통해 세부적으로 살펴보고자 한다.

Actions - 초기 설정

먼저 설정 부분을 통해 해당 workflow가 push, pull request, workflow_dispatch 기반으로 실행된다는 것을 알 수 있다.
여기에서의 workflow_dispatch는 직접 수동으로 해당 workflow를 실행할 수 있는 설정을 나타낸다.
(참고: https://docs.github.com/ko/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch)

name: PyTest
on:
  push:
  pull_request:
  workflow_dispatch:
env:
  PYTEST_SKIP_OPTION: "not test_no_trailing_rotate_event and not test_end_log_pos"

Actions - matrix 기반 Python 버전 설정

그 다음으로 실행할 작업이 담겨있는 jobs 부분에는 matrix 기능을 통해 python의 여러 구현체(CPython 3.7, CPython 3.11, PyPy 3.7, PyPy 3.10)가 설정되어 있다.
matrix는 이처럼 여러 버전을 기반으로 작업을 실행해야 할 때 주로 이용되며,
여러 python 버전은 actions/setup-python@v4 을 통해 설정된다.
(matrix 참고: https://docs.github.com/ko/enterprise-cloud@latest/actions/using-jobs/using-a-matrix-for-your-jobs
setup-python 참고: https://github.com/actions/setup-python)

jobs:
  test:
    strategy:
      fail-fast: false
      matrix:
        include:
          - {name: 'CPython 3.7', python: '3.7'}
          - {name: 'CPython 3.11', python: '3.11'}
          - {name: 'Pypy 3.7', python: 'pypy-3.7'}
          - {name: 'Pypy 3.10', python: 'pypy-3.10'}
    name: ${{ matrix.name }}
    runs-on: ubuntu-latest
    timeout-minutes: 3

Actions - steps 설정

이제 본격적으로 작업할 과정이 나오게 된다. 순차적으로 설명하자면 다음과 같다.

  1. 코드 업데이트 (checkout)
    : github repo 내용을 불러오는 역할을 하는 action으로,
    https://github.com/actions/checkout 을 통해 세부 내용을 확인할 수 있다.
  2. python 설정 (setup-python)
    : matrix에서 설정해주었던 python 버전에 대해 설정해주는 action으로,
    https://github.com/actions/setup-python 을 통해 세부 내용을 확인할 수 있다.
  3. docker compose 기반 dbms(MySQL, MariaDB 등) 띄우기
    : docker-compose.yml 에 정의되어 있는 MySQL 및 MariaDB 세팅에 대해 docker compose를 통해 동시에 띄워준다.
    (docker compose 파일에 대한 세부 설명은 하단에서 진행할 예정이다.)
  4. dependency(pytest 등) 설치 (install dependencies)
    : setup.pypyproject.toml를 기반으로 필요한 패키지들을 설치해준다.
  5. 각 dbms에 대해 pytest 진행
    : docker compose를 통해 띄워진 dbms에 대해서 각각 pytest를 순차적으로 진행한다.
    (dbms가 순차적으로 테스트되고 있는 부분은 사실 병렬처리를 해도 무방한 부분이지만,
    docker compose 세팅 및 로그 관리 등을 위해 순차적으로 진행하도록 설정되었다.)
    steps:
      - name: Check out code
        uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v4
        with:
          python-version: ${{ matrix.python }}

      - name: Run database server in docker
        run: |
          docker compose create
          docker compose start
          echo "wait mysql server"

          while :
          do
            if mysql -h 127.0.0.1 --user=root --execute "SELECT version();" 2>&1 >/dev/null && mysql -h 127.0.0.1 --port=3307 --user=root --execute "SELECT version();" 2>&1 >/dev/null; then
              break
            fi
            sleep 1
          done

          echo "run pytest"

      - name: Install dependencies
        run: |
          pip install .
          pip install pytest

      - name: Run tests for mysql-5
        working-directory: pymysqlreplication/tests
        run: pytest -k "$PYTEST_SKIP_OPTION" --db=mysql-5

      - name: Run tests for mysql-5-ctl
        working-directory: pymysqlreplication/tests
        run: pytest -k "$PYTEST_SKIP_OPTION" --db=mysql-5-ctl

      - name: Run tests for mysql-8
        working-directory: pymysqlreplication/tests
        run: pytest -k "$PYTEST_SKIP_OPTION" --db=mysql-8

      - name: Run tests for mariadb-10
        working-directory: pymysqlreplication/tests
        run: pytest -k "$PYTEST_SKIP_OPTION" -m mariadb --db=mariadb-10

 

이러한 설정을 기반으로 진행된 workflow 목록은 https://github.com/julien-duponchelle/python-mysql-replication/actions/workflows/pytest.yml 에서 확인할 수 있으며,
각 workflow를 클릭하면 아래와 같이 Python 버전 별로 job 목록을 구분지어 확인할 수 있다.

 

Docker Compose - MySQL / MariaDB 띄우기

이번에는 steps에서 진행해주었던 docker compose 기반으로 MySQL과 MariaDB를 띄운 yaml 설정 파일에 대해서 살펴보고자 한다.

먼저 docker compose에 대한 버전은 3.4로 지정되어 있으며,
MySQL과 MariaDB에 대한 기본 설정(환경 변수 및 실행 command 등)이 정의되어 있다.
이 내용은 뒤의 services 부분에서 "<<: *mysql"과 "<<: *mariadb"를 통해 사용된다.

version: '3.4'

x-mysql: &mysql
  environment:
    MYSQL_ALLOW_EMPTY_PASSWORD: true
  command: >
    mysqld
    --log-bin=mysql-bin.log
    --server-id 1
    --binlog-format=row
    --gtid_mode=on
    --enforce-gtid-consistency=on

x-mariadb: &mariadb
  environment:
    MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: 1
  command: >
    --log-bin=master-bin
    --server-id=1
    --default-authentication-plugin=mysql_native_password
    --binlog-format=row

 

그리고 services 부분을 통해 각 dbms를 띄우는 작업이 진행된다.
MySQL의 경우 성능이나 보안 측면에서 좀 더 최적화 되어 있는 Percona 이미지를 이용하며,
(참고: https://www.percona.com/mysql/software/percona-server-for-mysql)
각각 앞에서 정의된 dbms별 기본 설정에 따라 다른 port로 띄워지게 된다.

services:
  percona-5.7:
    <<: *mysql
    image: percona:5.7
    ports:
      - "3306:3306"

  percona-5.7-ctl:
    <<: *mysql
    image: percona:5.7
    ports:
      - "3307:3306"

  percona-8.0:
    <<: *mysql
    image: percona:8.0
    ports:
      - "3309:3306"

  mariadb-10.6:
    <<: *mariadb
    image: mariadb:10.6
    ports:
      - "3308:3306"
    volumes:
      - type: bind
        source: ./.mariadb
        target: /opt/key_file
      - type: bind
        source: ./.mariadb/my.cnf
        target: /etc/mysql/my.cnf

 

오픈소스 기여

사실 위에서 설명한 테스트 과정은 최신 버전의 내용이며,
실제로 2023 오픈소스 컨트리뷰션 아카데미를 통해 팀원분들과 기여했던 내용이 포함되어 있다.
대표적으로 변경한 부분은 다음과 같다.

1) MySQL 8.0 버전 테스트 추가

우선 기존 해당 오픈소스는 MySQL 5.7 버전을 중심으로 구현되어 있었다. 그렇기에 테스트 또한 5.7 버전으로만 진행되고 있었다.
하지만 여러 event를 추가로 구현하고, 8.0 버전에서만 사용되는 기능을 추가하게 되면서 8.0 버전에 대한 테스트도 필요하게 되었다.
해당 오픈소스 컨트리뷰션을 같이 진행한 다른 팀의 경우, MariaDB를 기반으로 하는 event를 신규로 추가하면서 MariaDB 테스트를 추가했다.

2) 테스트 구조 변경

위처럼 여러 버전의 dbms를 추가해주다 보니, 기존 방식으로는 모든 테스트를 진행하기에 한계가 존재했다.

이전 방식으로는 각 dbms 버전 별로 class를 만들어서 개별적으로 설정해주고 있었다.
이러다보니 새로 추가된 MySQL 8.0과 MariaDB는 일부 테스트만 진행하고 있었고,
전체 테스트를 진행하려면 중복으로 코드를 작성해야만 하는 문제가 발생했다.

따라서 테스트 관련 dbms가 세팅되는 대표 class를 하나만 두고 dbms 정보는 받아와서 설정하도록 구조를 변경했다.
각 dbms별로 설정되는 정보는 class 안에서 정의하는 것이 아닌, config.json 파일을 따로 만들어서 보관되도록 했다.

{
  "mysql-5": {
    "host": "localhost",
    "user": "root",
    "passwd": "",
    "port": 3306,
    "use_unicode": true,
    "charset": "utf8",
    "db": "pymysqlreplication_test"
  },
  "mysql-5-ctl": {
    "host": "localhost",
    "user": "root",
    "passwd": "",
    "port": 3307,
    "use_unicode": true,
    "charset": "utf8",
    "db": "pymysqlreplication_test"
  },
  "mariadb-10": {
    "host": "localhost",
    "user": "root",
    "passwd": "",
    "port": 3308,
    "use_unicode": true,
    "charset": "utf8",
    "db": "pymysqlreplication_test"
  },
  "mysql-8": {
    "host": "localhost",
    "user": "root",
    "passwd": "",
    "port": 3309,
    "use_unicode": true,
    "charset": "utf8",
    "db": "pymysqlreplication_test"
  }
}

 

 
그리고 실제 pytest를 실행할 때는 --db 파라미터를 통해 어떤 dbms를 대상으로 테스트를 진행할 것인지 설정해주었다.
pytest에서 변수를 받아오는 부분은 conftest.py에서 @pytest.fixture를 이용해서 설정할 수 있었다.

import pytest

def pytest_addoption(parser):
    parser.addoption("--db", action="store", default="mysql-5")

@pytest.fixture
def get_db(request):
    return request.config.getoption("--db")

 
그 결과 아래와 같이 기여를 할 수 있었고, 여러 버전의 dbms에 대해서 각각 전반적인 테스트를 진행하도록 구조를 바꿀 수 있었다 :)
(https://github.com/julien-duponchelle/python-mysql-replication/pull/502)

 

Modify test structure by heehehe · Pull Request #502 · julien-duponchelle/python-mysql-replication

Resolve #499 Overview As we add test for MySQL 8 in #484, we need to create an additional class to test everything against MySQL 8. So instead of creating a class based on the database, we'd like t...

github.com


이렇게 테스트에 대한 설정이 체계적으로 되어 있었기에,
전세계 사람들이 기여하는 오픈소스임에도 문제 없이 잘 배포되고 많은 개발자들이 이용할 수 있게 되었다고 생각한다.

테스트 부분을 기여하면서 테스트의 중요성을 깨달으면서 사내에서도 github actions를 기반으로 pytest를 도입하게 되었는데,
덕분에 직접 확인하기 이전에 pytest가 오류를 먼저 잡아줘서 작은 불씨를 미리 끌 수 있는 계기가 되기도 했다 😄
앞으로도 번거롭더라도 테스트 기반으로 탄탄하게 코드를 작성해서 문제 없는 좋은 기능들이 많이 구현할 수 있길..!!

반응형