肉球でキーボード

MLエンジニアの技術ブログです

pythonのテスト環境作成ツールNoxを使う

本文中コード

github.com

Noxとは

Noxはテスト用のpython仮想環境を作成し、テストを自動化するコマンドラインツールです。

Welcome to Nox — Nox 2024.10.9 documentation

Noxを利用することで以下のことを実現できます。

  • チームメンバーのローカルPC上の環境差分の解消
  • CIとローカルPCでの環境差分の解消
  • 複数のPython・ライブラリバージョンでの動作確認

Pythonのテスト環境作成ツールとして tox が有名です。
tox

Noxとtoxの簡単な比較です。

tox Nox
リリース年 2010 2018
設定ファイルフォーマット INI python
GitHubスター数 3664 1310

GitHub Start数の推移を見ても、toxの方が知名度の高さが伺えます。

github star history (tox vs nox)

Noxとtoxの大きな違いは、設定ファイルをtoxはDSLで記述するtox.iniで管理するのに対し、Noxはpythonファイルであるnoxfile.pyで管理することです。

NoxはPythonを用いた柔軟なテスト自動化を行えるのが、toxと比較した特徴です。

GitHub - wntrblm/nox: Flexible test automation for Python

インストール

Noxはプロジェクトの仮想環境ではなく、グローバルの環境にインストールするよう設計されています。

そのためNoxの公式ドキュメントではpipxによるインストールが推奨されてます。

$ pipx install nox

自分はuvを使ってpython環境を用意してるので、uv toolとしてnoxをinstallしました。

$ uv tool install nox 

実行方法

以下の一連のテストをnoxで自動化してみます

  • ruff check / format
  • mypy
  • pytest

サンプルプロジェクトとしてロジスティック回帰でirisデータセットを学習するコードを用意しました。

nox-github-actions-example/src/train_lr.py at main · nsakki55/nox-github-actions-example · GitHub

作成したnoxfile.pyです

import nox

@nox.session(venv_backend="uv", python=["3.12"], tags=["lint"])
def lint(session):
    session.install("ruff")
    session.run("uv", "run", "ruff", "check")
    session.run("uv", "run", "ruff", "format")

@nox.session(venv_backend="uv", python=["3.12"], tags=["lint"])
def mypy(session):
    session.install("pyproject.toml")
    session.install("mypy")
    session.run("uv", "run", "mypy", "src")

@nox.session(venv_backend="uv", python=["3.12"], tags=["test"])
def test(session):
    session.run("uv", "sync", "--dev")

    if session.posargs:
        test_files = session.posargs
    else:
        test_files = ["tests"]

    session.run("uv", "run", "pytest", *test_files)

Noxでは@nox.sessionデコレータがついた関数を作成して、静的解析やpytestなど実行したいテストをそれぞれ記述します。
sessionごとに仮想環境が作成されるため、session単位で仮想環境の設定を変更できます。
noxは20240年3月のリリース以降、python仮想環境のバックエンドにuvを指定することができるようになりました。
Release 2024.03.02 · wntrblm/nox · GitHub
Configuration & API — Nox 2024.10.9 documentation

uvによるパッケージインストールのため、noxの仮想環境の立ち上げが高速になります。

Noxの実行はnoxfile.pyがあるディレクトリ直下で、 nox コマンドを実行することで行えます。
上記のnoxfile.pyが読み取られ、各セッションが実行されています。

ruffとpyproject.tomlのパッケージインストールが uv pip install で行われているのが分かります。
session.run(”uv”, “run”, “sync”, “—dev”) コマンドでpyproject.tomlからdev用のパッケージをインストールできます。

$ uvx nox
nox > Running session lint-3.12
nox > Creating virtual environment (uv) using python3.12 in .nox/lint-3-12
nox > uv pip install ruff
nox > uv run ruff check
   Built nox-sandbox @ file:///Users/satsuki/github/nox-sandbox
Uninstalled 1 package in 0.45ms
Installed 1 package in 0.71ms
All checks passed!
nox > uv run ruff format
1 file reformatted, 5 files left unchanged
nox > Session lint-3.12 was successful.
nox > Running session mypy-3.12
nox > Creating virtual environment (uv) using python3.12 in .nox/mypy-3-12
nox > uv pip install pyproject.toml
nox > uv pip install mypy
nox > uv run mypy src
Success: no issues found in 3 source files
nox > Session mypy-3.12 was successful.
nox > Running session test-3.12
nox > Creating virtual environment (uv) using python3.12 in .nox/test-3-12
nox > uv sync --dev
Resolved 17 packages in 0.39ms
Audited 15 packages in 0.02ms
nox > uv run pytest tests
================================================================================================================================================================================ test session starts =================================================================================================================================================================================
platform darwin -- Python 3.12.5, pytest-8.3.3, pluggy-1.5.0
rootdir: /Users/satsuki/github/nox-sandbox
configfile: pyproject.toml
collected 4 items

tests/test_train_lr.py ....                                                                                                                                                                                                                                                                                                                                                    [100%]

================================================================================================================================================================================= 4 passed in 0.46s ==================================================================================================================================================================================
nox > Session test-3.12 was successful.
nox > Ran multiple sessions:
nox > * lint-3.12: success
nox > * mypy-3.12: success
nox > * test-3.12: success

python仮想環境が瞬時に立ち上がっているのを確認できます。

run nox

tagをsessionごとに割り当てて、tag単位で実行を行えます。
https://nox.thea.codes/en/stable/tutorial.html#session-tags

上記の例だとruffとmypyのsessionをlintというタグに割り当てているため、以下のようにlintタグを指定することでruffとmypyのみ実行できます。

$ uvx nox -t lint

実行引数を渡すことができます。test sessionの例ではテスト対象のファイルを実行引数で渡せるようにしてます。
https://nox.thea.codes/en/stable/config.html#passing-arguments-into-sessions

$ uvx nox -t test -- tests/test_train_lr.py

GitHub Actionsで実行する

uv経由でnoxをインストール・実行するGitHub Actionsを作成しました。

name: Run nox tests

on: [push]

jobs:
  tests:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
      - name: Set up Python 3.12
        uses: actions/setup-python@v2
        with:
          python-version: 3.12
      - name: Install uv and nox
        run: |
          pip install --upgrade pip
          pip install uv
          uv tool install nox
      - name: Run Nox
        run: |
          uvx nox

nox実行部分が11秒で完了しています。

add check · nsakki55/nox-github-actions-example@94ea041 · GitHub

その他の便利な機能

公式ドキュメントを見ていて、自分が便利に思った機能をピックアップして紹介します。

session options 設定

コマンドライン引数で設定できる値を、noxfile.pyで設定できます。sessionごとにpythonバージョンや仮想環境のバックエンドを指定してましたが、まとめて行えます。

https://nox.thea.codes/en/stable/config.html#modifying-nox-s-behavior-in-the-noxfile

nox.options.python = "3.12"
nox.options.default_venv_backend = "uv"

実行順序の設定

session.notify をsessionの最後に追記することで、次に実行するsessionを指定できます。

https://nox.thea.codes/en/stable/config.html#nox.sessions.Session.notify

以下の例だとlint終了後にmypyが実行されます。

@nox.session()
def lint(session):
    session.install("ruff")
    session.run("uv", "run", "ruff", "check")
    session.run("uv", "run", "ruff", "format")

    session.notify("mypy")

@nox.session()
def mypy(session):
    session.install("pyproject.toml")
    session.install("mypy")
    session.run("uv", "run", "mypy", "src")

環境変数の設定

session.run コマンドの機能の一つですが、環境変数をrun単位で設定できます。

https://nox.thea.codes/en/stable/config.html#nox.sessions.Session.run

session.run(
    'bash', '-c', 'echo $SOME_ENV',
    env={'SOME_ENV': 'Hello'})

runコマンドでは、以下のように1文で実行コマンドを渡せないので注意が必要です。

session.run('pytest -k fast tests/')

セッションパラメータの設定

事前に設定したパラメータの値ごとsessionを実行できます。

https://nox.thea.codes/en/stable/config.html#parametrizing-sessions

@nox.session
@nox.parametrize('django', ['1.9', '2.0'])
@nox.parametrize('database', ['postgres', 'mysql'])
def tests(session, django, database):
    ...

Nox自体のバージョン指定

実行するNox自体のバージョンをnoxfile.py内で指定することができます。

https://nox.thea.codes/en/stable/config.html#nox-version-requirements

import nox

nox.needs_version = ">=2019.5.30"

@nox.session(name="test")  # name argument was added in 2019.5.30
def pytest(session):
    session.run("pytest")

Tox or Nox ?

ToxとNoxについて分かりやすく説明してくれたYouTube動画があるのでこちらが参考になると思います。
www.youtube.com

ToxとNoxどちらの方が優れているというわけではなく、それぞれの良さがあるという話です。

知名度で言うとToxの方があり、Toxに関する記事の方がNoxよりも豊富にあります。
NoxはPythonで記述できることによる柔軟性を特徴とする一方、Toxはエンジニアに広く馴染みのあるDSLで記述できるという特徴があります。
どちらを採用するかはチームやプロジェクト内容に依存するところが大きいのかなと思います。

参考