Testing Strategy

Testing Strategy #

Menguji infrastruktur berbasis kode (Infrastructure as Code) memiliki tantangan yang sangat berbeda dibandingkan dengan pengujian aplikasi perangkat lunak konvensional. Kita tidak sedang menguji fungsi pemrograman deterministik yang menerima input murni dan mengembalikan output murni. Sebaliknya, kita menguji efek samping (side effects) pada sistem operasi dan server nyata: Apakah port jaringan terbuka? Apakah layanan database berjalan dan mampu menerima koneksi? Apakah konfigurasi server aman dari celah eksploitasi? Menjalankan deployment tanpa strategi pengujian yang matang adalah tindakan spekulatif yang berisiko tinggi. Kita memerlukan pendekatan pengujian terstruktur untuk memastikan kestabilan sistem tanpa membebani tim dengan proses pemeliharaan test suite yang rumit.


1. Memahami Piramida Testing Ansible #

Untuk membangun strategi pengujian yang efisien, kita harus mengadopsi model piramida pengujian yang disesuaikan untuk kebutuhan Ansible. Prinsip utamanya adalah memperbanyak pengujian yang cepat dan murah di bagian bawah, serta membatasi pengujian yang lambat dan mahal di bagian atas.

Berikut adalah visualisasi struktur piramida pengujian Ansible yang kita terapkan:

flowchart TD
    Integration["Integration<br/>(Sedikit, lambat, biaya tinggi, sangat mirip production)"]
    Molecule["Molecule Test<br/>(Pengujian fungsional per role di container/VM lokal)"]
    Syntax["Syntax Check<br/>(Pengujian validasi struktur file YAML playbook)"]
    Lint["ansible-lint<br/>(Banyak, sangat cepat (detik), dijalankan per commit)"]

    Integration --- Molecule
    Molecule --- Syntax
    Syntax --- Lint

Dengan struktur ini, kita menyaring sebagian besar kesalahan penulisan, ketidakpatuhan gaya, dan kesalahan logika dasar di lapisan terbawah (ansible-lint & syntax-check) dalam hitungan detik. Kita hanya menjalankan pengujian yang memakan waktu lama (seperti membuat mesin virtual di cloud) ketika kode kita telah terbukti lolos dari seleksi awal tersebut.


2. Pengecekan Sintaksis dan Simulasi Check Mode #

Lapisan pengujian pertama setelah linting adalah verifikasi sintaksis dan simulasi eksekusi tanpa mengubah state sistem asli (dry-run).

A. Syntax Checking #

Kita dapat menggunakan utilitas bawaan Ansible untuk memindai seluruh playbook dan memastikan tidak ada kesalahan indentasi atau pemanggilan parameter modul yang tidak valid:

# Melakukan syntax check pada semua playbook di direktori playbooks/
for playbook in playbooks/*.yml; do
  ansible-playbook "$playbook" --syntax-check
done

B. Check Mode dan Diff Mode #

Sebelum kita menerapkan perubahan ke server production, kita wajib melakukan simulasi perubahan menggunakan flag --check (untuk dry-run) dan --diff (untuk membandingkan perubahan file konfigurasi secara visual).

# Menjalankan simulasi deployment ke environment staging
ansible-playbook -i inventory/staging/ playbooks/site.yml --check --diff

Batasan Penting Check Mode: #

Kita harus menyadari bahwa --check memiliki keterbatasan. Jika playbook kita memiliki task yang bergantung pada output dari task sebelumnya yang seharusnya dibuat (misal, mengunduh file biner lalu mengekstraknya), task kedua akan gagal dalam check mode karena file biner tersebut belum benar-benar ada di sistem. Untuk mengatasinya, kita harus mengatur opsi pengkondisian secara tepat pada task yang bersifat dinamis.


3. Pengujian Mandiri Role Menggunakan Molecule #

Molecule adalah standar de-facto untuk melakukan unit testing pada role Ansible. Molecule memungkinkan kita untuk secara otomatis membuat lingkungan pengujian terisolasi (menggunakan Docker atau VM), menerapkan role kita di sana, memverifikasi bahwa hasilnya sesuai harapan, lalu menghancurkan lingkungan pengujian tersebut.

Berikut adalah siklus hidup (lifecycle) pengujian otomatis yang dijalankan oleh Molecule:

flowchart TD
    A["Mulai Uji Molecule"] --> B["Create (Buat Container/VM)"]
    B --> C["Prepare (Setup Awal Host)"]
    C --> D["Converge (Jalankan Playbook Pertama Kali)"]
    D --> E["Idempotence (Jalankan Playbook Kedua Kali)"]
    E --> F{"Apakah ada changed?"}
    F --|"Ya"| G["Gagal: Bug Idempotensi"]
    F --|"Tidak"| H["Verify (Jalankan Pengujian Assertions)"]
    H --> I{"Apakah semua assert lolos?"}
    I --|"Tidak"| J["Gagal: Verifikasi Sistem Salah"]
    I --|"Ya"| K["Destroy (Hapus Container/VM)"]
    K --> L["Sukses: Role Valid"]

A. Struktur Berkas Molecule #

Di dalam folder role (misal roles/postgresql/), kita membuat struktur folder molecule berikut:

roles/postgresql/
├── tasks/
│   └── main.yml
└── molecule/
    └── default/
        ├── molecule.yml    # Konfigurasi driver, platform, dan verifier
        ├── converge.yml    # Playbook untuk menerapkan role
        └── verify.yml      # Playbook untuk memverifikasi efek samping role

B. File Konfigurasi molecule.yml #

Berikut konfigurasi molecule.yml standar kita menggunakan driver Docker:

# roles/postgresql/molecule/default/molecule.yml
---
dependency:
  name: galaxy
driver:
  name: docker
platforms:
  # Menggunakan container Ubuntu yang memiliki systemd aktif
  - name: test-postgres-ubuntu
    image: geerlingguy/docker-ubuntu2204-ansible:latest
    command: ""
    volumes:
      - /sys/fs/cgroup:/sys/fs/cgroup:rw
    cgroupns_mode: host
    privileged: true
    pre_build_image: true
provisioner:
  name: ansible
  config_options:
    defaults:
      callbacks_enabled: profile_tasks
verifier:
  name: ansible

C. File Verifikasi verify.yml #

Jangan mengandalkan asumsi bahwa “jika playbook selesai tanpa error, berarti sistem sudah benar”. Kita wajib melakukan pengujian konkret (assertions) pada target server untuk memverifikasi status akhir sistem di dalam file verify.yml.

# roles/postgresql/molecule/default/verify.yml
# Memverifikasi efek samping instalasi PostgreSQL secara mendalam
---
- name: Verifikasi Instalasi dan Konfigurasi PostgreSQL
  hosts: all
  gather_facts: true

  tasks:
    - name: Pastikan binary postgresql sudah terinstal di sistem
      command: psql --version
      register: pg_version_check
      changed_when: false
      failed_when: pg_version_check.rc != 0

    - name: Ambil status service postgresql dari systemd
      systemd:
        name: postgresql
      register: pg_service_status

    - name: Verifikasi service postgresql aktif dan enabled di systemd
      assert:
        that:
          - pg_service_status.status.ActiveState == 'active'
          - pg_service_status.status.UnitFileState == 'enabled'
        fail_msg: "PostgreSQL tidak aktif atau tidak dikonfigurasi auto-start!"

    - name: Cek apakah port database 5432 sudah terbuka dan mendengarkan koneksi
      wait_for:
        port: 5432
        timeout: 5
        state: started

    - name: Uji otentikasi dan koneksi query database secara langsung
      command: >
        psql -U postgres -c "SELECT 1;"        
      become: true
      become_user: postgres
      register: pg_query_test
      changed_when: false
      failed_when:
        - pg_query_test.rc != 0
        - "'1' not in pg_query_test.stdout"

Kita menjalankan pengujian ini di lokal developer dengan mengetikkan perintah:

# Menjalankan seluruh alur siklus Molecule dari awal hingga akhir
molecule test

4. Menjamin Idempotensi Kode #

Idempotensi adalah prinsip fundamental dalam Ansible: menjalankan playbook berkali-kali pada server yang sama harus menghasilkan kondisi sistem yang identik, dan tidak boleh memicu perubahan baru setelah jalankan pertama selesai (run kedua wajib melaporkan status changed=0).

Mengapa Idempotensi Sering Rusak? #

Penyebab utama rusaknya idempotensi adalah penggunaan modul command atau shell secara sembarangan, karena modul tersebut selalu melaporkan status changed secara default meskipun tidak ada perubahan nyata pada server.

Cara Memperbaiki Bug Idempotensi: #

# ANTI-PATTERN: Menjalankan eksekusi script bash tanpa batasan idempotensi
- name: Unduh dan jalankan script inisialisasi database
  shell: /usr/local/bin/init-db.sh
# BENAR: Menentukan batasan changed_when atau creates agar task bersifat idempoten
- name: Jalankan script inisialisasi database secara aman dan idempoten
  shell: /usr/local/bin/init-db.sh
  register: init_db_result
  # Task hanya berstatus 'changed' jika output script menghasilkan teks 'database created'
  changed_when: "'database created' in init_db_result.stdout"
  # Task dilewati sepenuhnya jika file lock /var/lib/mysql/db_created sudah ada
  creates: /var/lib/mysql/db_created

Molecule secara otomatis mendeteksi masalah ini selama proses molecule test. Jika pada iterasi kedua (idempotence phase) ada task yang melaporkan status changed, Molecule akan menghentikan proses pengujian dan melaporkan kegagalan pengujian idempotensi.


5. Strategi Pengujian Integrasi (Integration Testing) #

Setelah memverifikasi setiap role secara mandiri di tingkat Molecule, kita memerlukan pengujian integrasi (integration testing) untuk memastikan bahwa seluruh rangkaian role (load balancer, app server, dan database) dapat bekerja sama dengan baik membentuk satu stack aplikasi yang fungsional.

Kita membuat file pengujian integrasi terpisah yang mensimulasikan deployment aplikasi ujung-ke-ujung (end-to-end).

Contoh file pengujian integrasi tests/integration/test-stack.yml:

# tests/integration/test-stack.yml
# Menjamin load balancer, web app, dan database terintegrasi sempurna
---
- name: Skenario Pengujian Integrasi Stack Aplikasi
  hosts: test_servers
  become: true

  roles:
    - role: common
    - role: postgresql
    - role: nodejs
    - role: myapp
    - role: nginx

  post_tasks:
    - name: Tunggu waktu inisialisasi port HTTP aplikasi di Nginx
      wait_for:
        port: 80
        timeout: 15
        state: started

    - name: Verifikasi integrasi web server dan aplikasi (HTTP Status Code)
      uri:
        url: "http://localhost/"
        status_code: 200
        return_content: true
      register: http_response
      failed_when: "'Welcome to MyApp' not in http_response.content"

    - name: Verifikasi aplikasi sukses berkomunikasi dengan database
      uri:
        url: "http://localhost/api/users"
        status_code: 200
        return_content: true
      register: api_response
      failed_when: 
        - api_response.status != 200
        - "'db_connection_status: connected' not in api_response.content"

    - name: Pastikan log sistem tidak mengandung pesan error kritis
      shell: "grep -i 'error' /var/log/myapp/error.log || true"
      register: log_audit
      changed_when: false
      failed_when: log_audit.stdout_lines | length > 5

Pengujian integrasi ini memastikan bahwa konfigurasi nginx kita benar-benar mengarah ke port aplikasi nodejs yang tepat, dan aplikasi nodejs tersebut berhasil terotentikasi ke database postgresql yang telah disiapkan oleh role database.


6. Memilih Driver untuk Test Environment (Docker vs Vagrant vs Cloud) #

Memilih platform tempat kita menjalankan pengujian otomatis adalah keputusan krusial. Kita harus menyeimbangkan antara kecepatan eksekusi dan tingkat kemiripan dengan production.

Berikut adalah tabel komparasi detail untuk membantu kita memilih driver pengujian yang tepat:

Aspek Evaluasi Driver: Docker Driver: Vagrant (VirtualBox) Driver: Cloud (EC2 / OpenStack)
Kecepatan Sangat Cepat (hitungan detik) Lambat (beberapa menit) Sangat Lambat (menit hingga puluh menit)
Penggunaan Resource Sangat Ringan Sangat Berat Ringan di lokal (berat di tagihan cloud)
Dukungan Systemd Terbatas (butuh konfigurasi khusus) Penuh (VM murni) Penuh (VM murni)
Kemiripan OS Menengah (kernel berbagi dengan host) Sangat Tinggi 100% Identik dengan production
Use Case Utama Unit testing role sehari-hari & CI Integration testing multi-node lokal Validasi final sebelum rilis major

Rekomendasi Alur Kita: #

  1. Gunakan Docker di komputer developer lokal untuk menulis task baru dan menguji idempotensi karena feedback loop-nya sangat instan.
  2. Gunakan Vagrant/VM jika role kita melakukan manipulasi kernel sistem operasi, memodifikasi tabel partisi disk, atau mengonfigurasi interface jaringan yang kompleks.
  3. Gunakan Cloud Driver (EC2/GCP) pada pipeline malam hari (nightly build) untuk menguji skenario upgrade server sebelum melakukan pemeliharaan besar-besaran.

7. Menentukan Apa yang Harus dan Tidak Boleh Di-Test #

Menulis pengujian untuk setiap parameter kecil akan membuat tim kita kelelahan menjaga (maintain) test suite setiap kali ada pembaruan konfigurasi minor. Kita harus bersikap pragmatis dalam menentukan fokus pengujian.

YANG WAJIB KITA UJI:
  ✓ Ketersediaan Layanan: Apakah port terbuka dan merespons query/request dengan benar.
  ✓ Fungsionalitas End-to-End: Apakah nginx sukses melakukan reverse-proxy ke socket aplikasi backend.
  ✓ Kebenaran Sintaks Konfigurasi: Apakah file konfigurasi yang dihasilkan bebas dari syntax error (misal nginx -t lolos).
  ✓ Idempotensi: Apakah run kedua playbook mengembalikan status changed=0.
  ✓ Penanganan Edge Case: Apakah database tetap aman jika role dijalankan kembali pada server yang telah berisi data lama.

YANG TIDAK PERLU KITA UJI:
  ✗ Logika Internal Modul Ansible: Jangan menguji apakah modul apt sukses menginstal nginx (itu tugas QA Ansible).
  ✗ Konsistensi Nilai Default: Jangan menguji apakah variabel default bernilai sesuai defaultnya jika kita tidak mengubahnya.
  ✗ Setiap Baris Text File: Jangan melakukan assert regexp untuk memverifikasi setiap baris pada file konfigurasi yang dihasilkan template.

Anti-Pattern dalam Strategi Testing #

Berikut adalah pola pengujian yang salah yang sering kali kita temui dan wajib kita hindari:

1. Membiarkan Uji Otomatis Selalu Gagal #

Membiarkan status pengujian berwarna merah (fail) di repository dan berasumsi “oh, itu memang error bawaan, abaikan saja” akan menurunkan kewaspadaan tim.

# ANTI-PATTERN: Membiarkan tes gagal di CI/CD
[Job Status: FAILED] - "Abaikan saja, server pengujian Docker memang sering bermasalah dengan cron job."
# BENAR: Selesaikan error tersebut atau nonaktifkan task spesifik di dalam environment test secara bersih
- name: Konfigurasikan cron job untuk backup database
  cron:
    name: "db_backup"
    minute: "0"
    hour: "2"
    job: "/usr/local/bin/backup.sh"
  # Hindari kegagalan di testing container jika cron daemon tidak terinstal
  when: ansible_virtualization_type != 'docker'

2. Melompati Verifikasi Idempotensi #

Mengabaikan pengecekan idempotensi dengan argumen “yang penting aplikasi jalan” akan menyebabkan konfigurasi server berubah-ubah secara tidak terkontrol pada setiap kali run, berisiko merusak kestabilan operasional server.


Checklist Strategi Pengujian #

Kita menggunakan daftar periksa berikut untuk memvalidasi kesiapan strategi pengujian project Ansible kita:

STANDAR PENGUJIAN:
  □ File playbook utama telah dilengkapi dengan pipeline otomatis syntax-check di repositori Git.
  □ Setiap role buatan internal kita memiliki minimal satu scenario pengujian Molecule default.
  □ Pengujian Molecule menggunakan base image OS yang sesuai dengan target sistem operasi production kita.
  □ Pengetesan Molecule mencakup pengujian idempotensi (run kedua mengembalikan changed=0).

ASSERTIONS (VERIFIKASI NYATA):
  □ Berkas verify.yml melakukan pemeriksaan port jaringan yang mendengarkan koneksi (listening port).
  □ Berkas verify.yml memverifikasi keaktifan service systemd secara eksplisit (active/enabled).
  □ Dilakukan pengujian fungsionalitas nyata (seperti eksekusi query SQL atau request HTTP) ke aplikasi.
  □ File konfigurasi yang dihasilkan dari template divalidasi kebenaran sintaksisnya (misal apachectl configtest).

MANAJEMEN PIPELINE CI/CD:
  □ Tahap linting dan syntax check berjalan secara otomatis pada setiap Pull Request.
  □ Hasil build testing dibersihkan secara otomatis (destroy) baik pada saat sukses maupun gagal.
  □ Kegagalan pada pengujian fungsionalitas memblokir proses merge ke branch utama secara otomatis.

Ringkasan #

  • Terapkan Piramida Pengujian — Kita harus mengalokasikan investasi pengujian terbesar pada linting (ansible-lint) dan syntax-check karena memberikan umpan balik (feedback) paling cepat dengan biaya terendah.
  • Wajibkan Unit Test via Molecule — Setiap role internal kita wajib memiliki skenario Molecule default untuk mengisolasi pengujian fungsional di container lokal sebelum dicoba ke server staging.
  • Jamin Status Idempotensi — Pastikan seluruh task di dalam playbook bersifat idempoten. Gunakan parameter creates atau changed_when pada modul command/shell untuk menjamin run kedua menghasilkan changed=0.
  • Fokus Pada Efek Samping Nyata — Uji sistem berdasarkan indikator fungsionalitas konkret (seperti status service aktif dan respon HTTP 200) alih-alih menguji implementasi syntax internal modul Ansible.
  • Pisahkan Strategi Pengujian Integrasi — Gunakan pengujian integrasi (integration testing) lintas role terpisah untuk memverifikasi fungsionalitas tumpukan stack aplikasi secara utuh (E2E).
  • Pilih Driver Pengujian Secara Bijak = Gunakan driver Docker untuk kecepatan iterasi pengujian harian developer, dan pilih VM (Vagrant/Cloud) untuk pengujian yang memodifikasi kernel atau network adapter OS.
  • Hindari Over-Testing — Bersikaplah pragmatis dengan menghindari pengujian berlebihan terhadap modul bawaan Ansible atau baris per baris file konfigurasi yang tidak kritis agar tidak memperberat maintenance test suite.

← Sebelumnya: Code Quality   Berikutnya: Team Workflow →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact