Kegagalan pengujian otomatis di pipeline CI/CD sering kali bukan disebabkan oleh regresi kode aplikasi, melainkan oleh faktor eksternal non-deterministik (flaky test). Salah satu sumber utama ketidakstabilan ini adalah kebocoran panggilan jaringan (outbound HTTP call) ke API pihak ketiga seperti gateway pembayaran, provider email, atau webhook eksternal.
Ketika tes unit atau integrasi mengeksekusi koneksi internet riil, suite pengujian rentan terhadap latensi jaringan, kegagalan DNS, batas laju request (rate limit), dan downtime vendor. Solusi arsitektural untuk masalah ini adalah pemblokiran total soket jaringan di level runtime pengujian, dengan tetap membuka akses untuk loopback database dan service lokal.
Penyebab Flaky Test Akibat Outbound Call
Panggilan jaringan riil di dalam test suite memicu tiga masalah utama:
- Non-deterministik: Respons jaringan pihak ketiga bergantung pada ketersediaan eksternal, bukan status basis kode lokal.
- Degradasi Performa: Request HTTP over internet memperkenalkan latensi I/O dalam hitungan ratusan milidetik per request, memperlambat siklus CI.
- Efek Samping Eksternal: Kebocoran data pengujian ke lingkungan sandbox/production penyedia API pihak ketiga yang dapat menghabiskan kuota pengujian.
Suite pengujian yang benar harus sepenuhnya terisolasi (hermetic tests). Komponen eksternal harus disimulasikan menggunakan stub atau mock, sementara koneksi jaringan riil di luar mesin host harus diputus paksa.
Metode 1: Blokir Socket Menggunakan pytest-socket
Jika proyek Django menggunakan pytest dan pytest-django, pustaka pytest-socket menyediakan mekanisme paling cepat dan minim konfigurasi untuk membatasi akses soket Python pada level standard library (socket.socket.connect).
Instalasi dan Konfigurasi
Pasang package ke environment virtual:
pip install pytest-socketKonfigurasikan pembatasan soket pada file pytest.ini atau setup.cfg:
[pytest]
DJANGO_SETTINGS_MODULE = config.settings.test
addopts =
--disable-socket
--allow-hosts=127.0.0.1,localhost,::1
Argumen --disable-socket akan melempar eksepsi SocketBlockedError setiap kali kode memanggil socket.connect(). Argumen --allow-hosts memberikan pengecualian eksplisit (whitelist) ke alamat IP loopback.
Mengizinkan Akses Socket Tertentu (Edge Case)
Jika pengujian integrasi spesifik (misalnya: live server testing atau end-to-end testing dengan Selenium) membutuhkan akses jaringan, gunakan marker pytest secara selektif:
import pytest
@pytest.mark.enable_socket
def test_third_party_integration_legacy():
# Tes ini diizinkan membuka soket jaringan riil
pass
Metode 2: Custom Django Test Runner (Native Django)
Untuk proyek yang tidak menggunakan pytest dan bergantung langsung pada manage.py test, pemblokiran soket dapat diimplementasikan via custom test runner yang mem-patch method connect pada modul standar socket.
Buat class test runner kustom pada common/runner.py:
import socket
from django.test.runner import DiscoverRunner
class SocketBlockedError(RuntimeError):
pass
def apply_socket_firewall(allowed_hosts):
original_connect = socket.socket.connect
def guarded_connect(self, address):
host = address[0]
# Resolusi tuple socket: (host, port)
if host not in allowed_hosts:
raise SocketBlockedError(
f"Outbound network call to {host} is forbidden in test suite."
)
return original_connect(self, address)
socket.socket.connect = guarded_connect
class IsolatedDiscoverRunner(DiscoverRunner):
def setup_test_environment(self, **kwargs):
super().setup_test_environment(**kwargs)
apply_socket_firewall(allowed_hosts={"127.0.0.1", "localhost", "::1"})
Daftarkan runner tersebut di settings/test.py:
TEST_RUNNER = "common.runner.IsolatedDiscoverRunner"
Konfigurasi Whitelist: PostgreSQL dan Redis Lokal
Saat soket dinonaktifkan, Django test runner tetap membutuhkan soket TCP/IP untuk berkomunikasi dengan database pengujian (PostgreSQL/MySQL) dan memory store (Redis/Memcached).
Jika database atau cache service dikonfigurasi menggunakan Unix Domain Sockets, pemblokiran TCP socket tidak akan berpengaruh. Namun, jika service menggunakan TCP host:
- Pastikan konfigurasi
DATABASES['default']['HOST']disetel ke127.0.0.1ataulocalhost. - Hindari penggunaan nama domain lokal yang memerlukan resolusi DNS upstream di luar loopback jika resolusi tersebut memicu network hit.
- Bila menggunakan container Docker terpisah di CI (misal: service container di GitHub Actions bernama
postgresatauredis), tambahkan hostname container tersebut ke parameter whitelist.
# pytest.ini jika database berada di service container Docker
addopts = --disable-socket --allow-hosts=127.0.0.1,localhost,postgres,redis
Mocking Outbound HTTP Call
Ketika soket eksternal diblokir, setiap service atau client HTTP (seperti requests, httpx, atau urllib) yang dipanggil oleh kode aplikasi harus di-mock. Pendekatan standar industri menggunakan library responses untuk client requests atau unittest.mock bawaan Python.
Contoh Implementasi dengan responses
import responses
import requests
from django.test import SimpleTestCase
def fetch_exchange_rate(currency: str) -> float:
response = requests.get(f"https://api.exchangerate.host/latest?base={currency}", timeout=5)
response.raise_for_status()
return response.json()["rates"]["IDR"]
class ExchangeRateServiceTests(SimpleTestCase):
@responses.activate
def test_fetch_exchange_rate_success(self):
# Mencegat HTTP request sebelum menyentuh standard socket layer
responses.add(
method=responses.GET,
url="https://api.exchangerate.host/latest?base=USD",
json={"rates": {"IDR": 15500.0}},
status=200,
)
rate = fetch_exchange_rate("USD")
self.assertEqual(rate, 15500.0)
Jika responses.add() tidak didefinisikan, pustaka responses akan mengabaikan URL atau melempar error koneksi. Jika responses tidak diaktifkan sama sekali, layer pytest-socket atau custom runner akan memutus eksekusi dengan SocketBlockedError.
Verifikasi pada Pipeline CI/CD
Pastikan isolasi jaringan teruji secara konsisten di server CI (misal: GitHub Actions). Sertakan satu unit test validasi yang secara eksplisit memverifikasi bahwa soket diblokir jika mencoba menghubungi domain luar.
import pytest
import urllib.request
from pytest_socket import SocketBlockedError
def test_outbound_network_is_strictly_blocked():
with pytest.raises(SocketBlockedError):
urllib.request.urlopen("https://1.1.1.1", timeout=1)
Contoh implementasi langkah pengujian pada job GitHub Actions:
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
ports:
- 5432:5432
env:
POSTGRES_DB: test_db
POSTGRES_USER: user
POSTGRES_PASSWORD: password
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install pytest-socket
- name: Run Test Suite
env:
DATABASE_URL: postgres://user:[email protected]:5432/test_db
run: |
pytest --disable-socket --allow-hosts=127.0.0.1,localhost
Pemisahan total I/O jaringan luar memastikan test suite Django berjalan deterministik, mengeliminasi false alarm pada CI, dan secara signifikan mempercepat waktu eksekusi run test.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!