Cara Memecahkan CAPTCHA di Test xUnit dengan CapSkip (C#)

Untuk memecahkan CAPTCHA di test xUnit, buat satu klien CapSkip di sebuah assembly fixture dan lakukan pemecahan di dalam setiap test, karena token hanya berlaku sekali. Teruskan cancellation token milik test ke SDK, sehingga timeout menghentikan pemecahan. Jaga agar jumlah pemecahan yang berjalan bersamaan tetap di bawah batas thread CapSkip. CapSkip berjalan di PC Windows Anda sendiri, sehingga test suite yang memecahkan CAPTCHA ratusan kali setiap malam tidak memiliki biaya per pemecahan. Penyiapan CAPTCHA xUnit ini memiliki jebakan tersendiri pada xUnit v3 dan .NET 10. Perintah dotnet test membutuhkan file opt-in. Secara default, test class berjalan paralel. Dan timeout hanya berfungsi jika kode Anda ikut bekerja sama. Panduan ini membahas masing-masingnya.
Apa yang Anda butuhkan
- CapSkip yang berjalan di PC Windows, dengan pemecah reCAPTCHA diaktifkan. Lihat panduan penyiapan yang menjelaskan cara menjalankan CapSkip untuk pertama kali.
- SDK .NET 10. Proyek test untuk xUnit v3 menargetkan net8.0 atau lebih baru, atau .NET Framework 4.7.2. Semua yang ada di bawah dijalankan di Windows 11 dengan SDK 10.0.100, di net10.0 lalu diulang di net8.0, dengan xunit.v3.mtp-v2 4.0.2 dan CapSkip 1.3.0 dari NuGet. SDK 8 dan 9 tidak diuji.
- Situs staging milik Anda sendiri, yaitu salinan situs yang dipakai untuk pengujian sebelum rilis, dengan kotak centang reCAPTCHA v2 di dalam sebuah form. Contohnya mengirim form pendaftaran yang membawa token CSRF. Ubah nama field agar sesuai dengan situs Anda.
- Sebuah alamat untuk pemecah CAPTCHA. Dalam mode Local, CapSkip menjawab di 127.0.0.1 hanya untuk perangkat itu. Dalam mode Server, CapSkip mendengarkan di alamat jaringan atau IP publik Anda, sehingga test di mesin lain dapat memanggilnya melalui API. Keduanya diatur di pengaturan koneksi.
Pengukuran di bawah berasal dari mock CapSkip (pengganti CapSkip yang dibuat khusus untuk pengujian) dan halaman pendaftaran mock di PC yang sama, dipakai bersama xUnit dan SDK yang asli. Perilaku xUnit dan perilaku SDK juga berlaku di situs Anda sendiri. Waktu pemecahan serta aturan menerima dan menolak pada halaman pendaftaran adalah milik mock itu sendiri.
Langkah 1: buat proyek test
SDK .NET tidak memiliki template xUnit v3 sampai Anda menginstal paket template-nya. Perintah pertama di bawah melakukannya. Template itu lalu menulis proyek, file xunit.runner.json, file global.json, dan sebuah test contoh bernama UnitTest1.cs. Blok di bawah menghapus test contoh itu, karena ia akan menambahkan test kedua ke setiap eksekusi.
# 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
Pertahankan global.json. Di SDK 10, dotnet test menolak proyek xUnit v3 tanpa file itu dan berhenti dengan pesan: Testing with VSTest target is no longer supported by Microsoft.Testing.Platform on .NET 10 SDK and later. File itu mengalihkan dotnet test ke runner baru, Microsoft.Testing.Platform, yang disebut MTP di komentar blok di bawah. Template meletakkan file itu di folder proyek, atau menambahkan bagian test ke global.json yang sudah ada di atasnya. Jalankan dotnet test dari folder proyek. Mengarahkannya ke proyek dari folder yang tidak memiliki file itu, misalnya dotnet test CapSkipTests, gagal dengan error yang sama.
{
"test": {
"runner": "Microsoft.Testing.Platform"
}
}Langkah 2: satu klien CapSkip untuk seluruh eksekusi
Setiap blok file C# mulai dari sini diawali dengan nama untuk menyimpannya. Yang pertama adalah fixture. Fixture itu membaca pengaturan CapSkip dari variabel lingkungan, karena SDK tidak membacanya dengan sendirinya, lalu membuat satu klien. Nilai default membuatnya berjalan tanpa penyiapan. CAPSKIP_API_KEY hanya berpengaruh jika Anda mengaktifkan validasi API key di CapSkip.
// 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 itu juga mencari tahu apakah CapSkip sedang berjalan. Koneksi TCP biasa menanyakan apakah ada yang sedang mendengarkan, dan koneksi itu tidak mengirim job pemecahan apa pun.
// 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;
}
}
}Daftarkan fixture pada assembly. xUnit lalu membuat satu instance untuk seluruh eksekusi. Instance itu diberikan ke setiap test class melalui constructor-nya. Lihat dokumentasi shared context xUnit untuk penjelasannya.
// 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 lebih unggul daripada dua gaya lainnya. Dengan enam test class, class fixture dibuat enam kali, sekali per class. Collection fixture dibuat sekali, tetapi memaksa test class itu berjalan satu demi satu. Enam class yang masing-masing memecahkan sekali memakan lebih dari 23 detik dengan collection fixture, dan kurang dari 5 detik dengan assembly fixture. Klien itu sendiri aman dibagikan. Klien tidak menyimpan state yang bisa berubah, setiap klien memakai HttpClient statis yang sama di dalam SDK, dan empat belas pemecahan sekaligus melalui satu klien berjalan dengan baik.
Langkah 3: helper kecil untuk situs staging Anda
Bagian ini milik Anda, bukan milik CapSkip. Helper ini membuka halaman pendaftaran lalu membaca sitekey dan token CSRF dari halaman itu. Sesuaikan kedua pola dengan markup Anda.
// 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();
}Lalu helper mengirim form dengan token di field g-recaptcha-response. HttpClient tidak mengirim Referer pada form post, dan situs yang ketat seperti Django melalui HTTPS menolak kiriman tanpa Referer, jadi helper menyetelnya.
// 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));
}
}Setiap test membuat helper-nya sendiri, dan karena itu HttpClient-nya sendiri. Itu disengaja. Dengan satu klien bersama yang menyimpan cookie dan enam test paralel, situs yang memulai sesi baru pada setiap kunjungan menerima satu kiriman dan menolak lima kiriman dengan CSRF mismatch. Test saling menimpa cookie milik test lainnya.
Langkah 4: tulis test-nya
Test CAPTCHA xUnit ini men-skip dirinya sendiri saat CapSkip tidak berjalan. Test ini memecahkan CAPTCHA-nya sendiri, mengirim form, lalu memeriksa jawabannya. Ingat lima hal berikut:
- Timeout pada Fact dinyatakan dalam milidetik. Timeout itu menggagalkan test saat waktunya habis, dan juga membatalkan cancellation token milik test.
- Cancellation token diteruskan ke RecaptchaAsync sebagai argumen bernama cancellationToken, sehingga membatalkan test juga menghentikan pemecahan.
- Assertion pada body ditulis sebelum assertion pada status. Saat situs menolak form, xUnit lalu menampilkan awal balasan situs, bukan hanya Forbidden. xUnit memotong string panjang pada sekitar 50 karakter, jadi untuk balasan HTML yang panjang, tulis pesan assertion Anda sendiri.
- Pilih Timeout di atas pemecahan normal Anda yang paling lambat dan di bawah batas SDK sendiri, yaitu 300 detik.
- Ganti welcome dengan teks yang tampil di halaman sukses Anda, dan ubah path /signup serta username di helper agar sesuai dengan form Anda. Username yang tetap bertabrakan pada eksekusi malam kedua, jadi pakai username baru setiap kali.
// 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);
}
}Langkah 5: jalankan
Setel variabel untuk sesi ini dan jalankan dotnet test dari folder proyek. Variabel yang tidak Anda setel memakai nilai default. Eksekusi test CAPTCHA xUnit yang lulus diakhiri dengan ringkasan yang berbunyi Passed!, dengan total 1, failed 0, dan succeeded 1.
# 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
Pecahkan di dalam setiap test
Jangan memecahkan sekali di sebuah fixture lalu membagikan tokennya. Token reCAPTCHA hanya bisa dipakai sekali. Dalam pengujian, sebuah fixture memecahkan sekali dan dua test mengirim token yang sama: yang pertama diterima dan yang kedua ditolak oleh situs. Test mana yang kalah bergantung pada urutan eksekusi, sehingga kegagalannya berpindah-pindah dan tampak flaky. Token juga kedaluwarsa setelah sekitar dua menit, jadi test yang terlambat gagal meskipun dijalankan sendirian. Lihat panduan kedaluwarsa token reCAPTCHA untuk penjelasan tentang sisi itu.
Satu pemecahan per test memakan beberapa detik. Jika sebuah test tidak membutuhkan CAPTCHA itu sendiri, beri test itu jalur yang berbeda, seperti klien fake di bagian bawah.
Jaga pemecahan paralel tetap di bawah batas CapSkip
xUnit menjalankan test class secara bersamaan. Nilai defaultnya adalah satu thread per logical core, dan PC uji memiliki 24. Test di dalam satu class berjalan satu demi satu. Artinya, test suite dengan banyak class yang memecahkan CAPTCHA memulai banyak pemecahan sekaligus. Pengaturan reCAPTCHA Max. Threads milik CapSkip secara default bernilai 10. Job tambahan menunggu thread kosong, dan job yang menunggu melewati Wait Timeout akan gagal. Berikut yang dilakukan pengaturan thread terhadap enam class yang masing-masing memecahkan sekali, dengan mock menahan setiap pemecahan selama tiga detik.
| Pengaturan | Waktu eksekusi | Pemecahan sekaligus |
|---|---|---|
| Bawaan | 4.4 detik | 6 |
| maxParallelThreads 3 | 8.1 detik | 3 |
| maxParallelThreads 2 | 11.9 detik | 2 |
| Parallelization nonaktif | 23.3 detik | 1 |
Dengan empat belas class terhadap batas 10 milik mock, sepuluh class berhasil memecahkan dan empat class menunggu di dalam mock selama tiga detik masing-masing. Batas yang paling sederhana adalah sebuah angka di xunit.runner.json. Template sudah membuat file itu.
{
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
"maxParallelThreads": 8
}Cara lainnya adalah gate di dalam fixture, sehingga hanya beberapa pemecahan yang berjalan sekaligus berapa pun jumlah thread-nya. Simpan blok di bawah di samping CapSkipFixture.cs. Lalu buat test memanggil gate itu. Ganti pemanggilan RecaptchaAsync dengan: var code = await capskip.SolveAsync(page.SiteKey, page.Url, ct); lalu kirim code, bukan solved.Code. Gate harus berada di assembly fixture, karena gate di class fixture tidak membatasi apa pun: setiap class mendapat gate-nya sendiri. Dengan gate disetel ke 8, empat belas pemecahan mencapai puncak 8 pemecahan yang berjalan bersamaan.
// 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(); }
}
}Dengan empat belas class, batas runner sebesar 8, gate sebesar 8, dan tanpa batas sama sekali memakan waktu yang hampir sama: antara 8 dan 9 detik. Perbedaannya ada pada tempat terjadinya penantian. Dengan batas, penantian terjadi di proses test Anda. Tanpa batas, penantian terjadi di dalam CapSkip, tempat penantian itu dihitung terhadap Wait Timeout. Lihat dokumentasi paralelisme xUnit untuk bagian sisanya.
Untuk menjalankan secara berurutan hanya test yang melakukan pemecahan, taruh class-nya di satu collection bernama. Tambahkan atribut di bawah ini tepat di atas class pada SignupTests.cs dan biarkan isi class apa adanya. Class dalam collection yang sama berjalan satu demi satu, dan collection lain tetap berjalan berdampingan dengannya. Dalam pengujian, enam class pemecah ditambah enam class yang mengerjakan pekerjaan lain selama tiga detik memakan 23.3 detik, dan keenam class lainnya berjalan bersamaan.
// 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 ...
}Satu jebakan dari kebiasaan xUnit yang lama. Pengaturan atribut DisableTestParallelization dan MaxParallelThreads pada CollectionBehavior sekarang menjadi error di 4.0.2. Keduanya gagal dengan CS0619 dan mengarahkan ke atribut Parallelization. Gunakan atribut itu dari Xunit.v3 sebagai gantinya, seperti pada [assembly: Parallelization(MaxThreads = 3)], atau setel batasnya di xunit.runner.json.
Buat timeout menghentikan pemecahan
Fact dengan Timeout 5000 terhadap pemecahan yang memakan 20 detik gagal pada 5.0 detik dengan pesan: Test execution timed out after 5000 milliseconds. Seluruh eksekusi berakhir sekitar setengah detik kemudian. Karena SDK menerima token milik test, token yang dibatalkan itu menghentikan pemanggilan. Saat SDK sedang melakukan polling untuk jawabannya, exception-nya adalah TaskCanceledException. Saat SDK sedang mengirim tugas, exception-nya adalah NetworkException milik CapSkip dengan pesan The operation was canceled.
Terkadang xUnit lalu melaporkan kegagalan kedua untuk test yang sama, yaitu kegagalan saat pembersihan (cleanup failure) yang membawa exception pembatalan. Guard di sekitar pemanggilan pemecahan menghilangkannya. Tambahkan baris using CapSkip; di bagian atas file test. Biarkan token dan pemuatan halaman di atas try, dan taruh pemecahan, pengiriman, serta kedua assertion di dalamnya. Guard hanya menelan exception ketika token milik test dibatalkan, karena timeout sudah menggagalkan test dengan pesannya sendiri.
// 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.
}Jika Anda tidak meneruskan token, test tetap gagal pada 5.0 detik, tetapi pemecah CAPTCHA terus melakukan polling di latar belakang. Test yang tidak pernah menyentuh token-nya bahkan bisa mengirim form setelah dilaporkan gagal. Analyzer xUnit1051 memperingatkan tepat soal ini. Peringatan itu muncul pada pemanggilan RecaptchaAsync tanpa token, dan juga pada pemanggilan HttpClient tanpa token.
Switch timeout untuk seluruh eksekusi pada dotnet test sama-sama bergantung pada kerja sama kode Anda. Switch itu tidak menghentikan test yang mengabaikan token-nya. Untuk eksekusi yang menggantung, gunakan hang dump. Blok pertama di bawah untuk runner baru, yang dipakai panduan ini. Blok kedua untuk jalur VSTest.
# 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
Dengan hang timeout 15 detik, runner baru mengakhiri eksekusi pada 16.2 detik. Runner itu menulis mini dump sebesar 2.7 MB dan tidak meninggalkan proses apa pun. Di jalur VSTest, dump type none mengakhiri eksekusi tetapi membiarkan proses test yang menggantung tetap hidup.
SDK memiliki batasnya sendiri, recaptchaTimeout, yaitu 300 detik. Batas itu hanya diperiksa di antara polling. Batas 8 detik terpicu setelah 12.9 detik dalam pengujian, dan nilai default terpicu setelah 5 menit 3 detik. Jaga Timeout Anda jauh di bawahnya.
Skip test saat CapSkip tidak berjalan
Test di Langkah 4 memanggil Assert.SkipUnless dengan flag IsUp milik fixture. Saat CapSkip ditutup, ia melaporkan test yang di-skip beserta alasannya, dan eksekusi keluar dengan kode 0. Saat semua test di-skip, runner mencetak Zero tests ran. Di job CI yang wajib memiliki CapSkip, tambahkan switch fail-skips dengan nilai on. Test yang di-skip lalu menggagalkan eksekusi dengan exit code 2.
Tanpa guard, CapSkip yang ditutup menggagalkan test dengan NetworkException dari CapSkip: No connection could be made because the target machine actively refused it. Itu memakan sekitar 2.1 detik di Windows. Opsi atribut SkipExceptions dengan tipe NetworkException mengubah exception itu menjadi skip. Opsi itu juga menyembunyikan CapSkip yang berhenti di tengah eksekusi, jadi probe lebih aman.
Probe juga bisa menyembunyikan salah ketik. CAPSKIP_HOST berisi nama host atau alamat saja, tanpa http:// di depannya. Nilai seperti http://127.0.0.1 membuat probe gagal, sehingga test di-skip dan bukan digagalkan. Tanpa guard, SDK melaporkan: No such host is known. (http:80), dan butuh beberapa detik untuk melaporkannya.
Coba lagi saat pemecahan gagal
xUnit tidak memiliki atribut retry. Sebagian pemecahan gagal, dan percobaan baru bisa berhasil. SDK melempar ApiException dengan pesan ERROR_CAPTCHA_UNSOLVABLE, kecuali Anda menyetel teks Error Replacement di CapSkip. Jika itu terjadi, exception membawa teks tersebut, dan filter di bawah tidak pernah cocok. Helper ini hanya mencoba ulang kegagalan itu, dan setiap percobaan mengambil tempatnya sendiri di gate. Simpan di samping blok gate, karena helper ini memanggil SolveAsync milik gate.
// 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")) { }
}
}
}Dengan mock yang menggagalkan dua job pertamanya, helper yang mengizinkan tiga percobaan berhasil dalam 5.8 detik. Eksekusi kedua yang mengizinkan dua percobaan gagal dengan CapSkip.ApiException: ERROR_CAPTCHA_UNSOLVABLE setelah 4.0 detik. Lihat ERROR_CAPTCHA_UNSOLVABLE untuk arti error tersebut.
Perbaiki bentrok nama TimeoutException
Proyek test mengaktifkan implicit usings, sehingga System selalu berada dalam cakupan. Dengan using CapSkip juga, klausa catch untuk TimeoutException gagal dikompilasi: error CS0104: TimeoutException is an ambiguous reference between CapSkip.TimeoutException and System.TimeoutException. Salah satu perbaikan di bawah ini berhasil, dan alias juga berfungsi di dalam Assert.ThrowsAsync.
// 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 hanya bertabrakan jika Anda menambahkan using System.ComponentModel.DataAnnotations, yang tidak ada di proyek test biasa.
Unit test yang tidak membutuhkan pemecah CAPTCHA
Test untuk kode Anda sendiri yang memanggil pemecah CAPTCHA tidak membutuhkan CapSkip. ApiClient milik SDK bersifat public, kedua metode request-nya virtual, dan properti ApiClient pada klien bisa disetel. Test ini lulus saat CapSkip ditutup.
// 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);
}
}Switch yang perlu diketahui
Berikut switch dotnet test yang penting saat menguji test suite pemecah CAPTCHA di runner baru. Test yang lulus tidak menampilkan output secara default, dan switch diagnostik menulis pesannya ke sebuah file, bukan ke konsol.
# 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
Menjalankan test terhadap CapSkip di mesin lain
Test dan pemecah CAPTCHA tidak harus berada di PC yang sama. Alihkan CapSkip ke mode Server di pengaturan koneksi, dan ia akan mendengarkan di alamat jaringan atau IP publik Anda. Lalu setel CAPSKIP_HOST dan CAPSKIP_PORT, dan test CAPTCHA xUnit yang sama berjalan tanpa perubahan. Dalam pengujian, suite lulus dengan CAPSKIP_HOST disetel ke alamat LAN PC uji dan mock mendengarkan di semua interface.
Perhatikan dua hal. SDK berkomunikasi dengan CapSkip melalui HTTP biasa, tanpa opsi TLS, sehingga API key dikirim sebagai teks biasa. Pertahankan koneksi itu hanya di jaringan privat atau VPN, dan aktifkan validasi API key. Lalu CapSkip membuka port-nya untuk semua alamat saat mode Server dimulai. Untuk membatasi akses hanya pada mesin yang Anda harapkan, tambahkan aturan blokir Windows Firewall untuk alamat lainnya. Pemecah CAPTCHA tetap berupa mesin Windows milik Anda sendiri, dan pemecahannya tetap tanpa kuota.
Seluruh file test
Berikut fixture, probe, helper, dan test dalam satu file, untuk memulai dengan cepat. Jika Anda tidak mengikuti Langkah 2 sampai 4, satu file ini menggantikan semuanya. Jika Anda sudah menyimpan file-file itu, hapus dulu. Class fixture bersifat partial, sehingga blok gate dan retry tetap cocok. Simpan sebagai SignupTests.cs.
// 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);
}
}Kesalahan umum pada test CAPTCHA xUnit
| Apa yang Anda lihat | Penyebab | Perbaiki |
|---|---|---|
| Testing with VSTest target is no longer supported | Proyek xUnit v3 tanpa global.json, di SDK 10 | Tambahkan global.json dan jalankan dari foldernya |
| CS0104 pada TimeoutException | System dan CapSkip sama-sama mendefinisikannya | Gunakan alias, atau tangkap CapSkipError |
| CS0619 pada DisableTestParallelization | Opsi itu sudah usang di xUnit v3 | Gunakan atribut Parallelization atau xunit.runner.json |
| Zero tests ran, dengan Program.cs di dalam proyek | Entry point milik proyek menggantikan entry point milik xUnit | Hapus Program.cs |
| Test execution timed out after 5000 milliseconds | Pemecahan memakan waktu lebih lama daripada Timeout | Naikkan Timeout, dan teruskan token ke SDK |
| Skipped: CapSkip is not running | Tidak ada yang mendengarkan di host dan port itu | Jalankan CapSkip, periksa pengaturan koneksi, gunakan host tanpa http:// |
| Hanya satu dari beberapa test yang lulus | Test berbagi satu token atau satu HttpClient yang menyimpan cookie | Pecahkan dan buka halaman di dalam setiap test |
| Test kedua gagal dengan CSRF mismatch | Test paralel saling menimpa cookie satu sama lain | Satu HttpClient per test |
| Error build xUnit1049 | Test async void | Kembalikan Task atau ValueTask |
| Peringatan xUnit1051 | Pemanggilan yang menerima token tidak diberi token | Teruskan TestContext.Current.CancellationToken |
FAQ
Bisakah agen CI hosted menjalankan test ini?
Agen hosted berjalan di mesin milik pihak lain, sehingga tidak bisa menjangkau 127.0.0.1 di PC Anda. Jalankan CapSkip dalam mode Server di mesin Windows yang Anda kendalikan, arahkan CAPSKIP_HOST ke sana, dan simpan API key di secret store milik sistem CI. Agen GitHub-hosted dan Azure DevOps hosted tidak diuji di sini. Hanya kasus LAN yang diuji. Agen itu juga harus diizinkan menjangkau alamat dan port tersebut.
Apakah ini berfungsi dengan xUnit v2, NUnit, atau MSTest?
Hanya xUnit v3 4.0.2 yang dijalankan untuk panduan ini. Beberapa hal di atas adalah fitur v3, seperti Assert.SkipUnless, TestContext.Current, dan atribut assembly fixture. Pemanggilan CapSkip sama di framework mana pun. Yang berubah adalah pengaturan fixture, timeout, dan paralelisme, jadi periksa dokumentasi framework Anda untuk masing-masingnya.
Bisakah saya tetap memakai runner VSTest yang lama?
Bisa. Gunakan xunit.v3.mtp-off 4.0.2, xunit.runner.visualstudio 4.0.1, dan Microsoft.NET.Test.Sdk 18.10.1 sebagai pengganti paket mtp-v2, dan jangan sertakan global.json. dotnet test lalu bekerja seperti sebelumnya. Alasan skip tidak dicetak oleh console logger default di jalur itu, dan switch hang-nya bernama blame-hang-timeout.
Haruskah setiap build menjalankan test pemecah CAPTCHA?
Sebaiknya tidak. Masing-masing memakan beberapa detik, membutuhkan CapSkip yang sedang berjalan, dan menyentuh situs staging yang sungguhan. Taruh test itu di proyek test tersendiri, supaya dotnet test biasa untuk unit test Anda tidak pernah menunggu pemecah CAPTCHA. Jalankan proyek itu sesuai jadwal atau sebelum rilis.
Versi singkatnya
Test suite CAPTCHA xUnit membutuhkan satu klien CapSkip di assembly fixture dan pemecahan baru di dalam setiap test. Teruskan token milik test ke SDK, sehingga Timeout pada Fact dapat menghentikan pemecahan. Batasi pemecahan paralel di bawah 10 thread milik CapSkip dengan pengaturan runner atau gate. Gunakan global.json di SDK 10. Skip, jangan gagalkan, saat CapSkip ditutup, dan gunakan fail-skips di CI.
- Setiap tipe CAPTCHA yang ditangani paket .NET: halaman pemecah CAPTCHA C#.
- reCAPTCHA kotak centang di program C# biasa: memecahkan reCAPTCHA v2 di C#.
- Cara kerja pemecah kotak centang: halaman pemecah reCAPTCHA v2.
Test suite malam yang memecahkan CAPTCHA beberapa ratus kali jumlahnya cepat menumpuk. Jika dijalankan melalui sebuah bypass captcha lokal di mesin Windows Anda sendiri, pemecahan itu tidak pernah berubah menjadi tagihan.
