Testing Python Projects
This page will discuss topics around defining and running Python tests using the Mill build tool
Defining Unit Test Suites
package build
import mill._, pythonlib._
object foo extends PythonModule {
def mainScript = Task.Source { millSourcePath / "src/foo.py" }
object test extends PythonTests with TestModule.Unittest {
def mainScript = Task.Source { millSourcePath / "src/test_foo.py" }
}
}
This build defines a single module with a test suite, configured to use
"Unittest" as the testing framework. Test suites are themselves PythonModule
s,
nested within the enclosing module, and have all the normal tasks
available to run, but with an additional .test
task
that runs the tests. You can also run the test suite directly, in which case
it will run the .test
task as the default task for that module.
> ./mill foo.run
Hello World
> ./mill foo.test.test
test_hello (test_foo.TestScript...) ... ok
test_mock (test_foo.TestScript...) ... ok
test_world (test_foo.TestScript...) ... ok
...Ran 3 tests...
OK
> ./mill foo.test # same as above, `.test` is the default task for the `test` module
...Ran 3 tests...
OK
> ./mill foo.test test_foo.TestScript.test_mock # explicitly select the test class you wish to run
test_mock (test_foo.TestScript...) ... ok
...Ran 1 test...
OK
For convenience, you can also use one of the predefined test frameworks:
Each testing framework has their own flags and configuration options that are documented on their respective websites, so please see the links above for more details on how to use each one from the command line.
object bar extends PythonModule {
def mainScript = Task.Source { millSourcePath / "src/bar.py" }
object test extends PythonTests with TestModule.Pytest
}
> ./mill bar.test
...test_bar.py::test_hello PASSED...
...test_bar.py::test_world PASSED...
...test_bar.py::test_mock PASSED...
...3 passed...
You can also select multiple test suites in one command using Mill’s Task Query Syntax
> ./mill __.test
test_hello (test_foo.TestScript...) ... ok
test_mock (test_foo.TestScript...) ... ok
test_world (test_foo.TestScript...) ... ok
...Ran 3 tests...
OK
...test_bar.py::test_hello PASSED...
...test_bar.py::test_world PASSED...
...test_bar.py::test_mock PASSED...
...3 passed...
Mill provides multiple ways of running tests
> ./mill foo.test
test_hello (test_foo.TestScript...) ... ok
test_mock (test_foo.TestScript...) ... ok
test_world (test_foo.TestScript...) ... ok
...Ran 3 tests...
OK
-
foo.test
: runs tests in a subprocess in an emptysandbox/
folder. This is short forfoo.test.test
, astest
is the default task forTestModule
s.
> ./mill foo.test.testCached
test_hello (test_foo.TestScript...) ... ok
test_mock (test_foo.TestScript...) ... ok
test_world (test_foo.TestScript...) ... ok
...Ran 3 tests...
OK
-
foo.test.testCached
: runs the tests in an emptysandbox/
folder and caches the results if successful. This can be handy if you are you working on some upstream modules and only want to run downstream tests which are affected: usingtestCached
, downstream tests which are not affected will be cached after the first run and not re-run unless you change some file upstream of them.
It is common to run tests with -w/--watch enabled, so that once you save a file to disk the selected tests get re-run.
Mill runs tests with the working directory set to an empty
sandbox/ folder by default.
Additional paths can be provided to test via forkEnv . See
Pythonpath and Filesystem Resources
for more details.
|
If you want to pass any arguments to the test framework, you can pass them after
foo.test
in the command line. e.g. Pytest
lets you pass in a selector to decide which test to run, which in Mill would be:
> ./mill bar.test bar/test/src/test_bar.py::test_mock # explicitly select the test class you wish to run
...test_bar.py::test_mock PASSED...
...1 passed...
This command only runs the test_mock
test case in the bar.test
test suite class.
Test Dependencies
You can use pythonDeps
to declare dependencies in test modules,
and test modules can use their moduleDeps
to also depend on each other
package build
import mill._, pythonlib._
object foo extends PythonModule {
def mainScript = Task.Source { millSourcePath / "src/foo.py" }
def moduleDeps = Seq(bar)
def pythonDeps = Seq("jinja2==3.1.4")
object test extends PythonTests with TestModule.Unittest {
def moduleDeps = super.moduleDeps ++ Seq(bar.test)
def pythonDeps = Seq("MarkupSafe==3.0.2")
}
}
object bar extends PythonModule {
def mainScript = Task.Source { millSourcePath / "src/bar.py" }
object test extends PythonTests with TestModule.Unittest
}
In this example, not only does foo
depend on bar
, but we also make
foo.test
depend on bar.test
.
That lets foo.test
make use of the
BarTestUtils
class that bar.test
defines, allowing us to re-use this
test helper throughout multiple modules test suites
> ./mill foo.test
...Using BarTestUtils.bar_assert_equals...
...test_equal_string (test.TestScript...)...ok...
...Ran 1 test...
...OK...
> ./mill bar.test
...Using BarTestUtils.bar_assert_equals...
...test_mean (test.TestScript...)...ok...
...Ran 1 test...
...OK...
> ./mill foo.run
<h1><XYZ></h1>
> ./mill bar.run
123
Defining Integration Test Suites
You can also define test suites with different names other than test
. For example,
the build below defines a test suite with the name itest
, in addition
to that named test
.
package build
import mill._, pythonlib._
object foo extends PythonModule {
def mainScript = Task.Source { millSourcePath / "src/foo.py" }
object test extends PythonTests with TestModule.Unittest
object itest extends PythonTests with TestModule.Unittest
}
These two test modules will expect their sources to be in their respective foo/test
and
foo/itest
folder respectively.
> ./mill foo.test # run Unit test suite
test_hello (test.TestScript...) ... ok
test_world (test.TestScript...) ... ok
...Ran 2 tests...
...OK...
> ./mill foo.itest # run Integration test suite
...test_hello_world (test.TestScript...) ... ok
...Ran 1 test...
...OK...
> ./mill 'foo.{test,itest}' # run both test suites
...test_hello (test.TestScript...)...ok...
...test_world (test.TestScript...)...ok...
...Ran 2 tests...
...test_hello_world (test.TestScript...)...ok...
...Ran 1 test...
...OK...
> ./mill __.itest.testCached # run all integration test suites
...test_hello_world (test.TestScript...) ... ok
...Ran 1 test...
...OK...