-
-
Notifications
You must be signed in to change notification settings - Fork 369
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
# Pull Request Adds First Class Python Support [Testing Examples] Part of #3928 ## Description Testing Examples for Python `1-test-suite`, `2-test-deps`, `3-integration-suite` Examples ## Related Issues - Link to related issue #3928. ## Checklist - [x] 1-test-suite - [x] 2-test-deps - [x] 3-integration-suite - [x] Updated Documentation ## Status WIP & Require Review!!!
- Loading branch information
1 parent
f962cce
commit b60ad32
Showing
19 changed files
with
400 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
= Testing Python Projects | ||
:page-aliases: Testing_Python_Projects.adoc | ||
|
||
include::partial$gtag-config.adoc[] | ||
|
||
This page will discuss topics around defining and running Python tests using the Mill build tool | ||
|
||
== Defining Unit Test Suites | ||
|
||
include::partial$example/pythonlib/testing/1-test-suite.adoc[] | ||
|
||
== Test Dependencies | ||
|
||
include::partial$example/pythonlib/testing/2-test-deps.adoc[] | ||
|
||
== Defining Integration Test Suites | ||
|
||
include::partial$example/pythonlib/testing/3-integration-suite.adoc[] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
class Bar: | ||
def main(self): | ||
return "Hello World" | ||
|
||
|
||
if __name__ == "__main__": | ||
print(Bar().main()) |
21 changes: 21 additions & 0 deletions
21
example/pythonlib/testing/1-test-suite/bar/test/src/test_bar.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
from unittest.mock import MagicMock | ||
from bar import Bar # type: ignore | ||
|
||
|
||
def test_hello(): | ||
result = Bar().main() | ||
assert result.startswith("Hello"), "Result does not start with 'Hello'" | ||
|
||
|
||
def test_world(): | ||
result = Bar().main() | ||
assert result.endswith("World"), "Result does not end with 'World'" | ||
|
||
|
||
def test_mock(): | ||
mock_bar = MagicMock(spec=Bar) | ||
mock_bar.main.return_value = "Hello Mockito World" | ||
result = mock_bar.main() | ||
|
||
assert (result == "Hello Mockito World"), "Mocked main() did not return expected value" | ||
mock_bar.main.assert_called_once() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
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. | ||
|
||
/** Usage | ||
|
||
> ./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: | ||
// | ||
// * `TestModule.Pytest`, using https://pytest.org[Pytest] | ||
// * `TestModule.Unittest`, using https://docs.python.org/3/library/unittest.html[Unittest] | ||
// | ||
// 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 | ||
} | ||
|
||
/** Usage | ||
|
||
> ./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 | ||
// xref:cli/query-syntax.adoc[Task Query Syntax] | ||
|
||
/** Usage | ||
|
||
> ./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 | ||
|
||
/** Usage | ||
|
||
> ./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 empty `sandbox/` folder. This is short | ||
// for `foo.test.test`, as `test` is the default task for ``TestModule``s. | ||
|
||
/** Usage | ||
|
||
> ./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 empty `sandbox/` 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: using `testCached`, 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 xref:cli/flags.adoc#_watch_w[-w/--watch] | ||
// enabled, so that once you save a file to disk the selected tests get re-run. | ||
// | ||
// NOTE: Mill runs tests with the working directory set to an empty | ||
// xref:depth/sandboxing.adoc[sandbox/ folder] by default. | ||
// Additional paths can be provided to test via `forkEnv`. See | ||
// xref:pythonlib/module-config.adoc#_pythonpath_and_filesystem_resources[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. https://pytest.org[Pytest] | ||
// lets you pass in a selector to decide which test to run, which in Mill would be: | ||
|
||
/** Usage | ||
|
||
> ./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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
class Foo: | ||
def main(self): | ||
return "Hello World" | ||
|
||
|
||
if __name__ == "__main__": | ||
print(Foo().main()) |
24 changes: 24 additions & 0 deletions
24
example/pythonlib/testing/1-test-suite/foo/test/src/test_foo.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import unittest | ||
from unittest.mock import MagicMock | ||
from foo import Foo # type: ignore | ||
|
||
|
||
class TestScript(unittest.TestCase): | ||
def test_hello(self) -> None: | ||
result = Foo().main() | ||
self.assertTrue(result.startswith("Hello")) | ||
|
||
def test_world(self) -> None: | ||
result = Foo().main() | ||
self.assertTrue(result.endswith("World")) | ||
|
||
def test_mock(self): | ||
mock_foo = MagicMock(spec=Foo) | ||
mock_foo.main.return_value = "Hello Mockito World" | ||
result = mock_foo.main() | ||
self.assertEqual( | ||
result, | ||
"Hello Mockito World", | ||
"Mocked hello() did not return expected value", | ||
) | ||
mock_foo.main.assert_called_once() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
def get_value() -> int: | ||
return 123 | ||
|
||
|
||
if __name__ == "__main__": | ||
print(get_value()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import unittest | ||
import statistics | ||
from bar import get_value # type: ignore | ||
from test_utils import BarTestUtils # type: ignore | ||
|
||
|
||
class TestScript(unittest.TestCase): | ||
def test_mean(self) -> None: | ||
BarTestUtils().bar_assert_equals(get_value(), statistics.mean([122, 124])) |
4 changes: 4 additions & 0 deletions
4
example/pythonlib/testing/2-test-deps/bar/test/src/test_utils.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
class BarTestUtils: | ||
def bar_assert_equals(self, a, b): | ||
print("Using BarTestUtils.bar_assert_equals") | ||
assert a == b, f"Expected {b} but got {a}" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
// 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`. | ||
// | ||
// ```graphviz | ||
// digraph G { | ||
// rankdir=LR | ||
// node [shape=box width=0 height=0] | ||
// "bar" -> "bar.test" -> "foo.test" | ||
// "bar" -> "foo" -> "foo.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 | ||
|
||
/** Usage | ||
|
||
> ./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 | ||
|
||
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
from jinja2 import Template | ||
|
||
|
||
def get_value(text: str) -> str: | ||
template = Template("<h1>{{ text | safe }}</h1>") | ||
return template.render(text=text) | ||
|
||
|
||
if __name__ == "__main__": | ||
print(get_value(text="<XYZ>")) |
12 changes: 12 additions & 0 deletions
12
example/pythonlib/testing/2-test-deps/foo/test/src/test.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import unittest | ||
from markupsafe import escape | ||
from foo import get_value # type: ignore | ||
from test_utils import BarTestUtils # type: ignore | ||
|
||
|
||
class TestScript(unittest.TestCase): | ||
def test_equal_string(self) -> None: | ||
escaped_text = escape("<XYZ>") | ||
BarTestUtils().bar_assert_equals( | ||
f"<h1>{escaped_text}</h1>", get_value(text="<XYZ>") | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
// 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. | ||
|
||
/** Usage | ||
|
||
> ./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... | ||
|
||
*/ |
Oops, something went wrong.