Testing

This guide explains how to test EZStitcher.

Test Organization

The tests are organized into the following directories:

  • `unit/`: Unit tests for individual components

  • `integration/`: Integration tests for the full workflow

  • `generators/`: Synthetic data generators for testing

Running Tests

To run all tests:

pytest

To run a specific test file:

pytest tests/unit/test_image_processor.py

To run a specific test class:

pytest tests/unit/test_image_processor.py::TestImageProcessor

To run a specific test method:

pytest tests/unit/test_image_processor.py::TestImageProcessor::test_blur

Test Coverage

To generate a test coverage report:

pytest --cov=ezstitcher tests/

For a detailed HTML report:

pytest --cov=ezstitcher --cov-report=html tests/

Writing Tests

When writing tests for EZStitcher, follow these guidelines:

  1. Use pytest fixtures: Use fixtures to set up test data and dependencies

  2. Test one thing at a time: Each test should test one specific functionality

  3. Use descriptive names: Test names should describe what is being tested

  4. Use assertions: Use assertions to verify expected behavior

  5. Clean up after tests: Clean up any temporary files or directories created during tests

Here’s an example of a unit test:

import pytest
import numpy as np
from ezstitcher.core.image_processor import ImageProcessor

class TestImageProcessor:
    """Tests for the ImageProcessor class."""

    def test_blur(self):
        """Test the blur method."""
        # Create a test image
        image = np.ones((100, 100), dtype=np.uint16) * 1000
        image[40:60, 40:60] = 5000  # Add a bright square

        # Apply blur
        blurred = ImageProcessor.blur(image, sigma=2.0)

        # Verify that the image was blurred
        assert blurred.shape == image.shape
        assert blurred.dtype == image.dtype
        assert np.mean(blurred[40:60, 40:60]) < 5000  # Blurring should reduce the intensity
        assert np.mean(blurred[40:60, 40:60]) > 1000  # But it should still be brighter than the background

    def test_normalize(self):
        """Test the normalize method."""
        # Create a test image
        image = np.ones((100, 100), dtype=np.uint16) * 1000
        image[40:60, 40:60] = 5000  # Add a bright square

        # Apply normalization
        normalized = ImageProcessor.normalize(image, target_min=0, target_max=65535)

        # Verify that the image was normalized
        assert normalized.shape == image.shape
        assert normalized.dtype == image.dtype
        assert np.min(normalized) == 0
        assert np.max(normalized) == 65535

Here’s an example of an integration test:

import pytest
import numpy as np
from pathlib import Path
from ezstitcher.core.config import PipelineConfig
from ezstitcher.core.pipeline_orchestrator import PipelineOrchestrator
from ezstitcher.core.pipeline import Pipeline
from ezstitcher.core.steps import Step, PositionGenerationStep, ImageStitchingStep
from ezstitcher.core.image_processor import ImageProcessor as IP
from ezstitcher.tests.generators.generate_synthetic_data import SyntheticMicroscopyGenerator

@pytest.fixture
def flat_plate_dir(tmp_path):
    """Create synthetic flat plate data for testing."""
    plate_dir = tmp_path / "flat_plate"

    # Generate synthetic data
    generator = SyntheticMicroscopyGenerator(
        output_dir=str(plate_dir),
        grid_size=(3, 3),
        tile_size=(128, 128),
        overlap_percent=10,
        wavelengths=2,
        z_stack_levels=1,  # Flat plate has only 1 Z-level
        cell_size_range=(5, 10),
        wells=["A01", "B01"],
        format="ImageXpress"
    )
    generator.generate_dataset()

    return plate_dir

def test_pipeline_architecture(flat_plate_dir):
    """Test the pipeline architecture with the orchestrator."""
    # Create configuration
    config = PipelineConfig(
        num_workers=2  # Use 2 worker threads
    )

    # Create orchestrator
    orchestrator = PipelineOrchestrator(
        config=config,
        plate_path=flat_plate_dir
    )

    # Create position generation pipeline
    position_pipeline = Pipeline(
        steps=[
            # Step 1: Flatten Z-stacks (using function tuple for parameters)
            Step(name="Z-Stack Flattening",
                 func=(IP.create_projection, {'method': 'max_projection'}),
                 variable_components=['z_index'],
                 input_dir=orchestrator.workspace_path),  # First step uses workspace_path

            # Step 2: Process channels with a sequence of functions
            Step(name="Image Enhancement",
                 func=[
                     (IP.sharpen, {'amount': 1.5}),
                     IP.stack_percentile_normalize
                 ]),

            # Step 3: Create composite with weights (70% channel 1, 30% channel 2)
            Step(func=(IP.create_composite, {'weights': [0.7, 0.3]}),  # Pass weights as a list
                 variable_components=['channel']),

            # Step 4: Generate positions
            PositionGenerationStep()
        ],
        name="Position Generation Pipeline"
    )

    # Create image assembly pipeline
    assembly_pipeline = Pipeline(
        steps=[
            # Step 1: Process images
            Step(name="Image Processing",
                 func=IP.stack_percentile_normalize,
                 input_dir=orchestrator.workspace_path),

            # Step 2: Stitch images
            ImageStitchingStep()
        ],
        name="Image Assembly Pipeline"
    )

    # Run the orchestrator with the pipelines
    success = orchestrator.run(pipelines=[position_pipeline, assembly_pipeline])

    # Verify that the pipeline ran successfully
    assert success, "Pipeline execution failed"

Generating Test Data

EZStitcher includes a synthetic data generator for testing. The preferred way to generate test data is using the SyntheticMicroscopyGenerator class:

from ezstitcher.tests.generators.generate_synthetic_data import SyntheticMicroscopyGenerator
from pathlib import Path

# Create a generator for synthetic data
generator = SyntheticMicroscopyGenerator(
    output_dir=str(Path("tests/data/synthetic_plate")),
    grid_size=(3, 3),           # 3x3 grid of tiles
    tile_size=(128, 128),       # Each tile is 128x128 pixels
    overlap_percent=10,         # 10% overlap between tiles
    wavelengths=2,              # 2 channels
    z_stack_levels=3,           # 3 Z-stack levels
    cell_size_range=(5, 10),    # Cell size range for synthetic cells
    wells=["A01", "A02"],       # Generate data for these wells
    format="ImageXpress"        # Use ImageXpress format
)

# Generate the dataset
generator.generate_dataset()

Mocking

When testing components that depend on external resources, use mocking to isolate the component being tested:

import pytest
from unittest.mock import Mock, patch
from pathlib import Path
from ezstitcher.core.stitcher import Stitcher
from ezstitcher.core.config import StitcherConfig
from ezstitcher.core.microscope_interfaces import MicroscopeHandler

def test_generate_positions_with_mock():
    """Test generate_positions with mocked dependencies."""
    # Create a mock microscope handler
    mock_handler = Mock(spec=MicroscopeHandler)
    mock_handler.parser.parse_filename.return_value = {
        'well': 'A01',
        'site': '1',
        'channel': '1',
        'extension': '.tif'
    }
    mock_handler.parser.construct_filename.return_value = "A01_s{iii}_w1.tif"

    # Create a list of mock image paths
    mock_image_paths = [
        Path("path/to/images/A01_s001_w1.tif"),
        Path("path/to/images/A01_s002_w1.tif"),
        Path("path/to/images/A01_s003_w1.tif"),
        Path("path/to/images/A01_s004_w1.tif")
    ]

    # Create a stitcher with the mock handler
    stitcher = Stitcher(StitcherConfig(), filename_parser=mock_handler)

    # Mock the find_images method to return our mock image paths
    with patch('ezstitcher.core.image_locator.ImageLocator.find_images',
              return_value=mock_image_paths):

        # Mock the _generate_positions_ashlar method
        with patch.object(stitcher, '_generate_positions_ashlar', return_value=True) as mock_method:
            # Call the method being tested
            result = stitcher.generate_positions(
                well="A01",
                image_dir=Path("path/to/images"),
                positions_path=Path("path/to/positions.csv")
            )

            # Verify that the method was called
            assert mock_method.called

            # Verify the result
            assert result is True

Debugging Tests

To debug tests, you can use the –pdb option to drop into the debugger when a test fails:

pytest --pdb

You can also use the breakpoint() function to set a breakpoint in your test:

def test_something():
    # Some test code
    breakpoint()  # Debugger will stop here
    # More test code