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:
.envdosyanı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.
