CapSkip ile xUnit Testlerinde CAPTCHA Nasıl Çözülür (C#)

xunit captcha - How to Solve CAPTCHAs in xUnit Tests With CapSkip (C#)

xUnit testlerinde CAPTCHA çözmek için bir assembly fixture içinde tek bir CapSkip istemcisi oluşturun ve çözümü her testin içinde yapın, çünkü bir token yalnızca bir kez işe yarar. Testin iptal token’ını SDK’ya verin; böylece bir zaman aşımı çözümü durdurur. Aynı anda süren çözümlerin sayısını CapSkip’in thread sınırının altında tutun. CapSkip kendi Windows bilgisayarınızda çalışır, bu yüzden bir gecede yüzlerce kez çözüm yapan bir test paketinin çözüm başına maliyeti yoktur. Bu xUnit CAPTCHA kurulumunun xUnit v3 ve .NET 10 üzerinde kendine özgü tuzakları vardır. dotnet test komutu, yeni davranışı açmak için eklemeniz gereken bir dosya ister. Test sınıfları varsayılan olarak paralel çalışır. Ve bir zaman aşımı yalnızca kodunuz iş birliği yaparsa, yani iptal token’ını kontrol edip istendiğinde durursa işe yarar. Bu rehber bunların her birini ele alır.

Neye ihtiyacınız var

  • reCAPTCHA çözücüsü açık hâlde bir Windows bilgisayarında çalışan CapSkip. İlk başlatmayı kurulum kılavuzu adım adım anlatıyor.
  • SDK olarak .NET 10. xUnit v3 test projeleri net8.0 veya üzerini ya da .NET Framework 4.7.2’yi hedefler. Aşağıdaki her şey Windows 11 üzerinde, SDK 10.0.100 ile net10.0 üzerinde ve yeniden net8.0 üzerinde, NuGet’ten xunit.v3.mtp-v2 4.0.2 ve CapSkip 1.3.0 ile çalıştırıldı. SDK 8 ve 9 test edilmedi.
  • Sahibi olduğunuz, formunda reCAPTCHA v2 onay kutusu bulunan bir staging sitesi (yayından önce denemek için kullanılan site kopyası). Örnek, bir CSRF token’ı taşıyan bir kayıt formunu gönderir. Alan adlarını formunuza uyacak şekilde değiştirin.
  • Çözücü için bir adres. Local modunda CapSkip 127.0.0.1 üzerinden yalnızca o cihaza yanıt verir. Server modunda ise ağ adresinizi veya genel IP’nizi dinler, böylece başka bir makinedeki testler onu API üzerinden çağırabilir. İkisi de şurada ayarlanır: bağlantı ayarları.

Aşağıdaki ölçümler, aynı bilgisayardaki sahte (mock) bir CapSkip ve sahte bir kayıt sayfasından, gerçek xUnit ve gerçek SDK ile alındı. xUnit davranışı ve SDK davranışı kendi siteniz için de geçerlidir. Çözüm süreleri ile kayıt sayfasının kabul ve ret kuralları ise mock’a özgüdür.

1. Adım: test projesini oluşturun

Şablon paketini kurana kadar .NET SDK’sında xUnit v3 şablonu yoktur. Aşağıdaki ilk komut bunu yapar. Şablon sonra projeyi, bir xunit.runner.json dosyasını, bir global.json dosyasını ve UnitTest1.cs adlı örnek bir testi yazar. Blok örnek testi siler, çünkü o her çalıştırmaya ikinci bir test eklerdi.

# The SDK has no xUnit v3 template until you install the template pack.
dotnet new install xunit.v3.templates
dotnet new xunit3 -n CapSkipTests -f net10.0
cd CapSkipTests
rm UnitTest1.cs          # the template's sample test; delete it
dotnet add package CapSkip

global.json dosyasını koruyun. SDK 10’da dotnet test, bu dosya olmadan bir xUnit v3 projesini reddeder ve şu mesajla durur: Testing with VSTest target is no longer supported by Microsoft.Testing.Platform on .NET 10 SDK and later. Dosya dotnet test komutunu, aşağıdaki blok yorumlarının MTP dediği yeni çalıştırıcıya, yani Microsoft.Testing.Platform’a geçirir. Şablon dosyayı proje klasörüne koyar ya da test bölümünü, üstte zaten bulunan bir global.json dosyasına ekler. dotnet test komutunu proje klasöründen çalıştırın. Komutu, dosyanın bulunmadığı bir klasörden projeye yöneltmek, örneğin dotnet test CapSkipTests, aynı hatayla başarısız olur.

{
  "test": {
    "runner": "Microsoft.Testing.Platform"
  }
}

2. Adım: bütün çalıştırma için tek bir CapSkip istemcisi

Buradan itibaren her C# dosya bloğu, dosyanın kaydedileceği adla başlar. İlki fixture’dır. CapSkip ayarlarını ortamdan okur, çünkü SDK onları kendi başına okumaz, ve tek bir istemci oluşturur. Varsayılan değerler sayesinde hiçbir kurulum olmadan çalışır. CAPSKIP_API_KEY yalnızca CapSkip’te API key validation’ı açtıysanız önemlidir.

// CapSkipFixture.cs
using CapSkip;

public sealed partial class CapSkipFixture : IAsyncLifetime
{
    // The SDK never reads CAPSKIP_* itself, so read them here.
    private static string Env(string name, string fallback) =>
        Environment.GetEnvironmentVariable(name) ?? fallback;

    private readonly string _host = Env("CAPSKIP_HOST", "127.0.0.1");
    private readonly int _port = int.Parse(Env("CAPSKIP_PORT", "8080"));

    public CapSkipClient Solver { get; }
    public bool IsUp { get; private set; }
    public string SiteUrl { get; } = Env("STAGING_URL", "https://staging.example.com");

    public CapSkipFixture() => Solver = new CapSkipClient(
        apiKey: Env("CAPSKIP_API_KEY", "capskip"), host: _host, port: _port);

    public async ValueTask InitializeAsync() =>
        IsUp = await CapSkipProbe.IsListeningAsync(_host, _port);

    public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}

Fixture ayrıca CapSkip’in çalışıp çalışmadığını öğrenir. Düz bir TCP bağlantısı, bir şeyin dinleyip dinlemediğini sorar ve hiçbir çözüm işi göndermez.

// CapSkipProbe.cs
using System.Net.Sockets;

public static class CapSkipProbe
{
    // A TCP connect asks "is anything listening?" and sends no solve job.
    public static async Task<bool> IsListeningAsync(string host, int port)
    {
        using var tcp = new TcpClient();
        using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(1));
        try
        {
            await tcp.ConnectAsync(host, port, timeout.Token);
            return true;
        }
        catch (Exception e) when (e is SocketException or OperationCanceledException)
        {
            return false;
        }
    }
}

Fixture’ı assembly üzerinde kaydedin. xUnit bunun üzerine bütün çalıştırma için tek bir örnek oluşturur. O örneği her test sınıfına, sınıfın constructor’ı aracılığıyla verir, xUnit shared context belgelerinde anlatıldığı gibi.

// AssemblyInfo.cs
// One CapSkipFixture for the whole run, handed to every test class
// through its constructor. No IClassFixture or [Collection] needed.
[assembly: AssemblyFixture(typeof(CapSkipFixture))]

Assembly fixture, diğer iki biçimden üstündür. Altı test sınıfıyla bir class fixture, her sınıf için bir kez olmak üzere altı kez oluşturuldu. Bir collection fixture bir kez oluşturuldu, ama bu sınıfları art arda çalışmaya zorlar. Her biri bir kez çözen altı sınıf onunla 23 saniyeden uzun, assembly fixture ile 5 saniyeden kısa sürdü. İstemcinin kendisini paylaşmak güvenlidir. Değiştirilebilir durum tutmaz, her istemci SDK içindeki aynı statik HttpClient’ı kullanır ve tek bir istemci üzerinden aynı anda on dört çözüm çalıştı.

3. Adım: staging siteniz için küçük bir yardımcı

Bu kısım sizindir, CapSkip’in değil. Yardımcı kayıt sayfasını açar ve sitekey’i ile CSRF token’ını ondan okur. İki deseni kendi HTML yapınıza göre ayarlayın.

// SignupSite.cs
using System.Text.RegularExpressions;

public sealed record SignupPage(string Url, string SiteKey, string Csrf);

public sealed partial class SignupSite(string baseUrl) : IDisposable
{
    // One HttpClient per test: it keeps this test's cookies and no
    // other test's.
    private readonly HttpClient _http = new();

    public async Task<SignupPage> OpenAsync(CancellationToken ct)
    {
        var url = baseUrl + "/signup";
        var html = await _http.GetStringAsync(url, ct);
        return new SignupPage(url,
            Regex.Match(html, "data-sitekey=\"([^\"]+)\"").Groups[1].Value,
            Regex.Match(html, "name=\"csrfmiddlewaretoken\" value=\"([^\"]+)\"").Groups[1].Value);
    }

    public void Dispose() => _http.Dispose();
}

Ardından formu, token g-recaptcha-response alanında olacak şekilde gönderir. HttpClient bir form gönderiminde Referer göndermez ve HTTPS üzerinden çalışan Django gibi katı bir site, Referer olmayan bir gönderimi reddeder; bu yüzden yardımcı onu ayarlar.

// SignupSite.Submit.cs
using System.Net;

public sealed partial class SignupSite
{
    public async Task<(HttpStatusCode Status, string Body)> SubmitAsync(
        SignupPage page, string token, CancellationToken ct)
    {
        using var request = new HttpRequestMessage(HttpMethod.Post, page.Url)
        {
            Content = new FormUrlEncodedContent(new Dictionary<string, string>
            {
                ["csrfmiddlewaretoken"] = page.Csrf,
                ["username"] = "ci-user",
                ["g-recaptcha-response"] = token,
            }),
        };
        request.Headers.Referrer = new Uri(page.Url); // HttpClient sends none
        using var response = await _http.SendAsync(request, ct);
        return (response.StatusCode, await response.Content.ReadAsStringAsync(ct));
    }
}

Her test kendi yardımcısını ve dolayısıyla kendi HttpClient’ını oluşturur. Bu bilerek yapılır. Çerezleri saklayan tek bir paylaşılan istemci ve altı paralel testle, her ziyarette yeni bir oturum başlatan bir site bir gönderimi kabul etti, beşini CSRF uyuşmazlığıyla reddetti. Testler birbirlerinin çerezlerinin üzerine yazdı.

4. Adım: testi yazın

xUnit CAPTCHA testi, CapSkip çalışmadığında kendini atlar. Kendi CAPTCHA’sını çözer, formu gönderir ve cevabı kontrol eder. Şu beş şeyi aklınızda tutun:

  • Fact üzerindeki Timeout milisaniye cinsindendir. Süre dolduğunda testi başarısız kılar ve ayrıca testin iptal token’ını da iptal eder.
  • İptal token’ı, RecaptchaAsync’e adlandırılmış cancellationToken argümanı olarak verilir; böylece testi iptal etmek çözümü durdurur.
  • Gövdeye yönelik assertion, durum koduna yönelik olandan önce gelir. Site formu reddettiğinde xUnit böylece yalnızca Forbidden yerine sitenin yanıtının başını gösterir. Uzun dizeleri yaklaşık 50 karakterde keser, bu yüzden uzun bir HTML yanıtı için kendi assertion mesajınızı yazın.
  • En yavaş normal çözümünüzün üstünde ve SDK’nın kendi 300 saniyelik sınırının altında bir Timeout seçin.
  • welcome yerine başarı sayfanızın gösterdiği bir metin koyun; /signup yolunu ve yardımcıdaki kullanıcı adını da formunuza uyacak şekilde değiştirin. Sabit bir kullanıcı adı ikinci gece çalıştırmasında çakışır, bu yüzden her seferinde yeni bir tane kullanın.
// SignupTests.cs
using System.Net;

public class SignupTests(CapSkipFixture capskip)
{
    [Fact(Timeout = 120_000)] // milliseconds; also cancels the test's token
    public async Task Signup_with_a_solved_captcha_is_accepted()
    {
        Assert.SkipUnless(capskip.IsUp, "CapSkip is not running");
        var ct = TestContext.Current.CancellationToken;
        using var site = new SignupSite(capskip.SiteUrl);

        var page = await site.OpenAsync(ct);
        var solved = await capskip.Solver.RecaptchaAsync(
            page.SiteKey, page.Url, cancellationToken: ct);
        var (status, body) = await site.SubmitAsync(page, solved.Code, ct);

        Assert.Contains("welcome", body); // on failure xUnit prints the site's reason
        Assert.Equal(HttpStatusCode.OK, status);
    }
}

5. Adım: çalıştırın

Değişkenleri oturum için ayarlayın ve dotnet test komutunu proje klasöründen çalıştırın. Dışarıda bıraktığınız değişkenler varsayılan değerlere döner. Başarılı geçen bir xUnit CAPTCHA çalıştırması, Passed! yazan, total 1, failed 0 ve succeeded 1 içeren bir özetle biter.

# PowerShell. The fixture reads these; unset ones fall back to the defaults.
$env:CAPSKIP_HOST    = "127.0.0.1"    # a Server mode machine: its LAN IP
$env:CAPSKIP_PORT    = "8080"
$env:CAPSKIP_API_KEY = "capskip"
$env:STAGING_URL     = "https://staging.example.com"
dotnet test

Çözümü her testin içinde yapın

Bir fixture içinde bir kez çözüp token’ı paylaşmayın. Bir reCAPTCHA token’ı yalnızca bir kez kullanılabilir. Testte, bir kez çözen bir fixture ile aynı token’ı gönderen iki test vardı: ilki kabul edildi, ikincisi site tarafından reddedildi. Hangi testin kaybettiği çalıştırma sırasına bağlıdır, bu yüzden hata yer değiştirir ve kararsız (flaky) görünür. Bir token ayrıca yaklaşık iki dakika sonra sona erer, bu yüzden geç kalan bir test tek başına çalışsa bile başarısız olur. Konunun süreyle ilgili tarafını reCAPTCHA token süresi rehberi anlatıyor.

Test başına bir çözüm, her biri birkaç saniyeye mal olur. Bir test CAPTCHA’nın kendisine ihtiyaç duymuyorsa ona farklı bir yol verin, örneğin aşağıdaki sahte istemci.

Paralel çözümleri CapSkip’in sınırının altında tutun

xUnit test sınıflarını aynı anda çalıştırır. Varsayılan, mantıksal çekirdek başına bir thread’dir ve test bilgisayarında 24 tane var. Aynı sınıfın içindeki testler art arda çalışır. Bu, çok sayıda çözüm yapan sınıfı olan bir test paketinin aynı anda çok sayıda çözüm başlattığı anlamına gelir. CapSkip’in reCAPTCHA Max. Threads ayarının varsayılanı 10’dur. Fazla işler boş bir thread için bekler ve Wait Timeout süresinden fazla bekleyen bir iş başarısız olur. İşte thread ayarlarının, her biri bir kez çözen altı sınıfa etkisi; mock her çözümü üç saniye boyunca tutuyordu.

AyarÇalışma süresiAynı anda çözüm sayısı
Varsayılan4.4 sn6
maxParallelThreads 38.1 sn3
maxParallelThreads 211.9 sn2
Parallelization kapalı23.3 sn1

On dört sınıf, mock’un 10’luk sınırına karşı çalıştığında on tanesi çözüldü, dördü mock içinde üçer saniye bekledi. En basit üst sınır, xunit.runner.json içindeki bir sayıdır. Şablon bu dosyayı zaten oluşturdu.

{
  "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
  "maxParallelThreads": 8
}

Diğer yol, fixture içinde bir gate kullanmaktır; böylece thread sayısı ne olursa olsun aynı anda yalnızca birkaç çözüm çalışır. Aşağıdaki bloğu CapSkipFixture.cs dosyasının yanına kaydedin. Ardından testin gate’i çağırmasını sağlayın. RecaptchaAsync çağrısını şununla değiştirin: var code = await capskip.SolveAsync(page.SiteKey, page.Url, ct); ve solved.Code yerine code değişkenini gönderin. Gate, assembly fixture içinde yaşamalıdır, çünkü bir class fixture içindeki gate hiçbir şeyi sınırlamaz: her sınıf kendi gate’ini alır. Gate 8’e ayarlandığında on dört çözüm, aynı anda en fazla 8’e çıktı.

// CapSkipFixture.Gate.cs
using CapSkip;

public sealed partial class CapSkipFixture
{
    // One gate for the whole run (that is why it lives in the assembly
    // fixture): at most 8 solves in flight, under CapSkip's Max. Threads.
    private readonly SemaphoreSlim _gate = new(8);

    public async Task<string> SolveAsync(string sitekey, string url, CancellationToken ct)
    {
        await _gate.WaitAsync(ct);
        try { return (await Solver.RecaptchaAsync(sitekey, url, cancellationToken: ct)).Code; }
        finally { _gate.Release(); }
    }
}

On dört sınıfla, 8’lik bir çalıştırıcı üst sınırı, 8’lik bir gate ve hiç üst sınır olmaması yaklaşık aynı sürede, 8 ile 9 saniye arasında bitti. Fark, beklemenin nerede gerçekleştiğidir. Bir üst sınır varsa bekleme sizin test sürecinizde gerçekleşir. Üst sınır yoksa CapSkip içinde gerçekleşir ve orada Wait Timeout süresine sayılır. Gerisini xUnit paralellik belgeleri anlatıyor.

Yalnızca çözüm yapan testleri art arda çalıştırmak için sınıflarını adlandırılmış tek bir collection içine koyun. Aşağıdaki özniteliği SignupTests.cs içindeki sınıfın üstüne ekleyin ve sınıf gövdesini olduğu gibi bırakın. Aynı collection içindeki sınıflar art arda çalışır; diğer collection’lar ise onlarla birlikte çalışmaya devam eder. Testte, çözüm yapan altı sınıf ile üç saniyelik başka iş yapan altı sınıf 23.3 saniye sürdü ve diğer altısı onlarla aynı anda çalıştı.

// SignupTests.cs (only the first line changes)
// Classes in the same collection run one after another; other
// collections still run in parallel with them.
[Collection("solver")]
public class SignupTests(CapSkipFixture capskip)
{
    // ... the same tests ...
}

Eski xUnit alışkanlıklarından gelen bir tuzak. CollectionBehavior üzerindeki DisableTestParallelization ve MaxParallelThreads öznitelik ayarları 4.0.2’de artık hatadır. CS0619 ile başarısız olurlar ve Parallelization özniteliğini gösterirler. Bunun yerine Xunit.v3 içindeki bu özniteliği, [assembly: Parallelization(MaxThreads = 3)] örneğindeki gibi kullanın ya da üst sınırı xunit.runner.json içinde ayarlayın.

Bir zaman aşımının çözümü durdurmasını sağlayın

Timeout değeri 5000 olan bir Fact, 20 saniye süren bir çözüme karşı 5.0 saniyede şu mesajla başarısız oldu: Test execution timed out after 5000 milliseconds. Bütün çalıştırma yaklaşık yarım saniye sonra bitti. SDK testin token’ını aldığı için iptal edilen token çağrıyı durdurdu. SDK cevap için yoklama yaparken istisna bir TaskCanceledException olur. Görevi gönderirken ise istisna, The operation was canceled mesajını taşıyan bir CapSkip NetworkException olur.

Bazen xUnit bundan sonra aynı test için ikinci bir hata bildirir: temizlik sırasında oluşan, iptal istisnasını taşıyan bir hata. Çözüm çağrısının etrafındaki bir koruma bunu ortadan kaldırır. Test dosyasının en üstüne using CapSkip; satırını ekleyin. Token’ı ve sayfayı açan satırı try’ın üstünde tutun; çözümü, gönderimi ve her iki assertion’ı onun içine koyun. Koruma, istisnayı yalnızca testin token’ı iptal edildiğinde yutar, çünkü zaman aşımı testi zaten kendi mesajıyla başarısız kılmıştır.

// SignupTests.cs (snippet: wrap the body of the test)
// When Timeout fires, the cancelled token makes the SDK throw: a
// TaskCanceledException while polling, CapSkip.NetworkException while
// submitting. Without this guard xUnit may report it as a second failure.
try
{
    var solved = await capskip.Solver.RecaptchaAsync(
        page.SiteKey, page.Url, cancellationToken: ct);
    // ... submit and assert ...
}
catch (Exception e) when (ct.IsCancellationRequested
                          && e is OperationCanceledException or CapSkipError)
{
    // The Timeout already failed this test with its own message.
}

İptal token’ını vermezseniz test yine de 5.0 saniyede başarısız olur, ama çözücü arka planda yoklamaya devam eder. İptal token’ına hiç dokunmayan bir test, başarısız olarak bildirildikten sonra formu bile gönderebilir. xUnit1051 analyzer’ı tam olarak bu konuda uyarır. İptal token’ı verilmemiş bir RecaptchaAsync çağrısında da, token verilmemiş HttpClient çağrılarında da tetiklenir.

dotnet test’in tüm çalıştırma için zaman aşımı anahtarı da aynı ölçüde iş birliğine dayanır. Token’ını yok sayan bir testi sonlandırmadı. Takılan bir çalıştırma için bir hang dump kullanın. Aşağıdaki ilk blok, bu rehberin kullandığı yeni çalıştırıcı içindir. İkincisi VSTest yolu içindir.

# A hung run. MTP: add the extension once, then ask for a mini dump.
dotnet add package Microsoft.Testing.Extensions.HangDump
dotnet test --hangdump --hangdump-timeout 10m --hangdump-type Mini

# VSTest route: built in. The default dump type is Full (about 280 MB here).
# Do not use "none": the run ends but the hung test process stays alive.
dotnet test --blame-hang-timeout 10m --blame-hang-dump-type mini

15 saniyelik bir hang zaman aşımıyla yeni çalıştırıcı çalıştırmayı 16.2 saniyede bitirdi. 2.7 MB’lık bir mini dump yazdı ve arkasında hiçbir süreç bırakmadı. VSTest yolunda none dump türü çalıştırmayı bitirir ama takılan test sürecini canlı bırakır.

SDK’nın 300 saniye olan kendi sınırı vardır: recaptchaTimeout. Yalnızca yoklamalar arasında kontrol edilir. Testte 8 saniyelik bir sınır 12.9 saniye sonra tetiklendi, varsayılan ise 5 dakika 3 saniye sonra tetiklendi. Timeout değerinizi bunun epey altında tutun.

CapSkip çalışmıyorsa testi atlayın

4. adımdaki test, fixture’ın IsUp bayrağıyla Assert.SkipUnless çağırır. CapSkip kapalıyken, atlama gerekçesiyle birlikte atlanmış bir test bildirir ve çalıştırma 0 koduyla çıkar. Her test atlandığında çalıştırıcı Zero tests ran yazdırır. CapSkip gerektiren bir CI işinde fail-skips anahtarını on değeriyle ekleyin. Atlanan bir test o zaman çalıştırmayı 2 çıkış koduyla başarısız kılar.

Koruma olmadan kapalı bir CapSkip, testi bir CapSkip NetworkException ile başarısız kılar: No connection could be made because the target machine actively refused it. Bu, Windows’ta yaklaşık 2.1 saniye sürdü. Bir öznitelik seçeneği, yani NetworkException türüyle SkipExceptions, bu istisnayı bir atlamaya çevirir. Bir çalıştırmanın ortasında duran bir CapSkip’i de gizler, bu yüzden probe daha güvenlidir.

Probe bir yazım hatasını da gizleyebilir. CAPSKIP_HOST, başında http:// olmayan yalın bir host adı ya da adrestir. http://127.0.0.1 gibi bir değer probe’un başarısız olmasına yol açar, bu yüzden testler başarısız olmak yerine atlanır. Koruma olmadan SDK şunu bildirir: No such host is known. (http:80) ve bunu söylemesi birkaç saniye sürebilir.

Bir çözüm başarısız olduğunda yeniden deneyin

xUnit’te yeniden deneme özniteliği yoktur. Bazı çözümler başarısız olur ve yeni bir deneme işe yarayabilir. SDK, mesajı ERROR_CAPTCHA_UNSOLVABLE olan bir ApiException fırlatır; ancak CapSkip’te bir Error Replacement metni ayarladıysanız istisna o metni taşır ve aşağıdaki filtre hiçbir zaman eşleşmez. Bu yardımcı yalnızca o hatayı yeniden dener ve her deneme gate’te kendi sırasını alır. Onu gate bloğunun yanına kaydedin, çünkü gate’in SolveAsync metodunu çağırır.

// CapSkipFixture.Retry.cs
using CapSkip;

public sealed partial class CapSkipFixture
{
    // xUnit has no retry attribute. Retry only "could not solve" and give
    // up after `attempts`; every attempt takes its own place at the gate.
    public async Task<string> SolveWithRetryAsync(
        string sitekey, string url, CancellationToken ct, int attempts = 3)
    {
        for (var attempt = 1; ; attempt++)
        {
            try { return await SolveAsync(sitekey, url, ct); }
            catch (ApiException e) when (attempt < attempts
                                         && e.Message.Contains("UNSOLVABLE")) { }
        }
    }
}

İlk iki işi başarısız olan bir mock ile üç denemeye izin veren bir yardımcı 5.8 saniyede geçti. İki denemeye izin veren ikinci bir çalıştırma, 4.0 saniye sonra CapSkip.ApiException: ERROR_CAPTCHA_UNSOLVABLE ile başarısız oldu. Hatanın ne anlama geldiğini görmek için ERROR_CAPTCHA_UNSOLVABLE sayfasına bakın.

TimeoutException ad çakışmasını düzeltin

Bir test projesinde örtük using’ler (implicit usings) açıktır, bu yüzden System her zaman kapsamdadır. using CapSkip satırı da varsa TimeoutException için bir catch yan tümcesi derlenmez: error CS0104: TimeoutException is an ambiguous reference between CapSkip.TimeoutException and System.TimeoutException. Bu düzeltmelerden herhangi biri işe yarar ve takma ad (alias) Assert.ThrowsAsync içinde de çalışır.

// Unambiguous.cs
using CapSkip;
using TimeoutException = CapSkip.TimeoutException; // the alias picks CapSkip's

public class Unambiguous
{
    public async Task Solve(CapSkipClient solver, string sitekey, string url)
    {
        try { await solver.RecaptchaAsync(sitekey, url); }
        catch (TimeoutException) { }          // CapSkip.TimeoutException
        catch (System.TimeoutException) { }   // or the BCL one, spelled out
        catch (CapSkipError) { }              // or the base of all four
    }
}

ValidationException yalnızca using System.ComponentModel.DataAnnotations eklerseniz çakışır; düz bir test projesinde bu yoktur.

Çözücüye ihtiyaç duymayan birim testleri

Çözücüyü çağıran kendi kodunuzun testleri CapSkip gerektirmez. SDK’nın ApiClient sınıfı public, iki istek metodu virtual ve istemcinin ApiClient özelliği ayarlanabilir. Bu test CapSkip kapalıyken geçer.

// FakeApiTests.cs
using CapSkip;

// ApiClient is public and InAsync/ResAsync are virtual: swap it out and
// the solver never touches the network.
public sealed class FakeApi() : ApiClient("fake", 0)
{
    public override Task<string> InAsync(
        IDictionary<string, object?> options, CancellationToken cancellationToken = default) =>
        Task.FromResult("OK|1");

    public override Task<string> ResAsync(
        IDictionary<string, object?> query, CancellationToken cancellationToken = default) =>
        Task.FromResult("OK|fake-token");
}

public class FakeApiTests
{
    [Fact]
    public async Task Code_comes_from_the_fake()
    {
        var solver = new CapSkipClient { ApiClient = new FakeApi() };
        var solved = await solver.RecaptchaAsync("key", "https://example.test/",
            cancellationToken: TestContext.Current.CancellationToken);
        Assert.Equal("fake-token", solved.Code);
    }
}

Bilmeye değer anahtarlar

Bunlar, yeni çalıştırıcıda bir çözücü test paketini sınarken önem taşıyan dotnet test anahtarlarıdır. Geçen testler varsayılan olarak çıktı göstermez ve tanılama anahtarı mesajlarını konsol yerine bir dosyaya yazar.

# Command-line switches that matter for a solver suite (MTP mode).
dotnet test --max-threads 4                   # parallel test collections
dotnet test --parallel none                   # one test at a time
dotnet test --fail-skips on                   # CI: a skipped test fails
dotnet test --filter-method "*Signup_with*"   # one test, wildcard at the ends
dotnet test --output Detailed                 # show passing tests' output
dotnet test --timeout 20m                     # whole run; only cooperative
dotnet test --xunit-diagnostics on --diagnostic   # diagnostics to a .diag file
dotnet test --report-xunit-trx                # a .trx file for your CI

Testleri başka bir makinedeki CapSkip’e karşı çalıştırmak

Testlerin ve çözücünün aynı bilgisayarı paylaşması gerekmez. Bağlantı ayarlarından CapSkip’i Server moduna alın; ağ adresinizi veya genel IP’nizi dinler. Ardından CAPSKIP_HOST ve CAPSKIP_PORT değerlerini ayarlayın; aynı xUnit CAPTCHA testleri değişmeden çalışır. Testte, CAPSKIP_HOST test bilgisayarının LAN adresine ayarlıyken ve mock tüm arayüzleri dinlerken test paketi geçti.

İki şeye dikkat edin. SDK, CapSkip ile düz HTTP üzerinden konuşur ve TLS seçeneği yoktur, bu yüzden API anahtarı açık metin olarak taşınır. Bu bağlantıyı özel bir ağda veya bir VPN üzerinde tutun ve API key validation’ı açın. Ayrıca CapSkip, Server modu başladığında portunu her adrese açar. Erişimi yalnızca beklediğiniz makinelerle sınırlamak için diğerleri için bir Windows Firewall engelleme kuralı ekleyin. Çözücü hâlâ sizin kendi Windows makinenizdir ve çözüm başına ücretlendirme hâlâ yoktur.

Test dosyasının tamamı

İşte fixture, probe, yardımcı ve test, hızlı bir başlangıç için tek bir dosyada. 2. ile 4. adımları atladıysanız bu tek dosya onların yerini alır. Bu dosyaları zaten kaydettiyseniz önce onları silin. Fixture sınıfı partial olduğundan gate ve yeniden deneme blokları yine uyar. Onu SignupTests.cs olarak kaydedin.

// SignupTests.cs: Steps 2 to 4 in one file
using System.Net;
using System.Net.Sockets;
using System.Text.RegularExpressions;
using CapSkip;

// One CapSkipFixture for the whole run, injected into every test class.
[assembly: AssemblyFixture(typeof(CapSkipFixture))]

public sealed partial class CapSkipFixture : IAsyncLifetime
{
    // The SDK never reads CAPSKIP_* itself, so read them here.
    private static string Env(string name, string fallback) =>
        Environment.GetEnvironmentVariable(name) ?? fallback;

    private readonly string _host = Env("CAPSKIP_HOST", "127.0.0.1");
    private readonly int _port = int.Parse(Env("CAPSKIP_PORT", "8080"));

    public CapSkipClient Solver { get; }
    public bool IsUp { get; private set; }
    public string SiteUrl { get; } = Env("STAGING_URL", "https://staging.example.com");

    public CapSkipFixture() => Solver = new CapSkipClient(
        apiKey: Env("CAPSKIP_API_KEY", "capskip"), host: _host, port: _port);

    // A TCP connect asks "is anything listening?" and sends no solve job.
    public async ValueTask InitializeAsync()
    {
        using var tcp = new TcpClient();
        using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(1));
        try
        {
            await tcp.ConnectAsync(_host, _port, timeout.Token);
            IsUp = true;
        }
        catch (Exception e) when (e is SocketException or OperationCanceledException) { }
    }

    public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}

public sealed record SignupPage(string Url, string SiteKey, string Csrf);

public sealed class SignupSite(string baseUrl) : IDisposable
{
    // One HttpClient per test: it keeps this test's cookies and no
    // other test's.
    private readonly HttpClient _http = new();

    public async Task<SignupPage> OpenAsync(CancellationToken ct)
    {
        var url = baseUrl + "/signup";
        var html = await _http.GetStringAsync(url, ct);
        return new SignupPage(url,
            Regex.Match(html, "data-sitekey=\"([^\"]+)\"").Groups[1].Value,
            Regex.Match(html, "name=\"csrfmiddlewaretoken\" value=\"([^\"]+)\"").Groups[1].Value);
    }

    public async Task<(HttpStatusCode Status, string Body)> SubmitAsync(
        SignupPage page, string token, CancellationToken ct)
    {
        using var request = new HttpRequestMessage(HttpMethod.Post, page.Url)
        {
            Content = new FormUrlEncodedContent(new Dictionary<string, string>
            {
                ["csrfmiddlewaretoken"] = page.Csrf,
                ["username"] = "ci-user",
                ["g-recaptcha-response"] = token,
            }),
        };
        request.Headers.Referrer = new Uri(page.Url); // HttpClient sends none
        using var response = await _http.SendAsync(request, ct);
        return (response.StatusCode, await response.Content.ReadAsStringAsync(ct));
    }

    public void Dispose() => _http.Dispose();
}

public class SignupTests(CapSkipFixture capskip)
{
    [Fact(Timeout = 120_000)] // milliseconds; also cancels the test's token
    public async Task Signup_with_a_solved_captcha_is_accepted()
    {
        Assert.SkipUnless(capskip.IsUp, "CapSkip is not running");
        var ct = TestContext.Current.CancellationToken;
        using var site = new SignupSite(capskip.SiteUrl);

        var page = await site.OpenAsync(ct);
        var solved = await capskip.Solver.RecaptchaAsync(
            page.SiteKey, page.Url, cancellationToken: ct);
        var (status, body) = await site.SubmitAsync(page, solved.Code, ct);

        Assert.Contains("welcome", body); // on failure xUnit prints the site's reason
        Assert.Equal(HttpStatusCode.OK, status);
    }
}

xUnit CAPTCHA testlerinde sık görülen hatalar

GördüğünüzNedenDüzeltme
Testing with VSTest target is no longer supportedSDK 10’da global.json olmayan bir xUnit v3 projesiglobal.json dosyasını ekleyin ve onun klasöründen çalıştırın
TimeoutException üzerinde CS0104System ve CapSkip ikisi de onu tanımlıyorTakma adı kullanın ya da CapSkipError yakalayın
DisableTestParallelization üzerinde CS0619Seçenek xUnit v3’te kullanımdan kaldırıldıParallelization özniteliğini ya da xunit.runner.json dosyasını kullanın
Zero tests ran, projede bir Program.cs varkenProjenin kendi giriş noktası xUnit’inkinin yerini aldıProgram.cs dosyasını silin
Test execution timed out after 5000 millisecondsÇözüm Timeout değerinden uzun sürdüTimeout değerini yükseltin ve token’ı SDK’ya verin
Skipped: CapSkip is not runningHost ve portta dinleyen hiçbir şey yokCapSkip’i başlatın, bağlantı ayarlarını kontrol edin, http:// olmayan yalın bir host kullanın
Birkaç testten yalnızca biri geçiyorTestler tek bir token’ı ya da çerezleri saklayan tek bir HttpClient’ı paylaşıyorÇözümü ve sayfayı açmayı her testin içinde yapın
İkinci test CSRF uyuşmazlığıyla başarısız oluyorParalel testler birbirlerinin çerezlerinin üzerine yazdıTest başına bir HttpClient
Derleme hatası xUnit1049async void bir testTask ya da ValueTask döndürün
Uyarı xUnit1051Token alan bir çağrıya token verilmediTestContext.Current.CancellationToken değerini verin

FAQ

Barındırılan bir CI agent’ı bu testleri çalıştırabilir mi?

Barındırılan bir agent başkasının makinesinde çalışır, bu yüzden bilgisayarınızdaki 127.0.0.1’e ulaşamaz. CapSkip’i kontrol ettiğiniz bir Windows makinesinde Server modunda çalıştırın, CAPSKIP_HOST’u ona yöneltin ve API anahtarını CI sisteminin gizli bilgi deposunda tutun. GitHub tarafından barındırılan ve Azure DevOps tarafından barındırılan agent’lar burada test edilmedi. Yalnızca LAN durumu test edildi. Agent’ın o adrese ve porta ulaşmasına da izin verilmiş olmalıdır.

Bu, xUnit v2, NUnit veya MSTest ile çalışır mı?

Bu rehber için yalnızca xUnit v3 4.0.2 çalıştırıldı. Yukarıdaki birkaç şey, Assert.SkipUnless, TestContext.Current ve assembly fixture özniteliği gibi v3 özellikleridir. CapSkip çağrıları her framework’te aynıdır. Değişen şeyler fixture, zaman aşımı ve paralellik ayarlarıdır, bu yüzden her biri için framework’ünüzün belgelerine bakın.

Eski VSTest çalıştırıcısını kullanmaya devam edebilir miyim?

Evet. mtp-v2 paketi yerine xunit.v3.mtp-off 4.0.2, xunit.runner.visualstudio 4.0.1 ve Microsoft.NET.Test.Sdk 18.10.1 kullanın ve global.json dosyasını dışarıda bırakın. dotnet test o zaman eskisi gibi çalışır. Bu yolda varsayılan konsol logger’ı atlama nedenlerini yazdırmaz ve takılma anahtarının adı blame-hang-timeout’tur.

Her derleme çözücü testlerini çalıştırmalı mı?

Muhtemelen hayır. Her biri saniyeler sürer, çalışan bir CapSkip gerektirir ve gerçek bir staging sitesine dokunur. Onları kendi test projelerine koyun; böylece birim testlerinizin sıradan bir dotnet test çalıştırması çözücüyü hiç beklemez. O projeyi bir zamanlamaya göre ya da bir sürümden önce çalıştırın.

Kısa özet

Bir xUnit CAPTCHA test paketi, bir assembly fixture içinde tek bir CapSkip istemcisine ve her testin içinde taze bir çözüme ihtiyaç duyar. Testin token’ını SDK’ya verin, böylece Fact Timeout bir çözümü durdurabilir. Paralel çözümleri bir çalıştırıcı ayarı ya da bir gate ile CapSkip’in 10 thread’inin altında tutun. SDK 10’da bir global.json kullanın. CapSkip kapalıyken testi başarısız saymak yerine atlayın ve CI’da fail-skips kullanın.

Gece çalışan ve birkaç yüz kez çözüm yapan bir test paketi çabuk birikir. Bunu kendi Windows makinenizde çalışan yerel bir captcha atlatma üzerinden yürütmek, bu çözümlerin asla bir faturaya dönüşmemesi demektir.