testing
Component Testing Strategies
This guide covers comprehensive testing strategies for GitLab CI/CD components.
Testing Levels
1. Syntax Validation
Test: YAML is valid
Tool: yq, yamllint
Speed: Seconds
Frequency: Every commit
2. Unit Tests
Test: Component logic works in isolation Tool: GitLab CI jobs Speed: Minutes Frequency: Every commit
3. Integration Tests
Test: Component works with real pipelines Tool: Test projects Speed: Minutes to hours Frequency: Before release
4. End-to-End Tests
Test: Complete workflows with components Tool: Real projects in staging Speed: Hours Frequency: Before major releases
5. Acceptance Tests
Test: Component meets user requirements Tool: Beta testing with real teams Speed: Days to weeks Frequency: Major releases only
Syntax Validation
Basic YAML Validation
# .gitlab-ci.yml validate-yaml: stage: validate image: alpine:latest before_script: - apk add --no-cache yq script: - | echo "Validating component YAML files..." for file in templates/*.yml templates/*/template.yml; do if [ -f "$file" ]; then echo "Checking $file" yq eval '.' "$file" > /dev/null || exit 1 echo " $file is valid" fi done
Advanced YAML Validation
# .gitlab-ci.yml validate-components: stage: validate image: alpine:latest before_script: - apk add --no-cache yq python3 py3-pip - pip3 install yamllint script: - | # Validate YAML syntax yamllint templates/ # Validate component structure for file in templates/*.yml templates/*/template.yml; do if [ -f "$file" ]; then echo "Validating $file" # Check for required spec section if ! yq eval '.spec' "$file" > /dev/null 2>&1; then echo "ERROR: $file missing spec section" exit 1 fi # Check for separator between spec and jobs if ! grep -q '^---$' "$file"; then echo "ERROR: $file missing --- separator" exit 1 fi echo " $file structure valid" fi done
Schema Validation
# component-schema.json { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "required": ["spec"], "properties": { "spec": { "type": "object", "properties": { "inputs": { "type": "object", "patternProperties": { "^[a-z_]+$": { "type": "object", "properties": { "type": { "enum": ["string", "number", "boolean", "array"] }, "default": {}, "options": { "type": "array" }, "regex": { "type": "string" } } } } } } } } }
# .gitlab-ci.yml validate-schema: stage: validate image: node:20 script: - npm install -g ajv-cli - | for file in templates/*.yml templates/*/template.yml; do if [ -f "$file" ]; then echo "Validating $file against schema" yq eval '.spec' "$file" -o json | ajv validate -s component-schema.json -d - fi done
Unit Testing
Testing Individual Components
# .gitlab-ci.yml test-docker-build: stage: test needs: [] variables: TEST_IMAGE_NAME: test-image:latest trigger: include: - local: templates/docker-build.yml strategy: depend script: - | # Set up inputs for component cat > .gitlab-ci.yml <<EOF include: - local: templates/docker-build.yml variables: INPUTS_IMAGE_NAME: ${TEST_IMAGE_NAME} INPUTS_DOCKERFILE: test/Dockerfile EOF # Trigger pipeline curl --request POST \ --form "token=${CI_JOB_TOKEN}" \ --form "ref=main" \ "https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/trigger/pipeline"
Input Validation Testing
# tests/test-inputs.yml test-valid-inputs: stage: test script: - | # Test with valid inputs include: - local: templates/k8s-deploy.yml # Verify component accepts valid inputs test-valid: stage: test script: - echo "Testing valid inputs" # Component should work artifacts: when: on_failure test-invalid-inputs: stage: test script: - | # Test with invalid inputs (should fail) include: - local: templates/k8s-deploy.yml # This should fail validation test-invalid: stage: test script: - echo "Testing invalid inputs" # Component should reject invalid inputs allow_failure: true # We expect this to fail
Mock Testing
# tests/test-docker-build.yml # Mock external dependencies test-docker-build-mock: stage: test image: docker:latest services: - docker:dind before_script: # Mock docker push (don't actually push) - | cat > /usr/local/bin/docker-push-mock.sh <<'EOF' #!/bin/sh echo "MOCK: docker push $1" exit 0 EOF chmod +x /usr/local/bin/docker-push-mock.sh alias docker push='docker-push-mock.sh' script: # Test component - | include: - local: templates/docker-build.yml # Build should work, push is mocked
Integration Testing
Test Project Pattern
Create dedicated test projects:
gitlab_components/
templates/
docker-build.yml
tests/
test-docker-simple/
.gitlab-ci.yml
Dockerfile
app.js
test-docker-multi-stage/
.gitlab-ci.yml
Dockerfile.multi
app.go
test-node-pipeline/
.gitlab-ci.yml
package.json
src/index.js
Test Project: Simple Docker Build
# tests/test-docker-simple/.gitlab-ci.yml include: - component: $CI_PROJECT_PATH/docker-build@$CI_COMMIT_SHA inputs: image_name: test-simple:$CI_COMMIT_SHA dockerfile: Dockerfile verify: stage: test image: docker:latest script: - docker pull test-simple:$CI_COMMIT_SHA - docker run test-simple:$CI_COMMIT_SHA node --version
Test Project: Complex Pipeline
# tests/test-node-pipeline/.gitlab-ci.yml include: - component: $CI_PROJECT_PATH/node-pipeline@$CI_COMMIT_SHA inputs: node_version: '20' enable_lint: true enable_tests: true enable_build: true verify-build: stage: deploy script: - test -d dist/ - ls -la dist/
Multi-Project Test Pipeline
# .gitlab-ci.yml in component repository stages: - validate - unit-test - integration-test - release # Syntax validation validate-yaml: stage: validate script: - yamllint templates/ # Unit tests .unit-test-template: stage: unit-test needs: [validate-yaml] test-docker-build: extends: .unit-test-template script: - test-component.sh docker-build test-k8s-deploy: extends: .unit-test-template script: - test-component.sh k8s-deploy # Integration tests .integration-test-template: stage: integration-test needs: [] trigger: strategy: depend test-simple-docker: extends: .integration-test-template trigger: project: my-org/gitlab_components-tests/test-docker-simple branch: main test-node-pipeline: extends: .integration-test-template trigger: project: my-org/gitlab_components-tests/test-node-pipeline branch: main test-multi-stage: extends: .integration-test-template trigger: project: my-org/gitlab_components-tests/test-docker-multi-stage branch: main # Release (only if all tests pass) release: stage: release image: registry.gitlab.com/gitlab-org/release-cli:latest rules: - if: $CI_COMMIT_TAG needs: - test-simple-docker - test-node-pipeline - test-multi-stage script: - echo "All tests passed, releasing $CI_COMMIT_TAG" release: tag_name: $CI_COMMIT_TAG description: 'Release $CI_COMMIT_TAG'
Test Coverage
Component Coverage Matrix
Track which scenarios are tested:
# tests/coverage-matrix.yml coverage: docker-build: tested_scenarios: - simple_dockerfile - multi_stage_dockerfile - custom_build_args - different_contexts input_combinations: - image_name: required - dockerfile: [Dockerfile, custom.Dockerfile] - context: [., ./app, ./services/api] - build_args: [null, "--build-arg VERSION=1.0"] k8s-deploy: tested_scenarios: - rolling_update - blue_green_deployment - canary_deployment - rollback input_combinations: - strategy: [rolling, blue-green, canary] - replicas: [1, 3, 5] - environment: [dev, staging, production]
Automated Coverage Reporting
// tests/coverage-report.ts interface TestCoverage { component: string; total_inputs: number; tested_inputs: number; coverage_percentage: number; untested_scenarios: string[]; } function generateCoverageReport(): TestCoverage[] { const components = scanComponents('templates/'); const tests = scanTests('tests/'); return components.map(component => { const inputs = extractInputs(component); const testedInputs = extractTestedInputs(tests, component.name); return { component: component.name, total_inputs: inputs.length, tested_inputs: testedInputs.length, coverage_percentage: (testedInputs.length / inputs.length) * 100, untested_scenarios: inputs.filter(i => !testedInputs.includes(i)) }; }); } // Generate report const report = generateCoverageReport(); console.table(report); // Fail if coverage < 80% const avgCoverage = report.reduce((sum, r) => sum + r.coverage_percentage, 0) / report.length; if (avgCoverage < 80) { console.error(`Test coverage ${avgCoverage.toFixed(1)}% below 80% threshold`); process.exit(1); }
Beta Testing
Beta Testing Process
Phase 1: Alpha Testing (Internal)
# Select 2-3 internal test projects alpha_test_projects: - test-agent-mesh # Complex microservices - test-simple-api # Simple REST API - test-static-site # Static site timeline: 1 week criteria: - All pipelines pass - No regressions - Performance acceptable
Phase 2: Beta Testing (Early Adopters)
# Select 5-10 early adopter projects beta_test_projects: - agent-mesh # Production service - documentation-site # Low risk - internal-tools # Internal users - experimental-api # Experimental - legacy-migration # Edge case timeline: 2 weeks criteria: - 90% of pipelines pass - <5% rollback rate - Positive user feedback - Migration guide validated
Beta Feedback Template
# Beta Testing Feedback: Component v3.0.0-beta.1 ## Project Information - **Project:** agent-mesh - **Team:** Platform Team - **Tester:** @alice - **Test Date:** 2026-01-15 ## Test Scenario Tested k8s-deploy component with blue-green deployment strategy. ## Results - [x] Pipeline succeeded - [ ] No issues encountered - [x] Found 2 minor issues ## Issues Found ### Issue 1: Timeout Too Short **Severity:** Medium **Description:** Blue-green deployment times out after 5 minutes, needs 10. **Workaround:** Manually set timeout in project config. **Recommendation:** Add `timeout` input to component. ### Issue 2: Unclear Error Message **Severity:** Low **Description:** When namespace doesn't exist, error message is cryptic. **Recommendation:** Add better error handling. ## Migration Experience - **Difficulty:** Easy - **Time:** 30 minutes - **Documentation Quality:** 4/5 - **Issues:** None ## Overall Feedback Component works well. Two minor issues need addressing. Migration guide was clear. ## Recommendation - [ ] Approve for stable release - [x] Approve after fixes - [ ] Needs more work
Collecting Beta Metrics
// beta-metrics.ts interface BetaMetrics { project: string; pipeline_success_rate: number; avg_pipeline_duration: number; errors_encountered: number; rollback_required: boolean; user_satisfaction: number; // 1-5 } async function collectBetaMetrics(projects: string[]): Promise<BetaMetrics[]> { return Promise.all( projects.map(async project => { const pipelines = await fetchPipelines(project, '2 weeks'); const issues = await fetchIssues(project, 'label:component-beta'); return { project, pipeline_success_rate: calculateSuccessRate(pipelines), avg_pipeline_duration: calculateAvgDuration(pipelines), errors_encountered: issues.length, rollback_required: checkRollback(project), user_satisfaction: await getUserRating(project) }; }) ); } // Generate beta report const metrics = await collectBetaMetrics(BETA_PROJECTS); const summary = { total_projects: metrics.length, avg_success_rate: average(metrics.map(m => m.pipeline_success_rate)), projects_with_issues: metrics.filter(m => m.errors_encountered > 0).length, rollbacks: metrics.filter(m => m.rollback_required).length, avg_satisfaction: average(metrics.map(m => m.user_satisfaction)) }; console.log('Beta Testing Summary:', summary); // Decide if ready for stable release if (summary.avg_success_rate > 90 && summary.avg_satisfaction >= 4.0) { console.log(' Ready for stable release'); } else { console.log(' Not ready - needs more work'); }
Regression Testing
Automated Regression Suite
# .gitlab-ci.yml regression-tests: stage: test rules: - if: $CI_COMMIT_TAG # Run on every tag parallel: matrix: - COMPONENT: [docker-build, k8s-deploy, security-scan, node-pipeline] TEST_SUITE: [basic, advanced, edge-cases] script: - run-regression-suite.sh $COMPONENT $TEST_SUITE artifacts: reports: junit: regression-${COMPONENT}-${TEST_SUITE}.xml
Regression Test Suite Structure
tests/
regression/
docker-build/
basic/
simple-build.yml
with-cache.yml
advanced/
multi-stage.yml
build-args.yml
edge-cases/
no-dockerfile.yml
large-context.yml
k8s-deploy/
basic/
advanced/
edge-cases/
Version Compatibility Testing
# Test new version with multiple GitLab versions compatibility-test: stage: test parallel: matrix: - GITLAB_VERSION: [15.11, 16.0, 16.5, 17.0] image: gitlab/gitlab-ce:${GITLAB_VERSION} script: - test-component-compatibility.sh
Performance Testing
Pipeline Duration Testing
# tests/performance-test.yml performance-test: stage: test script: - | echo "Testing pipeline duration..." START_TIME=$(date +%s) # Run component pipeline trigger-component-pipeline END_TIME=$(date +%s) DURATION=$((END_TIME - START_TIME)) echo "Pipeline duration: ${DURATION}s" # Fail if too slow if [ $DURATION -gt 300 ]; then echo "ERROR: Pipeline took ${DURATION}s (max 300s)" exit 1 fi
Resource Usage Testing
# Monitor resource usage during component execution resource-test: stage: test script: - | # Monitor memory usage watch-memory.sh & MONITOR_PID=$! # Run component run-component # Check peak memory usage kill $MONITOR_PID PEAK_MEMORY=$(cat /tmp/peak-memory) if [ $PEAK_MEMORY -gt 2048 ]; then echo "ERROR: Peak memory ${PEAK_MEMORY}MB exceeds 2GB limit" exit 1 fi
Load Testing
# Test component with high concurrency load-test: stage: test parallel: 10 # 10 concurrent pipelines script: - stress-test-component.sh
Continuous Testing
Nightly Test Suite
# .gitlab-ci.yml nightly-comprehensive-test: stage: test rules: - if: $CI_PIPELINE_SOURCE == "schedule" # Scheduled nightly script: - run-comprehensive-test-suite.sh artifacts: reports: junit: nightly-test-results.xml paths: - test-results/ when: always
Component Health Monitoring
// monitor-component-health.ts interface ComponentHealth { component: string; success_rate_24h: number; avg_duration_24h: number; error_count_24h: number; users_affected: number; } async function monitorComponentHealth(): Promise<ComponentHealth[]> { const components = ['docker-build', 'k8s-deploy', 'security-scan']; const health = []; for (const component of components) { const pipelines = await fetchPipelinesUsingComponent(component, '24h'); health.push({ component, success_rate_24h: calculateSuccessRate(pipelines), avg_duration_24h: calculateAvgDuration(pipelines), error_count_24h: pipelines.filter(p => p.status === 'failed').length, users_affected: new Set(pipelines.map(p => p.user)).size }); } // Alert on degradation for (const h of health) { if (h.success_rate_24h < 95) { await sendAlert( `Component ${h.component} success rate dropped to ${h.success_rate_24h}%` ); } } return health; } // Run every hour setInterval(monitorComponentHealth, 60 * 60 * 1000);
Test Automation
Automated Test Generation
// generate-tests.ts function generateComponentTests(component: Component) { const inputs = component.spec.inputs; // Generate test for each input for (const [name, input] of Object.entries(inputs)) { // Test with default value generateTest({ name: `test-${component.name}-default-${name}`, input_name: name, input_value: input.default }); // Test with each option if (input.options) { for (const option of input.options) { generateTest({ name: `test-${component.name}-${name}-${option}`, input_name: name, input_value: option }); } } // Test with invalid value (should fail) generateTest({ name: `test-${component.name}-invalid-${name}`, input_name: name, input_value: 'INVALID_VALUE', expect_failure: true }); } }
Best Practices
DO:
- Test every component before release
- Use test matrices for input combinations
- Create dedicated test projects for integration tests
- Run regression tests on every tag
- Collect beta feedback from real users
- Monitor component health continuously
- Automate everything possible
- Track test coverage and aim for 80%+
DON'T:
- Don't skip tests to save time
- Don't test only happy path - test edge cases
- Don't ignore beta feedback - users find real issues
- Don't forget performance tests - watch duration and resources
- Don't test in isolation only - integration tests are crucial
- Don't release without regression tests
- Don't ignore flaky tests - fix or remove them
- Don't forget to test rollback scenarios
Testing Checklist
Before releasing a new component version:
- YAML syntax validation passes
- Schema validation passes
- Unit tests pass (all input combinations)
- Integration tests pass (with test projects)
- Regression tests pass (no breaking changes)
- Performance tests pass (duration, memory)
- Beta testing complete (5+ projects)
- User feedback collected and addressed
- Migration guide tested by beta users
- Documentation updated
- Test coverage 80%
- No blocking issues remain
Next Steps
- Creating Components: Build testable components
- Patterns: Use proven patterns
- Governance: Release approval process