Repository: qBraid/qBraid (language: python)

Issue:
Handle Missing `pyqir` Dependencies in Test Suite
### Summary
The `pyqir` library is not yet supported across all platforms. As a result, optional dependencies listed in `requirements-dev.txt`, such as `qbraid-qir` and `qiskit-qir`, may not be installable on certain operating systems. Despite this limitation, developers should still be able to run the full test suite.

### Goal
Update the test suite so that any pytest cases relying on `pyqir`, `qiskit-qir`, or `qbraid-qir` are automatically skipped if the required packages are not installed.

### Implementation
This can be achieved in several ways. The `qbraid/tests` directory already contains many examples of pytests that are conditionally skipped based on package availability. You can find these examples by searching for:

- `pytest.skip`
- `pytest.mark.skip`
- `pytest.mark.skipif`
- `pytest.importorskip`


These examples demonstrate how to conditionally skip tests if a dependency is missing. Use similar patterns to address the issue.

### TODO
- Identify all tests dependent on `pyqir`, `qiskit-qir`, or `qbraid-qir`.
- Modify these tests to be conditionally skipped if the corresponding package is not installed.
- Ensure that the full test suite runs without errors on platforms lacking `pyqir` support.



Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 39eb092c2..f34b75a14 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ Types of changes:
 ### Added
 
 ### Improved / Modified
+- Unit tests that require the `pyqir` dependency are now automatically skipped if pyqir is not installed. ([#846](https://github.com/qBraid/qBraid/pull/846))
 
 ### Deprecated
 
diff --git a/CITATION.cff b/CITATION.cff
index 48378bdcc..48514c0ba 100644
--- a/CITATION.cff
+++ b/CITATION.cff
@@ -57,6 +57,10 @@ authors:
   - given-names: Pranet
     family-names: Sharma
     affiliation: Yale University
+  - given-names: Alvan
+    family-names: Arulandu
+    affiliation: Harvard University
+    orcid: https://orcid.org/0009-0000-0071-348X
   - given-names: Kanav
     family-names: Setia
     affiliation: qBraid Co.


Test patch:
diff --git a/tests/programs/test_program_type_registry.py b/tests/programs/test_program_type_registry.py
index 12f85ef72..6e06c4dde 100644
--- a/tests/programs/test_program_type_registry.py
+++ b/tests/programs/test_program_type_registry.py
@@ -20,7 +20,6 @@
 import unittest.mock
 
 import pytest
-from pyqir import BasicQisBuilder, Module, SimpleModule
 
 from qbraid.exceptions import QbraidError
 from qbraid.interface import random_circuit
@@ -28,7 +27,6 @@
     QPROGRAM_ALIASES,
     QPROGRAM_REGISTRY,
     QPROGRAM_TYPES,
-    ExperimentType,
     ProgramSpec,
     derive_program_type_alias,
     get_program_type_alias,
@@ -264,44 +262,8 @@ def test_program_type_error():
     assert message == "Unsupported quantum program type."
 
 
-@pytest.fixture
-def pyqir_bell() -> Module:
-    """Returns a QIR bell circuit with measurement over two qubits."""
-    bell = SimpleModule("test_qir_bell", num_qubits=2, num_results=2)
-    qis = BasicQisBuilder(bell.builder)
-
-    qis.h(bell.qubits[0])
-    qis.cx(bell.qubits[0], bell.qubits[1])
-    qis.mz(bell.qubits[0], bell.results[0])
-    qis.mz(bell.qubits[1], bell.results[1])
-
-    return bell._module
-
-
-@pytest.fixture
-def pyqir_spec() -> ProgramSpec:
-    """Returns a ProgramSpec for the QIR bell circuit."""
-    return ProgramSpec(Module, alias="pyqir", to_ir=lambda module: module.bitcode)
-
-
-def test_load_program_pyqir(pyqir_bell: Module, pyqir_spec: ProgramSpec):
-    """Test program spec to IR conversion for a QIR program."""
-    program_ir = pyqir_spec.to_ir(pyqir_bell)
-    assert isinstance(program_ir, bytes)
-    assert program_ir == pyqir_bell.bitcode
-
-
 def test_get_native_experiment_type_not_found():
     """Test error when trying to get the native experiment type that is not found."""
     with pytest.raises(ValueError) as excinfo:
         get_native_experiment_type("not_found")
     assert "Entry point 'not_found' not found in 'qbraid.programs'." in str(excinfo.value)
-
-
-def test_program_spec_specify_experiment_type():
-    """Test specifying the experiment type for a program spec."""
-    try:
-        spec = ProgramSpec(bytes, alias="llvm", experiment_type=ExperimentType.OTHER)
-        assert spec.experiment_type == ExperimentType.OTHER
-    finally:
-        unregister_program_type("llvm", raise_error=False)
diff --git a/tests/programs/test_program_type_registry_qir.py b/tests/programs/test_program_type_registry_qir.py
new file mode 100644
index 000000000..32525291e
--- /dev/null
+++ b/tests/programs/test_program_type_registry_qir.py
@@ -0,0 +1,63 @@
+# Copyright (C) 2024 qBraid
+#
+# This file is part of the qBraid-SDK
+#
+# The qBraid-SDK is free software released under the GNU General Public License v3
+# or later. You can redistribute and/or modify it under the terms of the GPL v3.
+# See the LICENSE file in the project root or <https://www.gnu.org/licenses/gpl-3.0.html>.
+#
+# THERE IS NO WARRANTY for the qBraid-SDK, as per Section 15 of the GPL v3.
+
+"""
+Unit test for quantum program registry for QIR
+
+"""
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import pytest
+
+from qbraid.programs import ExperimentType, ProgramSpec, unregister_program_type
+
+if TYPE_CHECKING:
+    import pyqir
+    from pyqir import Module
+
+pyqir = pytest.importorskip("pyqir")
+
+
+@pytest.fixture
+def pyqir_bell() -> Module:
+    """Returns a QIR bell circuit with measurement over two qubits."""
+    bell = pyqir.SimpleModule("test_qir_bell", num_qubits=2, num_results=2)
+    qis = pyqir.BasicQisBuilder(bell.builder)
+
+    qis.h(bell.qubits[0])
+    qis.cx(bell.qubits[0], bell.qubits[1])
+    qis.mz(bell.qubits[0], bell.results[0])
+    qis.mz(bell.qubits[1], bell.results[1])
+
+    return bell._module
+
+
+@pytest.fixture
+def pyqir_spec() -> ProgramSpec:
+    """Returns a ProgramSpec for the QIR bell circuit."""
+    return ProgramSpec(pyqir.Module, alias="pyqir", to_ir=lambda module: module.bitcode)
+
+
+def test_load_program_pyqir(pyqir_bell: Module, pyqir_spec: ProgramSpec):
+    """Test program spec to IR conversion for a QIR program."""
+    program_ir = pyqir_spec.to_ir(pyqir_bell)
+    assert isinstance(program_ir, bytes)
+    assert program_ir == pyqir_bell.bitcode
+
+
+def test_program_spec_specify_experiment_type():
+    """Test specifying the experiment type for a program spec."""
+    try:
+        spec = ProgramSpec(bytes, alias="llvm", experiment_type=ExperimentType.OTHER)
+        assert spec.experiment_type == ExperimentType.OTHER
+    finally:
+        unregister_program_type("llvm", raise_error=False)
diff --git a/tests/runtime/azure/conftest.py b/tests/runtime/azure/conftest.py
new file mode 100644
index 000000000..d9f264bbe
--- /dev/null
+++ b/tests/runtime/azure/conftest.py
@@ -0,0 +1,80 @@
+# Copyright (C) 2024 qBraid
+#
+# This file is part of the qBraid-SDK
+#
+# The qBraid-SDK is free software released under the GNU General Public License v3
+# or later. You can redistribute and/or modify it under the terms of the GPL v3.
+# See the LICENSE file in the project root or <https://www.gnu.org/licenses/gpl-3.0.html>.
+#
+# THERE IS NO WARRANTY for the qBraid-SDK, as per Section 15 of the GPL v3.
+
+"""
+Shared fixtures for Azure provider and testing circuit.
+"""
+
+import os
+from typing import Optional
+
+import pytest
+from azure.identity import ClientSecretCredential
+from azure.quantum import Workspace
+from azure.quantum._constants import ConnectionConstants, EnvironmentVariables
+from qiskit import QuantumCircuit
+
+from qbraid.runtime.azure import AzureQuantumProvider
+
+
+@pytest.fixture
+def credential() -> Optional[ClientSecretCredential]:
+    """Fixture for Azure client secret credential."""
+    tenant_id = os.getenv(EnvironmentVariables.AZURE_TENANT_ID)
+    client_id = os.getenv(EnvironmentVariables.AZURE_CLIENT_ID)
+    client_secret = os.getenv(EnvironmentVariables.AZURE_CLIENT_SECRET)
+    if client_id and tenant_id and client_secret:
+        return ClientSecretCredential(
+            tenant_id=tenant_id, client_id=client_id, client_secret=client_secret
+        )
+    return None
+
+
+@pytest.fixture
+def resource_id() -> Optional[str]:
+    """Fixture for Azure Quantum resource ID."""
+    subscription_id = os.getenv(EnvironmentVariables.QUANTUM_SUBSCRIPTION_ID)
+    resource_group = os.getenv(EnvironmentVariables.QUANTUM_RESOURCE_GROUP, "AzureQuantum")
+    workspace_name = os.getenv(EnvironmentVariables.WORKSPACE_NAME)
+    if subscription_id and resource_group and workspace_name:
+        return ConnectionConstants.VALID_RESOURCE_ID(
+            subscription_id=subscription_id,
+            resource_group=resource_group,
+            workspace_name=workspace_name,
+        )
+    return None
+
+
+@pytest.fixture
+def workspace(
+    credential: Optional[ClientSecretCredential], resource_id: Optional[str]
+) -> Workspace:
+    """Fixture for Azure Quantum workspace."""
+    location = os.getenv(EnvironmentVariables.QUANTUM_LOCATION, "eastus")
+    return Workspace(resource_id=resource_id, location=location, credential=credential)
+
+
+@pytest.fixture
+def provider(workspace: Workspace) -> AzureQuantumProvider:
+    """Fixture for AzureQuantumProvider."""
+    return AzureQuantumProvider(workspace)
+
+
+@pytest.fixture
+def qiskit_circuit() -> QuantumCircuit:
+    """Fixture for a Qiskit quantum circuit."""
+    circuit = QuantumCircuit(3, 3)
+    circuit.name = "main"
+    circuit.h(0)
+    circuit.cx(0, 1)
+    circuit.cx(1, 2)
+    circuit.measure([0, 1, 2], [0, 1, 2])
+
+    return circuit
diff --git a/tests/runtime/azure/test_azure_remote.py b/tests/runtime/azure/test_azure_remote.py
index da2f80cd0..f34e95df3 100644
--- a/tests/runtime/azure/test_azure_remote.py
+++ b/tests/runtime/azure/test_azure_remote.py
@@ -17,16 +17,10 @@
 from __future__ import annotations
 
 import os
-from typing import TYPE_CHECKING, Optional
+from typing import TYPE_CHECKING
 
-import pyqir
 import pytest
-from azure.identity import ClientSecretCredential
-from azure.quantum import Workspace
-from azure.quantum._constants import ConnectionConstants, EnvironmentVariables
-from azure.quantum.target.microsoft import MicrosoftEstimatorResult
 from qbraid_core._import import LazyLoader
-from qiskit import QuantumCircuit
 
 from qbraid.runtime import DeviceStatus, GateModelResultData, JobStatus, Result
 from qbraid.runtime.azure import AzureQuantumProvider
@@ -38,49 +32,6 @@
     import pyquil as pyquil_
 
 
-@pytest.fixture
-def credential() -> Optional[ClientSecretCredential]:
-    """Fixture for Azure client secret credential."""
-    tenant_id = os.getenv(EnvironmentVariables.AZURE_TENANT_ID)
-    client_id = os.getenv(EnvironmentVariables.AZURE_CLIENT_ID)
-    client_secret = os.getenv(EnvironmentVariables.AZURE_CLIENT_SECRET)
-    if client_id and tenant_id and client_secret:
-        return ClientSecretCredential(
-            tenant_id=tenant_id, client_id=client_id, client_secret=client_secret
-        )
-    return None
-
-
-@pytest.fixture
-def resource_id() -> Optional[str]:
-    """Fixture for Azure Quantum resource ID."""
-    subscription_id = os.getenv(EnvironmentVariables.QUANTUM_SUBSCRIPTION_ID)
-    resource_group = os.getenv(EnvironmentVariables.QUANTUM_RESOURCE_GROUP, "AzureQuantum")
-    workspace_name = os.getenv(EnvironmentVariables.WORKSPACE_NAME)
-    if subscription_id and resource_group and workspace_name:
-        return ConnectionConstants.VALID_RESOURCE_ID(
-            subscription_id=subscription_id,
-            resource_group=resource_group,
-            workspace_name=workspace_name,
-        )
-    return None
-
-
-@pytest.fixture
-def workspace(
-    credential: Optional[ClientSecretCredential], resource_id: Optional[str]
-) -> Workspace:
-    """Fixture for Azure Quantum workspace."""
-    location = os.getenv(EnvironmentVariables.QUANTUM_LOCATION, "eastus")
-    return Workspace(resource_id=resource_id, location=location, credential=credential)
-
-
-@pytest.fixture
-def provider(workspace: Workspace) -> AzureQuantumProvider:
-    """Fixture for AzureQuantumProvider."""
-    return AzureQuantumProvider(workspace)
-
-
 @pytest.mark.remote
 def test_submit_qasm2_to_quantinuum(provider: AzureQuantumProvider):
     """Test submitting an OpenQASM 2 string to run on the Quantinuum simulator."""
@@ -135,50 +86,6 @@ def test_submit_json_to_ionq(provider: AzureQuantumProvider):
     assert result.data.get_counts() == {"000": 50, "111": 50}
 
 
-@pytest.fixture
-def qiskit_circuit() -> QuantumCircuit:
-    """Fixture for a Qiskit quantum circuit."""
-    circuit = QuantumCircuit(3, 3)
-    circuit.name = "main"
-    circuit.h(0)
-    circuit.cx(0, 1)
-    circuit.cx(1, 2)
-    circuit.measure([0, 1, 2], [0, 1, 2])
-
-    return circuit
-
-
-@pytest.fixture
-def qir_bitcode(qiskit_circuit: QuantumCircuit) -> bytes:
-    """Fixture for QIR bitcode from a Qiskit quantum circuit."""
-    module: pyqir.Module = qiskit_to_pyqir(qiskit_circuit)
-    return module.bitcode
-
-
-@pytest.mark.remote
-@pytest.mark.parametrize("direct", [(True), (False)])
-def test_submit_qir_to_microsoft(
-    provider: AzureQuantumProvider, qiskit_circuit: QuantumCircuit, qir_bitcode: bytes, direct: bool
-):
-    """Test submitting Qiskit circuit or QIR bitcode to run on the Microsoft resource estimator."""
-    device = provider.get_device("microsoft.estimator")
-    assert device.status() == DeviceStatus.ONLINE
-
-    input_params = {"entryPoint": qiskit_circuit.name, "arguments": [], "count": 100}
-
-    if direct:
-        job = device.submit(qir_bitcode, input_params=input_params)
-    else:
-        job = device.run(qiskit_circuit, input_params=input_params)
-
-    job.wait_for_final_state()
-
-    assert job.status() == JobStatus.COMPLETED
-
-    result = job.result()
-    assert isinstance(result, MicrosoftEstimatorResult)
-
-
 @pytest.fixture
 def pyquil_program() -> pyquil_.Program:
     """Fixture for a PyQuil program."""
diff --git a/tests/runtime/azure/test_azure_remote_qir.py b/tests/runtime/azure/test_azure_remote_qir.py
new file mode 100644
index 000000000..ca7d771dd
--- /dev/null
+++ b/tests/runtime/azure/test_azure_remote_qir.py
@@ -0,0 +1,62 @@
+# Copyright (C) 2024 qBraid
+#
+# This file is part of the qBraid-SDK
+#
+# The qBraid-SDK is free software released under the GNU General Public License v3
+# or later. You can redistribute and/or modify it under the terms of the GPL v3.
+# See the LICENSE file in the project root or <https://www.gnu.org/licenses/gpl-3.0.html>.
+#
+# THERE IS NO WARRANTY for the qBraid-SDK, as per Section 15 of the GPL v3.
+
+"""
+Unit tests for Azure Quantum runtime (remote) for QIR
+
+"""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import pytest
+from azure.quantum.target.microsoft import MicrosoftEstimatorResult
+from qiskit import QuantumCircuit
+
+from qbraid.runtime import DeviceStatus, JobStatus
+from qbraid.runtime.azure import AzureQuantumProvider
+from qbraid.transpiler.conversions.qiskit import qiskit_to_pyqir
+
+if TYPE_CHECKING:
+    import pyqir
+
+pyqir = pytest.importorskip("pyqir")
+
+
+@pytest.fixture
+def qir_bitcode(qiskit_circuit: QuantumCircuit) -> bytes:
+    """Fixture for QIR bitcode from a Qiskit quantum circuit."""
+    module: pyqir.Module = qiskit_to_pyqir(qiskit_circuit)
+    return module.bitcode
+
+
+@pytest.mark.remote
+@pytest.mark.parametrize("direct", [(True), (False)])
+def test_submit_qir_to_microsoft(
+    provider: AzureQuantumProvider, qiskit_circuit: QuantumCircuit, qir_bitcode: bytes, direct: bool
+):
+    """Test submitting Qiskit circuit or QIR bitcode to run on the Microsoft resource estimator."""
+    device = provider.get_device("microsoft.estimator")
+    assert device.status() == DeviceStatus.ONLINE
+
+    input_params = {"entryPoint": qiskit_circuit.name, "arguments": [], "count": 100}
+
+    if direct:
+        job = device.submit(qir_bitcode, input_params=input_params)
+    else:
+        job = device.run(qiskit_circuit, input_params=input_params)
+
+    job.wait_for_final_state()
+
+    assert job.status() == JobStatus.COMPLETED
+
+    result = job.result()
+    assert isinstance(result, MicrosoftEstimatorResult)
diff --git a/tests/runtime/conftest.py b/tests/runtime/conftest.py
index 5c0b8fa78..7028c86cc 100644
--- a/tests/runtime/conftest.py
+++ b/tests/runtime/conftest.py
@@ -22,10 +22,19 @@
 import numpy as np
 import pytest
 
-from qbraid.programs import NATIVE_REGISTRY
-from qbraid.runtime import QbraidProvider
+from qbraid.programs import NATIVE_REGISTRY, ExperimentType
+from qbraid.runtime import TargetProfile
+from qbraid.runtime.native import QbraidDevice, QbraidProvider
+from qbraid.runtime.noise import NoiseModelSet
+from qbraid.transpiler import Conversion, ConversionGraph, ConversionScheme
 
-from ._resources import DEVICE_DATA_AQUILA, DEVICE_DATA_QIR, DEVICE_DATA_QUERA_QASM, MockClient
+from ._resources import (
+    DEVICE_DATA_AQUILA,
+    DEVICE_DATA_QIR,
+    DEVICE_DATA_QUERA_QASM,
+    MockClient,
+    MockDevice,
+)
 
 
 def _braket_circuit(meas=True):
@@ -176,6 +185,41 @@ def mock_provider(mock_client):
     return QbraidProvider(client=mock_client)
 
 
+@pytest.fixture
+def mock_profile():
+    """Mock profile for testing."""
+    return TargetProfile(
+        device_id="qbraid_qir_simulator",
+        simulator=True,
+        experiment_type=ExperimentType.GATE_MODEL,
+        num_qubits=64,
+        program_spec=QbraidProvider._get_program_spec("pyqir", "qbraid_qir_simulator"),
+        noise_models=NoiseModelSet.from_iterable(["ideal"]),
+    )
+
+
+@pytest.fixture
+def mock_scheme():
+    """Mock conversion scheme for testing."""
+    conv1 = Conversion("alice", "bob", lambda x: x + 1)
+    conv2 = Conversion("bob", "charlie", lambda x: x - 1)
+    graph = ConversionGraph(conversions=[conv1, conv2])
+    scheme = ConversionScheme(conversion_graph=graph)
+    return scheme
+
+
+@pytest.fixture
+def mock_qbraid_device(mock_profile, mock_scheme, mock_client):
+    """Mock QbraidDevice for testing."""
+    return QbraidDevice(profile=mock_profile, client=mock_client, scheme=mock_scheme)
+
+
+@pytest.fixture
+def mock_basic_device(mock_profile):
+    """Generic mock device for testing."""
+    return MockDevice(profile=mock_profile)
+
+
 def uniform_state_circuit(num_qubits: Optional[int] = None, measure: Optional[bool] = True):
     """
     Creates a Cirq circuit where all qubits are entangled to uniformly be in
diff --git a/tests/runtime/test_device.py b/tests/runtime/test_device.py
index bbab85b61..8ca287f49 100644
--- a/tests/runtime/test_device.py
+++ b/tests/runtime/test_device.py
@@ -26,7 +26,6 @@
 import numpy as np
 import pytest
 from pandas import DataFrame
-from pyqir import BasicQisBuilder, Module, SimpleModule
 from qbraid_core.services.quantum.exceptions import QuantumServiceRequestError
 
 from qbraid._caching import cache_disabled
@@ -47,7 +46,7 @@
     QbraidQirSimulatorResultData,
     QuEraQasmSimulatorResultData,
 )
-from qbraid.runtime.noise import NoiseModel, NoiseModelSet
+from qbraid.runtime.noise import NoiseModel
 from qbraid.runtime.options import RuntimeOptions
 from qbraid.runtime.schemas.experiment import QuboSolveParams
 from qbraid.runtime.schemas.job import RuntimeJobModel
@@ -64,19 +63,6 @@
 )
 
 
-@pytest.fixture
-def mock_profile():
-    """Mock profile for testing."""
-    return TargetProfile(
-        device_id="qbraid_qir_simulator",
-        simulator=True,
-        experiment_type=ExperimentType.GATE_MODEL,
-        num_qubits=64,
-        program_spec=QbraidProvider._get_program_spec("pyqir", "qbraid_qir_simulator"),
-        noise_models=NoiseModelSet.from_iterable(["ideal"]),
-    )
-
-
 @pytest.fixture
 def mock_quera_profile():
     """Mock profile for testing."""
@@ -122,30 +108,12 @@ def mock_quera_device(mock_quera_profile, mock_client):
     return QbraidDevice(profile=mock_quera_profile, client=mock_client)
 
 
-@pytest.fixture
-def mock_basic_device(mock_profile):
-    """Generic mock device for testing."""
-    return MockDevice(profile=mock_profile)
-
-
 @pytest.fixture
 def mock_nec_va_device(mock_nec_va_profile, mock_client):
     """Mock NEC vector annealer device."""
     return QbraidDevice(profile=mock_nec_va_profile, client=mock_client)
 
 
-@pytest.fixture
-def pyqir_module() -> Module:
-    """Returns a one-qubit PyQIR module with Hadamard gate and measurement."""
-    bell = SimpleModule("test_qir_program", num_qubits=1, num_results=1)
-    qis = BasicQisBuilder(bell.builder)
-
-    qis.h(bell.qubits[0])
-    qis.mz(bell.qubits[0], bell.results[0])
-
-    return bell._module
-
-
 def counts_to_measurements(counts: dict[str, Any]) -> np.ndarray:
     """Convert counts dictionary to measurements array."""
     measurements = []
@@ -191,6 +159,7 @@ def test_qbraid_device_str_representation(mock_qbraid_device):
     assert str(mock_qbraid_device) == "QbraidDevice('qbraid_qir_simulator')"
 
 
+@pytest.mark.skipif(importlib.util.find_spec("pyqir") is None, reason="pyqir is not installed")
 def test_qir_simulator_workflow(mock_provider, cirq_uniform):
     """Test qir simulator qbraid device job submission and result retrieval."""
     circuit = cirq_uniform(num_qubits=5)
@@ -304,6 +273,7 @@ def test_nec_vector_annealer_workflow(mock_provider):
     assert result.data._solutions == RESULTS_DATA_NEC["solutions"]
 
 
+@pytest.mark.skipif(importlib.util.find_spec("pyqir") is None, reason="pyqir is not installed")
 def test_run_forbidden_kwarg(mock_provider):
     """Test that invoking run method with forbidden kwarg raises value error."""
     circuit = Mock()
@@ -335,11 +305,6 @@ def test_device_noisey_run_raises_for_unsupported(mock_qbraid_device):
         mock_qbraid_device.run(Mock(), noise_model=NoiseModel("amplitude_damping"))
 
 
-def test_device_transform(pyqir_module, mock_qbraid_device):
-    """Test transform method on OpenQASM 2 string."""
-    assert mock_qbraid_device.to_ir(pyqir_module) == {"bitcode": pyqir_module.bitcode}
-
-
 def test_device_extract_qasm(valid_qasm2, mock_qbraid_device):
     """Test that the extracting OpenQASM representation function
     returns qasm2 string for qasm2 program spec."""
@@ -641,19 +606,6 @@ def test_set_options(mock_qbraid_device: QbraidDevice):
     assert dict(mock_qbraid_device._options) == updated_options
 
 
-def test_transform_to_ir_from_spec(mock_basic_device: MockDevice, pyqir_module: Module):
-    """Test transforming to run input to given IR from target profile program spec."""
-    run_input_transformed = mock_basic_device.transform(pyqir_module)
-    run_input_ir = mock_basic_device.to_ir(run_input_transformed)
-    assert isinstance(run_input_ir, dict)
-    assert isinstance(run_input_ir.get("bitcode"), bytes)
-
-    mock_basic_device._target_spec = None
-    run_input_transformed = mock_basic_device.transform(pyqir_module)
-    run_input_ir = mock_basic_device.to_ir(run_input_transformed)
-    assert isinstance(run_input_ir, Module)
-
-
 def test_set_options_raises_for_bad_key(mock_basic_device: MockDevice):
     """Test that the set options method raises AttributeError for key
     not already included in options."""
diff --git a/tests/runtime/test_device_qir.py b/tests/runtime/test_device_qir.py
new file mode 100644
index 000000000..2f6f77706
--- /dev/null
+++ b/tests/runtime/test_device_qir.py
@@ -0,0 +1,58 @@
+# Copyright (C) 2024 qBraid
+#
+# This file is part of the qBraid-SDK
+#
+# The qBraid-SDK is free software released under the GNU General Public License v3
+# or later. You can redistribute and/or modify it under the terms of the GPL v3.
+# See the LICENSE file in the project root or <https://www.gnu.org/licenses/gpl-3.0.html>.
+#
+# THERE IS NO WARRANTY for the qBraid-SDK, as per Section 15 of the GPL v3.
+
+"""
+Unit tests for QbraidDevice, QbraidJob, and QbraidGateModelResultBuilder
+classes using the qbraid_qir_simulator
+
+"""
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import pytest
+
+from ._resources import MockDevice
+
+if TYPE_CHECKING:
+    import pyqir
+    from pyqir import Module
+
+pyqir = pytest.importorskip("pyqir")
+
+
+@pytest.fixture
+def pyqir_module() -> Module:
+    """Returns a one-qubit PyQIR module with Hadamard gate and measurement."""
+    bell = pyqir.SimpleModule("test_qir_program", num_qubits=1, num_results=1)
+    qis = pyqir.BasicQisBuilder(bell.builder)
+
+    qis.h(bell.qubits[0])
+    qis.mz(bell.qubits[0], bell.results[0])
+
+    return bell._module
+
+
+def test_device_transform(pyqir_module: Module, mock_qbraid_device):
+    """Test transform method on OpenQASM 2 string."""
+    assert mock_qbraid_device.to_ir(pyqir_module) == {"bitcode": pyqir_module.bitcode}
+
+
+def test_transform_to_ir_from_spec(mock_basic_device: MockDevice, pyqir_module: Module):
+    """Test transforming to run input to given IR from target profile program spec."""
+    run_input_transformed = mock_basic_device.transform(pyqir_module)
+    run_input_ir = mock_basic_device.to_ir(run_input_transformed)
+    assert isinstance(run_input_ir, dict)
+    assert isinstance(run_input_ir.get("bitcode"), bytes)
+
+    mock_basic_device._target_spec = None
+    run_input_transformed = mock_basic_device.transform(pyqir_module)
+    run_input_ir = mock_basic_device.to_ir(run_input_transformed)
+    assert isinstance(run_input_ir, pyqir.Module)
diff --git a/tests/transpiler/test_conversion_extras.py b/tests/transpiler/test_conversion_extras.py
index de020acf4..9e4220521 100644
--- a/tests/transpiler/test_conversion_extras.py
+++ b/tests/transpiler/test_conversion_extras.py
@@ -17,7 +17,13 @@
 
 import braket.circuits
 import pytest
-from pyqir import Module
+
+try:
+    import pyqir
+
+    pyqir_installed = True
+except ImportError:
+    pyqir_installed = False
 
 from qbraid.transpiler.conversions.qiskit import qiskit_to_braket, qiskit_to_pyqir
 from qbraid.transpiler.converter import transpile
@@ -51,6 +57,7 @@ def test_qiskit_to_braket_extra(bell_circuit):
 
 
 @pytest.mark.skipif(not has_extra(qiskit_to_pyqir), reason="Extra not installed")
+@pytest.mark.skipif(not pyqir_installed, reason="pyqir not installed")
 @pytest.mark.parametrize("bell_circuit", ["qiskit"], indirect=True)
 def test_qiskit_to_pyqir_extra(bell_circuit):
     """Test qiskit-qir transpiler conversion extra."""
@@ -58,4 +65,4 @@ def test_qiskit_to_pyqir_extra(bell_circuit):
     conversions = [Conversion("qiskit", "pyqir", qiskit_to_pyqir)]
     graph = ConversionGraph(conversions)
     program = transpile(qiskit_circuit, "pyqir", conversion_graph=graph, max_path_depth=1)
-    assert isinstance(program, Module)
+    assert isinstance(program, pyqir.Module)
diff --git a/tests/transpiler/test_conversion_graph.py b/tests/transpiler/test_conversion_graph.py
index 27f0cc975..ea9a4c34d 100644
--- a/tests/transpiler/test_conversion_graph.py
+++ b/tests/transpiler/test_conversion_graph.py
@@ -19,7 +19,13 @@
 
 import pytest
 import rustworkx as rx
-from pyqir import Module
+
+try:
+    import pyqir
+
+    pyqir_installed = True
+except ImportError:
+    pyqir_installed = False
 
 from qbraid.programs import ExperimentType, register_program_type
 from qbraid.programs.ahs import submodules as ahs_submodules
@@ -137,6 +143,7 @@ def test_initialize_new_conversion():
     assert graph.num_edges() == 1
 
 
+@pytest.mark.skipif(not pyqir_installed, reason="pyqir not installed")
 @pytest.mark.parametrize("bell_circuit", ["qiskit"], indirect=True)
 def test_overwrite_new_conversion(bell_circuit):
     """Test dynamically overwriting a new conversion in the conversion graph"""
@@ -148,7 +155,7 @@ def test_overwrite_new_conversion(bell_circuit):
     graph.add_conversion(edge, overwrite=True)
     assert graph.num_edges() == 1
     module = transpile(qiskit_circuit, "pyqir", conversion_graph=graph)
-    assert isinstance(module, Module)
+    assert isinstance(module, pyqir.Module)
 
 
 def test_remove_conversion():