Test ve CI

GitHub Actions: gerçek e-posta okuyan uçtan uca bir iş

Onay kodunu bir posta kutusundan okuyan kayıt testi bir dizüstü bilgisayarda çalışır, ama sonra bir CI runner'ıyla karşılaşır: bağlanacak bir secret yoktur, bir giden trafik sorusu vardır, job'a sığması gereken bir son tarih vardır ve her çalıştırma ve her yeniden denemede taze olması gereken bir posta kutusu vardır. İşte Playwright ve pytest için workflow — yalnızca bir runner'da önem taşıyan kısımlarıyla birlikte.

  • Orta düzey
  • 15 dk okuma
Mavi bir zarfı üstünde mavi bir lamba bulunan gri bir kapıya taşıyan bandı döndüren üç gri dişli

Bir runner'da ne değişir

Testin kendisi değişmez. Değişen, onun etrafındaki her şeydir ve bunların her birinin omuz silkmek yerine belirli bir yanıtı vardır:

Bağlanacak bir secret yoktur
Genel bir posta kutusunu okumak anahtar, hesap ya da başlık gerektirmez, bu yüzden posta kutusu tarafı secrets'a hiçbir şey eklemez. Job'daki tek kimlik bilgisi, uygulamanızın zaten posta göndermek için ihtiyaç duyduğu şeydir — SendGrid, Postmark, SES, hangisiyse — ve bu, teste değil uygulamanıza aittir.
Runner'ın internete ulaşması gerekir
grabmail.io'e giden HTTPS ve göndereninizin kullandığı her şeye giden trafik. GitHub'ın barındırdığı runner'lar varsayılan olarak ikisine de izin verir; bir giden trafik filtresinin arkasındaki kendi barındırdığınız bir runner'ın bir kural eklemesi gerekir.
Çalıştırmalar çakışır
İki pull request, dört shard, kararsız bir job'ın yeniden denemesi — aynı testin birkaç kopyası aynı anda posta okur. Paylaşılan bir adres, birbirinin kodlarını okumalarına izin verirdi; test başına taze bir adres ise bu tür sorunların tamamını imkansız kılar.
Zaman sayaçla ölçülür
Posta için altmış saniye bekleyen bir test sorun değildir. Kırk testin her birinde altmış saniye bekleyen bir job ise faturalanan kırk dakikalık runner demektir. Beklemelerin sınırlı olması gerekir ve paket büyüdüğünde shard'lara ayrılması gerekir.

Bu workflow'ların çalıştırdığı test kodu, Playwright rehberinden ya da Python rehberinden gelen koddur: taze bir adres, bir son tarihli bekleme, şablonunuza sabitlenmiş bir extractor. İçinde CI'a özgü hiçbir şey yoktur ve asıl mesele de budur — runner'a özgü kısımların tamamı workflow dosyasında yaşar.

Workflow, Playwright için

Tek bir job. Uygulamanızı gerçek bir giden posta göndericisiyle başlatır, yanıt vermesini bekler, paketi çalıştırır ve raporu yalnızca bir şey başarısız olduğunda saklar.

.github/workflows/e2e.yml
name: e2e

on:
  push:
    branches: [main]
  pull_request:

jobs:
  e2e:
    runs-on: ubuntu-latest
    timeout-minutes: 20                 # the whole job, comfortably above every wait inside it

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm

      - run: npm ci
      - run: npx playwright install --with-deps chromium

      # Your application, started the way it runs in staging: a REAL outbound
      # mailer. Its credentials are YOUR secret; the mailbox side needs none.
      - name: Start the application
        run: npm run start:test &
        env:
          MAILER_API_KEY: ${{ secrets.MAILER_API_KEY }}
          APP_URL: http://localhost:3000

      - name: Wait for the application
        run: npx wait-on --timeout 60000 http://localhost:3000/health

      - name: Run the suite
        run: npx playwright test
        env:
          BASE_URL: http://localhost:3000

      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 7

Üç satır ağırlığı taşır. timeout-minutes: 20, geri kalan her şeyin içine yerleştiği dış sınırdır. Uygulama gerçek posta gönderici kimlik bilgileriyle başlatılır, çünkü gerçek postayı okuyan bir test, gerçek postanın gönderilmesine ihtiyaç duyar. Ve rapor yalnızca başarısızlık durumunda, kısa bir saklama süresiyle yüklenir — geçen bir çalıştırmanın saklamaya değer hiçbir şeyi yoktur.

Workflow, pytest için

Python araç zinciriyle aynı şekil: uygulamayı başlatın, health endpoint'ini bekleyin, paketi posta son tarihinin üzerinde test başına bir zaman aşımıyla çalıştırın, başarısızlıkta JUnit raporunu saklayın.

.github/workflows/e2e.yml
name: e2e

on:
  push:
    branches: [main]
  pull_request:

jobs:
  e2e:
    runs-on: ubuntu-latest
    timeout-minutes: 20

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
          cache: pip

      - run: pip install -r requirements.txt -r requirements-test.txt

      - name: Start the application
        run: python -m app.server &
        env:
          MAILER_API_KEY: ${{ secrets.MAILER_API_KEY }}
          APP_URL: http://localhost:8000

      - name: Wait for the application
        run: |
          for i in $(seq 1 60); do
            curl -sf http://localhost:8000/health && exit 0
            sleep 1
          done
          echo "application did not come up" >&2; exit 1

      - name: Run the suite
        run: pytest tests/e2e -q --timeout=120 --junitxml=report.xml
        env:
          BASE_URL: http://localhost:8000

      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: pytest-report
          path: report.xml

--timeout=120, pytest-timeout eklentisinden gelir ve test başına üst sınırdır; yardımcı fonksiyonun içindeki posta son tarihi altmış saniyedir, bu yüzden bir mesajı bekleyip ardından biraz tarayıcı işi yapan bir test yine de sığar. Health-check döngüsü bir action olarak içeri çekilmek yerine doğrudan yazılmıştır, çünkü sekiz satırdır ve içlerinde yanlış gidebilecek hiçbir şey yoktur.

Tek ağ kuralı

Bir posta kutusunu okumak, runner'dan grabmail.io'e giden bir HTTPS isteğidir. Ağ ayak izinin tamamı budur:

  • Gelen trafik yok. Runner'a hiçbir şey bağlanmaz. Alınacak bir webhook, çalıştırılacak bir SMTP sunucusu, dışa açılacak bir port yoktur.
  • Runner'dan SMTP yok. Posta, uygulamanız tarafından sağlayıcısı üzerinden, o sağlayıcının API'si ya da SMTP endpoint'i aracılığıyla gönderilir — tıpkı production'da yaptığı gibi. Runner'ın kendisi asla SMTP konuşmaz.
  • Bir giden trafik izin listesi olan bir runner'da eklenecek tek şey grabmail.io:443'tür — artı job'ın zaten ihtiyaç duyduğu posta sağlayıcınız ve paket kayıt defteriniz.
sertleştirilmiş bir runner'da, tam olarak buna izin verin
      - uses: step-security/harden-runner@v2
        with:
          egress-policy: block
          allowed-endpoints: >
            grabmail.io:443
            api.your-mail-provider.example:443
            registry.npmjs.org:443

Paket yerelde geçiyor ama CI'da yardımcı fonksiyondan bir bağlantı hatasıyla başarısız oluyorsa, kontrol edilecek ilk şey bu kuraldır ve neredeyse her zaman yanıtın tamamı budur. Kurumsal bir ağdaki kendi barındırılan bir runner'da, HTTPS giden trafiği genellikle ana bilgisayar adına göre filtrelenir; izin verilecek ana bilgisayar adı API'ninkidir ve istek, 443 üzerinde düz bir HTTPS'tir.

Shard'lar, matrix job'lar ve yeniden denemeler

Paket shard'lara ayrılacak kadar yavaşladığında, ayırın. Playwright, --shard ile bir çalıştırmayı job'lara böler ve her test kendi posta kutusunu açtığı için, shard'ların birbirinden hiçbir şeye ihtiyacı yoktur:

dört shard, her biri ayrı bir job
  e2e:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      # ... the same steps as above, then:
      - run: npx playwright test --shard=${{ matrix.shard }}/4

Aynı özellik, bir testin aynı anda iki kez çalışmasının diğer iki yolunu da kapsar:

Aynı anda iki pull request
İki job, iki rastgele adres kümesi, çakışma yok. API'deki istemci başına üst sınır dakikada 1200 istektir; bu da saniyede bir kez yoklanan yirmi posta kutusu demektir — bir seferde bir posta kutusunu yoklayan bir job buna hiç yaklaşmaz.
Yeniden denenen bir job
Bir yeniden deneme, test gövdesini yeniden çalıştırır, bu da yeni bir adresi yeniden uydurur. Eski posta kutusu, eski mesajı hâlâ 5 gün boyunca tutar ve onu hiçbir şey okumaz — yeniden deneme onu asla görmez.
Playwright'ın kendi retries
Aynı şey, bir seviye aşağıda: her deneme fixture'ı yeniden çalıştırır. Zamandan tasarruf etmek için adresi bir beforeAll'a taşımayın; bu, tam olarak bir denemenin önceki denemenin kodunu okumasına izin veren paylaşımdır.

Job'ın içine sığan zaman aşımları

Dört saat vardır ve iç içe geçmeleri gerekir, en içteki en kısa olacak şekilde. Böyle olmadığında, başarısızlık yanlış olan tarafından bildirilir ve yanlış nedene işaret eder.

SaatNerede ayarlanırMakul bir değer
Posta son tarihiYardımcı fonksiyonun içinde (timeoutMs, timeout=)60 sn. İşlemsel posta saniyeler içinde düşer; bir dakika, yavaş bir sağlayıcı kuyruğunu kapsar.
Test zaman aşımıplaywright.config.ts / --timeout120 sn. Son tarihin, artı etrafındaki tarayıcı işinin üzerinde.
Adım zaman aşımıAdım üzerinde timeout-minutes, varsaGenellikle ayarlanmaz; job sınırı yeterlidir.
Job zaman aşımıJob üzerinde timeout-minutes20 dk. Kurulum, başlatma, paket ve yükleme için yeterli; takılı kalan bir uygulamanın bir saat faturalandırmaması için yeterince düşük.

Bozuk bir iç içe geçmenin belirtisi bellidir: Playwright'ın varsayılan otuz saniyelik test zaman aşımı içinde altmış saniye bekleyen bir test, her seferinde test hakkında bir mesajla otuz saniyede ölür ve posta hakkında hiçbir şey söylemez. Önce test zaman aşımını ayarlayın, sonra dışa doğru her şeyi.

Bir başarısızlığı okumak

Başarısız bir çalıştırma, hiçbir şeyi yeniden çalıştırmadan üç şeyden hangisinin olduğunu size söylemelidir — posta hiç gelmedi, yanlış posta geldi ya da içindeki kod yanlıştı. Dört alışkanlık bunu doğru kılar:

  1. Adresi loglayın. Yardımcı fonksiyonun başarısızlık mesajı, beklediği posta kutusunu belirtir. Doğrulama başka bir yerde olsa bile job günlüğünde bulunsun diye testin en başında bir kez daha yazdırın.
  2. Posta kutusunu elle açın. Mesajlar 5 gün boyunca kalır, bu yüzden bu sitedeki /inbox/<address>, haftanın geri kalanında runner'ın tam olarak ne gördüğünü — ya da görmediğini — gösterir. Gerçek bir posta kutusunun mocklanmış birine karşı en yararlı tek yönü budur.
  3. Başarısızlıkta raporu saklayın. Playwright'ın trace'i, postayı göndermesi gereken tıklamayı gösterir; JUnit dosyası ise hangi testin ne kadar beklediğini gösterir.
  4. Testi suçlamadan önce hizmet durumunu kontrol edin. Durum sayfası, dışarıdan her iki dakikada bir yoklanır; çalıştırma sırasında gelen posta çökmüşse, başarısızlık gerçekti ve sizden kaynaklanmıyordu.

Temizlik yapmak, isteğe bağlı olarak

Kimse silmese de her şeyin süresi 5 gün sonra doluyor, bu yüzden temizliği atlayan bir çalıştırmanın hiçbir bedeli yoktur. Çalıştırmanın okuduğunu silmek yine de bir adıma değer, çünkü bir sonraki başarısızlık o zaman gerçekten boş bir posta kutusuna karşı okunur. İdempotenttir — iki kez silmek yine de 200 yanıtı verir — bu yüzden tek başına bir build'i asla başarısız kılamaz:

her zaman çalışan bir temizlik adımı
      - name: Delete what the run read
        if: always()
        run: |
          for addr in $(cat .e2e-addresses 2>/dev/null); do
            curl -sG https://grabmail.io/api/v1/mailbox --data-urlencode "address=$addr" \
            | jq -r '.messages[].id' \
            | xargs -r -I{} curl -sX DELETE -G "https://grabmail.io/api/v1/message/{}" --data-urlencode "mailbox=$addr" -o /dev/null
          done

Onu if: always() yapın ve asla zorunlu kılmayın: başarısız olan bir temizlik, kırmızı bir build değil, günlükte bir uyarı olmalıdır.

Bitti demeden önce

  • secrets'ta posta kutusu kimlik bilgisi yok; yalnızca uygulamanızın kendi posta gönderici anahtarı.
  • grabmail.io'e giden HTTPS'e izin verilmiş, gelen hiçbir şey yok.
  • Test gövdesinde uydurulan, test başına taze bir adres — shard'lar ve yeniden denemeler altında güvenli.
  • Dört zaman aşımı iç içe: son tarih < test < adım < job.
  • Başarısızlıkta yüklenen, günlükte adresi olan rapor.
  • Her zaman çalışan, asla zorunlu olmayan bir adım olarak temizlik.

Runner'ın eklediği her şey bu kadar. Altındaki test disiplini — son tarih, taze adres, sabitlenmiş desen — bir doğrulama akışını uçtan uca test etmek rehberinde, ve tek başına ayıklama kuralları da otomatik testlerde OTP kodları rehberinde.

Sorular

Depoya bir GrabMail secret'ı eklemem gerekir mi?

Hayır. Genel alan adları anahtar, hesap ya da başlık gerektirmez, bu yüzden secrets'a eklenecek hiçbir şey yoktur. Workflow'daki tek kimlik bilgisi, uygulamanızın posta göndermek için kullandığı ve zaten çalışması için ihtiyaç duyacağı bilgidir.

Bu, özel bir depoda ya da kendi barındırdığınız bir runner'da çalışır mı?

Evet. Runner, grabmail.io'e giden HTTPS istekleri yapar ve başka hiçbir şey yapmaz; runner'ı nerede barındırdığınız önemsizdir. Bir giden trafik filtresinin arkasındaki kendi barındırdığınız bir runner'da, o ana bilgisayar adına 443 portunda izin verin.

Eşzamanlı job'lar hız sınırına takılır mı?

Pratikte hayır. Adres başına sınır, yardımcı fonksiyonun uyduğu saniyede bir okumadır ve istemci başına üst sınır dakikada 1200 istektir — tek bir runner'dan, saniyede bir kez yoklanan yirmi posta kutusu. Birden fazla runner, birden fazla istemcidir. Bir 429, Retry-After ile yanıtlanır ve yardımcı fonksiyon başarısız olmak yerine bunu uyuyarak geçirir.

Bunu bir zamanlamayla, production kaydının sentetik bir kontrolü olarak çalıştırabilir miyim?

Evet, ve bu iyi bir kullanım şeklidir: her saat taze bir adresle kayıt olan ve kodu okuyan bir on: schedule workflow'u, sağlayıcı dahil tüm production posta yolunu kanıtlar. Kayıtların kendi tarafınızda kolayca temizlenebilmesi için adres önekini tanınabilir tutun.

Ya uygulamam tek kullanımlık alan adlarını reddederse?

Sahibi olduğunuz bir alan adını hizmete yönlendirin — tek bir MX kaydı, hesap yok — ve o alan adını fixture'da kullanın. Kurulum birkaç dakika sürer ve tek bir alan adında sınırsız test hesabı, bu deseni bir pakette gösterir.

Posta kutusundaki herhangi bir şey özel mi?

Hayır. Genel bir alan adında da kendi alan adınızda da, bir adresi bilen herkes onu okuyabilir. Tek bir kullan-at kod tutan rastgele bir adres için bu önemsizdir; gerçek müşteri verisi gönderen bir staging ortamı için ise bu diskalifiye edici bir durumdur — böyle birini buraya yönlendirmeyin.

Henüz tazeyken deneyin

Bir adres tek tıkla alınır, hesap ve kart gerekmez. Bu rehberdeki her şey onunla hemen çalışır.

Tekrar hoş geldiniz

Kutularınız ve alan adlarınız tek bir yerde.