Jenkins ile Otomatik Test Entegrasyonu: CI/CD Pipeline Kurulumu

Yazılım geliştirme süreçlerinde en sık karşılaştığım sorunlardan biri şu: Geliştirici kodu yazıyor, “çalışıyor” diyor, production’a gidiyor ve orada patlıyor. Bu döngüyü kırmak için otomatik test entegrasyonu şart. Jenkins, bu noktada hem esnek yapısı hem de geniş plugin ekosistemiyle sysadmin’lerin ve DevOps mühendislerinin vazgeçilmezi haline geldi. Bu yazıda Jenkins üzerinde otomatik test entegrasyonunu baştan sona kuracağız; gerçek dünya senaryolarıyla, hataları ve çözümleriyle birlikte.

Jenkins Neden Test Entegrasyonu İçin Tercih Ediliyor?

Jenkins’i salt bir CI aracı olarak görmek eksik bir bakış açısı. Aslında Jenkins, pipeline’larınızı tam anlamıyla kodla yönetmenizi sağlayan bir otomasyon platformu. Test entegrasyonu açısından bakıldığında Jenkins’in öne çıkan özellikleri şunlar:

  • Jenkinsfile desteği: Pipeline’ı versiyon kontrolüne alabilirsiniz
  • Parallel stage: Testleri paralel çalıştırarak süreyi dramatik şekilde düşürürsünüz
  • Test raporlama: JUnit, TestNG, pytest çıktılarını otomatik parse eder
  • Bildirim entegrasyonları: Slack, email, Teams ile başarı/başarısızlık bildirimi
  • Artifact yönetimi: Test sonuçlarını ve coverage raporlarını saklayabilirsiniz

Büyük bir e-ticaret şirketinde çalışırken 40 dakika süren manuel test sürecini Jenkins pipeline’ıyla 8 dakikaya düşürdüğümüzü söylesem abartmış olmam. Paralel test koşumu ve doğru yapılandırılmış bir Jenkinsfile bunu mümkün kıldı.

Jenkins Kurulumu ve Temel Yapılandırma

Temiz bir Ubuntu 22.04 üzerine Jenkins kurulumunu hızlıca geçelim. Detaylı kurulum yazısı ayrıca mevcut, burada test entegrasyonuna odaklanacağız.

# Java kurulumu (Jenkins için gerekli)
sudo apt update
sudo apt install -y openjdk-17-jdk

# Jenkins repository ekleme
curl -fsSL https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key | sudo tee 
  /usr/share/keyrings/jenkins-keyring.asc > /dev/null

echo deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc] 
  https://pkg.jenkins.io/debian-stable binary/ | sudo tee 
  /etc/apt/sources.list.d/jenkins.list > /dev/null

sudo apt update
sudo apt install -y jenkins

# Servisi başlat ve enable et
sudo systemctl start jenkins
sudo systemctl enable jenkins

# İlk admin şifresini al
sudo cat /var/lib/jenkins/secrets/initialAdminPassword

Jenkins kurulduktan sonra test entegrasyonu için bazı temel plugin’leri yüklemeniz gerekiyor. Web arayüzünden Manage Jenkins > Plugins yolunu izleyerek şunları kurun:

  • JUnit Plugin: XML tabanlı test raporlarını parse eder
  • HTML Publisher Plugin: Coverage raporları için
  • Pipeline: Jenkinsfile desteği
  • Git Plugin: Repository entegrasyonu
  • Workspace Cleanup Plugin: Build sonrası temizlik

İlk Test Pipeline’ını Oluşturmak

Gerçek dünyaya en yakın senaryoyla başlayalım. Bir Python Flask uygulaması var, pytest ile yazılmış birim ve entegrasyon testleri mevcut. Önce proje yapısına bakalım:

my-flask-app/
├── app/
│   ├── __init__.py
│   ├── routes.py
│   └── models.py
├── tests/
│   ├── unit/
│   │   └── test_models.py
│   └── integration/
│       └── test_routes.py
├── requirements.txt
├── requirements-test.txt
└── Jenkinsfile

Basit bir Jenkinsfile ile başlayalım:

pipeline {
    agent any

    environment {
        PYTHON_ENV = 'venv'
        APP_ENV = 'test'
    }

    stages {
        stage('Checkout') {
            steps {
                git branch: 'main',
                    url: 'https://github.com/sirketiniz/flask-app.git',
                    credentialsId: 'github-credentials'
                echo "Commit: ${env.GIT_COMMIT}"
            }
        }

        stage('Setup Environment') {
            steps {
                sh '''
                    python3 -m venv ${PYTHON_ENV}
                    . ${PYTHON_ENV}/bin/activate
                    pip install --upgrade pip
                    pip install -r requirements.txt
                    pip install -r requirements-test.txt
                '''
            }
        }

        stage('Unit Tests') {
            steps {
                sh '''
                    . ${PYTHON_ENV}/bin/activate
                    pytest tests/unit/ 
                        --junit-xml=reports/unit-test-results.xml 
                        --cov=app 
                        --cov-report=xml:reports/coverage.xml 
                        -v
                '''
            }
            post {
                always {
                    junit 'reports/unit-test-results.xml'
                }
            }
        }

        stage('Integration Tests') {
            steps {
                sh '''
                    . ${PYTHON_ENV}/bin/activate
                    pytest tests/integration/ 
                        --junit-xml=reports/integration-test-results.xml 
                        -v
                '''
            }
            post {
                always {
                    junit 'reports/integration-test-results.xml'
                }
            }
        }
    }

    post {
        always {
            cleanWs()
        }
        failure {
            echo 'Pipeline başarısız oldu!'
        }
        success {
            echo 'Tüm testler geçti!'
        }
    }
}

Bu yapı sizi bir yere kadar götürür. Ama production ortamında bu kadarıyla yetinemezsiniz.

Paralel Test Koşumu ile Süreyi Kısaltmak

Test sayısı arttıkça sequential çalıştırma ciddi bir sorun olmaya başlar. 200 test varsa ve her biri ortalama 2 saniye sürüyorsa 400 saniye beklediniz demektir. Parallel stages bu problemi çözer:

pipeline {
    agent any

    stages {
        stage('Checkout ve Setup') {
            steps {
                checkout scm
                sh '''
                    python3 -m venv venv
                    . venv/bin/activate
                    pip install -r requirements.txt -r requirements-test.txt
                '''
            }
        }

        stage('Test Koşumu') {
            parallel {
                stage('Unit Tests') {
                    steps {
                        sh '''
                            . venv/bin/activate
                            pytest tests/unit/ 
                                --junit-xml=reports/unit-results.xml 
                                --cov=app 
                                --cov-report=xml:reports/coverage-unit.xml 
                                -n auto
                        '''
                    }
                    post {
                        always {
                            junit 'reports/unit-results.xml'
                        }
                    }
                }

                stage('Integration Tests') {
                    steps {
                        sh '''
                            . venv/bin/activate
                            pytest tests/integration/ 
                                --junit-xml=reports/integration-results.xml 
                                -v
                        '''
                    }
                    post {
                        always {
                            junit 'reports/integration-results.xml'
                        }
                    }
                }

                stage('Linting') {
                    steps {
                        sh '''
                            . venv/bin/activate
                            flake8 app/ --format=pylint 
                                --output-file=reports/flake8-report.txt || true
                            pylint app/ --output-format=parseable 
                                --reports=y > reports/pylint-report.txt || true
                        '''
                    }
                }
            }
        }
    }
}

Önemli not: Paralel stage’lerde her test grubu farklı workspace kullanmıyorsa race condition yaşayabilirsiniz. Özellikle dosyaya yazan testlerde buna dikkat edin. stash/unstash mekanizmasını ya da ayrı agent’lar kullanmayı değerlendirin.

Java/Maven Projesi İçin Test Entegrasyonu

Python dünyasından çıkıp kurumsal Java projelerine geçelim. Maven projelerinde surefire ve failsafe plugin’leri birim ve entegrasyon testlerini otomatik olarak çalıştırır:

pipeline {
    agent {
        docker {
            image 'maven:3.9-openjdk-17'
            args '-v /root/.m2:/root/.m2'
        }
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Build ve Birim Testleri') {
            steps {
                sh 'mvn clean test -Dsurefire.failIfNoSpecifiedTests=false'
            }
            post {
                always {
                    junit '**/target/surefire-reports/*.xml'
                    jacoco(
                        execPattern: '**/target/jacoco.exec',
                        classPattern: '**/target/classes',
                        sourcePattern: '**/src/main/java'
                    )
                }
            }
        }

        stage('Entegrasyon Testleri') {
            steps {
                sh 'mvn verify -DskipUnitTests=true'
            }
            post {
                always {
                    junit '**/target/failsafe-reports/*.xml'
                }
            }
        }

        stage('SonarQube Analizi') {
            environment {
                SONAR_TOKEN = credentials('sonarqube-token')
            }
            steps {
                sh '''
                    mvn sonar:sonar 
                        -Dsonar.projectKey=my-java-app 
                        -Dsonar.host.url=http://sonarqube:9000 
                        -Dsonar.login=${SONAR_TOKEN}
                '''
            }
        }

        stage('Package') {
            steps {
                sh 'mvn package -DskipTests'
                archiveArtifacts artifacts: 'target/*.jar',
                                 fingerprint: true
            }
        }
    }
}

Maven cache’ini -v /root/.m2:/root/.m2 ile volume olarak mount etmek önemli. Yoksa her build’de tüm bağımlılıkları indirirsiniz ve bu çok zaman alır.

Test Coverage Eşik Değerleri Belirleme

“Testler geçti” demek yeterli değil. Coverage oranı da önemli. Jenkins’te coverage eşik değerleri belirleyip bu değerlerin altına düşüldüğünde build’i fail edebilirsiniz:

stage('Coverage Kontrolü') {
    steps {
        sh '''
            . venv/bin/activate
            pytest tests/ 
                --cov=app 
                --cov-report=xml:reports/coverage.xml 
                --cov-report=html:reports/htmlcov 
                --cov-fail-under=80
        '''
    }
    post {
        always {
            publishHTML([
                allowMissing: false,
                alwaysLinkToLastBuild: true,
                keepAll: true,
                reportDir: 'reports/htmlcov',
                reportFiles: 'index.html',
                reportName: 'Coverage Report',
                reportTitles: 'Test Coverage'
            ])
            cobertura(
                coberturaReportFile: 'reports/coverage.xml',
                onlyStable: false,
                failUnstable: false,
                failNoReports: true,
                lineCoverageTargets: '80, 75, 70',
                methodCoverageTargets: '80, 75, 70',
                classCoverageTargets: '80, 75, 70'
            )
        }
    }
}

--cov-fail-under=80 parametresi coverage %80’nin altına düştüğünde pytest’in non-zero exit code döndürmesini sağlar. Jenkins bu çıkışı yakalayıp build’i fail eder. Cobertura plugin ise HTML raporunu görsel olarak sunar ve trend grafiği oluşturur.

Ortam Bazlı Test Stratejisi

Gerçek hayatta genellikle şu senaryo karşınıza çıkar: geliştirici PR açtı, feature branch’e push yaptı, ya da main’e merge oldu. Her senaryoda farklı test setlerini çalıştırmak istersiniz:

pipeline {
    agent any

    environment {
        IS_MAIN_BRANCH = "${env.BRANCH_NAME == 'main' ? 'true' : 'false'}"
        IS_HOTFIX = "${env.BRANCH_NAME?.startsWith('hotfix/') ? 'true' : 'false'}"
    }

    stages {
        stage('Hızlı Kontroller') {
            steps {
                sh '''
                    . venv/bin/activate
                    # Sadece syntax ve linting - her branch için hızlı geri bildirim
                    flake8 app/ --max-line-length=120
                    python -m py_compile app/*.py
                '''
            }
        }

        stage('Birim Testleri') {
            steps {
                sh '''
                    . venv/bin/activate
                    pytest tests/unit/ 
                        --junit-xml=reports/unit-results.xml 
                        -x -q
                '''
            }
            post {
                always {
                    junit 'reports/unit-results.xml'
                }
            }
        }

        stage('Entegrasyon Testleri') {
            when {
                anyOf {
                    branch 'main'
                    branch 'develop'
                    expression { return env.IS_HOTFIX == 'true' }
                }
            }
            steps {
                sh '''
                    . venv/bin/activate
                    pytest tests/integration/ 
                        --junit-xml=reports/integration-results.xml 
                        -v
                '''
            }
        }

        stage('E2E Testleri') {
            when {
                branch 'main'
            }
            steps {
                sh '''
                    . venv/bin/activate
                    pytest tests/e2e/ 
                        --junit-xml=reports/e2e-results.xml 
                        --base-url=http://staging.sirket.com 
                        -v --timeout=120
                '''
            }
        }
    }
}

Bu stratejiyle feature branch’lerde sadece unit testler koşar (hızlı geri bildirim), develop ve main’de entegrasyon testleri de devreye girer, main’e merge sonrası ise E2E testler tetiklenir.

Slack Bildirimleri ile Test Sonuçlarını Raporlamak

Test başarısızlıklarını email yerine Slack’e göndermeyi tercih ediyorum. Ekip iletişimi açısından çok daha etkin:

pipeline {
    agent any

    stages {
        // ... test stage'leri ...
    }

    post {
        success {
            slackSend(
                channel: '#ci-cd',
                color: 'good',
                message: """
*Build Başarılı* :white_check_mark:
*Proje*: ${env.JOB_NAME}
*Branch*: ${env.BRANCH_NAME}
*Build No*: #${env.BUILD_NUMBER}
*Commit*: ${env.GIT_COMMIT?.take(8)}
*Süre*: ${currentBuild.durationString}
<${env.BUILD_URL}|Build Detayları>
                """
            )
        }

        failure {
            slackSend(
                channel: '#ci-cd',
                color: 'danger',
                message: """
*Build Başarısız* :x:
*Proje*: ${env.JOB_NAME}
*Branch*: ${env.BRANCH_NAME}
*Build No*: #${env.BUILD_NUMBER}
*Commit*: ${env.GIT_COMMIT?.take(8)}
*Açıklama*: Test başarısızlığı veya build hatası
<${env.BUILD_URL}console|Hata Logları>
                """
            )
        }

        unstable {
            slackSend(
                channel: '#ci-cd',
                color: 'warning',
                message: """
*Build Kararsız* :warning:
*Proje*: ${env.JOB_NAME}
*Test Sonuçları*: Bazı testler başarısız
<${env.BUILD_URL}testReport|Test Raporu>
                """
            )
        }
    }
}

Unstable durumu önemli. junit step, test başarısızlıklarını build’i fail etmek yerine unstable olarak işaretler. Bu sayede diğer stage’ler devam edebilir ve tüm test sonuçlarını görebilirsiniz. Bilerek başarısız olan testleri takip etmek için bu çok işe yarıyor.

Sık Karşılaşılan Sorunlar ve Çözümleri

Jenkins test entegrasyonunda karşılaşacağınız bazı yaygın problemler var. Bunlarla yüzleşmeden geçemezsiniz:

Test raporları bulunamıyor hatası

Jenkins No test report files were found diyorsa büyük ihtimalle path yanlış ya da testler çalışmadan önce hata oluştu. Şunu kontrol edin:

# Workspace'de hangi dosyalar oluştu?
find ${WORKSPACE} -name "*.xml" -path "*/reports/*"

# Dizin yapısını göster
ls -la ${WORKSPACE}/reports/ 2>/dev/null || echo "reports dizini yok"

Testler lokal çalışıyor ama Jenkins’te çalışmıyor

Bu klasik problem genellikle şu sebeplerden kaynaklanır:

  • Ortam değişkenleri eksik: .env dosyanız var mı?
  • PATH farklı: Jenkins user’ı farklı bir shell kullanıyor olabilir
  • Dosya izinleri: Jenkins’in yazma hakkı olmayan dizinlere yazmaya çalışıyor
# Jenkins user'ı olarak çalışan shell'i debug edin
sudo -u jenkins bash -c 'echo $PATH && python3 --version && which pytest'

Paralel testlerde port çakışması

Entegrasyon testleri genellikle bir port açar. Paralel çalıştırırsanız çakışma olur. Çözüm: testlerde dinamik port kullanın ya da her paralel stage’i ayrı Docker container’da çalıştırın:

stage('Entegrasyon Testleri') {
    parallel {
        stage('Test Grubu A') {
            agent {
                docker {
                    image 'python:3.11-slim'
                    // Her container izole edilmiş network'e sahip
                }
            }
            steps {
                sh 'pytest tests/integration/group_a/ -v'
            }
        }
        stage('Test Grubu B') {
            agent {
                docker {
                    image 'python:3.11-slim'
                }
            }
            steps {
                sh 'pytest tests/integration/group_b/ -v'
            }
        }
    }
}

Shared Library ile Pipeline Kodunu Tekrar Kullanmak

Birden fazla projeniz varsa her projede aynı pipeline kodunu kopyalamak yerine Jenkins Shared Library kullanın. Önce vars/runTests.groovy dosyasını oluşturun:

def call(Map config = [:]) {
    def testDir = config.get('testDir', 'tests/')
    def reportDir = config.get('reportDir', 'reports')
    def coverageThreshold = config.get('coverageThreshold', 80)
    def pythonVersion = config.get('pythonVersion', '3.11')

    sh """
        python${pythonVersion} -m venv venv
        . venv/bin/activate
        pip install -r requirements.txt -r requirements-test.txt -q

        mkdir -p ${reportDir}

        pytest ${testDir} 
            --junit-xml=${reportDir}/test-results.xml 
            --cov=app 
            --cov-report=xml:${reportDir}/coverage.xml 
            --cov-fail-under=${coverageThreshold} 
            -v
    """

    junit "${reportDir}/test-results.xml"
    cobertura coberturaReportFile: "${reportDir}/coverage.xml"
}

Artık tüm projelerinizde bu library’yi şöyle kullanabilirsiniz:

@Library('jenkins-shared-library') _

pipeline {
    agent any

    stages {
        stage('Tests') {
            steps {
                runTests(
                    testDir: 'tests/',
                    coverageThreshold: 85,
                    reportDir: 'test-reports'
                )
            }
        }
    }
}

Bu yaklaşım özellikle 10+ mikroservis yönettiğinizde hayat kurtarıyor. Pipeline mantığını tek yerden güncellersiniz, tüm projelere yansır.

Test Performansını İzlemek

Testler zamanla yavaşlar. Jenkins’in test trend raporları bunu yakalamanıza yardımcı olur ama daha proaktif olmak için şu scripti build sonrası çalıştırın:

#!/bin/bash
# slow_tests.sh - 5 saniyeden uzun süren testleri raporla

THRESHOLD=5.0
REPORT_FILE="reports/unit-results.xml"

echo "=== Yavaş Testler (>${THRESHOLD}s) ==="

python3 << 'EOF'
import xml.etree.ElementTree as ET
import sys

tree = ET.parse('reports/unit-results.xml')
root = tree.getroot()

slow_tests = []
for testcase in root.iter('testcase'):
    time = float(testcase.get('time', 0))
    if time > 5.0:
        name = f"{testcase.get('classname')}.{testcase.get('name')}"
        slow_tests.append((time, name))

slow_tests.sort(reverse=True)

if slow_tests:
    print(f"{'Süre (s)':<12} {'Test Adı'}")
    print("-" * 80)
    for time, name in slow_tests[:10]:
        print(f"{time:<12.2f} {name}")
    print(f"nToplam yavaş test: {len(slow_tests)}")
else:
    print("Tüm testler 5 saniyenin altında çalıştı.")
EOF

Bu scripti post { always { ... } } bloğuna ekleyerek her build’de otomatik çalıştırabilirsiniz.

Sonuç

Jenkins ile otomatik test entegrasyonu, ilk kurulumdan sonra “set and forget” bir yapı gibi görünse de aslında sürekli iyileştirme gerektiren bir süreç. Şu prensipleri aklınızda tutun:

  • Hızlı geri bildirim önceliği: Unit testler her commit’te çalışmalı, E2E testler sadece gerektiğinde devreye girmeli
  • Pipeline as Code: Jenkinsfile her zaman repository’de yaşamalı, web arayüzünden yapılandırma yapmaktan kaçının
  • Test sonuçları arşivlenmeli: Trend analizi yapabilmek için geçmiş build’lerin test verilerini saklayın
  • Bildirimleri akıllıca kullanın: Her başarısızlık için alarm yağdırırsanız ekip bildirim körlüğü yaşar
  • Docker agent’ları değerlendirin: İzole ve tekrarlanabilir test ortamları için Docker agent’ları Jenkins node’larına büyük avantaj sağlar

Başlangıçta basit bir pipeline kurun, çalıştırın, sonra adım adım geliştirin. Paralel testler, shared library, coverage eşikleri bunların hepsi sonradan eklenebilir. Önemli olan test döngüsünü otomatize etmek ve geliştiricilere anlamlı geri bildirim vermek. Bir kez çalışır hale getirdiğinizde bu pipeline’ın değerini production’da kaç kez patlama önlediğini sayarken anlayacaksınız.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir