CapSkip के साथ xUnit टेस्ट में कैप्चा कैसे हल करें (C#)

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

xUnit टेस्ट में कैप्चा हल करने के लिए, एक assembly fixture में एक ही CapSkip क्लाइंट बनाएँ और हर टेस्ट के अंदर हल करें, क्योंकि एक टोकन सिर्फ़ एक बार काम करता है। टेस्ट का cancellation token SDK को पास करें, ताकि timeout होने पर हल रुक जाए। एक साथ चल रहे हलों की संख्या को CapSkip की थ्रेड सीमा से नीचे रखें। CapSkip आपके अपने Windows PC पर चलता है, इसलिए जो suite हर रात सैकड़ों बार हल करता है, उसकी प्रति-हल कोई लागत नहीं। xUnit v3 और .NET 10 पर इस xUnit कैप्चा सेटअप के अपने जाल हैं। dotnet test कमांड को एक opt-in फ़ाइल चाहिए। टेस्ट classes डिफ़ॉल्ट रूप से एक साथ, parallel में चलती हैं। और timeout तभी काम करता है जब आपका कोड सहयोग करे, यानी token जाँचकर रुक जाए। यह गाइड इन सभी को कवर करती है।

आपको क्या चाहिए

  • किसी Windows PC पर चल रहा CapSkip, जिसमें reCAPTCHA सॉल्वर चालू हो। पहली बार शुरू करने के चरण सेटअप गाइड में समझाए गए हैं।
  • वर्ज़न 10 का .NET SDK। xUnit v3 के टेस्ट प्रोजेक्ट net8.0 या उसके बाद के, या .NET Framework 4.7.2 को टारगेट करते हैं। नीचे सब कुछ Windows 11 पर SDK 10.0.100 के साथ net10.0 पर और फिर net8.0 पर चलाया गया, NuGet के xunit.v3.mtp-v2 4.0.2 और CapSkip 1.3.0 के साथ। SDK 8 और 9 को टेस्ट नहीं किया गया।
  • आपकी अपनी एक staging साइट, यानी रिलीज़ से पहले टेस्ट करने के लिए आपकी वेबसाइट की कॉपी, जिसके फॉर्म में reCAPTCHA v2 checkbox हो। उदाहरण एक signup फॉर्म पोस्ट करता है जिसमें CSRF टोकन होता है। फ़ील्ड के नाम अपने फॉर्म के हिसाब से बदलें।
  • सॉल्वर के लिए एक पता। Local मोड में CapSkip केवल उसी डिवाइस के लिए 127.0.0.1 पर जवाब देता है। Server मोड में वह आपके नेटवर्क पते या पब्लिक IP पर सुनता है, ताकि किसी दूसरी मशीन पर चल रहे टेस्ट API के ज़रिए उसे कॉल कर सकें। दोनों यहाँ सेट किए जाते हैं: कनेक्शन सेटिंग्स.

नीचे की माप एक ही PC पर चल रहे mock CapSkip और mock signup पेज से ली गई हैं (यानी टेस्ट के लिए बनाए गए नकली CapSkip और नकली पेज), और इनके साथ असली xUnit और असली SDK इस्तेमाल हुआ। xUnit और SDK का व्यवहार आपकी साइट पर भी वैसा ही रहता है। हल करने का समय और signup पेज के स्वीकार व अस्वीकार करने के नियम mock के अपने हैं।

चरण 1: टेस्ट प्रोजेक्ट बनाएँ

template pack इंस्टॉल करने से पहले .NET SDK में xUnit v3 टेम्पलेट नहीं होता। नीचे की पहली कमांड यही करती है। फिर टेम्पलेट प्रोजेक्ट, एक xunit.runner.json फ़ाइल, एक global.json फ़ाइल और UnitTest1.cs नाम का एक सैंपल टेस्ट लिखता है। ब्लॉक सैंपल टेस्ट को हटा देता है, क्योंकि वह हर रन में एक दूसरा टेस्ट जोड़ देता।

# 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 को रखें। SDK 10 पर, dotnet test उसके बिना xUnit v3 प्रोजेक्ट को चलाने से मना कर देता है और इस संदेश के साथ रुक जाता है: Testing with VSTest target is no longer supported by Microsoft.Testing.Platform on .NET 10 SDK and later. यह फ़ाइल dotnet test को नए runner, Microsoft.Testing.Platform, पर ले जाती है, जिसे नीचे के ब्लॉक के comments MTP कहते हैं। टेम्पलेट यह फ़ाइल प्रोजेक्ट फ़ोल्डर में रखता है, या अगर ऊपर पहले से कोई global.json है तो उसमें test सेक्शन जोड़ देता है। dotnet test को प्रोजेक्ट फ़ोल्डर से चलाएँ। ऐसे फ़ोल्डर से चलाना जिसमें यह फ़ाइल नहीं है और प्रोजेक्ट का नाम देना, जैसे dotnet test CapSkipTests, उसी error के साथ फेल होता है।

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

चरण 2: पूरे रन के लिए एक CapSkip क्लाइंट

यहाँ से हर C# फ़ाइल ब्लॉक की शुरुआत उस नाम से होती है जिससे उसे सेव करना है। पहला ब्लॉक fixture है। वह CapSkip की सेटिंग्स एनवायरनमेंट से पढ़ता है, क्योंकि SDK उन्हें खुद नहीं पढ़ता, और एक क्लाइंट बनाता है। डिफ़ॉल्ट वैल्यू की वजह से वह बिना किसी सेटअप के चल जाता है। CAPSKIP_API_KEY तभी मायने रखता है जब आपने CapSkip में API key validation चालू किया हो।

// 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 यह भी पता लगाता है कि CapSkip चल रहा है या नहीं। एक सादा TCP connect पूछता है कि कुछ सुन रहा है या नहीं, और वह कोई solve जॉब नहीं भेजता।

// 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 पर रजिस्टर करें। फिर xUnit पूरे रन के लिए एक instance बनाता है और उसे हर test class को उसके constructor के ज़रिए देता है, जैसा xUnit shared context डॉक्स में बताया गया है।

// 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 बाकी दोनों तरीकों से बेहतर है। छह test classes के साथ, class fixture छह बार बना, हर class के लिए एक बार। collection fixture एक बार बना, पर वह उन classes को एक के बाद एक चलने पर मजबूर करता है। हर एक ने एक बार हल करने वाली छह classes को collection fixture के साथ 23 सेकंड से ज़्यादा लगे, और assembly fixture के साथ 5 सेकंड से कम। क्लाइंट को साझा करना सुरक्षित है। वह कोई बदलने वाली state नहीं रखता, हर क्लाइंट SDK के अंदर के उसी static HttpClient का इस्तेमाल करता है, और एक क्लाइंट से एक साथ चौदह हल चले।

चरण 3: आपकी staging साइट के लिए एक छोटा helper

यह हिस्सा आपका है, CapSkip का नहीं। helper signup पेज खोलता है और उससे sitekey और CSRF टोकन पढ़ता है। दोनों patterns को अपने markup के हिसाब से बदलें।

// 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();
}

फिर वह टोकन को g-recaptcha-response फ़ील्ड में रखकर फॉर्म पोस्ट करता है। HttpClient फॉर्म पोस्ट पर कोई Referer नहीं भेजता, और HTTPS पर Django जैसी सख़्त साइट उसके बिना आई पोस्ट को ठुकरा देती है, इसलिए helper उसे सेट करता है।

// 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));
    }
}

हर टेस्ट अपना helper बनाता है, और इसलिए अपना HttpClient भी। यह जानबूझकर है। cookies रखने वाले एक साझा क्लाइंट और छह parallel टेस्ट के साथ, हर विज़िट पर नया session शुरू करने वाली एक साइट ने एक पोस्ट स्वीकार की और पाँच को CSRF mismatch के साथ ठुकरा दिया। टेस्ट एक-दूसरे की cookies को ओवरराइट कर रहे थे।

चरण 4: टेस्ट लिखें

CapSkip न चल रहा हो तो xUnit कैप्चा टेस्ट खुद को skip कर देता है। वह अपना कैप्चा खुद हल करता है, फॉर्म पोस्ट करता है और जवाब जाँचता है। इन पाँच बातों का ध्यान रखें:

  • Fact का Timeout मिलीसेकंड में होता है। समय पूरा होने पर वह टेस्ट को फेल कर देता है, और टेस्ट के cancellation token को भी cancel कर देता है।
  • cancellation token, RecaptchaAsync को named cancellationToken argument के रूप में जाता है, ताकि टेस्ट के cancel होने पर हल रुक जाए।
  • body पर assertion, status वाले assertion से पहले आता है। तब साइट के फॉर्म को ठुकराने पर xUnit सिर्फ़ Forbidden दिखाने के बजाय साइट के जवाब की शुरुआत दिखाता है। वह लंबी strings को करीब 50 कैरेक्टर पर काट देता है, इसलिए लंबे HTML जवाब के लिए अपना खुद का assertion message लिखें।
  • ऐसा Timeout चुनें जो आपके सबसे धीमे सामान्य हल से ऊपर हो और SDK की अपनी 300 सेकंड की सीमा से नीचे।
  • welcome की जगह वह टेक्स्ट रखें जो आपका सफलता वाला पेज दिखाता है, और helper में /signup पाथ और username को अपने फॉर्म के हिसाब से बदलें। तय username दूसरे रात वाले रन पर टकरा जाता है, इसलिए हर बार नया इस्तेमाल करें।
// 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: इसे चलाएँ

सेशन के लिए वेरिएबल सेट करें और प्रोजेक्ट फ़ोल्डर से dotnet test चलाएँ। जिन वेरिएबल को आप छोड़ देते हैं, वे डिफ़ॉल्ट पर लौट जाते हैं। पास होने वाला xUnit कैप्चा रन एक summary के साथ खत्म होता है जिसमें Passed! लिखा होता है, total 1, failed 0 और 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

हर टेस्ट के अंदर हल करें

fixture में एक बार हल करके टोकन को साझा न करें। reCAPTCHA टोकन सिर्फ़ एक बार इस्तेमाल हो सकता है। टेस्टिंग में, एक fixture ने एक बार हल किया और दो टेस्ट ने वही टोकन पोस्ट किया: पहला स्वीकार हुआ और दूसरे को साइट ने ठुकरा दिया। कौन-सा टेस्ट हारता है, यह रन के क्रम पर निर्भर करता है, इसलिए नाकामी इधर-उधर होती रहती है और flaky लगती है। टोकन करीब दो मिनट बाद एक्सपायर भी हो जाता है, इसलिए देर से चलने वाला टेस्ट अकेले चलने पर भी फेल हो जाता है। इस पहलू को reCAPTCHA token expiration की गाइड में समझाया गया है।

हर टेस्ट में एक बार हल करने में कुछ सेकंड लगते हैं। अगर किसी टेस्ट को कैप्चा की खुद ज़रूरत नहीं है, तो उसे कोई अलग रास्ता दें, जैसे आगे दिया गया नकली क्लाइंट।

parallel हलों को CapSkip की सीमा से नीचे रखें

xUnit टेस्ट classes को एक साथ चलाता है। डिफ़ॉल्ट हर logical core के लिए एक थ्रेड है, और टेस्ट वाले PC में 24 हैं। एक class के अंदर के टेस्ट एक के बाद एक चलते हैं। इसका मतलब है कि बहुत सारी solving classes वाला suite एक साथ बहुत सारे हल शुरू कर देता है। CapSkip की reCAPTCHA Max. Threads सेटिंग का डिफ़ॉल्ट 10 है। अतिरिक्त जॉब खाली थ्रेड का इंतज़ार करती हैं, और जो जॉब Wait Timeout से ज़्यादा इंतज़ार करती है, वह फेल हो जाती है। यहाँ देखें कि थ्रेड सेटिंग्स ने छह classes पर क्या असर डाला, जिनमें से हर एक ने एक बार हल किया, जबकि mock हर हल को तीन सेकंड रोके रखता था।

सेटिंगचलने का समयएक साथ हल
डिफ़ॉल्ट4.4 सेकंड6
maxParallelThreads 38.1 सेकंड3
maxParallelThreads 211.9 सेकंड2
Parallelization बंद23.3 सेकंड1

चौदह classes और mock की 10 की सीमा के साथ, दस हल चले और चार हल mock के अंदर तीन-तीन सेकंड इंतज़ार करते रहे। सबसे सरल सीमा xunit.runner.json में एक संख्या है। टेम्पलेट यह फ़ाइल पहले ही बना चुका है।

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

दूसरा तरीका fixture में एक gate है, ताकि थ्रेड की संख्या चाहे जो हो, एक साथ सिर्फ़ कुछ ही हल चलें। नीचे के ब्लॉक को CapSkipFixture.cs के बगल में सेव करें। फिर टेस्ट से gate को कॉल करवाएँ। RecaptchaAsync कॉल की जगह यह रखें: var code = await capskip.SolveAsync(page.SiteKey, page.Url, ct); और solved.Code की जगह code को पोस्ट करें। gate को assembly fixture में रहना होगा, क्योंकि class fixture में रखा gate कुछ भी सीमित नहीं करता: हर class को अपना gate मिल जाता है। gate को 8 पर सेट करने पर, चौदह हलों में एक साथ चलने वालों की अधिकतम संख्या 8 रही।

// 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(); }
    }
}

चौदह classes के साथ, 8 की runner सीमा, 8 का gate और बिना किसी सीमा के, तीनों में लगभग एक जैसा समय लगा: 8 से 9 सेकंड के बीच। फ़र्क़ इस बात का है कि इंतज़ार कहाँ होता है। सीमा के साथ, वह आपके टेस्ट प्रोसेस में होता है। सीमा के बिना, वह CapSkip के अंदर होता है, जहाँ वह Wait Timeout में गिना जाता है। बाकी बातें xUnit parallelism डॉक्स में दी गई हैं।

सिर्फ़ solving टेस्ट को एक के बाद एक चलाने के लिए, उनकी classes को एक named collection में रखें। नीचे वाला attribute SignupTests.cs में class के ऊपर जोड़ें और class की body को जैसा है वैसा रहने दें। एक ही collection की classes एक के बाद एक चलती हैं, और बाकी collections उनके साथ फिर भी चलती रहती हैं। टेस्टिंग में, छह solving classes और तीन सेकंड का दूसरा काम करने वाली छह classes को 23.3 सेकंड लगे, और बाकी छह classes ने उनके साथ ओवरलैप किया।

// 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 ...
}

पुरानी xUnit आदतों का एक जाल। CollectionBehavior पर attribute सेटिंग्स DisableTestParallelization और MaxParallelThreads अब 4.0.2 में errors हैं। वे CS0619 के साथ फेल होती हैं और Parallelization attribute की ओर इशारा करती हैं। इसके बजाय Xunit.v3 का वही attribute इस्तेमाल करें, जैसे [assembly: Parallelization(MaxThreads = 3)] में, या सीमा xunit.runner.json में सेट करें।

timeout से हल को रुकवाएँ

5000 के Timeout वाला एक Fact, 20 सेकंड लेने वाले हल के सामने, 5.0 सेकंड पर इस संदेश के साथ फेल हुआ: Test execution timed out after 5000 milliseconds. पूरा रन करीब आधे सेकंड बाद खत्म हुआ। क्योंकि SDK को टेस्ट का cancellation token मिला था, cancel हुए token ने कॉल को रोक दिया। जब SDK जवाब के लिए poll कर रहा होता है, तब exception TaskCanceledException होता है। जब वह submit कर रहा होता है, तब exception CapSkip का NetworkException होता है जिसका संदेश The operation was canceled है।

कभी-कभी xUnit उसी टेस्ट के लिए एक दूसरी नाकामी भी रिपोर्ट करता है, एक cleanup failure जिसमें cancellation exception होता है। solve कॉल के आसपास लगा guard उसे हटा देता है। टेस्ट फ़ाइल के ऊपर using CapSkip; वाली लाइन जोड़ें। cancellation token और पेज का lookup try के ऊपर रखें, और हल, submit और दोनों assertions उसके अंदर रखें। guard exception को तभी निगलता है जब टेस्ट का token cancel हुआ हो, क्योंकि timeout पहले ही टेस्ट को अपने संदेश के साथ फेल कर चुका है।

// 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.
}

अगर आप token पास नहीं करते, तो टेस्ट फिर भी 5.0 सेकंड पर फेल होता है, पर solver बैकग्राउंड में poll करता रहता है। जो टेस्ट अपने token को कभी छूता ही नहीं, वह फेल रिपोर्ट होने के बाद भी फॉर्म पोस्ट कर सकता है। xUnit1051 analyzer ठीक इसी की चेतावनी देता है। वह बिना token वाली RecaptchaAsync कॉल पर चलता है, और बिना token वाली HttpClient कॉल पर भी।

dotnet test का पूरे रन वाला timeout switch भी तभी काम करता है जब टेस्ट सहयोग करे। उसने ऐसे टेस्ट को खत्म नहीं किया जो अपने token को अनदेखा करता है। जो रन अटक जाए, उसके लिए hang dump इस्तेमाल करें। नीचे का पहला ब्लॉक नए runner के लिए है, जिसका यह गाइड इस्तेमाल करती है। दूसरा 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

15 सेकंड के hang timeout के साथ, नए runner ने रन को 16.2 सेकंड पर खत्म कर दिया। उसने 2.7 MB का एक mini dump लिखा और कोई प्रोसेस पीछे नहीं छोड़ा। VSTest वाले रास्ते पर, dump type none रन को खत्म कर देता है, पर अटका हुआ टेस्ट प्रोसेस ज़िंदा छोड़ देता है।

SDK की अपनी एक सीमा है, recaptchaTimeout, जो 300 सेकंड है। वह सिर्फ़ polls के बीच जाँची जाती है। टेस्टिंग में 8 सेकंड की सीमा 12.9 सेकंड बाद चली, और डिफ़ॉल्ट वाली 5 मिनट 3 सेकंड बाद। अपने Timeout को उससे काफ़ी नीचे रखें।

CapSkip न चल रहा हो तो टेस्ट skip करें

चरण 4 का टेस्ट fixture के IsUp flag के साथ Assert.SkipUnless कॉल करता है। CapSkip बंद होने पर वह कारण के साथ एक skip किया गया टेस्ट रिपोर्ट करता है और रन 0 के साथ खत्म होता है। जब हर टेस्ट skip हो जाता है, तो runner Zero tests ran प्रिंट करता है। जिस CI जॉब में CapSkip होना ज़रूरी है, उसमें fail-skips switch को on वैल्यू के साथ जोड़ें। तब skip हुआ टेस्ट रन को exit code 2 के साथ फेल कर देता है।

guard के बिना, बंद CapSkip टेस्ट को CapSkip NetworkException के साथ फेल कर देता है: No connection could be made because the target machine actively refused it. Windows पर इसमें करीब 2.1 सेकंड लगे। एक attribute ऑप्शन, NetworkException type के साथ SkipExceptions, उस exception को skip में बदल देता है। यह उस CapSkip को भी छिपा देता है जो रन के बीच में बंद हो जाए, इसलिए probe ज़्यादा सुरक्षित है।

probe किसी typo को भी छिपा सकता है। CAPSKIP_HOST सिर्फ़ host नाम या पता होता है, उसके आगे http:// नहीं होता। http://127.0.0.1 जैसी वैल्यू probe को फेल कर देती है, इसलिए टेस्ट फेल होने के बजाय skip हो जाते हैं। guard के बिना SDK बताता है: No such host is known. (http:80), और यह बताने में उसे कई सेकंड लग सकते हैं।

हल फेल होने पर दोबारा कोशिश करें

xUnit में कोई retry attribute नहीं है। कुछ हल फेल होते हैं, और नई कोशिश काम कर सकती है। SDK एक ApiException उठाता है जिसका संदेश ERROR_CAPTCHA_UNSOLVABLE होता है, जब तक आप CapSkip में कोई Error Replacement text सेट न करें। तब exception उसी टेक्स्ट को लेकर आता है, और नीचे का फ़िल्टर कभी मेल नहीं खाता। यह helper सिर्फ़ उसी नाकामी पर दोबारा कोशिश करता है, और हर कोशिश gate पर अपनी अलग बारी लेती है। इसे gate वाले ब्लॉक के बगल में सेव करें, क्योंकि यह gate के SolveAsync को कॉल करता है।

// 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")) { }
        }
    }
}

एक ऐसे mock के साथ जिसने अपनी पहली दो जॉब फेल कीं, तीन कोशिशों की अनुमति देने वाला helper 5.8 सेकंड में पास हुआ। दो कोशिशों की अनुमति वाला दूसरा रन 4.0 सेकंड बाद CapSkip.ApiException: ERROR_CAPTCHA_UNSOLVABLE के साथ फेल हुआ। इस error का मतलब जानने के लिए ERROR_CAPTCHA_UNSOLVABLE देखें।

TimeoutException के नाम के टकराव को ठीक करें

टेस्ट प्रोजेक्ट में implicit usings चालू होते हैं, इसलिए System हमेशा scope में रहता है। using CapSkip भी होने पर, TimeoutException का catch clause compile नहीं होता: error CS0104: TimeoutException is an ambiguous reference between CapSkip.TimeoutException and System.TimeoutException. इनमें से कोई भी ठीक करने का तरीका काम करता है, और alias 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 तभी टकराता है जब आप using System.ComponentModel.DataAnnotations जोड़ें, जो सादे टेस्ट प्रोजेक्ट में नहीं होता।

ऐसे unit tests जिन्हें सॉल्वर की ज़रूरत नहीं

सॉल्वर को कॉल करने वाले आपके अपने कोड के टेस्ट को CapSkip की ज़रूरत नहीं। SDK का ApiClient public है, उसके दो request method virtual हैं, और क्लाइंट की ApiClient प्रॉपर्टी सेट की जा सकती है। यह टेस्ट CapSkip बंद होने पर भी पास होता है।

// 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);
    }
}

जानने लायक switches

ये वे dotnet test switches हैं जो नए runner पर सॉल्वर suite को टेस्ट करते समय काम के रहे। पास होने वाले टेस्ट डिफ़ॉल्ट रूप से कोई output नहीं दिखाते, और diagnostics switch अपने संदेश console के बजाय एक फ़ाइल में लिखता है।

# 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

दूसरी मशीन पर चल रहे CapSkip के सामने टेस्ट चलाना

टेस्ट और सॉल्वर का एक ही PC पर होना ज़रूरी नहीं। कनेक्शन सेटिंग्स में CapSkip को Server मोड पर कर दें, तो वह आपके नेटवर्क पते या पब्लिक IP पर सुनता है। फिर CAPSKIP_HOST और CAPSKIP_PORT सेट करें, और वही xUnit कैप्चा टेस्ट बिना बदलाव के चलते हैं। टेस्टिंग में, suite तब पास हुआ जब CAPSKIP_HOST टेस्ट वाले PC के LAN पते पर सेट था और mock सभी interfaces पर सुन रहा था।

दो बातों का ध्यान रखें। SDK CapSkip से सादे HTTP पर बात करता है, TLS का कोई ऑप्शन नहीं है, इसलिए API key सादे टेक्स्ट में जाती है। उस लिंक को किसी प्राइवेट नेटवर्क या VPN पर रखें, और API key validation चालू करें। और Server मोड शुरू होते ही CapSkip अपना पोर्ट हर पते के लिए खोल देता है। पहुँच को सिर्फ़ अपेक्षित मशीनों तक सीमित रखने के लिए बाकी पतों के लिए Windows Firewall में एक block नियम जोड़ें। सॉल्वर अब भी आपकी अपनी Windows मशीन है, और हल अब भी बिना मीटर वाले हैं।

पूरी टेस्ट फ़ाइल

यह रहा fixture, probe, helper और टेस्ट, जल्दी शुरू करने के लिए एक ही फ़ाइल में। अगर आपने चरण 2 से 4 छोड़ दिए, तो यह एक फ़ाइल उनकी जगह ले लेती है। अगर आप वे फ़ाइलें पहले ही सेव कर चुके हैं, तो पहले उन्हें हटा दें। fixture class partial है, इसलिए gate और retry वाले ब्लॉक अब भी फिट बैठते हैं। इसे 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);
    }
}

xUnit कैप्चा टेस्ट की आम errors

आप जो देखते हैंकारणफिक्स
Testing with VSTest target is no longer supportedSDK 10 पर global.json के बिना एक xUnit v3 प्रोजेक्टglobal.json जोड़ें और उसी के फ़ोल्डर से चलाएँ
TimeoutException पर CS0104System और CapSkip, दोनों इसे परिभाषित करते हैंalias इस्तेमाल करें, या CapSkipError पकड़ें
DisableTestParallelization पर CS0619यह ऑप्शन xUnit v3 में obsolete हैParallelization attribute या xunit.runner.json इस्तेमाल करें
प्रोजेक्ट में Program.cs होने पर Zero tests ranप्रोजेक्ट के अपने entry point ने xUnit वाले की जगह ले लीProgram.cs हटा दें
Test execution timed out after 5000 millisecondsहल में Timeout से ज़्यादा समय लगाTimeout बढ़ाएँ, और token को SDK को पास करें
Skipped: CapSkip is not runninghost और port पर कुछ नहीं सुन रहाCapSkip शुरू करें, कनेक्शन सेटिंग्स जाँचें, http:// के बिना सिर्फ़ host इस्तेमाल करें
कई टेस्ट में से सिर्फ़ एक पास होता हैटेस्ट एक ही टोकन या cookies रखने वाला एक ही HttpClient साझा करते हैंहर टेस्ट के अंदर हल करें और पेज खोलें
दूसरा टेस्ट CSRF mismatch के साथ फेल होता हैparallel टेस्ट ने एक-दूसरे की cookies ओवरराइट कर दींहर टेस्ट के लिए एक HttpClient
xUnit1049 build errorएक async void टेस्टTask या ValueTask लौटाएँ
xUnit1051 warningcancellation token लेने वाली कॉल को कोई token नहीं मिलाTestContext.Current.CancellationToken पास करें

FAQ

क्या होस्टेड CI एजेंट ये टेस्ट चला सकता है?

होस्टेड एजेंट किसी और की मशीन पर चलता है, इसलिए वह आपके PC के 127.0.0.1 तक नहीं पहुँच सकता। CapSkip को अपने नियंत्रण वाली किसी Windows मशीन पर Server मोड में चलाएँ, CAPSKIP_HOST को उसी पर लगाएँ, और API key को CI सिस्टम के secret store में रखें। GitHub-hosted और Azure DevOps hosted एजेंट यहाँ टेस्ट नहीं किए गए। सिर्फ़ LAN वाला मामला टेस्ट हुआ। एजेंट को उस पते और port तक पहुँचने की अनुमति भी होनी चाहिए।

क्या यह xUnit v2, NUnit या MSTest के साथ काम करता है?

इस गाइड के लिए सिर्फ़ xUnit v3 4.0.2 चलाया गया। ऊपर की कई चीज़ें v3 के फ़ीचर हैं, जैसे Assert.SkipUnless, TestContext.Current और assembly fixture attribute। CapSkip की कॉल किसी भी framework में एक जैसी हैं। fixture, timeout और parallelism की सेटिंग्स ही बदलती हैं, इसलिए उनमें से हर एक के लिए अपने framework के डॉक्स देखें।

क्या मैं पुराना VSTest runner रख सकता हूँ?

हाँ। mtp-v2 पैकेज की जगह xunit.v3.mtp-off 4.0.2, xunit.runner.visualstudio 4.0.1 और Microsoft.NET.Test.Sdk 18.10.1 इस्तेमाल करें, और global.json को छोड़ दें। तब dotnet test पहले की तरह काम करता है। उस रास्ते पर डिफ़ॉल्ट console logger skip के कारण प्रिंट नहीं करता, और hang वाले switch का नाम blame-hang-timeout है।

क्या हर build को सॉल्वर वाले टेस्ट चलाने चाहिए?

शायद नहीं। हर एक में कुछ सेकंड लगते हैं, उसे चलता हुआ CapSkip चाहिए और वह एक असली staging साइट को छूता है। उन्हें उनके अपने टेस्ट प्रोजेक्ट में रखें, ताकि आपके unit tests का सामान्य dotnet test कभी सॉल्वर का इंतज़ार न करे। उस प्रोजेक्ट को किसी शेड्यूल पर या रिलीज़ से पहले चलाएँ।

संक्षेप में

xUnit कैप्चा suite को assembly fixture में एक CapSkip क्लाइंट चाहिए और हर टेस्ट के अंदर एक नया हल। टेस्ट का token SDK को पास करें, ताकि Fact का Timeout हल को रोक सके। parallel हलों को runner सेटिंग या gate से CapSkip के 10 थ्रेड से नीचे रखें। SDK 10 पर global.json इस्तेमाल करें। CapSkip बंद हो तो टेस्ट को फेल नहीं, skip करें, और CI में fail-skips इस्तेमाल करें।

हर रात चलने वाला suite कुछ सौ बार हल करता है, और यह गिनती जल्दी ही बहुत बढ़ जाती है। अपनी खुद की Windows मशीन पर चल रहे लोकल कैप्चा बायपास से इसे चलाने का मतलब है कि ये हल कभी बिल में नहीं बदलते।