airbyte_cdk.test.standard_tests.connector_base
Base class for connector test suites.
1# Copyright (c) 2024 Airbyte, Inc., all rights reserved. 2"""Base class for connector test suites.""" 3 4from __future__ import annotations 5 6import importlib 7import os 8from pathlib import Path 9from typing import TYPE_CHECKING, cast 10 11from boltons.typeutils import classproperty 12 13from airbyte_cdk.test import entrypoint_wrapper 14from airbyte_cdk.test.models import ( 15 ConnectorTestScenario, 16) 17from airbyte_cdk.test.standard_tests._job_runner import IConnector, run_test_job 18from airbyte_cdk.test.standard_tests.docker_base import DockerConnectorTestSuite 19 20if TYPE_CHECKING: 21 from collections.abc import Callable 22 23 from airbyte_cdk.test import entrypoint_wrapper 24 25 26class ConnectorTestSuiteBase(DockerConnectorTestSuite): 27 """Base class for Python connector test suites.""" 28 29 connector: type[IConnector] | Callable[[], IConnector] | None # type: ignore [reportRedeclaration] 30 """The connector class or a factory function that returns an scenario of IConnector.""" 31 32 @classproperty # type: ignore [no-redef] 33 def connector(cls) -> type[IConnector] | Callable[[], IConnector] | None: 34 """Get the connector class for the test suite. 35 36 This assumes a python connector and should be overridden by subclasses to provide the 37 specific connector class to be tested. 38 """ 39 connector_root = cls.get_connector_root_dir() 40 connector_name = cls.connector_name 41 42 expected_module_name = connector_name.replace("-", "_").lower() 43 expected_class_name = connector_name.replace("-", "_").title().replace("_", "") 44 45 # dynamically import and get the connector class: <expected_module_name>.<expected_class_name> 46 47 cwd_snapshot = Path().absolute() 48 os.chdir(connector_root) 49 50 # Dynamically import the module 51 try: 52 module = importlib.import_module(expected_module_name) 53 except ModuleNotFoundError as e: 54 raise ImportError( 55 f"Could not import module '{expected_module_name}'. " 56 "Please ensure you are running from within the connector's virtual environment, " 57 "for instance by running `poetry run airbyte-cdk connector test` from the " 58 "connector directory. If the issue persists, check that the connector " 59 f"module matches the expected module name '{expected_module_name}' and that the " 60 f"connector class matches the expected class name '{expected_class_name}'. " 61 "Alternatively, you can run `airbyte-cdk image test` to run a subset of tests " 62 "against the connector's image." 63 ) from e 64 finally: 65 # Change back to the original working directory 66 os.chdir(cwd_snapshot) 67 68 # Dynamically get the class from the module 69 try: 70 return cast(type[IConnector], getattr(module, expected_class_name)) 71 except AttributeError as e: 72 # We did not find it based on our expectations, so let's check if we can find it 73 # with a case-insensitive match. 74 matching_class_name = next( 75 (name for name in dir(module) if name.lower() == expected_class_name.lower()), 76 None, 77 ) 78 if not matching_class_name: 79 raise ImportError( 80 f"Module '{expected_module_name}' does not have a class named '{expected_class_name}'." 81 ) from e 82 return cast(type[IConnector], getattr(module, matching_class_name)) 83 84 @classmethod 85 def create_connector( 86 cls, 87 scenario: ConnectorTestScenario | None, 88 ) -> IConnector: 89 """Instantiate the connector class.""" 90 connector = cls.connector # type: ignore 91 if connector: 92 if callable(connector) or isinstance(connector, type): 93 # If the connector is a class or factory function, instantiate it: 94 return cast(IConnector, connector()) # type: ignore [redundant-cast] 95 96 # Otherwise, we can't instantiate the connector. Fail with a clear error message. 97 raise NotImplementedError( 98 "No connector class or connector factory function provided. " 99 "Please provide a class or factory function in `cls.connector`, or " 100 "override `cls.create_connector()` to define a custom initialization process." 101 ) 102 103 # Test Definitions 104 105 def test_check( 106 self, 107 scenario: ConnectorTestScenario, 108 ) -> None: 109 """Run `connection` acceptance tests.""" 110 result: entrypoint_wrapper.EntrypointOutput = run_test_job( 111 self.create_connector(scenario), 112 "check", 113 test_scenario=scenario, 114 connector_root=self.get_connector_root_dir(), 115 ) 116 assert len(result.connection_status_messages) == 1, ( 117 f"Expected exactly one CONNECTION_STATUS message. " 118 "Got: {result.connection_status_messages!s}" 119 )
27class ConnectorTestSuiteBase(DockerConnectorTestSuite): 28 """Base class for Python connector test suites.""" 29 30 connector: type[IConnector] | Callable[[], IConnector] | None # type: ignore [reportRedeclaration] 31 """The connector class or a factory function that returns an scenario of IConnector.""" 32 33 @classproperty # type: ignore [no-redef] 34 def connector(cls) -> type[IConnector] | Callable[[], IConnector] | None: 35 """Get the connector class for the test suite. 36 37 This assumes a python connector and should be overridden by subclasses to provide the 38 specific connector class to be tested. 39 """ 40 connector_root = cls.get_connector_root_dir() 41 connector_name = cls.connector_name 42 43 expected_module_name = connector_name.replace("-", "_").lower() 44 expected_class_name = connector_name.replace("-", "_").title().replace("_", "") 45 46 # dynamically import and get the connector class: <expected_module_name>.<expected_class_name> 47 48 cwd_snapshot = Path().absolute() 49 os.chdir(connector_root) 50 51 # Dynamically import the module 52 try: 53 module = importlib.import_module(expected_module_name) 54 except ModuleNotFoundError as e: 55 raise ImportError( 56 f"Could not import module '{expected_module_name}'. " 57 "Please ensure you are running from within the connector's virtual environment, " 58 "for instance by running `poetry run airbyte-cdk connector test` from the " 59 "connector directory. If the issue persists, check that the connector " 60 f"module matches the expected module name '{expected_module_name}' and that the " 61 f"connector class matches the expected class name '{expected_class_name}'. " 62 "Alternatively, you can run `airbyte-cdk image test` to run a subset of tests " 63 "against the connector's image." 64 ) from e 65 finally: 66 # Change back to the original working directory 67 os.chdir(cwd_snapshot) 68 69 # Dynamically get the class from the module 70 try: 71 return cast(type[IConnector], getattr(module, expected_class_name)) 72 except AttributeError as e: 73 # We did not find it based on our expectations, so let's check if we can find it 74 # with a case-insensitive match. 75 matching_class_name = next( 76 (name for name in dir(module) if name.lower() == expected_class_name.lower()), 77 None, 78 ) 79 if not matching_class_name: 80 raise ImportError( 81 f"Module '{expected_module_name}' does not have a class named '{expected_class_name}'." 82 ) from e 83 return cast(type[IConnector], getattr(module, matching_class_name)) 84 85 @classmethod 86 def create_connector( 87 cls, 88 scenario: ConnectorTestScenario | None, 89 ) -> IConnector: 90 """Instantiate the connector class.""" 91 connector = cls.connector # type: ignore 92 if connector: 93 if callable(connector) or isinstance(connector, type): 94 # If the connector is a class or factory function, instantiate it: 95 return cast(IConnector, connector()) # type: ignore [redundant-cast] 96 97 # Otherwise, we can't instantiate the connector. Fail with a clear error message. 98 raise NotImplementedError( 99 "No connector class or connector factory function provided. " 100 "Please provide a class or factory function in `cls.connector`, or " 101 "override `cls.create_connector()` to define a custom initialization process." 102 ) 103 104 # Test Definitions 105 106 def test_check( 107 self, 108 scenario: ConnectorTestScenario, 109 ) -> None: 110 """Run `connection` acceptance tests.""" 111 result: entrypoint_wrapper.EntrypointOutput = run_test_job( 112 self.create_connector(scenario), 113 "check", 114 test_scenario=scenario, 115 connector_root=self.get_connector_root_dir(), 116 ) 117 assert len(result.connection_status_messages) == 1, ( 118 f"Expected exactly one CONNECTION_STATUS message. " 119 "Got: {result.connection_status_messages!s}" 120 )
Base class for Python connector test suites.
def
connector(unknown):
The connector class or a factory function that returns an scenario of IConnector.
@classmethod
def
create_connector( cls, scenario: airbyte_cdk.test.models.ConnectorTestScenario | None) -> airbyte_cdk.test.standard_tests._job_runner.IConnector:
85 @classmethod 86 def create_connector( 87 cls, 88 scenario: ConnectorTestScenario | None, 89 ) -> IConnector: 90 """Instantiate the connector class.""" 91 connector = cls.connector # type: ignore 92 if connector: 93 if callable(connector) or isinstance(connector, type): 94 # If the connector is a class or factory function, instantiate it: 95 return cast(IConnector, connector()) # type: ignore [redundant-cast] 96 97 # Otherwise, we can't instantiate the connector. Fail with a clear error message. 98 raise NotImplementedError( 99 "No connector class or connector factory function provided. " 100 "Please provide a class or factory function in `cls.connector`, or " 101 "override `cls.create_connector()` to define a custom initialization process." 102 )
Instantiate the connector class.
106 def test_check( 107 self, 108 scenario: ConnectorTestScenario, 109 ) -> None: 110 """Run `connection` acceptance tests.""" 111 result: entrypoint_wrapper.EntrypointOutput = run_test_job( 112 self.create_connector(scenario), 113 "check", 114 test_scenario=scenario, 115 connector_root=self.get_connector_root_dir(), 116 ) 117 assert len(result.connection_status_messages) == 1, ( 118 f"Expected exactly one CONNECTION_STATUS message. " 119 "Got: {result.connection_status_messages!s}" 120 )
Run connection
acceptance tests.