From cb82aa2f1adf9cd7455a76a768afcada350dcb09 Mon Sep 17 00:00:00 2001 From: Jochem van Dooren Date: Wed, 20 Mar 2024 14:23:30 +0100 Subject: [PATCH] Add unit tests. --- tests/conftest.py | 148 ++++++++++++++++++++++++++++++++++++++++++- tests/test_models.py | 18 ++++++ tests/test_rule.py | 46 ++++++++++++++ 3 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 tests/test_models.py create mode 100644 tests/test_rule.py diff --git a/tests/conftest.py b/tests/conftest.py index 8bd86b1..77c61bd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,155 @@ """Test configuration.""" +from typing import Any, Type -from pytest import ExitCode, Session +from dbt_score.models import Model +from dbt_score.rule import Rule, RuleViolation, rule +from pytest import ExitCode, Session, fixture def pytest_sessionfinish(session: Session, exitstatus: int): """Avoid ci failure if no tests are found.""" if exitstatus == ExitCode.NO_TESTS_COLLECTED: session.exitstatus = ExitCode.OK + + +@fixture +def raw_manifest() -> dict[str, Any]: + """Mock the raw manifest.""" + return { + "nodes": { + "analysis.package.analysis1": {"resource_type": "analysis"}, + "model.package.model1": { + "resource_type": "model", + "unique_id": "model.package.model1", + "name": "model1", + "relation_name": "database.schema.model1", + "description": "Description1.", + "original_file_path": "/path/to/model1.sql", + "config": {}, + "meta": {}, + "columns": { + "a": { + "name": "column_a", + "description": "Column A.", + "data_type": "string", + "meta": {}, + "constraints": [], + "tags": [], + } + }, + "package_name": "package", + "database": "db", + "schema": "schema", + "raw_code": "SELECT x FROM y", + "alias": "model1_alias", + "patch_path": "/path/to/model1.yml", + "tags": [], + "depends_on": {}, + }, + "model.package.model2": { + "resource_type": "model", + "unique_id": "model.package.model2", + "name": "model2", + "relation_name": "database.schema.model2", + "description": "Description2.", + "original_file_path": "/path/to/model2.sql", + "config": {}, + "meta": {}, + "columns": { + "a": { + "name": "column_a", + "description": "Column A.", + "data_type": "string", + "meta": {}, + "constraints": [], + "tags": [], + } + }, + "package_name": "package", + "database": "db", + "schema": "schema", + "raw_code": "SELECT x FROM y", + "alias": "model2_alias", + "patch_path": "/path/to/model2.yml", + "tags": [], + "depends_on": {}, + }, + "test.package.test1": { + "resource_type": "test", + "attached_node": "model.package.model1", + "name": "test1", + "test_metadata": {"name": "type", "kwargs": {"column_name": "a"}}, + "tags": [], + }, + "test.package.test2": { + "resource_type": "test", + "attached_node": "model.package.model1", + "name": "test2", + "test_metadata": {"name": "type", "kwargs": {}}, + "tags": [], + }, + "test.package.test3": {"resource_type": "test"}, + } + } + + +@fixture +def model1(raw_manifest) -> Model: + """Model 1.""" + return Model.from_node(raw_manifest["nodes"]["model.package.model1"], []) + + +@fixture +def model2(raw_manifest) -> Model: + """Model 2.""" + return Model.from_node(raw_manifest["nodes"]["model.package.model2"], []) + + +@fixture +def decorator_rule() -> Type[Rule]: + """An example rule created with the rule decorator.""" + + @rule() + def example_rule(model: Model) -> RuleViolation | None: + """Description of the rule.""" + if model.name == "model1": + return RuleViolation(message="Model1 is a violation.") + return None + + return example_rule + + +@fixture +def class_rule() -> Type[Rule]: + """An example rule created with a class.""" + + class ExampleRule(Rule): + """Example rule.""" + + description = "Description of the rule." + + def evaluate(self, model: Model) -> RuleViolation | None: + """Evaluate model.""" + if model.name == "model1": + return RuleViolation(message="Model1 is a violation.") + return None + + return ExampleRule + + +@fixture +def invalid_class_rule() -> Type[Rule]: + """An example rule created with a class.""" + + class ExampleRule(Rule): + """Example rule.""" + + description = "Description of the rule." + + def evaluate(self, model: Model) -> RuleViolation | None: + """Evaluate model.""" + if model.name == "model1": + return RuleViolation(message="Model1 is a violation.") + return None + + return ExampleRule diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..645c9b4 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,18 @@ +"""Test models.""" + +from pathlib import Path +from unittest.mock import patch + +from dbt_score.models import ManifestLoader + + +@patch("dbt_score.models.Path.read_text") +def test_manifest(read_text, raw_manifest): + """Test loading a manifest.""" + with patch("dbt_score.models.json.loads", return_value=raw_manifest): + loader = ManifestLoader(Path("manifest.json")) + assert len(loader.models) == len([node for node in + raw_manifest["nodes"].values() + if node["resource_type"] == "model"]) + assert loader.models[0].tests[0].name == "test2" + assert loader.models[0].columns[0].tests[0].name == "test1" diff --git a/tests/test_rule.py b/tests/test_rule.py new file mode 100644 index 0000000..5494e3a --- /dev/null +++ b/tests/test_rule.py @@ -0,0 +1,46 @@ +"""Test rule.""" + +import pytest +from dbt_score.models import Model +from dbt_score.rule import Rule, RuleViolation, Severity + + +def test_rule_decorator(decorator_rule, class_rule, model1, model2): + """Test rule creation with the rule decorator and class.""" + decorator_rule_instance = decorator_rule() + class_rule_instance = class_rule() + + def assertions(rule_instance): + assert isinstance(rule_instance, Rule) + assert rule_instance.severity == Severity.MEDIUM + assert rule_instance.description == "Description of the rule." + assert rule_instance.evaluate(model1) == RuleViolation( + message="Model1 is a violation.") + assert rule_instance.evaluate(model2) is None + + assertions(decorator_rule_instance) + assertions(class_rule_instance) + + +def test_missing_description_rule_class(class_rule): + """Test missing description in rule class.""" + with pytest.raises(TypeError): + class BadRule(Rule): + """Bad example rule.""" + + def evaluate(self, model: Model) -> RuleViolation | None: + """Evaluate model.""" + return None + + +def test_missing_evaluate_rule_class(class_rule, model1): + """Test missing evaluate implementation in rule class.""" + class BadRule(Rule): + """Bad example rule.""" + description = "Description of the rule." + + rule = BadRule() + + with pytest.raises(NotImplementedError): + rule.evaluate(model1) +