Package: pytest-pyodide
pytest-pyodidePytest plugin for testing applications that use PyodideInstallationpytest-pyodide requires Python 3.10+ and can be installed withpip install pytest-pyodideYou would also one at least one of the following runtimes,Chrome and chromedriverFirefox and geckodriverSafari and safaridrivernode v18+Github Reusable workflowpytest-pyodide also supports testing on github actions by means of a reusable workflow in/.github/workflows/main.ymlThis allows you to test on a range of browser/OS combinations without having to install all the testing stuff, and integrate it easily into your CI process.In your github actions workflow, call it with as a aseparate job. To pass in your build wheel use an upload-artifact step in your build step.This will run your tests on the given browser/pyodide version/OS configuration. It runs pytest in the root of your repo, which should catch any test_*.py files in subfolders.jobs:
  # Build for pyodide 0.24.1
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - uses: actions/setup-python@v4
      with:
        python-version: 3.11
    - uses: mymindstorm/setup-emsdk@v11
      with:
        version: 3.1.45
    - run: pip install pyodide-build==0.24.1
    - run: pyodide build
    - uses: actions/upload-artifact@v3
      with:
        name: pyodide wheel
        path: dist
  # this is the job which you add to run pyodide-test
  test:
    needs: build
    uses: pyodide/pytest-pyodide/.github/workflows/main.yaml@main
    with:
      build-artifact-name: pyodide wheel
      build-artifact-path: dist
      browser: firefox
      runner: selenium
      pyodide-version: 0.24.1If you want to run on multiple browsers / pyodide versions etc., you can either use a matrix strategy and run main.yaml as above, or you can use testall.yaml. This by default tests on all browsers (and node) with multiple configurations. If you want to reduce the configurations you can filter with lists of browsers, runners, pyodide-versions as shown below.test:
    needs: build
    uses: pyodide/pytest-pyodide/.github/workflows/testall.yaml@main
    with:
      build-artifact-name: pyodide wheel
      build-artifact-path: dist
      pyodide-versions: "0.23.4, 0.24.1"
      runners: "selenium, playwright"
      browsers: "firefox, chrome, node"
      os: "ubuntu-latest, macos-latest"UsageFirst you need a compatible version of Pyodide. You can download the Pyodide build artifacts from releases with,wgethttps://github.com/pyodide/pyodide/releases/download/0.24.1/pyodide-build-0.24.1.tar.bz2
tarxjfpyodide-build-0.24.1.tar.bz2
mvpyodidedist/You can then use the provided fixtures (selenium,selenium_standalone) in tests,deftest_a(selenium):selenium.run("assert 1+1 == 2")# run Python with Pyodidewhich you can run withpytest--dist-dir=./dist/run_in_pyodideSome tests simply involve running a chunk of code in Pyodide and ensuring it
doesn't error. In this case, one can use therun_in_pyodidedecorate frompytest_pyodide, e.g.frompytest_pyodideimportrun_in_pyodide@run_in_pyodidedeftest_add(selenium):assert1+1==2In this case, the body of the function will automatically be run in Pyodide. The
decorator can also be called with apackagesargument to load packages before
running the test. For example:frompytest_pyodideimportrun_in_pyodide@run_in_pyodide(packages=["regex"])deftest_regex(selenium_standalone):importregexassertregex.search("o","foo").end()==2You can also use@run_in_pyodidewithpytest.mark.parametrize, withhypothesis, etc.@run_in_pyodideMUST be the
innermost decorator. Any decorators inside of@run_in_pyodidewill be have no
effect on the behavior of the test.frompytest_pyodideimportrun_in_pyodide@pytest.mark.parametrize("x",[1,2,3])@run_in_pyodide(packages=["regex"])deftest_type_of_int(selenium,x):asserttype(x)isintThe first argument to a@run_in_pyodidefunction must be a browser runner,
generally aseleniumfixture. The remaining arguments and the return value of
the@run_in_pyodidefunction must be picklable. The arguments will be pickled
in the host Python and unpickled in the Pyodide Python. The reverse will happen
to the return value. The firstseleniumargument will beNoneinside the
body of the function (it is used internally by the fixture). Note that a
consequence of this is that the received arguments are copies. Changes made to
an argument will not be reflected in the host Python:@run_in_pyodidedefmutate_dict(selenium,x):x["a"]=-1returnxdeftest_mutate_dict():d={"a":9,"b":7}assertmutate_dict(d)=={"a":-1,"b":7}# d is unchanged because it was implicitly copied into the Pyodide runtime!assertd=={"a":9,"b":7}You can also use fixtures as long as the return values of the fixtures are
picklable (most commonly, if they areNone). As a special case, the function
will see theseleniumfixture asNoneinside the test.If you need to return a persistent reference to a Pyodide Python object, you can
use the specialPyodideHandleclass:@run_in_pyodidedefget_pyodide_handle(selenium):frompytest_pyodide.decoratorimportPyodideHandled={"a":2}returnPyodideHandle(d)@run_in_pyodidedefset_value(selenium,h,key,value):h[key]=value@run_in_pyodidedefget_value(selenium,h,key):returnh[key]deftest_pyodide_handle(selenium):h=get_pyodide_handle(selenium)assertget_value(selenium,h,"a")==2set_value(selenium,h,"a",3)assertget_value(selenium,h,"a")==3This can be used to create fixtures for use with@run_in_pyodide.It is possible to userun_in_pyodideas an inner function:deftest_inner_function(selenium):@run_in_pyodidedefinner_function(selenium,x):assertx==6return7assertinner_function(selenium_mock,6)==7However, the function will not see closure variables at all:deftest_inner_function_closure(selenium):x=6@run_in_pyodidedefinner_function(selenium):assertx==6return7# Raises `NameError: 'x' is not defined`assertinner_function(selenium_mock)==7Thus, the only value of inner@run_in_pyodidefunctions is to limit the scope
of the function definition. If you need a closure, you will have to wrap it in a
second function call.Copying files to PyodideYou can copy files to the pyodide filesystem using thecopy_files_to_pyodidedecorator. This takes two arguments - a list of(src,destination)pairs. These can be any of: 1) A filename, 2) A folder name, which is copied to the destination path (along with all subdirectories ifrecurse_directoriesis True), 3) A glob pattern, which will fetch all files matching the pattern and copy them to a destination directory, whilst preserving the folder structure.If you setinstall_wheelsto True, any.whlfiles will be installed on pyodide. This is useful for installing your package.frompytest_pyodide.decoratorimportcopy_files_to_pyodide@copy_files_to_pyodide(file_list=[(src,dest)],install_wheels=True,recurse_directories=True)Running non-pyodide tests in PyodideThis plugin also supports running standard pytest tests on pyodide in a browser. So if you have an existing codebase and
you want to check if your pyodide build works, just run it like this:# Make the emscripten/wasm32  wheel in the dist folder
pyodide build
# the following code
# a) copies the test_path directory and subfolders to a Pyodide instance, and
# b) installs any wheels in the dist subfolder so that this package is available on the Pyodide VM
pytest --run-in-pyodide test_path --runtime <runtime> --dist-dir=<pyodide/dist>Specifying a browserYou can specify a browser runtime using--runtime(--rt) commandline option.Possible options for--runtimeare:node (default)firefoxchromesafarihost (do not run browser-based tests)pytest--runtimefirefox
pytest--runtimefirefox--runtimechrome# Adding -no-host suffix will disable running host testspytest--runtimechrome-no-hostRunning tests with Playwright (optional)By default, the tests will be run with Selenium.
It is possible to run tests withplaywrightinstead as follows.First install playwright browserspython-mplaywrightinstall--with-depsThen use the--runnerargument to specify to run tests with playwright.pytest --runner playwrightCustom test marksCustom test marks supported bypytest-pyodide:pytest.mark.driver_timeout(timeout): Set script timeout in WebDriver. If the
test is known to take a long time, you can extend the deadline with this marker.pytest.mark.xfail_browsers(chrome="why chrome fails"): xfail a test in
specific browsers.ExamplesSeeexamples.Compatible Pyodide versionsSeecompatibility table.Licensepytest-pyodide uses theMozilla Public License Version
2.0.