# Testing Guide for `gvit`

Comprehensive guide for testing `gvit` - from quick start to advanced coverage analysis.

## 📋 Table of Contents

- 🚀 [Quick Start](#-quick-start)
- 📁 [Test Structure](#-test-structure)
- 🏃 [Running Tests](#-running-tests)
- ✍️ [Writing Tests](#-writing-tests)
- 📊 [Coverage](#-coverage)
- 🔧 [Fixtures](#-fixtures)
- ✅ [Best Practices](#-best-practices)
- 🐛 [Troubleshooting](#-troubleshooting)
- 🎯 [Next Steps](#-next-steps)
- 📚 [Resources](#-resources)
- 📈 [Current Status](#-current-status)

---

## 🚀 Quick Start

### Install Dependencies

```bash
# Install test dependencies
pip install -e ".[test]"
```

### Run Tests

When testing CLI commands built with Typer (or Click), the test runner internally captures and redirects standard output (**stdout**). At the same time, pytest also captures stdout by default. This double capture can interfere with how Typer detects and writes to the terminal, causing missing or inconsistent output during tests. Running tests with the `-s` flag disables pytest’s output capture, allowing Typer’s console output (including echo and secho) to behave normally.

In short, use `pytest -s` to ensure CLI tests run with the same behavior as when executing the commands directly in a real terminal.

```bash
# Run all tests
pytest -s

# Run with coverage
pytest -s --cov=src/gvit --cov-report=html
open tests/htmlcov/index.html
```

### Current Status

- ✅ **49 tests** passing (38 unit + 11 integration).
- ✅ **33% coverage** (target: 80%+).
- ✅ **Fully isolated** (no system side effects).

---

## 📁 Test Structure

```
tests/
├── unit/                       # Unit tests (fast, isolated)
│   ├── test_env_registry.py    # EnvRegistry class tests
│   ├── test_backends/          # Backend implementations
│   └── test_utils/
│       └── test_utils.py       # Utility functions
├── integration/                # Integration tests (e2e)
│   └── test_envs.py            # `gvit envs` commands
├── fixtures/                   # Test data
├── conftest.py                 # Shared fixtures
├── .coverage                   # Coverage data (generated)
├── coverage.xml                # Coverage XML (generated)
├── htmlcov/                    # Coverage HTML (generated)
└── README.md                   # This file
```

### Test Organization

**Unit Tests (38):**
- `test_env_registry.py` - 14 tests for EnvRegistry.
- `test_utils.py` - 24 tests for utilities.

**Integration Tests (11):**
- `test_envs.py` - Commands: list, delete, prune, show-activate, show-deactivate.

---

## 🏃 Running Tests

### Basic Commands

```bash
# All tests
pytest -s

# With verbose output
pytest -s -v

# Stop at first failure
pytest -s -x
```

### By Category

```bash
# Unit tests only
pytest tests/unit/

# Integration tests only
pytest tests/integration/

# Specific file
pytest tests/unit/test_env_registry.py

# Specific test
pytest tests/unit/test_env_registry.py::TestEnvRegistry::test_list_environments_empty

# Pattern matching
pytest -k "test_list"
```

### With Markers

```bash
# Run only unit tests
pytest -m unit

# Run only integration tests
pytest -m integration

# Run venv-specific tests
pytest -m venv
```

**Available markers:**
- `unit` - Unit tests (fast, isolated).
- `integration` - Integration tests (slower).
- `slow` - Long-running tests.
- `venv`, `conda`, `virtualenv` - Backend-specific.

---

## ✍️ Writing Tests

### Unit Test Example

```python
def test_extract_repo_name_from_url():
    """Test extracting repo name from HTTPS URL."""
    url = "https://github.com/user/repo.git"
    assert extract_repo_name_from_url(url) == "repo"
```

**Unit tests should:**
- ✅ Be fast (< 1 second each).
- ✅ Test single function/method.
- ✅ Use mocks for external deps.
- ✅ Be completely isolated.

### Integration Test Example

```python
from typer.testing import CliRunner
from gvit.cli import app

runner = CliRunner()

def test_envs_list_command(temp_config_dir, temp_repo):
    """Test 'gvit envs list' command."""
    result = runner.invoke(app, ["envs", "list"])
    assert result.exit_code == 0
    assert "No environments in registry" in result.output
```

**Integration tests should:**
- ✅ Test complete workflows.
- ✅ Use `CliRunner` for CLI.
- ✅ Mock only external services.
- ✅ Use temp directories.

### Test Isolation

Every test runs in complete isolation:
- Temporary config directories.
- Temporary git repos.
- Mocked subprocess calls.
- No side effects on system.

This is achieved by the `isolate_tests` auto-use fixture in `conftest.py`.

---

## 📊 Coverage

### Understanding Coverage Files

Coverage reports are generated in the `tests/` directory:

| File | Purpose | Commit? |
|------|---------|---------|
| `tests/.coverage` | Binary data (SQLite) | ❌ No |
| `tests/coverage.xml` | XML for CI/CD tools | ❌ No |
| `tests/htmlcov/` | Interactive HTML report | ❌ No |

### Viewing Coverage

```bash
# Run tests with coverage
pytest -s

# Open HTML report
open tests/htmlcov/index.html

# Terminal report only
pytest -s --cov=src/gvit --cov-report=term-missing

# Single module
pytest -s --cov=src/gvit/env_registry.py --cov-report=term-missing

# With minimum threshold
pytest -s --cov=src/gvit --cov-fail-under=80
```

### HTML Coverage Report Features

- 📊 Overview with % per module.
- 🔍 Line-by-line highlighting (green=covered, red=not covered).
- 🎯 Branch coverage visualization.
- 📈 Trends (if history saved).

### Coverage Levels

- 🔴 **< 60%** - Insufficient, risky.
- 🟡 **60-80%** - Acceptable, improvable.
- 🟢 **80-90%** - Good, professional level.
- 💎 **> 90%** - Excellent, high confidence.

### Interpreting Coverage

```python
# Example: 75% coverage
def suma(a, b):          # Covered ✅
    if a < 0:            # Covered ✅
        return 0         # NOT covered ❌ (branch not tested)
    return a + b         # Covered ✅

# Test only covers positive case
def test_suma():
    assert suma(5, 3) == 8
```

**Coverage = 3/4 lines = 75%**

To reach 100%:
```python
def test_suma_negativo():
    assert suma(-5, 3) == 0  # Now covers negative branch
```

### Advanced Coverage

```bash
# Branch coverage (if/else paths)
pytest -s --cov=src/gvit --cov-branch

# Combine multiple runs
pytest tests/unit/ --cov=src/gvit
pytest tests/integration/ --cov=src/gvit --cov-append
coverage report

# Exclude code from coverage
def debug_func():  # pragma: no cover
    print("Debug")
```

### Coverage Configuration

Configuration is in `.coveragerc`:

```ini
[run]
source = src/gvit              # What to measure
data_file = tests/.coverage    # Where to store data
omit = */tests/*               # What to exclude

[html]
directory = tests/htmlcov      # HTML output location

[xml]
output = tests/coverage.xml    # XML output location
```

---

## 🔧 Fixtures

Common fixtures available in `conftest.py`:

### Directory Fixtures

```python
def test_example(temp_dir):
    """temp_dir: Temporary directory"""
    assert temp_dir.exists()

def test_example(temp_repo):
    """temp_repo: Initialized git repository"""
    assert (temp_repo / ".git").exists()

def test_example(temp_config_dir):
    """temp_config_dir: Isolated gvit config directory"""
    assert temp_config_dir.exists()
```

### Data Fixtures

```python
def test_example(sample_config):
    """sample_config: Sample gvit config file"""
    # Returns path to config.toml

def test_example(sample_requirements):
    """sample_requirements: Sample requirements.txt"""
    # Returns path to requirements file

def test_example(sample_env_info):
    """sample_env_info: Sample environment dict"""
    # Returns complete env info structure
```

### Service Fixtures

```python
def test_example(env_registry):
    """env_registry: EnvRegistry instance"""
    envs = env_registry.list_environments()

def test_example(mock_venv_creation, mocker):
    """mock_venv_creation: Mocked subprocess.run"""
    # Prevents actual venv creation
```

### Auto-used Fixtures

- `isolate_tests` - Automatically patches all paths for isolation

---

### Coverage Services

Compatible with:
- [Codecov](https://codecov.io/)
- [Coveralls](https://coveralls.io/)
- [SonarQube](https://www.sonarqube.org/)

---

## ✅ Best Practices

### DO:

✅ **Descriptive names** - `test_load_config_from_file` not `test_1`.  
✅ **One concept per test** - Test one thing at a time.  
✅ **Use fixtures** - DRY principle for setup.  
✅ **Test edge cases** - Empty strings, None, negative numbers.  
✅ **Test error paths** - Not just happy path.  
✅ **Independent tests** - No shared state.  
✅ **Fast unit tests** - Keep under 1 second.  
✅ **Clear assertions** - Explicit expected values.  

### DON'T:

❌ **Sleep in tests** - Use mocks instead.  
❌ **Test implementation** - Test behavior.  
❌ **Share state** - Each test isolated.  
❌ **Ignore failures** - Fix or remove.  
❌ **100% coverage obsession** - Quality > quantity.  

### Coverage Tips

1. **Prioritize important code** - Business logic first.
2. **Coverage ≠ quality** - Can have 100% with bad tests.
3. **Use HTML report** - Visual identification of gaps.
4. **Monitor trends** - Track coverage over time.

---

## 🐛 Troubleshooting

### "Config directory not found"

**Cause:** `isolate_tests` fixture not patching correctly  
**Solution:** Check that monkeypatch is working in conftest.py

### Git errors in tests

**Cause:** Git not installed or temp_repo fixture issue  
**Solution:** 
```bash
# Ensure git is installed
git --version

# Check temp_repo fixture in conftest.py
```

### Import errors

**Cause:** Package not installed in editable mode  
**Solution:**
```bash
pip install -e ".[test]"
```

### Tests passing locally but failing in CI

**Cause:** Environment differences  
**Solution:**
- Check Python version compatibility
- Verify all dependencies in pyproject.toml
- Check for absolute paths in code

### Slow tests

**Solution:**
```bash
# Identify slow tests
pytest -s --durations=10

# Run only fast tests
pytest -s -m "not slow"
```

---

## 🎯 Next Steps

### Areas Needing Tests

1. **Backends** - venv, conda, virtualenv operations.
2. **Commands** - clone, init, setup, pull, commit, status.
3. **Git integration** - git operations and fallback.
4. **Dependency management** - install, validate, sync.
5. **Error handling** - edge cases and failures.

### Adding Tests

1. Identify code to test.
2. Choose unit vs integration.
3. Write test with clear name + docstring.
4. Use appropriate fixtures.
5. Assert expected behavior.
6. Run to verify passing.
7. Check coverage impact.

---

## 📚 Resources

- [pytest documentation](https://docs.pytest.org/)
- [pytest-cov documentation](https://pytest-cov.readthedocs.io/)
- [pytest-mock documentation](https://pytest-mock.readthedocs.io/)
- [Typer testing guide](https://typer.tiangolo.com/tutorial/testing/)
- [Coverage.py docs](https://coverage.readthedocs.io/)

---

## 📈 Current Status

**Tests:** 49 passing (38 unit + 11 integration)  
**Coverage:** 33% (target: 80%+)  
**Status:** ✅ Fully functional  
**Last Updated:** 2025-11-03

---

**Tech Stack:** pytest • pytest-cov • pytest-mock • typer.testing  
**Configuration:** `pytest.ini` • `.coveragerc`  
**Location:** All coverage files in `tests/` directory
