From 920cadcd1cbdaedaf3b4a8854488344534b8ba6a Mon Sep 17 00:00:00 2001 From: Owen Kephart Date: Mon, 26 Aug 2024 16:22:17 -0400 Subject: [PATCH] [DOC-348] Docs for asset and op unit testing --- .../unit-tests-assets-and-ops.md | 95 ++++++++++++++++++- .../asset-combo.py | 25 +++++ .../asset-config.py | 18 ++++ .../asset-context.py | 14 +++ .../asset-dependency.py | 18 ++++ .../asset-no-argument.py | 13 +++ .../asset-resource.py | 21 ++++ .../unit-testing-assets-and-ops/op-combo.py | 25 +++++ .../unit-testing-assets-and-ops/op-config.py | 18 ++++ .../unit-testing-assets-and-ops/op-context.py | 14 +++ .../op-dependency.py | 12 +++ .../op-no-argument.py | 13 +++ .../op-resource.py | 21 ++++ examples/docs_beta_snippets/setup.py | 8 +- 14 files changed, 308 insertions(+), 7 deletions(-) create mode 100644 examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/asset-combo.py create mode 100644 examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/asset-config.py create mode 100644 examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/asset-context.py create mode 100644 examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/asset-dependency.py create mode 100644 examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/asset-no-argument.py create mode 100644 examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/asset-resource.py create mode 100644 examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/op-combo.py create mode 100644 examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/op-config.py create mode 100644 examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/op-context.py create mode 100644 examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/op-dependency.py create mode 100644 examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/op-no-argument.py create mode 100644 examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/op-resource.py diff --git a/docs/docs-beta/docs/guides/quality-testing/unit-tests-assets-and-ops.md b/docs/docs-beta/docs/guides/quality-testing/unit-tests-assets-and-ops.md index 5af992a89756f..b27661d41ca09 100644 --- a/docs/docs-beta/docs/guides/quality-testing/unit-tests-assets-and-ops.md +++ b/docs/docs-beta/docs/guides/quality-testing/unit-tests-assets-and-ops.md @@ -4,4 +4,97 @@ sidebar_position: 30 sidebar_label: "Unit testing" --- -# Unit tests for assets and ops +Unit testing is an important tool for verifying that a computation works as intended. While this is traditionally difficult in the context of data pipelines, Dagster helps simplify this process by allowing you to directly invoke your underlying transforms with specific input values and mocked resources. + +This guide covers how to write unit tests for a variety of different assets and ops. + +
+Prerequisites + +- Familiarity with [Assets](/concepts/assets) +- Familiarity with [Ops and Jobs](/concepts/ops-jobs) +
+ +## Testing assets and ops with no arguments + +The simplest assets and ops to test are those with no arguments. In these cases, you can directly invoke your definition. + + + + + + + + + + +## Testing assets and ops that have upstream dependencies + +If your asset or op has an upstream dependency, then you can directly pass a value for that dependency when invoking your definition. + + + + + + + + + + +## Testing assets and ops with config + +If your asset or op uses [config](/todo), you can construct an instance of the required config object and pass it in directly. + + + + + + + + + + +## Testing assets and ops with resources + +If your asset or op uses a [resource](/concepts/resources), it can be useful to create a mock instance of that resource to avoid interacting with external services. + + + + + + + + + + +## Testing assets and ops with context + +If your asset or op uses a context argument, you can use `build_asset_context()` or `build_op_context()` to construct a context object. + + + + + + + + + + +## Testing assets and ops with a combination of parameters + +If your asset or op uses a combination of multiple types of parameters, it can be convenient to use keyword arguments, rather than relying on the order of parameters. + + + + + + + + + +## Next steps + +- Learn more about assets in [Understanding Assets](/concepts/assets) +- Learn more about ops in [Understanding Assets](/concepts/ops-jobs) +- Learn more about config in [Config](/todo) +- Learn more about resources in [Resources](/concepts/resources) \ No newline at end of file diff --git a/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/asset-combo.py b/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/asset-combo.py new file mode 100644 index 0000000000000..c5481d213afaa --- /dev/null +++ b/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/asset-combo.py @@ -0,0 +1,25 @@ +import dagster as dg + + +class SeparatorConfig(dg.Config): + separator: str + + +@dg.asset +def processed_file( + primary_file: str, secondary_file: str, config: SeparatorConfig +) -> str: + return f"{primary_file}{config.separator}{secondary_file}" + + +# highlight-start +def test_processed_file() -> None: + assert ( + processed_file( + primary_file="abc", + secondary_file="def", + config=SeparatorConfig(separator=","), + ) + == "abc,def" + ) + # highlight-end diff --git a/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/asset-config.py b/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/asset-config.py new file mode 100644 index 0000000000000..be07db77ffdd5 --- /dev/null +++ b/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/asset-config.py @@ -0,0 +1,18 @@ +import dagster as dg + + +class FilepathConfig(dg.Config): + path: str + + +@dg.asset +def loaded_file(config: FilepathConfig) -> str: + with open(config.path) as file: + return file.read() + + +# highlight-start +def test_loaded_file() -> None: + assert loaded_file(FilepathConfig(path="path1.txt")) == "contents1" + assert loaded_file(FilepathConfig(path="path2.txt")) == "contents2" + # highlight-end diff --git a/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/asset-context.py b/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/asset-context.py new file mode 100644 index 0000000000000..5738a6d2b5dd9 --- /dev/null +++ b/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/asset-context.py @@ -0,0 +1,14 @@ +import dagster as dg + + +@dg.asset(partitions_def=dg.DailyPartitionsDefinition("2024-01-01")) +def loaded_file(context: dg.AssetExecutionContext) -> str: + with open(f"path_{context.partition_key}.txt") as file: + return file.read() + + +# highlight-start +def test_loaded_file() -> None: + context = dg.build_asset_context(partition_key="2024-08-16") + assert loaded_file(context) == "Contents for August 16th, 2024" + # highlight-end diff --git a/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/asset-dependency.py b/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/asset-dependency.py new file mode 100644 index 0000000000000..6c0b822dc6437 --- /dev/null +++ b/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/asset-dependency.py @@ -0,0 +1,18 @@ +import dagster as dg + + +@dg.asset +def loaded_file() -> str: + with open("path.txt") as file: + return file.read() + + +@dg.asset +def processed_file(loaded_file: str) -> str: + return loaded_file.strip() + + +# highlight-start +def test_processed_file() -> None: + assert processed_file(" contents ") == "contents" + # highlight-end diff --git a/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/asset-no-argument.py b/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/asset-no-argument.py new file mode 100644 index 0000000000000..eac1226a86bda --- /dev/null +++ b/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/asset-no-argument.py @@ -0,0 +1,13 @@ +import dagster as dg + + +@dg.asset +def loaded_file() -> str: + with open("path.txt") as file: + return file.read() + + +# highlight-start +def test_loaded_file() -> None: + assert loaded_file() == "contents" + # highlight-end diff --git a/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/asset-resource.py b/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/asset-resource.py new file mode 100644 index 0000000000000..a2d55d73b0f78 --- /dev/null +++ b/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/asset-resource.py @@ -0,0 +1,21 @@ +import mock +from dagster_aws.s3 import S3FileHandle, S3FileManager + +import dagster as dg + + +@dg.asset +def loaded_file(file_manager: S3FileManager) -> str: + return file_manager.read_data(S3FileHandle("bucket", "path.txt")) + + +# highlight-start +def test_file() -> None: + mocked_resource = mock.Mock(spec=S3FileManager) + mocked_resource.read_data.return_value = "contents" + + assert loaded_file(mocked_resource) == "contents" + assert mocked_resource.read_data.called_once_with( + S3FileHandle("bucket", "path.txt") + ) + # highlight-end diff --git a/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/op-combo.py b/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/op-combo.py new file mode 100644 index 0000000000000..976ad98091ae6 --- /dev/null +++ b/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/op-combo.py @@ -0,0 +1,25 @@ +import dagster as dg + + +class SeparatorConfig(dg.Config): + separator: str + + +@dg.op +def process_file( + primary_file: str, secondary_file: str, config: SeparatorConfig +) -> str: + return f"{primary_file}{config.separator}{secondary_file}" + + +# highlight-start +def test_process_file() -> None: + assert ( + process_file( + primary_file="abc", + secondary_file="def", + config=SeparatorConfig(separator=","), + ) + == "abc,def" + ) + # highlight-end diff --git a/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/op-config.py b/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/op-config.py new file mode 100644 index 0000000000000..fb7af3b02fdb4 --- /dev/null +++ b/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/op-config.py @@ -0,0 +1,18 @@ +import dagster as dg + + +class FilepathConfig(dg.Config): + path: str + + +@dg.op +def load_file(config: FilepathConfig) -> str: + with open(config.path) as file: + return file.read() + + +# highlight-start +def test_load_file() -> None: + assert load_file(FilepathConfig(path="path1.txt")) == "contents1" + assert load_file(FilepathConfig(path="path2.txt")) == "contents2" + # highlight-end diff --git a/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/op-context.py b/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/op-context.py new file mode 100644 index 0000000000000..e1df3e402c02f --- /dev/null +++ b/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/op-context.py @@ -0,0 +1,14 @@ +import dagster as dg + + +@dg.op +def load_file(context: dg.OpExecutionContext) -> str: + with open(f"path_{context.partition_key}.txt") as file: + return file.read() + + +# highlight-start +def test_load_file() -> None: + context = dg.build_asset_context(partition_key="2024-08-16") + assert load_file(context) == "Contents for August 16th, 2024" + # highlight-end diff --git a/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/op-dependency.py b/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/op-dependency.py new file mode 100644 index 0000000000000..b59fa9f0b507f --- /dev/null +++ b/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/op-dependency.py @@ -0,0 +1,12 @@ +import dagster as dg + + +@dg.op +def process_file(loaded_file: str) -> str: + return loaded_file.strip() + + +# highlight-start +def test_process_file() -> None: + assert process_file(" contents ") == "contents" + # highlight-end diff --git a/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/op-no-argument.py b/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/op-no-argument.py new file mode 100644 index 0000000000000..7ea57bccc76c1 --- /dev/null +++ b/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/op-no-argument.py @@ -0,0 +1,13 @@ +import dagster as dg + + +@dg.op +def load_file() -> str: + with open("path.txt") as file: + return file.read() + + +# highlight-start +def test_load_file() -> None: + assert load_file() == "contents" + # highlight-end diff --git a/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/op-resource.py b/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/op-resource.py new file mode 100644 index 0000000000000..7a0df73c2d15f --- /dev/null +++ b/examples/docs_beta_snippets/docs_beta_snippets/guides/quality-testing/unit-testing-assets-and-ops/op-resource.py @@ -0,0 +1,21 @@ +import mock +from dagster_aws.s3 import S3FileHandle, S3FileManager + +import dagster as dg + + +@dg.op +def load_file(file_manager: S3FileManager) -> str: + return file_manager.read_data(S3FileHandle("bucket", "path.txt")) + + +# highlight-start +def test_load_file() -> None: + mocked_resource = mock.Mock(spec=S3FileManager) + mocked_resource.read_data.return_value = "contents" + + assert load_file(mocked_resource) == "contents" + assert mocked_resource.read_data.called_once_with( + S3FileHandle("bucket", "path.txt") + ) + # highlight-end diff --git a/examples/docs_beta_snippets/setup.py b/examples/docs_beta_snippets/setup.py index a81dc0496efc1..ac0ba1affc599 100755 --- a/examples/docs_beta_snippets/setup.py +++ b/examples/docs_beta_snippets/setup.py @@ -14,10 +14,6 @@ "Operating System :: OS Independent", ], packages=find_packages(exclude=["test"]), - install_requires=["dagster-cloud"], - extras_require={ - "test": [ - "pytest", - ] - }, + install_requires=["dagster-cloud", "dagster-aws"], + extras_require={"test": ["pytest", "mock"]}, )