# Testing Approach
This document explains the testing strategies and methodologies used in `vika-cli`.
> **For practical testing instructions**, see [testing.md](testing.md).
## Testing Philosophy
We use a **multi-layered testing approach** combining unit tests, integration tests, and property-based testing to ensure reliability and correctness.
## Testing Strategies
### 1. **Unit Testing** (Isolated Function Testing)
**Purpose:** Test individual functions in isolation with controlled inputs/outputs.
**Approach:**
- Tests live alongside code in `#[cfg(test)] mod tests` blocks
- Test pure functions with known inputs → expected outputs
- Test edge cases (empty strings, special characters, boundaries)
**Example from `src/generator/utils.rs`:**
```rust
#[test]
fn test_to_pascal_case_simple() {
assert_eq!(to_pascal_case("hello"), "Hello");
assert_eq!(to_pascal_case("world"), "World");
}
#[test]
fn test_to_pascal_case_empty() {
assert_eq!(to_pascal_case(""), ""); // Edge case
}
```
**Characteristics:**
- ✅ Fast execution
- ✅ Tests specific behavior
- ✅ Easy to debug failures
- ✅ Can test private functions
### 2. **Property-Based Testing** (Multiple Input Variations)
**Purpose:** Test functions with various input combinations to find edge cases.
**Approach:**
- Test the same function with different input formats
- Verify consistent behavior across variations
- Discover unexpected edge cases
**Example from `src/generator/utils.rs`:**
```rust
// Same function, different input formats
test_to_pascal_case_with_underscore() // "hello_world" → "HelloWorld"
test_to_pascal_case_with_hyphen() // "hello-world" → "HelloWorld"
test_to_pascal_case_with_space() // "hello world" → "HelloWorld"
test_to_pascal_case_mixed() // "hello_world-test" → "HelloWorldTest"
```
**Benefits:**
- Finds bugs across input variations
- Ensures consistent behavior
- Documents expected behavior for different formats
### 3. **Integration Testing** (End-to-End Workflows)
**Purpose:** Test complete workflows from input to output, verifying components work together.
**Approach:**
- Test full user workflows (parse → generate → write)
- Use real-world scenarios
- Verify file system operations
- Test async operations
**Example from `tests/integration/generate_test.rs`:**
```rust
#[tokio::test]
async fn test_full_generation_workflow() {
// 1. Setup: Create temp directory and spec file
let temp_dir = setup_test_env();
let spec_json = r#"{...}"#;
fs::write(&spec_path, spec_json).unwrap();
// 2. Parse: Convert spec to internal representation
let parsed = fetch_and_parse_spec(spec_path).await.unwrap();
// 3. Generate: Create TypeScript types, Zod schemas, API client
let types = generate_typings(...).unwrap();
let zod_schemas = generate_zod_schemas(...).unwrap();
let api_functions = generate_api_client(...).unwrap();
// 4. Write: Save files to disk
write_schemas(...).unwrap();
write_api_client(...).unwrap();
// 5. Verify: Check files exist and are correct
assert!(output_dir.join("users/types.ts").exists());
}
```
**Characteristics:**
- ✅ Tests real-world usage
- ✅ Catches integration bugs
- ✅ Validates file I/O
- ⚠️ Slower than unit tests
- ⚠️ More complex setup
### 4. **Fixture-Based Testing** (Test Data Builders)
**Purpose:** Create reusable test data to reduce duplication and improve maintainability.
**Approach:**
- Build test fixtures in `tests/common/fixtures.rs`
- Create factory functions for common test scenarios
- Build complex objects incrementally
**Example from `tests/common/fixtures.rs`:**
```rust
// Minimal fixture
pub fn create_minimal_openapi_spec() -> OpenAPI { ... }
// Complex fixture with dependencies
pub fn create_multi_module_spec() -> OpenAPI {
let mut openapi = create_minimal_openapi_spec(); // Reuse base
// Add modules, schemas, etc.
}
// Specialized fixtures
pub fn create_user_schema() -> Schema { ... }
pub fn create_enum_schema() -> Schema { ... }
pub fn create_nested_schema() -> Schema { ... }
```
**Benefits:**
- ✅ DRY (Don't Repeat Yourself)
- ✅ Consistent test data
- ✅ Easy to update when schemas change
- ✅ Clear test intent
### 5. **Error Testing** (Error Handling Validation)
**Purpose:** Ensure errors are properly created, converted, and displayed.
**Approach:**
- Test error creation with various inputs
- Verify error messages contain expected information
- Test error type conversions
- Validate error context preservation
**Example from `src/error.rs`:**
```rust
#[test]
fn test_schema_error_display() {
let error = SchemaError::NotFound { name: "User".to_string() };
let error_msg = error.to_string();
assert!(error_msg.contains("User"));
assert!(error_msg.contains("not found"));
}
#[test]
fn test_error_conversion() {
let schema_error = SchemaError::NotFound { ... };
let vika_error: VikaError = schema_error.into();
// Verify conversion preserves information
}
```
**Benefits:**
- ✅ Ensures helpful error messages
- ✅ Validates error type system
- ✅ Tests error propagation
### 6. **State-Based Testing** (Testing State Changes)
**Purpose:** Verify that operations correctly modify state.
**Approach:**
- Test before/after state
- Verify state transitions
- Test state persistence
**Example from `tests/integration/common_schemas_test.rs`:**
```rust
#[test]
fn test_detect_common_schemas() {
// Initial state: schemas in multiple modules
let mut module_schemas = HashMap::new();
module_schemas.insert("users", vec!["User", "CommonResponse"]);
module_schemas.insert("products", vec!["Product", "CommonResponse"]);
// Operation: Filter common schemas
let (filtered, common_schemas) = filter_common_schemas(&module_schemas, &selected);
// Verify state changes:
// 1. Common schemas extracted
assert!(common_schemas.contains("CommonResponse"));
// 2. Removed from individual modules
assert!(!filtered.get("users").contains("CommonResponse"));
// 3. Other schemas preserved
assert!(filtered.get("users").contains("User"));
}
```
### 7. **Boundary Testing** (Edge Cases)
**Purpose:** Test limits, empty inputs, and extreme values.
**Approach:**
- Test empty inputs
- Test single-item inputs
- Test maximum values
- Test invalid inputs
**Examples:**
```rust
test_to_pascal_case_empty() // Empty string
test_no_common_schemas_single_module() // Single module (no commons possible)
test_get_cached_spec_miss() // Cache miss scenario
```
### 8. **Contract Testing** (API Contract Validation)
**Purpose:** Ensure functions meet their documented contracts.
**Approach:**
- Test function signatures match expectations
- Verify return types
- Test error conditions
- Validate side effects
**Example:**
```rust
// Contract: generate_typings returns Result<Vec<TypeScriptType>>
let types = generate_typings(...).unwrap(); // Must not panic
assert!(!types.is_empty()); // Must return items
```
## Test Organization Patterns
### **Arrange-Act-Assert (AAA) Pattern**
Most tests follow the AAA pattern:
```rust
#[test]
fn test_example() {
// Arrange: Set up test data
let input = "hello_world";
// Act: Execute the function
let result = to_pascal_case(input);
// Assert: Verify the result
assert_eq!(result, "HelloWorld");
}
```
### **Given-When-Then Pattern** (for integration tests)
```rust
#[tokio::test]
async fn test_full_generation_workflow() {
// Given: A valid OpenAPI spec file
let spec_json = r#"{...}"#;
fs::write(&spec_path, spec_json).unwrap();
// When: We run the full generation workflow
let parsed = fetch_and_parse_spec(...).await.unwrap();
let types = generate_typings(...).unwrap();
write_schemas(...).unwrap();
// Then: Files should be created correctly
assert!(output_dir.join("users/types.ts").exists());
}
```
## Testing Tools & Utilities
### **Test Fixtures** (`tests/common/fixtures.rs`)
- Factory functions for creating test data
- Reusable OpenAPI spec builders
- Schema builders for different types
### **Test Helpers** (`tests/common/helpers.rs`)
- `setup_test_env()` - Create temporary directories
- `assert_file_contents()` - Verify file contents
- `create_mock_config()` - Create test configurations
### **External Libraries**
- `tempfile` - Temporary directories for file I/O tests
- `tokio-test` - Async test utilities
- `insta` - Snapshot testing (for generated code)
## Test Coverage Goals
1. **Unit Tests:** Cover all public and critical private functions
2. **Integration Tests:** Cover all major user workflows
3. **Error Tests:** Cover all error paths
4. **Edge Cases:** Test boundaries and empty inputs
5. **Property Tests:** Test functions with various input formats
## Running Tests
```bash
# All tests
cargo test
# Unit tests only
cargo test --lib
# Integration tests only
cargo test --test '*'
# Specific test
cargo test test_to_pascal_case
# With output
cargo test -- --nocapture
```
## Best Practices
1. ✅ **One assertion per test** (when possible) - makes failures clear
2. ✅ **Descriptive test names** - `test_to_pascal_case_with_underscore` not `test1`
3. ✅ **Test edge cases** - empty strings, null values, boundaries
4. ✅ **Use fixtures** - Don't duplicate test data setup
5. ✅ **Test errors** - Verify error handling works
6. ✅ **Fast tests** - Unit tests should be fast (< 1ms)
7. ✅ **Isolated tests** - Tests shouldn't depend on each other
8. ✅ **Clear assertions** - Use `assert_eq!` with expected/actual