Testing¶
Overview¶
Permian uses a comprehensive testing strategy with three types of tests:
Lint tests - Code quality checks using pylint
Unit tests - Fast, isolated tests of individual components
Integration tests - End-to-end tests of complete workflows
All tests can be run using the Makefile targets inside the container:
./in_container make test # Run all tests
./in_container make test.lint # Run only lint tests
./in_container make test.unit # Run only unit tests
./in_container make test.integration # Run only integration tests
Test Organization¶
Directory Structure¶
Tests are organized as follows:
permian/
├── libpermian/ # Source code
│ ├── events/
│ │ └── test_events.py # Unit tests alongside source
│ ├── plugins/
│ │ ├── compose/
│ │ │ └── test_compose.py # Plugin unit tests
│ │ └── test_plugins.py # Plugin system tests
│ └── ...
├── tests/ # Test-specific files
│ ├── integration/ # Integration test suites
│ │ ├── test_example/ # One directory per test suite
│ │ │ └── test_0.sh # Shell-based integration test
│ │ └── test_testing_plugin/
│ │ ├── test_1.sh
│ │ └── test_1_result/ # Expected results
│ ├── plugins/ # Test plugin fixtures
│ │ ├── test1_enabled/
│ │ └── test2_disabled/
│ ├── test_library/ # Test library for integration tests
│ └── test_settings.ini # Test configuration
├── run_unit_tests.py # Unit test runner
├── run_integration_tests.sh # Integration test runner
└── run_pylint.sh # Linter
Unit Tests¶
Unit tests use Python’s built-in unittest framework and test individual components in isolation.
Writing Unit Tests¶
Unit test files are named test_*.py and placed alongside the source code they test.
Basic Structure:
import unittest
from unittest.mock import patch, MagicMock
class TestMyComponent(unittest.TestCase):
def setUp(self):
"""Called before each test method."""
pass
def tearDown(self):
"""Called after each test method."""
pass
@classmethod
def setUpClass(cls):
"""Called once before all tests in the class."""
pass
@classmethod
def tearDownClass(cls):
"""Called once after all tests in the class."""
pass
def test_basic_functionality(self):
"""Test names must start with 'test_'."""
self.assertEqual(2 + 2, 4)
Mocking¶
Use unittest.mock for isolating components:
Common Mock Patterns:
from unittest.mock import patch, MagicMock, call
# Mock environment variables
@patch('os.environ', new={'VAR': 'value'})
def test_with_env(self):
pass
# Mock function calls
class ResultMock200():
status_code = 200
text = 'test'
@classmethod
def json(cls):
return {'head': {'sha': 'abc123'}}
@patch('requests.get')
def test_reporting(self, requests_get):
requests_get.return_value = ResultMock200()
Running Unit Tests¶
Unit tests are discovered and run using script:
./run_unit_tests.py
Or inside the container:
./in_container make test.unit
The test runner:
Discovers all
test*.pyin project root and all loaded plugin directoriesRuns all discovered tests
Exits with code 2 if any test fails, debug output written to
test_debug.log
Integration Tests¶
Integration tests verify complete workflows by running the pipeline with real or simulated inputs and comparing output to expected results.
Writing Integration Tests¶
Integration tests are bash scripts that define three functions: setup, test, and cleanup.
Basic Structure:
setup() {
echo "Test description"
# Prepare test environment
TEST_REPORT_DIR=$(mktemp -d)
}
test() {
# Run the pipeline or specific commands
./pipeline example -o library.directPath=./tests/test_library
# Optionally compare results
diff -Naur expected_result $TEST_REPORT_DIR
}
cleanup() {
# Clean up resources
rm -rf $TEST_REPORT_DIR
}
Running Integration Tests¶
Integration tests are run using:
./run_integration_tests.sh
Or inside the container:
./in_container make test.integration
The runner:
Discovers all
test_*.shfiles intests/integration/*/For each test:
Runs
setup()(exits if setup fails)Runs
test()(marks as PASSED or FAILED)Runs
cleanup()(reports if cleanup fails)
Exits with:
0- All tests passed1- One or more tests failed2- Setup error3- Cleanup error
Lint Tests¶
Pylint is used to check code quality and catch common errors.
Running Lint Tests¶
Linting is run using script:
./run_pylint.sh
Or inside the container:
./in_container make test.lint
The linter checks:
Core libpermian code
All loaded plugins (discovered via
python3 -m libpermian.plugins list --paths)Only errors are reported (
-Eflag)
Pylint configuration is in .pylintrc
Continuous Integration¶
Tests are designed to run in CI environments. Github Actions is used to run all tests and build documentation on every pull request. No change should be merged without all tests passing.