[{"content":"Graphify: The Missing Map for Brownfield Codebases Back when I was digging into OpenSpec, I kept circling the same problem: on a brownfield codebase, nobodycan tell you with confidence what\u0026rsquo;s actually connected to what. Specs help, but a spec still describes behaviour. That\u0026rsquo;s the gap that pulled me into Graphify.\nGraphify turns a codebase — code, docs, SQL schemas, configs, even PDFs — into a queryable knowledge graph, and ships as a skill for Claude Code, Cursor, Codex, Gemini CLI, Copilot, Aider, and a dozen-plus other assistants. Its like a dependency map for your code base. Instead of an agent guessing at architecture from whatever files happen to be open, it gets a real map to query.\nWhy This Is Different From the Usual RAG Story Most \u0026ldquo;understand my codebase\u0026rdquo; tools reach for embeddings and a vector store. Graphify deliberately doesn\u0026rsquo;t:\nLocal AST parsing, zero LLM calls for code. It uses tree-sitter to build the code graph, which means \u0026ldquo;code maps for free\u0026rdquo; — no API key, no token spend, no hallucinated relationships for the parts it can parse directly. Every edge is labelled. Connections are tagged EXTRACTED (explicit in source) or INFERRED (resolved by Graphify). That\u0026rsquo;s a genuinely useful trust signal — you know exactly which parts of the map to double-check. Real graph traversal, not similarity search. Questions like \u0026ldquo;what connects auth to the database\u0026rdquo; get answered by walking actual edges, not by hoping the nearest embedding is the right one. Broad coverage. 36+ languages, plus docs, PDFs, and video/audio (transcribed locally via faster-whisper). Local-first privacy. Code never leaves your machine; there\u0026rsquo;s no telemetry or usage tracking by default. Setting It Up Prerequisites: Python 3.10+, and uv (recommended) or pipx.\n1. Install the package. Note the PyPI package is graphifyy (double-y) but the command you actually run is graphify:\nuv tool install graphifyy # or pipx install graphifyy 2. Register it with your AI assistant. For Claude Code:\ngraphify install Or scope it to just the current project instead of globally:\ngraphify install --project Other assistants have their own subcommand:\ngraphify cursor install graphify codex install graphify gemini install graphify copilot install graphify aider install 3. Grab optional extras if you need them:\nuv tool install \u0026#34;graphifyy[pdf]\u0026#34; # PDF extraction uv tool install \u0026#34;graphifyy[video]\u0026#34; # video/audio transcription uv tool install \u0026#34;graphifyy[sql]\u0026#34; # SQL schema extraction uv tool install \u0026#34;graphifyy[all]\u0026#34; # everything 4. Fix PATH issues if the graphify command isn\u0026rsquo;t found after install:\nuv tool update-shell # after uv install pipx ensurepath # after pipx install 5. Generate the graph. Inside your AI assistant:\n/graphify . (PowerShell users: drop the leading slash — graphify .)\nThis drops three files into graphify-out/: graph.html (an interactive, clickable force-directed graph), GRAPH_REPORT.md (key concepts and suggested questions), and graph.json (the queryable graph itself).\n6. Query it directly from the CLI once it exists:\ngraphify query \u0026#34;How does the login page connect to authentication?\u0026#34; graphify path \u0026#34;UserService\u0026#34; \u0026#34;DatabasePool\u0026#34; graphify explain \u0026#34;RateLimiter\u0026#34; Useful extraction flags for larger or evolving repos:\ngraphify extract ./src --code-only # local AST only, no API key needed graphify extract ./docs --update # re-extract only changed files graphify extract ./docs --mode deep # richer semantic pass 7. Wire it into the team workflow. One person runs /graphify . and commits graphify-out/; everyone else pulls it and their assistant has the map immediately. graphify hook install auto-rebuilds the graph on every commit, and there\u0026rsquo;s a git merge driver so graph.json unions cleanly instead of conflicting.\nWhat sold me isn\u0026rsquo;t the visualisation.it\u0026rsquo;s graphify path and graphify query as blunt instruments for regression impact analysis. \u0026ldquo;What connects checkout page to database\u0026rdquo; is, word for word, the question I ask before scoping a test suite on an unfamiliar service. Having an agent answer it by walking a real, locally-built graph instead of guessing from vibes is exactly the kind of grounding brownfield work has been missing.\n","permalink":"https://abygeorgea.com/blog/2026/08/05/graphify-mapping-the-codebase-ai-agents-actually-need/","summary":"\u003ch1 id=\"graphify-the-missing-map-for-brownfield-codebases\"\u003eGraphify: The Missing Map for Brownfield Codebases\u003c/h1\u003e\n\u003cp\u003eBack when I was digging into OpenSpec, I kept circling the same problem: on a brownfield codebase, nobodycan tell you with confidence what\u0026rsquo;s actually connected to what. Specs help, but a spec still describes \u003cem\u003ebehaviour\u003c/em\u003e.  That\u0026rsquo;s the gap that pulled me into \u003cstrong\u003e\u003ca href=\"https://github.com/Graphify-Labs/graphify\"\u003eGraphify\u003c/a\u003e\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eGraphify turns a codebase — code, docs, SQL schemas, configs, even PDFs — into a queryable knowledge graph, and ships as a skill for Claude Code, Cursor, Codex, Gemini CLI, Copilot, Aider, and a dozen-plus other assistants. Its like a dependency map for your code base. Instead of an agent guessing at architecture from whatever files happen to be open, it gets a real map to query.\u003c/p\u003e","title":"Graphify - Mapping the Codebase AI Agents Actually Need"},{"content":"In the previous post, we automated our tests in a pipeline. This is the last post in the series, and it covers two things that only really become relevant once a framework has been running for a while. Outgrowing a single machine, and keeping the whole thing maintainable months after the initial build.\nWhen One Machine Is Not Enough Gatling\u0026rsquo;s engine is efficient, and a single reasonably sized machine can simulate a genuinely large number of virtual users before it becomes the bottleneck rather than the system under test. That said, at some point, usually when you are stress testing a system designed for very high real world traffic, the load generator itself becomes the limiting factor, not the application you are trying to test.\nThe first sign of this is usually the load generator\u0026rsquo;s own resource usage. If CPU or network throughput on the machine running Gatling is maxed out while the application under test still has headroom, you are no longer measuring the application\u0026rsquo;s limits, you are measuring your own load generator\u0026rsquo;s limits.\nSplitting Load Across Multiple Machines The most straightforward way to scale beyond one machine is to run the same simulation from several machines at once, each generating a portion of the total intended load, and combine the results afterward.\npublic static int totalUsersPerSec() { return Integer.parseInt(System.getProperty(\u0026#34;targetUsersPerSec\u0026#34;, \u0026#34;100\u0026#34;)); } public static int nodeCount() { return Integer.parseInt(System.getProperty(\u0026#34;nodeCount\u0026#34;, \u0026#34;4\u0026#34;)); } public static int usersPerSecForThisNode() { return totalUsersPerSec() / nodeCount(); } setUp( checkoutScenario.injectOpen( constantUsersPerSec(EnvironmentConfig.usersPerSecForThisNode()) .during(Duration.ofMinutes(15)) ) ).protocols(httpProtocol); Run the same simulation on four separate machines, each configured with nodeCount=4, and together they produce the combined target load, each one only responsible for a quarter of it. This keeps every individual load generator comfortably within its own capacity.\nGatling Enterprise, the commercial offering built on top of the open source engine, handles this kind of distributed orchestration and result aggregation for you directly, including automatically merging reports from every injector into a single unified view. For a self managed setup without that product, running several CI jobs in parallel, each targeting its share of the load, and then manually comparing or combining the resulting reports, is a reasonable and common approach for teams that only occasionally need this level of scale.\nLong-Term Maintenance Habits A framework that works well on day one can still quietly rot over months of active development, the same as any other codebase. A handful of habits keep a Gatling and Java framework healthy well after the initial build.\nTreat simulation code with the same standards as production code. Code review for new scenarios, consistent naming for requests so reports stay readable, and the same linting and formatting tools you would already use on any other Java module in the codebase.\nKeep test data and configuration close to where they are used, not scattered across the repository. We set this up back in part one with a clear folder structure, and it is worth periodically checking that new simulations and scenarios still follow it, since drift tends to creep in as different people add things under time pressure.\nReview your assertions periodically, not just when they are first written. An SLA that made sense against last year\u0026rsquo;s infrastructure and traffic levels might be too lenient or too strict against a system that has scaled up or been re architected since. Revisit the thresholds from part nine on a regular cadence, alongside whatever the current production monitoring data actually shows.\nRetire scenarios that no longer reflect real usage. If analytics show a feature is barely used anymore, a scenario built entirely around it is spending your test run\u0026rsquo;s time and your load generator\u0026rsquo;s capacity on something that no longer matters. Performance testing time is a finite resource on any given run, and it deserves to be spent where real traffic actually goes.\nWrapping Up the Series Over these twelve posts, we went from an empty folder to a real Gatling and Java performance testing framework. Project setup, the Java DSL, injection profiles, feeders, correlation and authentication, checks that catch genuine failures, realistic scenario pacing, the different categories of performance tests, SLAs backed by real assertions, actually reading the report, a working CI pipeline with proper environment handling, and finally scaling beyond a single machine along with the habits that keep it all healthy.\nNone of this needs to land in a single sprint on a real project. Build it up in roughly this order, the same way we walked through it here, and each piece supports the ones that come after it.\n","permalink":"https://abygeorgea.com/blog/2026/06/11/scaling-gatling-distributed-load-and-maintenance/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2026/05/28/running-gatling-in-cicd-pipelines-and-environments/\"\u003eprevious post\u003c/a\u003e, we automated our tests in a pipeline. This is the last post in the series, and it covers two things that only really become relevant once a framework has been running for a while. Outgrowing a single machine, and keeping the whole thing maintainable months after the initial build.\u003c/p\u003e\n\u003ch2 id=\"when-one-machine-is-not-enough\"\u003eWhen One Machine Is Not Enough\u003c/h2\u003e\n\u003cp\u003eGatling\u0026rsquo;s engine is efficient, and a single reasonably sized machine can simulate a genuinely large number of virtual users before it becomes the bottleneck rather than the system under test. That said, at some point, usually when you are stress testing a system designed for very high real world traffic, the load generator itself becomes the limiting factor, not the application you are trying to test.\u003c/p\u003e","title":"Scaling Up: Distributed Load Generation and Long-Term Test Maintenance"},{"content":"In the previous post, we learned to actually read what Gatling produces. None of this is worth much long term if it only ever runs on someone\u0026rsquo;s laptop before a release, run manually and inconsistently. Today we automate it properly, and we sort out configuration across environments while we are at it, since the two problems tend to show up together in practice.\nExternalizing the Base URL Every simulation so far has hardcoded a base URL directly in the protocol configuration. That falls apart the moment you want to run the exact same simulation against a local environment, a staging environment, and occasionally production itself for a controlled test. The fix is to read it from a system property instead.\n// config/EnvironmentConfig.java package config; public class EnvironmentConfig { public static String baseUrl() { return System.getProperty(\u0026#34;baseUrl\u0026#34;, \u0026#34;http://localhost:8080\u0026#34;); } } // config/HttpProtocolConfig.java package config; import io.gatling.javaapi.http.HttpProtocolBuilder; import static io.gatling.javaapi.http.HttpDsl.*; public class HttpProtocolConfig { public static final HttpProtocolBuilder httpProtocol = http .baseUrl(EnvironmentConfig.baseUrl()) .acceptHeader(\u0026#34;application/json\u0026#34;); } Now the base URL defaults to a sensible local value, but it can be overridden at run time from the command line without touching any code.\nmvn gatling:test -DbaseUrl=https://staging.example.com This same pattern extends naturally to anything else that varies by environment, like credentials for a test account, or the injection profile itself, since a staging environment might warrant a much smaller load than a production adjacent performance environment sized closer to real capacity.\npublic static int targetUsersPerSec() { return Integer.parseInt(System.getProperty(\u0026#34;targetUsersPerSec\u0026#34;, \u0026#34;10\u0026#34;)); } A GitHub Actions Workflow With configuration externalized, wiring this into a pipeline is a matter of installing Java, running Maven, and passing the right system properties for whichever environment the workflow targets.\nname: Performance Test on: workflow_dispatch: inputs: baseUrl: description: \u0026#39;Target environment base URL\u0026#39; required: true default: \u0026#39;https://staging.example.com\u0026#39; targetUsersPerSec: description: \u0026#39;Target users per second\u0026#39; required: true default: \u0026#39;10\u0026#39; jobs: performance-test: runs-on: ubuntu-latest timeout-minutes: 30 steps: - uses: actions/checkout@v4 - name: Set up JDK uses: actions/setup-java@v4 with: distribution: \u0026#39;temurin\u0026#39; java-version: \u0026#39;21\u0026#39; - name: Run Gatling simulation run: | mvn gatling:test \\ -DbaseUrl=${{ inputs.baseUrl }} \\ -DtargetUsersPerSec=${{ inputs.targetUsersPerSec }} - name: Upload Gatling report if: always() uses: actions/upload-artifact@v4 with: name: gatling-report path: target/gatling/ retention-days: 30 This one uses workflow_dispatch, meaning it runs on demand rather than on every push, which is the right trigger for most performance tests. Unlike a fast functional suite, a meaningful load test can take anywhere from several minutes to several hours, and running one automatically on every single commit to a shared branch is rarely the right tradeoff. A manual trigger, or a nightly scheduled run against a dedicated performance environment, both fit the actual rhythm of performance testing much better than running on every pull request the way you might for unit tests.\nThe if: always() on the upload step matters here just as much as it would in a functional test pipeline. You want the report from a failing run, when an assertion trips and the build goes red, at least as much as you want one from a passing run.\nScheduling a Regular Baseline Run Alongside on demand runs, it is worth scheduling a recurring run against a stable environment, purely to track how performance trends over time rather than just checking it at a single point before a release.\non: schedule: - cron: \u0026#39;0 2 * * *\u0026#39; workflow_dispatch: This runs automatically every night at two in the morning, in addition to still being triggerable manually whenever needed. Having this history matters more than any single run, since it is what lets you notice a gradual regression creeping in over several weeks, something that a single one-off test run before a release would never catch, since it only ever compares against whatever the baseline happened to be on that one day.\nKeeping Credentials Out of the Workflow File If your simulations need real credentials for a staging or performance environment, never put them directly in the workflow YAML. Use your CI platform\u0026rsquo;s secrets management and reference them the same way you would for any other pipeline.\n- name: Run Gatling simulation env: TEST_USER_PASSWORD: ${{ secrets.PERF_TEST_USER_PASSWORD }} run: | mvn gatling:test \\ -DbaseUrl=${{ inputs.baseUrl }} \\ -DtestUserPassword=$TEST_USER_PASSWORD And read it in Java the same way as any other system property, through your EnvironmentConfig class, keeping the actual secret value out of both the simulation code and the workflow file itself.\nWrapping Up Externalizing environment specific values turns a hardcoded simulation into one that runs cleanly against dev, staging, or production adjacent environments without any code changes. Wiring that into a scheduled and on demand CI pipeline, with reports archived automatically, is what turns performance testing from an occasional manual exercise into an ongoing, trustworthy part of how the team ships software.\nNext time, in our final post of this series, we look at what happens once a single Gatling instance is not enough, through distributed load generation, and the long term habits that keep a performance testing framework healthy over months of active use.\n","permalink":"https://abygeorgea.com/blog/2026/05/28/running-gatling-in-cicd-pipelines-and-environments/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2026/05/14/reading-gatling-reports-percentiles-and-throughput/\"\u003eprevious post\u003c/a\u003e, we learned to actually read what Gatling produces. None of this is worth much long term if it only ever runs on someone\u0026rsquo;s laptop before a release, run manually and inconsistently. Today we automate it properly, and we sort out configuration across environments while we are at it, since the two problems tend to show up together in practice.\u003c/p\u003e\n\u003ch2 id=\"externalizing-the-base-url\"\u003eExternalizing the Base URL\u003c/h2\u003e\n\u003cp\u003eEvery simulation so far has hardcoded a base URL directly in the protocol configuration. That falls apart the moment you want to run the exact same simulation against a local environment, a staging environment, and occasionally production itself for a controlled test. The fix is to read it from a system property instead.\u003c/p\u003e","title":"Running Gatling in CI/CD: Pipelines and Environment Configuration"},{"content":"In the previous post, we set up assertions so a build fails automatically when performance regresses. A passing build is a good start, but the report itself still holds a lot of useful detail worth understanding properly, especially when something does go wrong and you need to figure out why.\nEvery Gatling run produces a self contained HTML report. Open it with the show command from part one.\nmvn gatling:test # once the run finishes, Gatling prints the path to the report, # or open the latest one directly under target/gatling/ The Global Stats Page The landing page of the report summarizes the entire simulation. A few numbers here matter more than the rest. The percentile breakdown, shown as a chart and a table, tells you the distribution of response times across every single request in the run, not just an average. The requests per second chart over time shows whether your injection profile actually produced the load shape you intended, which is worth checking even on a passing run, since a misconfigured injection profile can silently produce far less load than you think it did.\nThe error percentage for the whole run is shown prominently too, and it is worth treating any non zero error rate as something to investigate, even if it stays under whatever threshold your assertions allow. A small number of consistent errors, appearing steadily throughout a run, usually points at a real and reproducible issue rather than random noise.\nPer-Request Breakdown Below the global summary, the report breaks every named request out individually, with its own response time distribution and error rate. This is where you actually find which specific request is dragging the whole simulation down. A global ninety fifth percentile of one and a half seconds could mean every request is moderately slow, or it could mean nine out of ten requests are fast and one specific endpoint is consistently terrible. The per-request view is what tells you which of those two very different situations you are actually looking at.\nCross reference this against the request names you chose back when writing the scenario. This is exactly why naming requests clearly, like \u0026quot;Get Product Catalog\u0026quot; instead of a generic name, pays off later, since a report full of clearly named requests is far easier to scan for the one that actually needs attention.\nResponse Time Over Time A chart most people skim past too quickly is the response time distribution over the duration of the run, rather than aggregated across the whole test. This view is what tells you whether performance degraded progressively as load increased, stayed flat and consistent throughout, or spiked briefly at one specific point in time. If you ran a staged injection profile like the warm up, hold, and cool down pattern from earlier in this series, this chart is where you can actually see each of those phases reflected in the response time behavior, confirming the test ran the way you intended.\nActive Users Over Time This chart shows how many virtual users were actually active at each point during the run, which is a direct visual confirmation of your injection profile. Comparing this against the response time chart side by side is one of the more useful habits to build. If response times start climbing at the exact moment active users cross a certain threshold, that threshold is a genuinely useful data point, arguably more useful than any single aggregate number in the whole report, since it tells you approximately where the system\u0026rsquo;s real capacity limit sits.\nDistinguishing Errors From Slowness The report separates failed requests from slow ones, and it is worth checking both independently rather than assuming one implies the other. A request can be fast and wrong, like the misconfigured error page returning a 200 status we talked about back in the post on checks. A request can also be slow but eventually correct, timing out right at the edge of an acceptable threshold without technically failing. Neither of these shows up clearly if you only glance at the top level pass or fail assertion result, which is exactly why it is worth opening the actual report even when the build goes green.\nBuilding a Habit Around the Report The most useful habit here is simple. Do not just check whether the assertions passed and move on. Open the report, at minimum on any run against a new environment, after any significant application change, and any time a test result surprises you in either direction. The report holds detail that a single pass or fail signal from CI cannot express, and the small amount of time it takes to actually look at it regularly pays off the first time it catches something a plain assertion would have missed entirely.\nWrapping Up The global stats page tells you the headline numbers. The per-request breakdown tells you where a problem actually lives. The time series charts tell you how behavior evolved over the course of the run. Together they turn a report from a single pass or fail signal into a genuinely useful diagnostic tool.\nNext time, we take everything we have built and wire it into a CI pipeline, including how to manage configuration across different environments cleanly.\n","permalink":"https://abygeorgea.com/blog/2026/05/14/reading-gatling-reports-percentiles-and-throughput/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2026/04/30/setting-performance-slas-with-gatling-assertions/\"\u003eprevious post\u003c/a\u003e, we set up assertions so a build fails automatically when performance regresses. A passing build is a good start, but the report itself still holds a lot of useful detail worth understanding properly, especially when something does go wrong and you need to figure out why.\u003c/p\u003e\n\u003cp\u003eEvery Gatling run produces a self contained HTML report. Open it with the show command from part one.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003emvn gatling:test\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# once the run finishes, Gatling prints the path to the report,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# or open the latest one directly under target/gatling/\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"the-global-stats-page\"\u003eThe Global Stats Page\u003c/h2\u003e\n\u003cp\u003eThe landing page of the report summarizes the entire simulation. A few numbers here matter more than the rest. The percentile breakdown, shown as a chart and a table, tells you the distribution of response times across every single request in the run, not just an average. The requests per second chart over time shows whether your injection profile actually produced the load shape you intended, which is worth checking even on a passing run, since a misconfigured injection profile can silently produce far less load than you think it did.\u003c/p\u003e","title":"Reading Gatling Reports: Percentiles, Throughput, and What Actually Matters"},{"content":"Beyond the Co-Pilot: My Q2 Plan for Multi-Agent Automation First off, if you noticed complete radio silence on here over the last quarter, I have a pretty solid excuse! Work has been an absolute whirlwind. Between non-stop flights between Australia, Singapore and India, setting up new offshore engineering teams, and establishing new engineering ways of working from scratch, my calendar completely ate my side projects for breakfast.\nOn top of that, team resource constraints meant I had to jump back into active microservice development alongside my management duties—building out core service APIs, handling edge cases, and pushing production code.\nNow that the dust is starting to settle, I\u0026rsquo;m eager to get back to my AI agentic experiments.\nLooking at where quality engineering is heading right now in Q23 2026, the concept of a single \u0026ldquo;AI co-pilot\u0026rdquo; sitting in your IDE writing test scripts is already starting to feel outdated. Where I’m spending my cycles over the next few months is diving deep into Specialized Multi-Agent Systems (MAS)—building networks of dedicated agents that communicate, execute, and evaluate test suites dynamically.\nWhat I’m Planning \u0026amp; Experimenting With Next Instead of relying on one giant prompt to write, run, and debug a test suite, I\u0026rsquo;m mapping out a coordinated pipeline of specialized, single-responsibility agents:\nThe Analyst Agent: Reads raw user stories, PR diffs, and OpenAPI schemas to map out the exact test surface and required test scenarios. The Authoring Agent: Takes that scope and generates lean, idiomatic test automation scripts across python/playwright or REST clients. The Execution \u0026amp; Healing Agent: Monitors headless browser or API runs, dynamically resolving broken locators or altered parameters on the fly. The Triage Agent: Digs through failure logs, stack traces, and DOM snapshots to present developers with an actionable root-cause diagnosis. The Next Experiment: LLM-as-a-Judge Evals To keep everything reliable, the second half of this project will focus on evaluation. How do you automatically verify that an agent-generated test suite actually covers the right business logic without manually reviewing every line?\nI’m setting up an evaluation loop using LLM-as-a-Judge patterns to score generated suites against coverage metrics, readability, and execution stability before anything gets merged into our pipelines.\nExpect a few technical deep-dives and repository walkthroughs as I get these agent handoffs wired up!\n","permalink":"https://abygeorgea.com/blog/2026/05/10/agentic-qa/","summary":"\u003ch1 id=\"beyond-the-co-pilot-my-q2-plan-for-multi-agent-automation\"\u003eBeyond the Co-Pilot: My Q2 Plan for Multi-Agent Automation\u003c/h1\u003e\n\u003cp\u003eFirst off, if you noticed complete radio silence on here over the last quarter, I have a pretty solid excuse! Work has been an absolute whirlwind. Between non-stop flights between Australia, Singapore and India, setting up new offshore engineering teams, and establishing new engineering ways of working from scratch, my calendar completely ate my side projects for breakfast.\u003c/p\u003e\n\u003cp\u003eOn top of that, team resource constraints meant I had to jump back into active microservice development alongside my management duties—building out core service APIs, handling edge cases, and pushing production code.\u003c/p\u003e","title":"Agentic QA"},{"content":"In the previous post, we covered the different types of performance tests. Today we cover something that turns any of those tests from \u0026ldquo;someone eyeballs the report and makes a judgment call\u0026rdquo; into an objective, automatable pass or fail result. Assertions.\nWithout assertions, a Gatling run finishes and hands you a report, and a human has to decide whether the numbers in it are acceptable. That does not scale, and it definitely does not work inside a CI pipeline where nobody is watching the run happen live. Assertions let you encode your performance requirements directly into the simulation, so the build itself fails when those requirements are not met.\nGlobal Assertions Assertions attach to the setUp call, alongside the protocol.\nsetUp( checkoutScenario.injectOpen( rampUsersPerSec(1).to(20).during(Duration.ofMinutes(3)), constantUsersPerSec(20).during(Duration.ofMinutes(15)) ) ).protocols(httpProtocol) .assertions( global().responseTime().percentile3().lt(1500), global().successfulRequests().percent().gt(99.0) ); This says two things at once. The ninety fifth percentile response time across every single request in the whole run must be under one and a half seconds, and at least ninety nine percent of all requests must succeed. If either condition fails, Gatling exits with a non zero status code, which is exactly the signal a CI pipeline needs to fail the build automatically.\nWhy Percentiles Instead of Averages It is worth being deliberate about using a percentile rather than an average here. An average hides outliers completely. If ninety percent of your users get a response in two hundred milliseconds and ten percent wait eight full seconds, the average might still look perfectly reasonable, while a meaningful chunk of your real users are having a genuinely bad experience. A percentile like the ninety fifth or ninety ninth tells you what the slower end of your user base is actually experiencing, which is almost always the more useful number for a real SLA.\nGatling gives you access to several percentiles directly.\nglobal().responseTime().percentile1().lt(500) // 50th percentile, the median global().responseTime().percentile2().lt(900) // 75th percentile global().responseTime().percentile3().lt(1500) // 95th percentile global().responseTime().percentile4().lt(3000) // 99th percentile A common pattern is asserting on more than one percentile at once, a tight bound on the median for the typical experience, and a looser bound on the ninety ninth percentile to catch the genuinely bad outliers without being so strict that normal variance fails the build constantly.\nPer-Request Assertions Global assertions cover the whole simulation, but sometimes one specific request matters more than the rest, and deserves its own explicit bound. You can scope an assertion down to a single named request.\n.assertions( global().responseTime().percentile3().lt(1500), global().successfulRequests().percent().gt(99.0), details(\u0026#34;Checkout\u0026#34;).responseTime().percentile3().lt(2000), details(\u0026#34;Checkout\u0026#34;).failedRequests().percent().lt(1.0) ); details(\u0026quot;Checkout\u0026quot;) scopes the assertion to just the request named \u0026ldquo;Checkout\u0026rdquo; in your scenario. This matters because a single slow endpoint can easily hide inside a healthy looking global average across dozens of other fast requests. If checkout specifically is the part of the journey your business cares most about, it deserves its own explicit, and often stricter, threshold.\nAssertions on Throughput Assertions are not limited to timing and success rate. You can also assert on the request throughput the system actually achieved during the run.\n.assertions( global().requestsPerSec().gt(15.0) ); This is a useful sanity check for a scenario where you expect the system to sustain a certain throughput. If the assertion fails, it usually means the system could not keep up with the intended injection rate, which is a meaningful finding on its own, separate from whatever the individual response times looked like.\nMaking SLAs a Team Conversation, Not a Guess The numbers themselves matter less than where they come from. Picking a response time threshold because it sounds reasonable is a weak foundation for an SLA. A better approach pulls the number from somewhere real, an existing production monitoring dashboard showing what current response times actually look like, a documented business requirement, or a competitor benchmark if the application is customer facing. Whatever the source, write it down and treat the assertion as living documentation of an agreed target, not just a number embedded in test code that nobody remembers agreeing to six months later.\nWrapping Up Assertions turn a load test from something that produces a report someone has to interpret into something that produces a clear, automatable pass or fail result. Combine global assertions with per-request thresholds on the parts of the journey that matter most, and you get a build that fails loudly the moment performance genuinely regresses.\nNext time, we go back to the HTML report itself and actually learn how to read it properly, since a passing assertion does not mean there is nothing worth investigating in the details.\n","permalink":"https://abygeorgea.com/blog/2026/04/30/setting-performance-slas-with-gatling-assertions/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2026/04/16/load-stress-soak-spike-testing-with-gatling/\"\u003eprevious post\u003c/a\u003e, we covered the different types of performance tests. Today we cover something that turns any of those tests from \u0026ldquo;someone eyeballs the report and makes a judgment call\u0026rdquo; into an objective, automatable pass or fail result. Assertions.\u003c/p\u003e\n\u003cp\u003eWithout assertions, a Gatling run finishes and hands you a report, and a human has to decide whether the numbers in it are acceptable. That does not scale, and it definitely does not work inside a CI pipeline where nobody is watching the run happen live. Assertions let you encode your performance requirements directly into the simulation, so the build itself fails when those requirements are not met.\u003c/p\u003e","title":"Setting Performance SLAs With Gatling Assertions"},{"content":"Every test suite past a certain size accumulates flaky tests, ones that fail occasionally for reasons that have nothing to do with the code under test. A slow CI runner, a race condition in test setup, a shared resource another job happened to be using at the same time. The problem is never that flaky tests exist. The problem is triage time. Someone has to look at a failure, decide whether it is real or noise, and that decision eats far more time across a team than it should, especially on a suite with hundreds of tests running on every merge.\nThat is the specific, narrow problem I wanted an AI coding agent to help with, using Claude Code CLI against our own CI failure history.\nWhy This Is a Good Fit for an Agent Triage is fundamentally a pattern matching task before it is a fixing task. Has this exact test failed before. Does the failure message match a known flaky signature. Did other, unrelated tests fail in the same run, which usually points at an environment problem rather than the test itself. A human doing this well is mostly cross referencing history, not reasoning from first principles, and that is exactly the kind of task an agent with access to CI logs and test history can do quickly and consistently.\nIt is worth being explicit about what this does not mean. The agent is not deciding what is broken and fixing it unattended. It is doing the first, most repetitive step, sorting failures into likely-flaky and likely-real, so a human spends their attention on the smaller, harder pile.\nSetting Up the Input The agent needs failure history to pattern match against, not just the single failure in front of it. A simple approach is exporting recent CI run results into a structured log the agent can read.\ngh run list --workflow=ci.yml --limit 50 --json databaseId,conclusion \\ | jq -c \u0026#39;.[] | select(.conclusion == \u0026#34;failure\u0026#34;)\u0026#39; \u0026gt; recent-failures.json for id in $(jq -r \u0026#39;.databaseId\u0026#39; recent-failures.json); do gh run view \u0026#34;$id\u0026#34; --log-failed \u0026gt;\u0026gt; failure-logs.txt done That gives Claude Code CLI a real dataset, actual failure messages and stack traces across recent runs, rather than asking it to guess from a single failing test in isolation.\nAsking for Triage, Not a Fix The prompt matters here. Asking an agent to fix a failing test invites it to change assertions or add retries just to make the red go green, which is the opposite of what you want. Asking it to triage keeps the scope narrow and the output reviewable.\nHere is our test failure history from the last 50 CI runs (failure-logs.txt) and today\u0026#39;s failing test output (today-failure.txt). For the failure in today-failure.txt, tell me: 1. Has this exact test failed before in the history, and how often 2. Does the failure message match a pattern seen in other, unrelated tests in the same time window (suggesting an environment issue) 3. Your confidence this is flaky vs a real regression, with reasoning 4. What evidence would change your assessment Do not suggest a fix. Only triage. A response worth trusting looks something like this, specific and checkable rather than a vague guess.\nPaymentAuthorizationTest.testConcurrentAuthorization has failed 6 times in the last 50 runs, always with a connection pool timeout, never with an assertion failure on the actual authorization logic. Two other, unrelated tests failed in the same run window on 3 of those 6 occasions, suggesting a shared database connection pool under load during CI, not a bug in the test or the code it exercises. Confidence: likely flaky (not a regression) This assessment would change if the failure ever included an assertion mismatch on the authorization result itself, rather than a timeout during setup. That last section, what evidence would change the assessment, is worth insisting on in every triage prompt. It forces the agent to state its reasoning in a falsifiable way, rather than handing back a confident sounding conclusion with nothing underneath it.\nWhere Trust Has to Stop This workflow earns trust for exactly one narrow claim, sorting failures by likely cause based on pattern matching against history the agent can actually see. It does not earn trust for silently marking a test as flaky and skipping it going forward, and it should never be allowed to do that without a human confirming first. A genuinely new bug can absolutely look like noise on its very first occurrence, before any history exists to distinguish it, and an agent triaging purely on pattern frequency has no way to catch that on the first pass.\nThe practical guardrail that works well is treating the agent\u0026rsquo;s output as a suggestion attached to the CI failure notification, not an automatic action. A test flagged as likely flaky still shows up for a human to glance at and confirm, it just gets deprioritized in the queue rather than dropped from consideration entirely. The time saved is real, most of a team\u0026rsquo;s triage time goes into the easy, obviously flaky cases, and this clears those quickly so people can spend their attention on the smaller number of failures that actually need real investigation.\nWhat This Looks Like at Scale Once this triage step is reliable enough to trust for prioritization, wiring it into the CI failure notification itself is a natural next step, so a failing build in a pull request comes with a triage note attached automatically rather than requiring someone to run the prompt by hand each time. That is the direction worth taking this next, treating the agent\u0026rsquo;s triage output as one more piece of context in the failure report, right alongside the stack trace, rather than a separate manual step someone has to remember to run.\n","permalink":"https://abygeorgea.com/blog/2026/04/22/flaky-test-triage-with-claude-code-cli/","summary":"\u003cp\u003eEvery test suite past a certain size accumulates flaky tests, ones that fail occasionally for reasons that have nothing to do with the code under test. A slow CI runner, a race condition in test setup, a shared resource another job happened to be using at the same time. The problem is never that flaky tests exist. The problem is triage time. Someone has to look at a failure, decide whether it is real or noise, and that decision eats far more time across a team than it should, especially on a suite with hundreds of tests running on every merge.\u003c/p\u003e","title":"Using Claude Code CLI for Flaky Test Triage"},{"content":"In the previous post, we made scenarios behave more like real users. With everything we have built so far, injection profiles, feeders, correlation, checks, and pacing, we finally have enough pieces to talk about the different types of performance tests properly, since \u0026ldquo;run a load test\u0026rdquo; actually covers several genuinely different testing goals.\nLoad Testing: Expected Traffic A load test answers a simple question. Does the system perform acceptably under the traffic level you actually expect. This is the baseline test you should have running regularly, ideally on every significant release.\nsetUp( checkoutScenario.injectOpen( rampUsersPerSec(1).to(20).during(Duration.ofMinutes(3)), constantUsersPerSec(20).during(Duration.ofMinutes(15)) ) ).protocols(httpProtocol); Ramp up to your expected peak rate, hold it there long enough to see stable behavior, and check response times and error rates stay within your targets throughout. Twenty users per second here is a stand in for whatever your actual expected peak traffic looks like, based on real analytics rather than a guess.\nStress Testing: Finding the Breaking Point A stress test deliberately pushes past expected traffic to find out where the system actually starts to fail, and how it fails when it does. This is not about proving the system is fine. It is about understanding its limits before a real traffic spike finds them for you.\nsetUp( checkoutScenario.injectOpen( rampUsersPerSec(1).to(100).during(Duration.ofMinutes(10)) ) ).protocols(httpProtocol); Ramp well beyond your expected peak, in this case to a rate five times higher than the load test above, and watch closely for where things start to degrade. Response times climbing steadily is one signal. A sudden spike in error rates is another. The most useful outcome of a stress test is not a pass or fail, it is knowing exactly which component buckles first, whether that is a database connection pool, a downstream API, or the application server itself running out of threads.\nSoak Testing: Long Duration Stability A soak test, sometimes called an endurance test, runs a moderate and sustainable load for a long duration, often several hours, looking for problems that only show up over time. Memory leaks, slow resource exhaustion, log files filling up disk space, database connections that never quite get released properly.\nsetUp( checkoutScenario.injectOpen( rampUsersPerSec(1).to(10).during(Duration.ofMinutes(5)), constantUsersPerSec(10).during(Duration.ofHours(4)) ) ).protocols(httpProtocol); The load level here is deliberately moderate, well within normal capacity. The point is not to stress the system, it is to give slow, gradual problems enough time to actually surface. A memory leak that adds a few megabytes per hour is invisible in a fifteen minute load test and very visible after four hours.\nSpike Testing: Sudden Bursts A spike test checks how a system handles a sudden, sharp increase in traffic, then how it recovers once that spike passes. This is the closest fit for atOnceUsers, the profile we were cautious about back in part three.\nsetUp( checkoutScenario.injectOpen( constantUsersPerSec(5).during(Duration.ofMinutes(5)), atOnceUsers(300), constantUsersPerSec(5).during(Duration.ofMinutes(5)) ) ).protocols(httpProtocol); This holds a light baseline load, throws three hundred users in all at once to simulate something like a flash sale opening or a marketing email going out to a large list, and then drops back to baseline. What you want to see here is that the system survives the spike without falling over entirely, and that it recovers cleanly back to normal response times once the spike passes, rather than staying degraded long after the burst of traffic is gone.\nChoosing the Right Test for the Right Question None of these test types replace the others, and running only one of them gives you an incomplete picture. A load test tells you whether normal operation is healthy. A stress test tells you where the ceiling is. A soak test tells you whether the system stays healthy over time. A spike test tells you how gracefully the system handles sudden change. A mature performance testing practice runs all four at different points in a release cycle, load tests frequently since they are quick, and stress, soak, and spike tests at a slower cadence since they take longer and are usually run against a more production like environment.\nWrapping Up The injection profile you choose is really a direct expression of the question you are trying to answer. Being explicit about which of these four test types you are running, before you write a single line of Gatling code, keeps the results focused and the report meaningful to whoever reads it afterward.\nNext time, we look at turning \u0026ldquo;the results looked fine to me\u0026rdquo; into something objective, by setting explicit performance SLAs with Gatling\u0026rsquo;s assertion API.\n","permalink":"https://abygeorgea.com/blog/2026/04/16/load-stress-soak-spike-testing-with-gatling/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2026/04/02/modeling-realistic-user-journeys-in-gatling/\"\u003eprevious post\u003c/a\u003e, we made scenarios behave more like real users. With everything we have built so far, injection profiles, feeders, correlation, checks, and pacing, we finally have enough pieces to talk about the different types of performance tests properly, since \u0026ldquo;run a load test\u0026rdquo; actually covers several genuinely different testing goals.\u003c/p\u003e\n\u003ch2 id=\"load-testing-expected-traffic\"\u003eLoad Testing: Expected Traffic\u003c/h2\u003e\n\u003cp\u003eA load test answers a simple question. Does the system perform acceptably under the traffic level you actually expect. This is the baseline test you should have running regularly, ideally on every significant release.\u003c/p\u003e","title":"Load, Stress, Soak, and Spike Testing: Choosing the Right Injection Strategy"},{"content":"Spec-Driven Development for the Rest of Us: OpenSpec Last month I found GitHub\u0026rsquo;s Spec Kit and how it flips the traditional dev workflow — spec as the durable source of truth, code as the disposable output. Spec Kit\u0026rsquo;s /constitution-first, plan-then-build ceremony assumes you\u0026rsquo;re starting mostly from a blank slate. Almost nothing I touch day-to-day looks like that. It\u0026rsquo;s always years old services, inherited conventions nobody remembers agreeing to, and test suites that are more archaeology than architecture.\nThat\u0026rsquo;s the gap OpenSpec is aimed at, and it\u0026rsquo;s why it\u0026rsquo;s grabbed my attention this month.\nSpecs as a Mirror, Not a Blueprint The core difference in philosophy is subtle but important. Spec Kit\u0026rsquo;s specs describe what a system should become. OpenSpec\u0026rsquo;s specs describe what a system currently does — and then layers proposed changes on top as explicit, reviewable deltas.\nIn practice that means:\nA living specs/ directory that mirrors actual, current system behaviour — not an aspirational design doc. Change proposals as the unit of work: before an AI agent touches code, it drafts a proposal describing the delta against the existing spec — what capability is changing, why, and what the new expected behaviour is. Proposals get reviewed like a pull request, before implementation starts, not after. The agent implements against both the existing spec and the approved delta, which keeps it anchored to established conventions instead of quietly inventing a fresh architecture halfway through a feature. It\u0026rsquo;s a much lighter-weight loop than Spec Kit\u0026rsquo;s full constitution-to-tasks pipeline, and that\u0026rsquo;s deliberate — it\u0026rsquo;s built to slot into normal sprint-sized changes on a codebase that already exists, rather than kicking off a project.\nThe Bit I Actually Care About The thing that got me interested isn\u0026rsquo;t the workflow ceremony, it\u0026rsquo;s the byproduct: an always-current spec of what the system actually does. Every brownfield project I\u0026rsquo;ve worked on has the same problem — nobody can tell you with confidence what\u0026rsquo;s actually supported without reading the code (or the tests, when they\u0026rsquo;re trustworthy, which is inconsistent). If change proposals genuinely keep the spec directory honest, that\u0026rsquo;s a standing artifact I can use directly for regression impact analysis and test-scope decisions, without reverse-engineering behaviour from a diff.\nThe other appeal is that it plays nicer with AI coding agents on legacy code specifically. An agent regenerating code from a green-field spec is one thing; an agent that has to respect eleven years of quiet architectural decisions is another. Anchoring it to a delta against documented current behaviour, instead of turning it loose with a fresh mental model, feels like the safer default for anything I\u0026rsquo;d actually let near production.\nSpec Kit and OpenSpec aren\u0026rsquo;t really competitors — they\u0026rsquo;re solving for opposite ends of a project\u0026rsquo;s lifecycle. Spec Kit wants to plan a system into existence; OpenSpec wants to safely evolve one that already has scars. Given how much of my work lives in the second category, I suspect I\u0026rsquo;ll get more day-to-day mileage out of this one. Next step is trying a real change proposal against one of our older services and seeing how well the generated spec actually matches reality.\n","permalink":"https://abygeorgea.com/blog/2026/04/04/openspec-spec-driven-development-for-brownfield-projects/","summary":"\u003ch1 id=\"spec-driven-development-for-the-rest-of-us-openspec\"\u003eSpec-Driven Development for the Rest of Us: OpenSpec\u003c/h1\u003e\n\u003cp\u003eLast month I found GitHub\u0026rsquo;s Spec Kit and how it flips the traditional dev workflow — spec as the durable source of truth, code as the disposable output.  Spec Kit\u0026rsquo;s \u003ccode\u003e/constitution\u003c/code\u003e-first, plan-then-build ceremony assumes you\u0026rsquo;re starting mostly from a blank slate. Almost nothing I touch day-to-day looks like that. It\u0026rsquo;s always years old services, inherited conventions nobody remembers agreeing to, and test suites that are more archaeology than architecture.\u003c/p\u003e","title":"OpenSpec - Spec driven development for brownfield projects"},{"content":"In the previous post, we made sure our checks actually catch real failures. Today we look at something just as important but easier to overlook, whether your scenario actually behaves like a real user in the first place.\nA scenario that fires request after request with zero delay between them does not represent any real visitor to your site. It represents a script racing through steps as fast as the network allows. That produces load numbers, but not necessarily useful ones, since real traffic has gaps in it while people actually read a page, think about what to click next, or get distracted by something else entirely.\nBasic Pauses We have already used the simplest form of this, a fixed pause between steps.\n.exec(http(\u0026#34;View Product\u0026#34;).get(\u0026#34;/products/42\u0026#34;)) .pause(3) .exec(http(\u0026#34;Add To Cart\u0026#34;).post(\u0026#34;/cart/items\u0026#34;)) This waits exactly three seconds between the two requests. It is better than nothing, but every single virtual user pausing for exactly the same amount of time is itself a little unrealistic. Real users vary a lot in how long they take.\nRandomized Pauses Gatling supports a range instead of a fixed value, which spreads that pause out more naturally across your population of virtual users.\n.exec(http(\u0026#34;View Product\u0026#34;).get(\u0026#34;/products/42\u0026#34;)) .pause(2, 8) .exec(http(\u0026#34;Add To Cart\u0026#34;).post(\u0026#34;/cart/items\u0026#34;)) Now each user pauses somewhere between two and eight seconds, picked randomly. Across thousands of virtual users, this produces a much smoother, more realistic distribution of request timing than everyone pausing for the exact same duration.\nSetting a Global Pause Policy Rather than tuning every single pause call by hand, you can set a default pause type for the whole simulation, which changes how Gatling interprets the numbers you give it.\nsetUp( scn.injectOpen(rampUsers(100).during(Duration.ofMinutes(5))) ).protocols(httpProtocol) .pauses(exponentialPauses()); exponentialPauses shapes pause durations around an exponential distribution centered on whatever value you pass to pause, which tends to model real human think time more naturally than a flat uniform range, since most people pause for a moderate amount of time and a smaller number pause for much longer, rather than everyone being equally likely to pause for any duration in a fixed window.\nModeling Multiple User Paths Real traffic is not one single journey repeated by everyone. Some visitors browse and leave. Some search directly for a specific item. Some are returning customers who go straight to their order history. Gatling lets you weight different scenarios against each other to reflect this mix.\nScenarioBuilder casualBrowser = scenario(\u0026#34;Casual Browser\u0026#34;) .exec(http(\u0026#34;Home Page\u0026#34;).get(\u0026#34;/\u0026#34;)) .pause(3, 6) .exec(http(\u0026#34;Browse Category\u0026#34;).get(\u0026#34;/category/electronics\u0026#34;)) .pause(2, 5) .exec(http(\u0026#34;View Product\u0026#34;).get(\u0026#34;/products/42\u0026#34;)); ScenarioBuilder directSearcher = scenario(\u0026#34;Direct Searcher\u0026#34;) .exec(http(\u0026#34;Search\u0026#34;).get(\u0026#34;/search?q=wireless+mouse\u0026#34;)) .pause(1, 3) .exec(http(\u0026#34;View Product\u0026#34;).get(\u0026#34;/products/42\u0026#34;)); ScenarioBuilder returningCustomer = scenario(\u0026#34;Returning Customer\u0026#34;) .exec(http(\u0026#34;Login\u0026#34;).post(\u0026#34;/auth/login\u0026#34;)) .pause(1, 2) .exec(http(\u0026#34;Order History\u0026#34;).get(\u0026#34;/account/orders\u0026#34;)); setUp( casualBrowser.injectOpen(rampUsers(60).during(Duration.ofMinutes(5))), directSearcher.injectOpen(rampUsers(30).during(Duration.ofMinutes(5))), returningCustomer.injectOpen(rampUsers(10).during(Duration.ofMinutes(5))) ).protocols(httpProtocol); Running all three scenarios in the same setUp call, with different proportions of users, produces a much more realistic mix of traffic hitting the application at once than a single scenario ever could. Sixty percent casual browsing, thirty percent direct search, ten percent returning customers checking their orders, roughly matching whatever your actual analytics tell you about how people use the site.\nRandomizing Choices Within a Scenario Within a single scenario, you can also randomize which path a virtual user takes at a given step, using randomSwitch to weight different branches.\nScenarioBuilder browse = scenario(\u0026#34;Browse\u0026#34;) .exec(http(\u0026#34;Home Page\u0026#34;).get(\u0026#34;/\u0026#34;)) .pause(2, 4) .randomSwitch().on( Choice.withWeight(70.0, exec(http(\u0026#34;Browse Electronics\u0026#34;).get(\u0026#34;/category/electronics\u0026#34;))), Choice.withWeight(20.0, exec(http(\u0026#34;Browse Clothing\u0026#34;).get(\u0026#34;/category/clothing\u0026#34;))), Choice.withWeight(10.0, exec(http(\u0026#34;Browse Books\u0026#34;).get(\u0026#34;/category/books\u0026#34;))) ); Seventy percent of virtual users following this scenario browse electronics, twenty percent browse clothing, and ten percent browse books, all within the same scenario definition, which keeps related traffic grouped together logically while still reflecting a realistic split in behavior.\nWrapping Up A load test is only as useful as how closely it resembles real traffic. Randomized pauses instead of fixed ones, multiple weighted scenarios instead of a single repeated path, and randomized branching within a scenario all push your test closer to something that reflects actual user behavior rather than a mechanical script.\nNext time, we look at the different broad categories of performance testing, load, stress, soak, and spike, and how to configure each of them properly using everything we have covered so far.\n","permalink":"https://abygeorgea.com/blog/2026/04/02/modeling-realistic-user-journeys-in-gatling/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2026/03/19/gatling-checks-and-validations/\"\u003eprevious post\u003c/a\u003e, we made sure our checks actually catch real failures. Today we look at something just as important but easier to overlook, whether your scenario actually behaves like a real user in the first place.\u003c/p\u003e\n\u003cp\u003eA scenario that fires request after request with zero delay between them does not represent any real visitor to your site. It represents a script racing through steps as fast as the network allows. That produces load numbers, but not necessarily useful ones, since real traffic has gaps in it while people actually read a page, think about what to click next, or get distracted by something else entirely.\u003c/p\u003e","title":"Modeling Realistic User Journeys: Pacing, Think Time, and Scenario Design"},{"content":"In the previous post, we looked at controlling network traffic during a test. This is the last post in this short series comparing Playwright and Selenium, and it covers what happens once your suite grows from ten tests to a thousand and you need them to run fast without stepping on each other.\nIsolation in Selenium Is Something You Build A single WebDriver instance is a single browser session, with its own cookies and storage. If two tests reuse that same session one after another, state can leak between them without you noticing.\n@Test void firstTest() { driver.get(\u0026#34;https://example.com\u0026#34;); driver.manage().addCookie(new Cookie(\u0026#34;session\u0026#34;, \u0026#34;abc123\u0026#34;)); } @Test void secondTest() { driver.get(\u0026#34;https://example.com\u0026#34;); // if this reuses the same driver instance, that cookie from firstTest // might still be sitting there } The common fix is either recreating the driver fresh for every test method, which we already covered in the first post of this series, or explicitly clearing state between tests.\n@AfterEach void cleanUp() { driver.manage().deleteAllCookies(); ((JavascriptExecutor) driver).executeScript( \u0026#34;window.localStorage.clear(); window.sessionStorage.clear();\u0026#34; ); } Either approach works. Both are things you have to remember to do, and both are easy to get quietly wrong when someone adds a new test class without following the same pattern as the rest of the suite.\nParallel Execution Needs Its Own Driver Per Thread Parallelization is not part of Selenium at all. It comes from whatever test runner you use, TestNG\u0026rsquo;s parallel attribute, JUnit 5\u0026rsquo;s parallel execution config, or pytest-xdist. And the moment tests run on different threads, each thread needs its own WebDriver instance, or they will collide trying to drive the same browser session at once.\nThe standard pattern here is a ThreadLocal driver factory.\npublic class DriverFactory { private static final ThreadLocal\u0026lt;WebDriver\u0026gt; driverThreadLocal = new ThreadLocal\u0026lt;\u0026gt;(); public static WebDriver getDriver() { if (driverThreadLocal.get() == null) { driverThreadLocal.set(new ChromeDriver()); } return driverThreadLocal.get(); } public static void quitDriver() { WebDriver driver = driverThreadLocal.get(); if (driver != null) { driver.quit(); driverThreadLocal.remove(); } } } Paired with something like this in your TestNG suite file.\n\u0026lt;suite name=\u0026#34;Suite\u0026#34; parallel=\u0026#34;methods\u0026#34; thread-count=\u0026#34;4\u0026#34;\u0026gt; \u0026lt;test name=\u0026#34;RegressionTests\u0026#34;\u0026gt; \u0026lt;classes\u0026gt; \u0026lt;class name=\u0026#34;com.example.tests.LoginTest\u0026#34;/\u0026gt; \u0026lt;/classes\u0026gt; \u0026lt;/test\u0026gt; \u0026lt;/suite\u0026gt; This works, and plenty of large Selenium suites run this way in production every day. But it is infrastructure you built and now own. Forget to call quitDriver() at the end of a thread\u0026rsquo;s work and you leak browser processes across your whole CI fleet. And this only gets you parallelism on one machine. To actually scale across multiple machines, you need Selenium Grid, or a cloud provider that hosts one for you, which is a separate piece of infrastructure with its own hub and node architecture to configure and keep running.\nPlaywright Isolates by Default Every Playwright test gets its own browser context automatically, with its own cookies, storage, and cache, completely separate from every other test, even ones running in the same worker process at the same time.\ntest(\u0026#39;first test sets a cookie\u0026#39;, async ({ context }) =\u0026gt; { await context.addCookies([ { name: \u0026#39;session\u0026#39;, value: \u0026#39;abc123\u0026#39;, url: \u0026#39;https://example.com\u0026#39; }, ]); }); test(\u0026#39;second test starts with a clean slate\u0026#39;, async ({ page }) =\u0026gt; { const cookies = await page.context().cookies(); expect(cookies).toHaveLength(0); }); There is no ThreadLocal pattern here, because there is nothing shared to protect against. Each test\u0026rsquo;s context is its own sandbox, torn down automatically when the test finishes.\nParallel Workers and Sharding Are Built In Playwright\u0026rsquo;s own test runner handles worker based parallelism through config, not through a separate test framework bolted on afterward.\nexport default defineConfig({ fullyParallel: true, workers: process.env.CI ? 4 : undefined, }); And scaling beyond one machine is a command line flag, not a hub and node deployment.\nnpx playwright test --shard=1/4 Run that same command four times with 1/4 through 4/4 across four CI jobs, and the full suite splits across them automatically. No Grid to stand up, no infrastructure to keep patched and running between test runs. I went into tuning this properly, including merging reports back together from multiple shards, in an earlier post.\nThe Practical Difference None of this means Selenium suites cannot be fast and safe at scale. Plenty are, running on Grid infrastructure that teams have tuned over years. But that safety and scale is something you build and then maintain indefinitely, thread local driver factories, explicit state cleanup, a Grid deployment with its own health to monitor. Playwright gives you the same outcome, isolated tests running safely in parallel, as the default behavior, and scaling further is a config value and a shard flag rather than new infrastructure.\nWrapping Up the Series Across these three posts we looked at setup and teardown, network interception, and isolation and parallelization. In every case the pattern was similar. Selenium can do most of what Playwright does, but it usually takes more code, more infrastructure, or more team discipline to get there safely. Playwright bakes these concerns into the tool itself, at the cost of being a newer, more opinionated piece of software than a WebDriver implementation that has been around since long before any of this comparison mattered.\nIf you are starting fresh, that is worth weighing seriously. If you have years of investment in a mature Selenium suite, none of this is a mandate to rewrite it. It is a map of where the friction actually is, so you know exactly what you are trading away, and what you are gaining, if you ever do make the move.\n","permalink":"https://abygeorgea.com/blog/2026/03/24/playwright-vs-selenium-test-isolation-and-parallelization/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2026/03/13/playwright-vs-selenium-network-interception-and-api-mocking/\"\u003eprevious post\u003c/a\u003e, we looked at controlling network traffic during a test. This is the last post in this short series comparing Playwright and Selenium, and it covers what happens once your suite grows from ten tests to a thousand and you need them to run fast without stepping on each other.\u003c/p\u003e\n\u003ch2 id=\"isolation-in-selenium-is-something-you-build\"\u003eIsolation in Selenium Is Something You Build\u003c/h2\u003e\n\u003cp\u003eA single WebDriver instance is a single browser session, with its own cookies and storage. If two tests reuse that same session one after another, state can leak between them without you noticing.\u003c/p\u003e","title":"Playwright vs Selenium: Test Isolation and Parallelization"},{"content":"In the previous post, we used checks to extract tokens for correlation. This time we look at checks purely from a validation angle, because a surprising number of load tests quietly pass while the application under test is actually broken.\nThe Trap of Checking Status Codes Only Here is a scenario I have seen play out more than once. A test checks only that every response comes back with a 200 status. Midway through the run, the application starts returning a generic error page, but that error page itself happens to render with a 200 status code, because the server is misconfigured to return success even for its own error pages. The load test finishes green. The report shows a great response time. Meanwhile every single user in that window got an error page instead of what they asked for.\nStatus codes are necessary, but they are not sufficient on their own for anything beyond the most trivial smoke test.\nValidating Response Content Checking that the body actually contains what you expect closes this gap.\n.exec( http(\u0026#34;Get Product Detail\u0026#34;) .get(\u0026#34;/products/42\u0026#34;) .check(status().is(200)) .check(jsonPath(\u0026#34;$.name\u0026#34;).is(\u0026#34;Wireless Mouse\u0026#34;)) .check(jsonPath(\u0026#34;$.price\u0026#34;).exists()) .check(jsonPath(\u0026#34;$.inStock\u0026#34;).is(\u0026#34;true\u0026#34;)) ) Now the check fails if the product name does not match, if the price field is missing entirely, or if the stock flag says the item is unavailable when your test expects it to be in stock. A response that looks superficially fine but has the wrong data underneath gets caught immediately.\nChecking Response Headers Sometimes the thing you actually care about lives in a header rather than the body. Content type mismatches, caching headers, or a custom header your application uses for tracing are all fair game.\n.exec( http(\u0026#34;Get Product Detail\u0026#34;) .get(\u0026#34;/products/42\u0026#34;) .check(status().is(200)) .check(header(\u0026#34;Content-Type\u0026#34;).is(\u0026#34;application/json\u0026#34;)) .check(header(\u0026#34;X-Cache\u0026#34;).is(\u0026#34;HIT\u0026#34;)) ) That last check is a genuinely useful one for performance testing specifically. If you expect a cache layer to be serving most of your traffic, asserting on the cache header lets you confirm that assumption is actually true during the run, rather than guessing at it after the fact.\nChecking Response Time Per Request Checks are not limited to correctness. You can assert on timing at the level of an individual request too, which is useful for catching a single slow endpoint hiding inside an otherwise healthy scenario.\n.exec( http(\u0026#34;Search Products\u0026#34;) .get(\u0026#34;/products/search?q=mouse\u0026#34;) .check(status().is(200)) .check(responseTimeInMillis().lte(800)) ) This fails the specific request if it takes longer than eight hundred milliseconds, independent of whatever global assertions you have configured for the whole simulation, which we will cover properly in a later post on setting performance SLAs.\nCombining Multiple Checks on One Request A single request can carry as many checks as it needs, and Gatling evaluates all of them.\n.exec( http(\u0026#34;Checkout\u0026#34;) .post(\u0026#34;/checkout\u0026#34;) .body(StringBody(\u0026#34;{\\\u0026#34;cartId\\\u0026#34;: \\\u0026#34;#{cartId}\\\u0026#34;}\u0026#34;)) .check(status().is(200)) .check(jsonPath(\u0026#34;$.orderId\u0026#34;).exists()) .check(jsonPath(\u0026#34;$.status\u0026#34;).is(\u0026#34;confirmed\u0026#34;)) .check(jsonPath(\u0026#34;$.total\u0026#34;).ofType(Double.class).gt(0.0)) .check(responseTimeInMillis().lte(2000)) ) This one request now confirms the call succeeded, an order id came back, the order status is actually confirmed rather than pending or failed, the total charged is a sensible positive number, and the whole thing happened inside a reasonable time budget. That is a load test that actually tells you something meaningful about the checkout flow, not just that a server responded.\nDeciding What to Check, Practically Checking everything on every single request is not the goal, since overly strict checks can make a test brittle in ways that have nothing to do with performance, like failing because a non-critical field\u0026rsquo;s formatting changed slightly. A practical rule I follow is to check the things that would represent a genuine failure from a user\u0026rsquo;s point of view. Did the order actually go through. Did the search actually return results. Is the price actually correct. Save strict field by field validation for your functional test suite, and keep performance test checks focused on the handful of signals that tell you the request genuinely succeeded and returned something usable.\nWrapping Up A load test that only checks status codes can hide real problems behind a green summary. Validating response content, relevant headers, and per request timing where it matters turns a load test into something that actually catches failures, not just something that measures how fast a broken response comes back.\nNext time, we step back from individual requests and look at scenario design as a whole, focusing on pacing and think time, and how to make a scenario actually behave like a real user rather than a script racing through steps as fast as possible.\n","permalink":"https://abygeorgea.com/blog/2026/03/19/gatling-checks-and-validations/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2026/03/05/gatling-correlation-and-authentication/\"\u003eprevious post\u003c/a\u003e, we used checks to extract tokens for correlation. This time we look at checks purely from a validation angle, because a surprising number of load tests quietly pass while the application under test is actually broken.\u003c/p\u003e\n\u003ch2 id=\"the-trap-of-checking-status-codes-only\"\u003eThe Trap of Checking Status Codes Only\u003c/h2\u003e\n\u003cp\u003eHere is a scenario I have seen play out more than once. A test checks only that every response comes back with a 200 status. Midway through the run, the application starts returning a generic error page, but that error page itself happens to render with a 200 status code, because the server is misconfigured to return success even for its own error pages. The load test finishes green. The report shows a great response time. Meanwhile every single user in that window got an error page instead of what they asked for.\u003c/p\u003e","title":"Checks and Validations: Making Sure Your Load Test Catches Real Failures"},{"content":"In the previous post, we compared how each tool handles setup and teardown. Today we look at something that trips up a lot of teams moving from Selenium to Playwright for the first time. Controlling what actually happens on the network during a test.\nWhy Selenium Was Never Built for This It helps to understand where Selenium comes from. The WebDriver protocol, the actual W3C specification Selenium implements, was designed to simulate a real user driving a real browser. Click here, type there, read what is on the page. Network traffic was never part of that picture.\nSelenium 4 added a way in through the Chrome DevTools Protocol, which gives you low level access to what Chromium is actually doing under the hood, including its network layer. More recently, Selenium has been investing in WebDriver BiDi, a newer W3C standard aiming to bring this kind of capability to every browser through one consistent API, not just Chromium through CDP.\nHere is roughly what intercepting a request looks like using the DevTools approach in Java.\nChromeDriver driver = new ChromeDriver(); DevTools devTools = driver.getDevTools(); devTools.createSession(); devTools.send(Network.enable(Optional.empty(), Optional.empty(), Optional.empty())); devTools.addListener(Network.requestIntercepted(), interceptedRequest -\u0026gt; { if (interceptedRequest.getRequest().getUrl().contains(\u0026#34;/api/products\u0026#34;)) { devTools.send(Network.continueInterceptedRequest( interceptedRequest.getInterceptionId(), Optional.empty(), Optional.of(\u0026#34;200\u0026#34;), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty() )); } }); Notice how much is happening here just to react to one request. You open a DevTools session, enable the network domain, register a listener, and manually construct the continuation response with mostly empty optional parameters. Building an actual mocked response body means base64 encoding it yourself and setting headers by hand. It is powerful, but it reads like the low level protocol access it actually is, and it is tied closely to Chromium. BiDi improves the cross browser story over time, but the tooling and documentation around it are still catching up to how mature CDP support already is, and either way you are writing noticeably more code than the equivalent Playwright test.\nPlaywright Treats This as a First Class Feature Playwright\u0026rsquo;s page.route() was built from day one with the assumption that tests need to control the network, not just observe it as an afterthought.\ntest(\u0026#39;shows fallback UI when the product API fails\u0026#39;, async ({ page }) =\u0026gt; { await page.route(\u0026#39;**/api/products/42\u0026#39;, (route) =\u0026gt; { route.fulfill({ status: 500, contentType: \u0026#39;application/json\u0026#39;, body: JSON.stringify({ error: \u0026#39;Internal Server Error\u0026#39; }), }); }); await page.goto(\u0026#39;/products/42\u0026#39;); await expect(page.locator(\u0026#39;.error-message\u0026#39;)).toBeVisible(); }); That is the whole thing. A glob pattern to match the URL, and a plain object describing the response you want back. No sessions, no listeners, no base64 encoding.\nroute.fulfill() mocks a response entirely. route.continue() lets the real request through, optionally after you inspect or modify it. route.abort() simulates the request failing outright, which is exactly what you want for testing how the UI handles a dropped connection.\ntest(\u0026#39;blocks third party analytics calls during the test\u0026#39;, async ({ page }) =\u0026gt; { await page.route(\u0026#39;**/analytics.example.com/**\u0026#39;, (route) =\u0026gt; route.abort()); await page.goto(\u0026#39;/\u0026#39;); }); Inspecting Traffic Without Changing It Sometimes you do not want to mock anything. You just want to confirm the frontend sent the right request. Playwright gives you this through simple event listeners on the page.\ntest(\u0026#39;checkout sends the correct payload\u0026#39;, async ({ page }) =\u0026gt; { let checkoutPayload: any; page.on(\u0026#39;request\u0026#39;, (request) =\u0026gt; { if (request.url().includes(\u0026#39;/api/checkout\u0026#39;) \u0026amp;\u0026amp; request.method() === \u0026#39;POST\u0026#39;) { checkoutPayload = request.postDataJSON(); } }); await page.goto(\u0026#39;/checkout\u0026#39;); await page.click(\u0026#39;#place-order\u0026#39;); expect(checkoutPayload.items.length).toBeGreaterThan(0); }); This is a genuinely useful pattern. It confirms the frontend built the request correctly, independent of whatever the backend actually does with it.\nWhy This Matters Beyond Convenience The real value here is not just writing less code. It is what becomes practical to test at all. Error states like a 500 response or a malformed payload are often hard to trigger reliably against a real backend, since you would need to coordinate with whoever owns that service, or find a way to force a failure condition on demand. Mocking makes these trivial to set up on your own terms.\nIt also lets you test the frontend in isolation from backend availability, which matters a lot in CI where a flaky downstream dependency can fail your UI tests for reasons that have nothing to do with the UI. And you can simulate conditions that are awkward to reproduce naturally, like a slow response.\ntest(\u0026#39;shows a loading spinner while the search request is in flight\u0026#39;, async ({ page }) =\u0026gt; { await page.route(\u0026#39;**/api/search**\u0026#39;, async (route) =\u0026gt; { await new Promise((resolve) =\u0026gt; setTimeout(resolve, 2000)); await route.continue(); }); await page.goto(\u0026#39;/search?q=mouse\u0026#39;); await expect(page.locator(\u0026#39;.loading-spinner\u0026#39;)).toBeVisible(); }); Delaying a real request by two seconds and letting it continue afterward gives you a reliable way to test a loading state without needing the backend to actually be slow.\nWrapping Up Selenium can get you some of this through CDP or the newer BiDi support, but it takes real effort and reads like protocol level plumbing rather than a testing feature. Playwright treats controlling the network as something you will need constantly, and the API reflects that from the first line of code.\nNext time, we look at test isolation and parallelization, and what it actually takes to run each tool\u0026rsquo;s tests safely at scale.\n","permalink":"https://abygeorgea.com/blog/2026/03/13/playwright-vs-selenium-network-interception-and-api-mocking/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2026/03/03/playwright-vs-selenium-fixtures-vs-manual-setup-teardown/\"\u003eprevious post\u003c/a\u003e, we compared how each tool handles setup and teardown. Today we look at something that trips up a lot of teams moving from Selenium to Playwright for the first time. Controlling what actually happens on the network during a test.\u003c/p\u003e\n\u003ch2 id=\"why-selenium-was-never-built-for-this\"\u003eWhy Selenium Was Never Built for This\u003c/h2\u003e\n\u003cp\u003eIt helps to understand where Selenium comes from. The WebDriver protocol, the actual W3C specification Selenium implements, was designed to simulate a real user driving a real browser. Click here, type there, read what is on the page. Network traffic was never part of that picture.\u003c/p\u003e","title":"Playwright vs Selenium: Network Interception and API Mocking"},{"content":"In the previous post, we fed real data into our scenarios. Today we cover something almost every real application needs before you can test anything interesting behind a login screen. Correlation.\nCorrelation just means grabbing a value out of one response and reusing it in a later request. The most common example by far is authentication. You log in once, get back a token, and then attach that token to every request that follows.\nExtracting a Token With saveAs Gatling\u0026rsquo;s check mechanism does double duty. It validates a response, and it can also save a piece of that response into the session for later use. Here is a login step that does both.\nScenarioBuilder login = scenario(\u0026#34;Login\u0026#34;) .exec( http(\u0026#34;Login\u0026#34;) .post(\u0026#34;/auth/login\u0026#34;) .body(StringBody(\u0026#34;{\\\u0026#34;username\\\u0026#34;: \\\u0026#34;testuser\\\u0026#34;, \\\u0026#34;password\\\u0026#34;: \\\u0026#34;Password123\\\u0026#34;}\u0026#34;)) .check(status().is(200)) .check(jsonPath(\u0026#34;$.token\u0026#34;).saveAs(\u0026#34;authToken\u0026#34;)) ); jsonPath(\u0026quot;$.token\u0026quot;) pulls the token field out of a JSON response body, and .saveAs(\u0026quot;authToken\u0026quot;) stores it in the session under that name. From this point on in the scenario, #{authToken} refers to that value anywhere it is needed.\nUsing the Token in Later Requests Once the token is in the session, attaching it to subsequent requests is just a matter of referencing it in a header.\nScenarioBuilder browseAsAuthenticatedUser = scenario(\u0026#34;Authenticated Browse\u0026#34;) .exec( http(\u0026#34;Login\u0026#34;) .post(\u0026#34;/auth/login\u0026#34;) .body(StringBody(\u0026#34;{\\\u0026#34;username\\\u0026#34;: \\\u0026#34;testuser\\\u0026#34;, \\\u0026#34;password\\\u0026#34;: \\\u0026#34;Password123\\\u0026#34;}\u0026#34;)) .check(status().is(200)) .check(jsonPath(\u0026#34;$.token\u0026#34;).saveAs(\u0026#34;authToken\u0026#34;)) ) .exec( http(\u0026#34;Get Profile\u0026#34;) .get(\u0026#34;/account/profile\u0026#34;) .header(\u0026#34;Authorization\u0026#34;, \u0026#34;Bearer #{authToken}\u0026#34;) .check(status().is(200)) ) .exec( http(\u0026#34;Get Order History\u0026#34;) .get(\u0026#34;/account/orders\u0026#34;) .header(\u0026#34;Authorization\u0026#34;, \u0026#34;Bearer #{authToken}\u0026#34;) .check(status().is(200)) ); Every request after login carries the token automatically, because it lives in the virtual user\u0026rsquo;s session for the rest of the scenario run.\nSharing an Authorization Header Across a Whole Scenario Repeating .header(\u0026quot;Authorization\u0026quot;, \u0026quot;Bearer #{authToken}\u0026quot;) on every single request works, but it gets repetitive fast, and it is easy to forget on a new request as the scenario grows. A cleaner approach sets it once at the protocol level, since the protocol builder also supports common headers.\nHttpProtocolBuilder authenticatedProtocol = http .baseUrl(\u0026#34;https://api.example.com\u0026#34;) .acceptHeader(\u0026#34;application/json\u0026#34;) .authorizationHeader(\u0026#34;Bearer #{authToken}\u0026#34;); As long as authToken exists in the session by the time a request under this protocol fires, every request picks up the header automatically, with no need to repeat it anywhere in the scenario itself.\nExtracting Values With Regex Instead of JSON Not every application returns a clean JSON response. Some older systems embed a token or a CSRF value inside an HTML page, often in a hidden form field. Gatling\u0026rsquo;s regex check handles this the same way jsonPath handles JSON.\n.exec( http(\u0026#34;Get Login Page\u0026#34;) .get(\u0026#34;/login\u0026#34;) .check(status().is(200)) .check(regex(\u0026#34;name=\\\u0026#34;csrf_token\\\u0026#34; value=\\\u0026#34;(.*?)\\\u0026#34;\u0026#34;).saveAs(\u0026#34;csrfToken\u0026#34;)) ) .exec( http(\u0026#34;Submit Login\u0026#34;) .post(\u0026#34;/login\u0026#34;) .formParam(\u0026#34;username\u0026#34;, \u0026#34;testuser\u0026#34;) .formParam(\u0026#34;password\u0026#34;, \u0026#34;Password123\u0026#34;) .formParam(\u0026#34;csrf_token\u0026#34;, \u0026#34;#{csrfToken}\u0026#34;) .check(status().is(200)) ) This pattern, grab a CSRF token from a form page and submit it back with the login request, is one of the most common correlation problems you will run into against traditional server rendered applications.\nHandling Multi-Step Auth Flows Some login flows involve more than a single request and response. Think a token exchange, or an initial call that returns a session id you need before you can even submit credentials. The approach does not really change, you just chain more exec steps together, each one saving what the next one needs.\nScenarioBuilder multiStepLogin = scenario(\u0026#34;Multi Step Login\u0026#34;) .exec( http(\u0026#34;Start Session\u0026#34;) .post(\u0026#34;/auth/session\u0026#34;) .check(status().is(200)) .check(jsonPath(\u0026#34;$.sessionId\u0026#34;).saveAs(\u0026#34;sessionId\u0026#34;)) ) .exec( http(\u0026#34;Submit Credentials\u0026#34;) .post(\u0026#34;/auth/login\u0026#34;) .body(StringBody(\u0026#34;{\\\u0026#34;sessionId\\\u0026#34;: \\\u0026#34;#{sessionId}\\\u0026#34;, \\\u0026#34;username\\\u0026#34;: \\\u0026#34;testuser\\\u0026#34;, \\\u0026#34;password\\\u0026#34;: \\\u0026#34;Password123\\\u0026#34;}\u0026#34;)) .check(status().is(200)) .check(jsonPath(\u0026#34;$.token\u0026#34;).saveAs(\u0026#34;authToken\u0026#34;)) ); Each step is small and focused, and the session object carries whatever the next step needs, exactly the way a real browser or API client would carry that state through the flow.\nWrapping Up Correlation is really just extracting a value now and using it later, and Gatling\u0026rsquo;s check and saveAs cover the vast majority of cases you will run into, whether that value comes from a clean JSON API or an older HTML form. Getting authentication right at this level unlocks testing almost anything behind a login screen.\nNext time, we look at checks in more depth beyond simple status codes and saved values, and talk about what actually makes a load test catch a real failure instead of quietly passing when something is wrong.\n","permalink":"https://abygeorgea.com/blog/2026/03/05/gatling-correlation-and-authentication/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2026/02/19/gatling-feeders-csv-json-data-driven-tests/\"\u003eprevious post\u003c/a\u003e, we fed real data into our scenarios. Today we cover something almost every real application needs before you can test anything interesting behind a login screen. Correlation.\u003c/p\u003e\n\u003cp\u003eCorrelation just means grabbing a value out of one response and reusing it in a later request. The most common example by far is authentication. You log in once, get back a token, and then attach that token to every request that follows.\u003c/p\u003e","title":"Correlation and Authentication: Extracting Tokens and Handling Login Flows"},{"content":"Most comparisons between Playwright and Selenium start with API syntax. Click this way, find an element that way. That is not where the real difference shows up day to day. The real difference shows up the moment you write your tenth test and notice how much setup code you are copying between files.\nThis is the first of three posts comparing Playwright and Selenium on things that actually matter once a suite grows past a handful of tests. Today it is setup and teardown. The examples use Java for Selenium and TypeScript for Playwright, since that is a pairing a lot of teams genuinely work with side by side.\nSelenium Has No Built-In Lifecycle This is worth saying plainly. Selenium WebDriver is a browser automation library. It gives you a driver object and a way to find elements and interact with them. It has no opinion at all about test structure, setup order, or cleanup. Every bit of that comes from whatever test framework you pair it with, JUnit, TestNG, NUnit, or pytest.\nA typical JUnit 5 test class looks like this.\npublic class LoginTest { private WebDriver driver; @BeforeEach void setUp() { driver = new ChromeDriver(); driver.manage().window().maximize(); } @Test void userCanLogIn() { driver.get(\u0026#34;https://example.com/login\u0026#34;); driver.findElement(By.id(\u0026#34;username\u0026#34;)).sendKeys(\u0026#34;testuser\u0026#34;); driver.findElement(By.id(\u0026#34;password\u0026#34;)).sendKeys(\u0026#34;Password123\u0026#34;); driver.findElement(By.cssSelector(\u0026#34;button[type=\u0026#39;submit\u0026#39;]\u0026#34;)).click(); assertTrue(driver.findElement(By.className(\u0026#34;welcome-banner\u0026#34;)).isDisplayed()); } @AfterEach void tearDown() { if (driver != null) { driver.quit(); } } } This works fine. But notice how much of it is plumbing you wrote yourself. Create the driver. Remember to null check it in teardown. Remember to actually call quit, since a forgotten quit leaves an orphaned browser process running on whatever machine the test executed on. None of this is Selenium\u0026rsquo;s fault exactly. It is just the cost of a library that does not manage test lifecycle for you.\nThe Inheritance Trap The real pain shows up once different tests need different starting states. Say half your suite needs a logged in user. The common pattern is a base class.\npublic abstract class BaseAuthenticatedTest { protected WebDriver driver; @BeforeEach void setUpAuthenticated() { driver = new ChromeDriver(); driver.get(\u0026#34;https://example.com/login\u0026#34;); driver.findElement(By.id(\u0026#34;username\u0026#34;)).sendKeys(\u0026#34;testuser\u0026#34;); driver.findElement(By.id(\u0026#34;password\u0026#34;)).sendKeys(\u0026#34;Password123\u0026#34;); driver.findElement(By.cssSelector(\u0026#34;button[type=\u0026#39;submit\u0026#39;]\u0026#34;)).click(); } @AfterEach void tearDown() { driver.quit(); } } Every test class needing a logged in user extends this. It works, right up until you need a second independent precondition. Say some tests also need a cart pre populated with items. Java does not let you extend two classes at once, so now you are either duplicating the login logic into a second base class, or building a deeper inheritance chain, or moving everything into static helper methods called manually inside each @BeforeEach. None of these options are terrible on their own. All of them get messy fast once you have three or four independent preconditions that need to combine in different ways across your suite.\nFixtures Solve This With Composition Instead of Inheritance Playwright\u0026rsquo;s test.extend gives you a fixture for the same login flow.\nimport { test as base } from \u0026#39;@playwright/test\u0026#39;; import type { Page } from \u0026#39;@playwright/test\u0026#39;; type AuthFixtures = { authenticatedPage: Page; }; export const test = base.extend\u0026lt;AuthFixtures\u0026gt;({ authenticatedPage: async ({ page }, use) =\u0026gt; { await page.goto(\u0026#39;https://example.com/login\u0026#39;); await page.fill(\u0026#39;#username\u0026#39;, \u0026#39;testuser\u0026#39;); await page.fill(\u0026#39;#password\u0026#39;, \u0026#39;Password123\u0026#39;); await page.click(\u0026#39;button[type=\u0026#34;submit\u0026#34;]\u0026#39;); await use(page); }, }); test(\u0026#39;user can access their profile\u0026#39;, async ({ authenticatedPage }) =\u0026gt; { await authenticatedPage.goto(\u0026#39;/account/profile\u0026#39;); await expect(authenticatedPage.locator(\u0026#39;.profile-name\u0026#39;)).toBeVisible(); }); Now add the second precondition, a cart with items in it, as its own separate fixture that depends on the first one.\ntype Fixtures = { authenticatedPage: Page; cartWithItems: Page; }; export const test = base.extend\u0026lt;Fixtures\u0026gt;({ authenticatedPage: async ({ page }, use) =\u0026gt; { await page.goto(\u0026#39;https://example.com/login\u0026#39;); await page.fill(\u0026#39;#username\u0026#39;, \u0026#39;testuser\u0026#39;); await page.fill(\u0026#39;#password\u0026#39;, \u0026#39;Password123\u0026#39;); await page.click(\u0026#39;button[type=\u0026#34;submit\u0026#34;]\u0026#39;); await use(page); }, cartWithItems: async ({ authenticatedPage }, use) =\u0026gt; { await authenticatedPage.request.post(\u0026#39;/api/cart/items\u0026#39;, { data: { productId: 42 } }); await use(authenticatedPage); }, }); test(\u0026#39;checkout works with items already in cart\u0026#39;, async ({ cartWithItems }) =\u0026gt; { await cartWithItems.goto(\u0026#39;/checkout\u0026#39;); await expect(cartWithItems.locator(\u0026#39;.order-summary\u0026#39;)).toBeVisible(); }); cartWithItems depends on authenticatedPage, and Playwright resolves that dependency automatically. There is no inheritance chain here at all. Any test can ask for any combination of fixtures it needs, and Playwright figures out the order to set them up in. This is the actual difference. It is not that Playwright has a nicer syntax for the same idea. It is that fixtures compose, and inheritance does not.\nIf you want a deeper look at building out a fixture library on its own, outside of this Selenium comparison, I covered that in an earlier post.\nCleanup Happens Even When the Test Fails The other quiet advantage is teardown. In the fixture above, everything after await use(page) runs after the test finishes, whether it passed or failed. You do not write a try or a finally block for this. It is built into how fixtures work.\nCompare this to the Selenium example from earlier. If setUp() throws an exception partway through, before the driver variable is even assigned, JUnit will still call tearDown(), and your null check saves you there. But that null check is something you had to remember to write. Multiply that across every base class and helper method in a large suite, and it becomes one more category of thing that quietly breaks when someone refactors without thinking about it.\nWhich One Should You Actually Use If you already have a mature Selenium suite built around base classes, this is not, on its own, a reason to rewrite it. Inheritance based setup has shipped reliable test suites for well over a decade, and plenty of teams manage it fine with good discipline. But if you are starting a new project, or your existing base class hierarchy is already starting to strain under too many combinations of preconditions, the fixture model scales in a way inheritance structurally cannot. Composition does not hit a wall the way a single inheritance chain does.\nWrapping Up Selenium leaves lifecycle management entirely in your hands, paired with whatever hooks your test framework provides. Playwright bakes it in, and its fixture system composes cleanly instead of forcing everything through inheritance.\nNext time, we look at a different kind of test, ones that need to mock or inspect network traffic, and how differently each tool handles it.\n","permalink":"https://abygeorgea.com/blog/2026/03/03/playwright-vs-selenium-fixtures-vs-manual-setup-teardown/","summary":"\u003cp\u003eMost comparisons between Playwright and Selenium start with API syntax. Click this way, find an element that way. That is not where the real difference shows up day to day. The real difference shows up the moment you write your tenth test and notice how much setup code you are copying between files.\u003c/p\u003e\n\u003cp\u003eThis is the first of three posts comparing Playwright and Selenium on things that actually matter once a suite grows past a handful of tests. Today it is setup and teardown. The examples use Java for Selenium and TypeScript for Playwright, since that is a pairing a lot of teams genuinely work with side by side.\u003c/p\u003e","title":"Playwright vs Selenium: Fixtures vs Manual Setup and Teardown"},{"content":"Flipping the Script: A Look at GitHub\u0026rsquo;s Spec Kit I\u0026rsquo;ve spent the last few posts obsessing over agents that write, run, and heal tests. This time I want to zoom out one level, because I think I\u0026rsquo;ve been skipping over the artifact that actually matters most to an AI coding agent: the spec itself.\nThat\u0026rsquo;s what pulled me into Spec Kit, GitHub\u0026rsquo;s open-source toolkit for what they\u0026rsquo;re calling \u0026ldquo;Spec-Driven Development\u0026rdquo; (SDD). The pitch is deceptively simple but genuinely inverts how most of us have worked for the last twenty years. In traditional development, the spec (if one even exists past the kickoff meeting) is a scaffold — you lean on it briefly, then throw it away the moment code starts shipping. Code becomes the source of truth, and the spec rots in Confluence somewhere, quietly lying to whoever reads it next.\nSpec Kit flips that. The spec doesn\u0026rsquo;t get discarded — it becomes the durable, executable artifact, and the code becomes the disposable, regenerable output of it.\nHow It Actually Works Spec Kit structures the workflow into a handful of slash commands that map cleanly onto how a good tech lead would run a project:\n/constitution — establish the non-negotiable principles and constraints for the project up front, before any feature work starts. /specify — define the what and the why of a feature, deliberately keeping implementation details out. /clarify — force the ambiguity-resolution step that normally happens three days into a sprint via a confused Slack thread. /plan — translate the spec into a concrete technical approach, tech stack included. /tasks — break the plan into small, reviewable, executable chunks. /implement — let the agent actually build it, task by task, against the spec. Why This Matters to a QA Brain Reading through this workflow, I kept mentally overlaying it onto the multi-agent pipeline I sketched out a couple of months back. /specify and /clarify are essentially doing the job I assigned to my hypothetical \u0026ldquo;Analyst Agent\u0026rdquo; — mapping out the exact test surface before anyone writes a line of test automation. The difference is Spec Kit does it as a first-class, structured step rather than something bolted on afterward.\nThe part I find most promising from a quality standpoint is /clarify. So much test flakiness and \u0026ldquo;well, that\u0026rsquo;s not what I meant\u0026rdquo; bug triage traces back to ambiguous acceptance criteria that nobody forced anyone to resolve before implementation started. Baking that resolution into the workflow, before the agent starts generating code, should in theory produce specs that are already halfway to being good test cases.\nThe Catch Spec Kit is clearly built with greenfield work in mind — a /constitution step assumes you\u0026rsquo;re setting principles for a project that doesn\u0026rsquo;t have fifteen years of legacy decisions already baked in. Most of what crosses my desk is nowhere near that clean. Which, conveniently, is exactly the gap I want to dig into next.\n","permalink":"https://abygeorgea.com/blog/2026/03/02/spec-kit-spec-driven-development/","summary":"\u003ch1 id=\"flipping-the-script-a-look-at-githubs-spec-kit\"\u003eFlipping the Script: A Look at GitHub\u0026rsquo;s Spec Kit\u003c/h1\u003e\n\u003cp\u003eI\u0026rsquo;ve spent the last few posts obsessing over agents that write, run, and heal tests. This time I want to zoom out one level, because I think I\u0026rsquo;ve been skipping over the artifact that actually matters most to an AI coding agent: the spec itself.\u003c/p\u003e\n\u003cp\u003eThat\u0026rsquo;s what pulled me into \u003cstrong\u003eSpec Kit\u003c/strong\u003e, GitHub\u0026rsquo;s open-source toolkit for what they\u0026rsquo;re calling \u0026ldquo;Spec-Driven Development\u0026rdquo; (SDD). The pitch is deceptively simple but genuinely inverts how most of us have worked for the last twenty years. In traditional development, the spec (if one even exists past the kickoff meeting) is a scaffold — you lean on it briefly, then throw it away the moment code starts shipping. Code becomes the source of truth, and the spec rots in Confluence somewhere, quietly lying to whoever reads it next.\u003c/p\u003e","title":"Spec Kit- Spec Driven development"},{"content":"In the previous post, we looked at how many users to inject and when. Today we tackle a different problem. If every one of those users logs in with the same username, or adds the exact same product to their cart, you are not really testing realistic load. You are testing one specific code path over and over.\nFeeders solve this by handing each virtual user its own piece of data before it runs through the scenario.\nCSV Feeders The simplest and most common feeder reads from a CSV file. Put this in src/test/resources/data/users.csv.\nusername,password user1,Password1! user2,Password2! user3,Password3! Wiring it into a scenario looks like this.\nFeederBuilder\u0026lt;String\u0026gt; users = csv(\u0026#34;data/users.csv\u0026#34;).random(); ScenarioBuilder login = scenario(\u0026#34;Login\u0026#34;) .feed(users) .exec( http(\u0026#34;Login\u0026#34;) .post(\u0026#34;/auth/login\u0026#34;) .body(StringBody(\u0026#34;{\\\u0026#34;username\\\u0026#34;: \\\u0026#34;#{username}\\\u0026#34;, \\\u0026#34;password\\\u0026#34;: \\\u0026#34;#{password}\\\u0026#34;}\u0026#34;)) .check(status().is(200)) ); The #{username} and #{password} syntax is Gatling\u0026rsquo;s expression language. It pulls the value the feeder just placed into the session for that virtual user. Calling .random() on the feeder means each user grabs a random row, rather than reading through the file in strict order, which matters once you have more virtual users than rows and Gatling needs to decide how to reuse them.\nControlling How Rows Get Reused Gatling gives you a few strategies for what happens when a feeder runs out of unique rows partway through a test.\ncsv(\u0026#34;data/users.csv\u0026#34;).random(); // random row every time, can repeat csv(\u0026#34;data/users.csv\u0026#34;).shuffle(); // shuffled once, then read in that order, wraps around csv(\u0026#34;data/users.csv\u0026#34;).circular(); // reads top to bottom, wraps to the top when it runs out If uniqueness genuinely matters, like a registration flow that needs a fresh username every single time, none of these strategies are quite right on their own, since all of them eventually repeat rows. That is exactly the situation where a small amount of dynamic generation, which we will cover next, becomes the better tool.\nJSON Feeders For data with more structure than flat CSV columns comfortably allow, a JSON feeder works the same way. Put this in src/test/resources/data/products.json.\n[ { \u0026#34;productId\u0026#34;: 42, \u0026#34;name\u0026#34;: \u0026#34;Wireless Mouse\u0026#34;, \u0026#34;price\u0026#34;: 29.99 }, { \u0026#34;productId\u0026#34;: 51, \u0026#34;name\u0026#34;: \u0026#34;Mechanical Keyboard\u0026#34;, \u0026#34;price\u0026#34;: 89.99 }, { \u0026#34;productId\u0026#34;: 67, \u0026#34;name\u0026#34;: \u0026#34;USB-C Hub\u0026#34;, \u0026#34;price\u0026#34;: 34.99 } ] FeederBuilder\u0026lt;Object\u0026gt; products = jsonFile(\u0026#34;data/products.json\u0026#34;).random(); ScenarioBuilder addToCart = scenario(\u0026#34;Add To Cart\u0026#34;) .feed(products) .exec( http(\u0026#34;Add To Cart\u0026#34;) .post(\u0026#34;/cart/items\u0026#34;) .body(StringBody(\u0026#34;{\\\u0026#34;productId\\\u0026#34;: #{productId}, \\\u0026#34;quantity\\\u0026#34;: 1}\u0026#34;)) .check(status().is(201)) ); Each field in the JSON object becomes its own session variable, the same as a CSV column would, so #{productId} and #{name} both become available to reference anywhere later in the scenario.\nGenerating Data In Memory With a Custom Feeder Sometimes a static file is not the right fit, especially when you need genuinely unique values, like a fresh email address for every single registration attempt across a long running test. Gatling lets you build a feeder directly from a Java Iterator, which means you can generate values on demand rather than reading from a fixed file.\nimport java.util.Iterator; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; AtomicInteger counter = new AtomicInteger(0); Iterator\u0026lt;Map\u0026lt;String, Object\u0026gt;\u0026gt; newUserFeeder = Stream.generate(() -\u0026gt; { int id = counter.incrementAndGet(); return Map.\u0026lt;String, Object\u0026gt;of( \u0026#34;email\u0026#34;, \u0026#34;loadtest_user\u0026#34; + id + \u0026#34;@example.com\u0026#34;, \u0026#34;username\u0026#34;, \u0026#34;loadtest_user\u0026#34; + id ); }).iterator(); ScenarioBuilder register = scenario(\u0026#34;Register\u0026#34;) .feed(newUserFeeder) .exec( http(\u0026#34;Register\u0026#34;) .post(\u0026#34;/auth/register\u0026#34;) .body(StringBody(\u0026#34;{\\\u0026#34;email\\\u0026#34;: \\\u0026#34;#{email}\\\u0026#34;, \\\u0026#34;username\\\u0026#34;: \\\u0026#34;#{username}\\\u0026#34;}\u0026#34;)) .check(status().is(201)) ); Because the counter increments atomically, this stays safe even when many virtual users are pulling from the same feeder concurrently across multiple threads. Every user gets a genuinely unique email, no matter how long the test runs or how many users you throw at it.\nA Word on Feeder Scope By default, a feeder is shared across every virtual user in the simulation, which is exactly what you want for something like the CSV example above, where you have a fixed pool of test accounts everyone draws from. Just be careful with feeders that represent something finite and stateful in the system under test, like a limited stock of a specific product. If your feeder hands out the same product id to two different virtual users at the same time, and your application only has one unit of that product in stock, you can end up with a test failure that has nothing to do with a real defect and everything to do with the test data itself being reused unsafely. This connects back to the data scoping concerns worth thinking through any time you parameterize a test against a shared environment.\nWrapping Up Feeders are what turn a single hardcoded request into something that represents a real population of users, each with their own data. CSV and JSON cover most static data needs, and a custom Java based feeder covers the cases where you need guaranteed uniqueness on demand.\nNext time, we look at correlation and authentication together, since almost every real application requires handling a login flow and threading a token through the rest of the scenario.\n","permalink":"https://abygeorgea.com/blog/2026/02/19/gatling-feeders-csv-json-data-driven-tests/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2026/02/05/gatling-scenarios-virtual-users-injection-profiles/\"\u003eprevious post\u003c/a\u003e, we looked at how many users to inject and when. Today we tackle a different problem. If every one of those users logs in with the same username, or adds the exact same product to their cart, you are not really testing realistic load. You are testing one specific code path over and over.\u003c/p\u003e\n\u003cp\u003eFeeders solve this by handing each virtual user its own piece of data before it runs through the scenario.\u003c/p\u003e","title":"Feeders 101: Driving Tests With CSV, JSON, and In-Memory Data"},{"content":"In the previous post, we built a proper chained scenario and separated it cleanly from load setup. Today we focus entirely on that load setup, because how you inject virtual users into a scenario has a huge effect on what your test actually measures.\nA lot of people new to Gatling reach for atOnceUsers and stop there. It has its place, but it does not represent how real traffic behaves, and using it for everything will give you misleading results.\natOnceUsers: The Blunt Instrument atOnceUsers fires every single virtual user at the exact same instant.\nscn.injectOpen(atOnceUsers(50)) This is useful for exactly one thing, deliberately testing how your system handles a sudden burst, like a flash sale starting at midnight or a cache expiring across your whole user base at once. Outside of that specific scenario, it is a poor default, because it puts an unrealistic instantaneous spike on your system that most real world traffic never actually produces.\nrampUsers: A Gradual Increase rampUsers spreads a fixed number of users evenly across a time window, arriving progressively rather than all at once.\nscn.injectOpen(rampUsers(200).during(Duration.ofMinutes(5))) This ramps two hundred users in over five minutes. It is a solid default for a basic load test, since it gives your system time to warm up caches and scale connection pools naturally, the same way real traffic tends to build up over the course of a morning rather than appearing instantly.\nconstantUsersPerSec: A Steady Arrival Rate Sometimes you care less about a fixed total number of users and more about a consistent rate of new arrivals, which maps closely to how you would describe real traffic in terms of requests per second.\nscn.injectOpen( constantUsersPerSec(10).during(Duration.ofMinutes(10)) ) This injects ten new users every second, for ten minutes straight. It is a good fit when your capacity planning is expressed in terms of throughput, like \u0026ldquo;we need to handle two hundred checkouts per minute during a sale.\u0026rdquo;\nrampUsersPerSec: Building Up a Rate Gradually Combine the idea of a ramp with the idea of a rate, and you get rampUsersPerSec, which increases the arrival rate smoothly over time rather than jumping straight to a fixed rate.\nscn.injectOpen( rampUsersPerSec(1).to(20).during(Duration.ofMinutes(5)) ) This starts at one new user per second and climbs steadily to twenty per second over five minutes. This is one of the most realistic shapes for a genuine load test, since it mimics traffic gradually building through a busy period instead of appearing as a step function.\nStaging Multiple Phases Together Real world traffic rarely follows one single pattern for an entire test. A more realistic run stages several profiles back to back, and Gatling lets you chain injection steps directly.\nscn.injectOpen( rampUsersPerSec(1).to(10).during(Duration.ofMinutes(2)), constantUsersPerSec(10).during(Duration.ofMinutes(10)), rampUsersPerSec(10).to(1).during(Duration.ofMinutes(2)) ) This warms up over two minutes, holds a steady sustained load for ten minutes, and then ramps back down over two minutes. This shape, warm up, hold, cool down, is close to what I use as a starting template for most sustained load tests, and it produces far more useful data than a single flat profile, because you can clearly see in the report how the system behaves during ramp up versus during a sustained steady state.\nOpen Versus Closed Models Everything above uses injectOpen, which describes an open workload model. New users arrive according to a schedule you define, completely independent of how quickly existing users finish their scenarios. This matches most public facing web traffic well, since real visitors do not wait for someone else to finish before showing up.\nGatling also supports a closed model through injectClosed, where you specify a fixed number of concurrent users active at any time, and a new virtual user only starts once an existing one finishes.\nscn.injectClosed( constantConcurrentUsers(50).during(Duration.ofMinutes(10)) ) This fits systems where concurrency itself is the constraint you care about, like an internal tool used by a fixed size support team, or a system in front of a resource pool with a hard concurrent connection limit. Choosing between open and closed models is really a question about what your real traffic actually looks like, not a question of which one is technically more advanced.\nPicking a Profile That Matches Your Goal Before writing any injection profile, it is worth being explicit about what question the test is trying to answer. If the question is \u0026ldquo;can we survive a sudden spike,\u0026rdquo; reach for atOnceUsers on top of some existing baseline load. If the question is \u0026ldquo;how does the system behave under steady sustained traffic,\u0026rdquo; a staged ramp up, hold, and ramp down like the example above is the right shape. If the question is about a hard concurrency limit somewhere in the system, a closed model fits better than an open one.\nWrapping Up Injection profiles are not just a technical detail, they define what your test is actually measuring. Match the shape of the profile to the real traffic pattern or the specific question you are trying to answer, and the results will actually mean something.\nNext time, we look at feeders, and how to drive a scenario with real data instead of hardcoded values, so every virtual user is not making the exact same request.\n","permalink":"https://abygeorgea.com/blog/2026/02/05/gatling-scenarios-virtual-users-injection-profiles/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2026/01/22/your-first-gatling-simulation-java-dsl/\"\u003eprevious post\u003c/a\u003e, we built a proper chained scenario and separated it cleanly from load setup. Today we focus entirely on that load setup, because how you inject virtual users into a scenario has a huge effect on what your test actually measures.\u003c/p\u003e\n\u003cp\u003eA lot of people new to Gatling reach for \u003ccode\u003eatOnceUsers\u003c/code\u003e and stop there. It has its place, but it does not represent how real traffic behaves, and using it for everything will give you misleading results.\u003c/p\u003e","title":"Scenarios and Virtual Users: Understanding Injection Profiles"},{"content":"In the previous post, we got a bare bones Gatling project running with a single request. That is enough to prove your setup works, but it is nowhere near what a real load test looks like. Today we build something closer to reality, a scenario with several requests chained together, and we talk properly about how the Java DSL fits together.\nHow the DSL Reads Gatling\u0026rsquo;s Java DSL is built to be read almost like a script, top to bottom. Once you get used to the shape of it, most of what you write ends up looking like a sentence describing user behavior. The three static imports you will use constantly are these.\nimport static io.gatling.javaapi.core.CoreDsl.*; import static io.gatling.javaapi.http.HttpDsl.*; CoreDsl gives you the general building blocks, things like scenario, exec, and feed, which we will get to in a later post. HttpDsl gives you everything specific to HTTP, like http, status, and the various header helpers. Almost every Gatling file you write starts with both of these.\nConfiguring the HTTP Protocol Properly The protocol builder is where shared HTTP behavior lives, so you are not repeating the same headers and settings on every single request. Here is a more complete version than the minimal one from the last post.\nHttpProtocolBuilder httpProtocol = http .baseUrl(\u0026#34;https://api.example.com\u0026#34;) .acceptHeader(\u0026#34;application/json\u0026#34;) .contentTypeHeader(\u0026#34;application/json\u0026#34;) .userAgentHeader(\u0026#34;gatling-load-test\u0026#34;) .maxConnectionsPerHost(20) .shareConnections(); maxConnectionsPerHost and shareConnections matter more than they look like they do. They control how Gatling manages its connection pool per virtual user, and getting this wrong is a common reason people see artificially bad response times that have nothing to do with the application under test and everything to do with the load generator itself starving for connections.\nChaining Requests Into a Real Scenario A scenario is a sequence of steps a virtual user performs. Chaining them together with exec reads naturally.\nScenarioBuilder browseAndCheckout = scenario(\u0026#34;Browse and Checkout\u0026#34;) .exec( http(\u0026#34;Get Product Catalog\u0026#34;) .get(\u0026#34;/products\u0026#34;) .check(status().is(200)) ) .pause(2) .exec( http(\u0026#34;View Product Detail\u0026#34;) .get(\u0026#34;/products/42\u0026#34;) .check(status().is(200)) .check(jsonPath(\u0026#34;$.name\u0026#34;).exists()) ) .pause(1) .exec( http(\u0026#34;Add To Cart\u0026#34;) .post(\u0026#34;/cart/items\u0026#34;) .body(StringBody(\u0026#34;{\\\u0026#34;productId\\\u0026#34;: 42, \\\u0026#34;quantity\\\u0026#34;: 1}\u0026#34;)) .check(status().is(201)) ) .pause(3) .exec( http(\u0026#34;Checkout\u0026#34;) .post(\u0026#34;/checkout\u0026#34;) .check(status().is(200)) .check(jsonPath(\u0026#34;$.orderId\u0026#34;).exists()) ); Every exec block is one HTTP call, and pause between them simulates a real user actually reading the page and deciding what to do next, rather than hammering the server with zero delay between requests. We will spend a whole post later in this series on getting pacing right, since it makes a big difference to how realistic your load actually is.\nSeparating Scenarios From Simulations Back in the folder structure from part one, we set aside a scenarios package separate from simulations. The reasoning is simple. A scenario describes a user journey. A simulation describes how many of those users you want, and when. Keeping them apart means you can reuse the same scenario across different load profiles without copying the request logic.\n// scenarios/CheckoutScenario.java package scenarios; import io.gatling.javaapi.core.ScenarioBuilder; import static io.gatling.javaapi.core.CoreDsl.*; import static io.gatling.javaapi.http.HttpDsl.*; public class CheckoutScenario { public static ScenarioBuilder browseAndCheckout() { return scenario(\u0026#34;Browse and Checkout\u0026#34;) .exec( http(\u0026#34;Get Product Catalog\u0026#34;) .get(\u0026#34;/products\u0026#34;) .check(status().is(200)) ) .pause(2) .exec( http(\u0026#34;View Product Detail\u0026#34;) .get(\u0026#34;/products/42\u0026#34;) .check(status().is(200)) ) .pause(1) .exec( http(\u0026#34;Add To Cart\u0026#34;) .post(\u0026#34;/cart/items\u0026#34;) .body(StringBody(\u0026#34;{\\\u0026#34;productId\\\u0026#34;: 42, \\\u0026#34;quantity\\\u0026#34;: 1}\u0026#34;)) .check(status().is(201)) ) .pause(3) .exec( http(\u0026#34;Checkout\u0026#34;) .post(\u0026#34;/checkout\u0026#34;) .check(status().is(200)) ); } } And the simulation class becomes short, focused only on load configuration, not request detail.\n// simulations/CheckoutLoadSimulation.java package simulations; import config.HttpProtocolConfig; import io.gatling.javaapi.core.Simulation; import scenarios.CheckoutScenario; import static io.gatling.javaapi.core.CoreDsl.*; public class CheckoutLoadSimulation extends Simulation { { setUp( CheckoutScenario.browseAndCheckout() .injectOpen(atOnceUsers(10)) ).protocols(HttpProtocolConfig.httpProtocol); } } This mirrors a pattern you have probably seen in other kinds of test automation. Keep the thing that describes behavior separate from the thing that describes how much load to throw at it. It means a new load profile is a one line change in the simulation class, with zero risk of accidentally breaking the scenario logic itself.\nA Shared Protocol Config While we are at it, let\u0026rsquo;s move the protocol builder into its own class too, so every simulation in the project shares one consistent configuration.\n// config/HttpProtocolConfig.java package config; import io.gatling.javaapi.http.HttpProtocolBuilder; import static io.gatling.javaapi.http.HttpDsl.*; public class HttpProtocolConfig { public static final HttpProtocolBuilder httpProtocol = http .baseUrl(\u0026#34;https://api.example.com\u0026#34;) .acceptHeader(\u0026#34;application/json\u0026#34;) .contentTypeHeader(\u0026#34;application/json\u0026#34;) .userAgentHeader(\u0026#34;gatling-load-test\u0026#34;); } Now if the base URL ever needs to change, or a shared header needs adding, there is exactly one place to make that change across the whole framework.\nWrapping Up We now have a chained, multi step scenario, a clean separation between scenario logic and load setup, and a shared protocol configuration. This is starting to look like an actual framework rather than a single script.\nNext time, we look at virtual users and injection profiles properly. atOnceUsers is the simplest possible profile, and there is a lot more control available once you need something closer to a realistic ramp up.\n","permalink":"https://abygeorgea.com/blog/2026/01/22/your-first-gatling-simulation-java-dsl/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2026/01/08/getting-started-gatling-java-project-setup/\"\u003eprevious post\u003c/a\u003e, we got a bare bones Gatling project running with a single request. That is enough to prove your setup works, but it is nowhere near what a real load test looks like. Today we build something closer to reality, a scenario with several requests chained together, and we talk properly about how the Java DSL fits together.\u003c/p\u003e\n\u003ch2 id=\"how-the-dsl-reads\"\u003eHow the DSL Reads\u003c/h2\u003e\n\u003cp\u003eGatling\u0026rsquo;s Java DSL is built to be read almost like a script, top to bottom. Once you get used to the shape of it, most of what you write ends up looking like a sentence describing user behavior. The three static imports you will use constantly are these.\u003c/p\u003e","title":"Your First Real Simulation: The Gatling Java DSL Explained"},{"content":"Performance testing has a reputation for being complicated to get into. Heavy tools, confusing scripting languages, a steep learning curve before you even run your first load test. Gatling is one of the tools that actually breaks that pattern, especially now that it has a proper Java DSL. If you already write Java for a living, you can be productive in Gatling within a day.\nThis is the first post in a twelve part series on building a real performance testing framework with Gatling and Java. We start right at the beginning, with project setup, and build up from there.\nWhat Gatling Actually Is Gatling is a load testing tool built on top of an asynchronous, non blocking engine. That matters practically because a single Gatling instance can simulate a large number of concurrent virtual users without needing a thread per user the way some older tools do. You write a simulation once, describing what a user does and how many users you want to run, and Gatling handles the heavy lifting of actually generating that load.\nThe part that matters most for this series is that you write simulations in Java, using Gatling\u0026rsquo;s Java DSL. No separate scripting language, no proprietary IDE plugin required. It is a Maven or Gradle project like any other Java project you already know how to work with.\nSetting Up the Project With Maven The quickest way to get a working project is Gatling\u0026rsquo;s official Maven archetype. Open a terminal and run this.\nmvn archetype:generate \\ -DarchetypeGroupId=io.gatling.highcharts \\ -DarchetypeArtifactId=gatling-highcharts-maven-archetype \\ -DarchetypeVersion=LATEST Maven will ask you for a group id, an artifact id, and a version, the same as any archetype based project. Once it finishes, you get a working folder structure with the Gatling Maven plugin already wired up.\nIf you prefer to add Gatling to an existing project instead of generating a fresh one, add the plugin and dependency directly to your pom.xml.\n\u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;io.gatling.highcharts\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;gatling-charts-highcharts\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;LATEST\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;build\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;io.gatling\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;gatling-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;LATEST\u0026lt;/version\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; Check the Maven Central page for the current version numbers before pinning them, since both the plugin and the charts dependency move forward together and need to stay in sync with each other.\nA Folder Layout That Scales The archetype gives you a src/test/java folder for your simulations and a src/test/resources folder for data files. As the framework grows, it is worth organizing this further, the same way you would structure any real Java project.\nsrc/test/java/ ├── simulations/ │ └── CheckoutLoadSimulation.java ├── scenarios/ │ └── CheckoutScenario.java ├── config/ │ └── HttpProtocolConfig.java └── utils/ └── EnvironmentConfig.java src/test/resources/ ├── data/ │ └── users.csv └── bodies/ └── login-request.json simulations holds the actual Gatling simulation classes, the ones you run directly. scenarios holds reusable scenario definitions that simulations can compose together. config holds shared setup like your HTTP protocol configuration. utils holds small helpers, like reading environment variables for base URLs. We will fill most of these in properly over the next few posts.\nWriting a Minimal Simulation Let\u0026rsquo;s confirm everything works with the smallest possible simulation. Create this file under src/test/java/simulations.\npackage simulations; import io.gatling.javaapi.core.ScenarioBuilder; import io.gatling.javaapi.core.Simulation; import io.gatling.javaapi.http.HttpProtocolBuilder; import static io.gatling.javaapi.core.CoreDsl.*; import static io.gatling.javaapi.http.HttpDsl.*; public class SmokeTestSimulation extends Simulation { HttpProtocolBuilder httpProtocol = http .baseUrl(\u0026#34;https://example.com\u0026#34;) .acceptHeader(\u0026#34;application/json\u0026#34;); ScenarioBuilder scn = scenario(\u0026#34;Smoke Test\u0026#34;) .exec( http(\u0026#34;Home Page\u0026#34;) .get(\u0026#34;/\u0026#34;) .check(status().is(200)) ); { setUp( scn.injectOpen(atOnceUsers(1)) ).protocols(httpProtocol); } } A quick walk through of what each piece does. httpProtocol sets shared HTTP settings, in this case the base URL that every relative path in the simulation resolves against. scn describes a scenario, a single named request in this case, with a check that the response comes back with a 200 status. The block inside the curly braces is where you actually configure the run, telling Gatling to inject one single user at once against this protocol.\nRun it directly with the Maven plugin.\nmvn gatling:test Gatling will print a live summary in the terminal as the run happens, and once it finishes, it generates a full HTML report on disk. We will dig into reading that report properly a few posts from now, but for now, a green summary with no failed requests is exactly what you want to see.\nWrapping Up At this point you have a working Gatling and Java project, a folder structure ready to grow, and one passing simulation to prove the setup works end to end.\nNext time, we go deeper into the Java DSL itself. We will build a proper simulation with multiple requests, look at how exec and chaining work, and start separating scenario logic from simulation setup the way a real framework should.\n","permalink":"https://abygeorgea.com/blog/2026/01/08/getting-started-gatling-java-project-setup/","summary":"\u003cp\u003ePerformance testing has a reputation for being complicated to get into. Heavy tools, confusing scripting languages, a steep learning curve before you even run your first load test. Gatling is one of the tools that actually breaks that pattern, especially now that it has a proper Java DSL. If you already write Java for a living, you can be productive in Gatling within a day.\u003c/p\u003e\n\u003cp\u003eThis is the first post in a twelve part series on building a real performance testing framework with Gatling and Java. We start right at the beginning, with project setup, and build up from there.\u003c/p\u003e","title":"Getting Started: Setting Up a Gatling and Java Project From Scratch"},{"content":"The Hype: Jason Huggins Just Announced Vibium, and Browser Automation Will Never Be the Same If you’ve spent any time in the software testing space over the last two decades, you know the name Jason Huggins. He’s the guy who created Selenium back in 2004, basically founding modern web test automation, and later gave us Appium for mobile.\nSo when Jason Huggins drops a new open-source project, the entire testing community stops what it\u0026rsquo;s doing and looks up.\nWhen he announced Vibium—an AI-native browser automation framework designed as the modern, spiritual successor to Selenium—the hype in the QA and dev world went from 0 to 100 overnight.\nWhy Everyone Lost Their Minds To understand the excitement, you have to remember why Selenium became frustrating: the infamous \u0026ldquo;maintenance tax.\u0026rdquo; We’ve all spent countless hours fixing brittle CSS selectors, debugging flaky locators, and patching tests that broke simply because someone renamed a CSS class or moved a button 10 pixels to the left.\nVibium flips the whole approach on its head by leaning into intent-driven automation and the WebDriver BiDi (Bi-Directional) protocol.\nInstead of writing brittle code that searches for exact DOM nodes, Vibium lets you express semantic intent. The underlying AI layer maps the page visually and semantically rather than as a rigid tree of code tags. If a button\u0026rsquo;s text or styling changes slightly, the framework uses its AI engine to infer what you meant, self-healing the interaction in real time instead of crashing your entire CI build.\nThe Verdict It feels like we’ve officially moved past the phase of just using LLMs to generate boilerplate code, and into an era where open-source, AI-native infrastructure actually handles the heavy lifting of browser interaction and self-verification.\nIt’s early days for the project, but if history is any indicator, whenever Jason Huggins reimagines browser automation, the rest of the industry eventually follows\nI definitely like the concept of having a framework on WebDriver BiDI , so that we have an option outside of corporate giants ( Playwright)\n","permalink":"https://abygeorgea.com/blog/2025/12/01/vibium/","summary":"\u003ch1 id=\"the-hype-jason-huggins-just-announced-vibium-and-browser-automation-will-never-be-the-same\"\u003eThe Hype: Jason Huggins Just Announced Vibium, and Browser Automation Will Never Be the Same\u003c/h1\u003e\n\u003cp\u003eIf you’ve spent any time in the software testing space over the last two decades, you know the name \u003cstrong\u003eJason Huggins\u003c/strong\u003e. He’s the guy who created Selenium back in 2004, basically founding modern web test automation, and later gave us Appium for mobile.\u003c/p\u003e\n\u003cp\u003eSo when Jason Huggins drops a new open-source project, the entire testing community stops what it\u0026rsquo;s doing and looks up.\u003c/p\u003e","title":"Vibium"},{"content":"Running huge regression suites every time I push a small change to a repo is super inefficient and slow, especially as projects start growing and getting complex. Lately, I\u0026rsquo;ve been diving deep into AI-Driven Risk-Based Selection to make my testing workflow fast, targeted, and lean.\nSmarter Builds with Risk-Based Selection Instead of blindly firing off every single test on every commit, I’ve been tinkering with a script that inspects the exact files changed in a Git diff. The script parses the modified code, maps out the underlying dependencies, and picks only the top 15% or so of tests that are actually affected by those changes.\nBy focusing execution strictly on what could actually break, I\u0026rsquo;ve managed to cut my local test run and CI build times in half. It turns a ten-minute wait into a quick coffee sip, all without letting obvious bugs slip through into the main branch.\nHow claude code build it ( not me :-) ) To get this working, I hooked a lightweight Python script into my local Git workflow and CI pipeline. Here is the step-by-step mechanism:\nExtracting the Git Diff: First, the script runs git diff --name-only HEAD~1 to snag a precise list of modified files in the latest commit. AST Parsing for Dependency Graphs: For Python or JavaScript files, I feed the changed code into an Abstract Syntax Tree (AST) parser (like Python\u0026rsquo;s ast module or @babel/parser). This builds a call-graph map showing every class, function, and module importing or touching those changed lines. LLM Context Vector Matching: For harder-to-trace relationships—like API routes or decoupled event listeners—I pass the file diffs alongside a lightweight JSON map of my test suite to a small LLM endpoint. The prompt asks the model to rank test files by relevance (0.0 to 1.0 confidence score) based on semantic overlap and risk impact. Dynamic Test Filtering: The script filters out any test scoring under a 0.7 confidence threshold and outputs a custom execution flag straight into the test runner (e.g., passing specific file paths to pytest or playwright test). The Bumpy Part: Getting the Dependency Mapping Right The trickiest part of getting this working was avoiding false negatives—situations where the script gets too aggressive with trimming tests and skips a suite that should have run.\n","permalink":"https://abygeorgea.com/blog/2025/09/25/smart-regression-testing/","summary":"\u003cp\u003eRunning huge regression suites every time I push a small change to a repo is super inefficient and slow, especially as projects start growing and getting complex. Lately, I\u0026rsquo;ve been diving deep into \u003cstrong\u003eAI-Driven Risk-Based Selection\u003c/strong\u003e to make my testing workflow fast, targeted, and lean.\u003c/p\u003e\n\u003ch3 id=\"smarter-builds-with-risk-based-selection\"\u003eSmarter Builds with Risk-Based Selection\u003c/h3\u003e\n\u003cp\u003eInstead of blindly firing off every single test on every commit, I’ve been tinkering with a script that inspects the exact files changed in a Git diff. The script parses the modified code, maps out the underlying dependencies, and picks only the top 15% or so of tests that are actually affected by those changes.\u003c/p\u003e","title":"Smart Regression Testing"},{"content":"In the previous post, ZAP gave us automated coverage against a running API, probing for common vulnerability patterns in requests and responses. There is an entire category of risk that scan never touches, because it does not live in API behavior at all. It lives in what dependencies a service pulls in, and what a commit accidentally includes. This closing post in the OWASP series covers both.\nWhy Dependency Scanning Is Its Own Category Every Spring Boot service pulls in dozens, often hundreds, of transitive dependencies, and any one of them can have a known vulnerability disclosed after you first added it. A service can pass every API test and every ZAP scan cleanly while still shipping a logging library with a critical, publicly known remote code execution flaw. Nothing about API level testing catches this, because the vulnerability is not in your code\u0026rsquo;s behavior, it is in a jar sitting in your classpath.\nOWASP Dependency-Check OWASP Dependency-Check scans a project\u0026rsquo;s dependencies against the National Vulnerability Database and flags known CVEs by version.\n\u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.owasp\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;dependency-check-maven\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;10.0.4\u0026lt;/version\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;failBuildOnCVSS\u0026gt;7\u0026lt;/failBuildOnCVSS\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; mvn org.owasp:dependency-check-maven:check failBuildOnCVSS set to 7 means the build fails on high and critical severity findings, based on the standard CVSS scoring scale, while lower severity issues still show up in the report without blocking the pipeline outright. That threshold is worth tuning to a level the team can realistically act on, since setting it too aggressively on a large existing project can surface an overwhelming first report.\nWiring Dependency Scanning Into CI jobs: dependency-check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run OWASP Dependency-Check run: mvn org.owasp:dependency-check-maven:check - name: Upload report uses: actions/upload-artifact@v4 with: name: dependency-check-report path: target/dependency-check-report.html Running this on a schedule, nightly for example, in addition to on every pull request, matters here in a way it does not for API tests. A dependency can go from safe to vulnerable overnight, the moment a new CVE is published against a version you already shipped months ago, with no code change on your side at all.\nSecrets Scanning A different but related risk is a credential accidentally committed to the repository, an API key pasted into a config file during local debugging, or a database password hardcoded while testing something quickly and never removed. Secrets scanning tools like Gitleaks scan commit history and new commits for patterns that look like credentials.\njobs: secrets-scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Run Gitleaks uses: gitleaks/gitleaks-action@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} Running this on every pull request, not just on a schedule, is worth the extra few seconds it adds to the pipeline, since the goal is catching a leaked credential before it merges to main and becomes part of permanent git history, at which point simply deleting the file no longer removes it.\nWhat to Do When Something Is Found A dependency finding usually means one of two paths, upgrade to a patched version if one exists, or, if no patched version is available yet, document the accepted risk explicitly with a suppression entry that includes a reason and a reference back to the CVE, rather than silently ignoring the finding.\n\u0026lt;suppress\u0026gt; \u0026lt;notes\u0026gt;No patched version available yet, tracked in JIRA-4821, mitigated by network isolation\u0026lt;/notes\u0026gt; \u0026lt;cve\u0026gt;CVE-2025-XXXXX\u0026lt;/cve\u0026gt; \u0026lt;/suppress\u0026gt; A secrets finding is different and more urgent. The credential needs to be rotated immediately, not just removed from the codebase, since the moment it existed in a commit it should be treated as compromised regardless of whether the repository is private.\nPulling the OWASP Series Together Across this series we covered BOLA and authentication as targeted, hand written tests catching specific business logic mistakes, ZAP as a broader automated scan against a running API, and now dependency and secrets scanning covering an entirely different layer, what your service depends on and what accidentally ends up in its history. None of these four approaches replaces the others. Together, running continuously in CI rather than as a periodic manual audit, they form a reasonable first layer of security coverage that a QE team can own directly, well before anything needs to escalate to a dedicated security review.\n","permalink":"https://abygeorgea.com/blog/2025/07/01/dependency-and-secrets-scanning-in-cicd/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2025/06/17/automating-security-scans-with-owasp-zap/\"\u003eprevious post\u003c/a\u003e, ZAP gave us automated coverage against a running API, probing for common vulnerability patterns in requests and responses. There is an entire category of risk that scan never touches, because it does not live in API behavior at all. It lives in what dependencies a service pulls in, and what a commit accidentally includes. This closing post in the OWASP series covers both.\u003c/p\u003e\n\u003ch2 id=\"why-dependency-scanning-is-its-own-category\"\u003eWhy Dependency Scanning Is Its Own Category\u003c/h2\u003e\n\u003cp\u003eEvery Spring Boot service pulls in dozens, often hundreds, of transitive dependencies, and any one of them can have a known vulnerability disclosed after you first added it. A service can pass every API test and every ZAP scan cleanly while still shipping a logging library with a critical, publicly known remote code execution flaw. Nothing about API level testing catches this, because the vulnerability is not in your code\u0026rsquo;s behavior, it is in a jar sitting in your classpath.\u003c/p\u003e","title":"Dependency and Secrets Scanning: Closing the Gap ZAP Doesn't Cover"},{"content":"The last two posts covered specific, hand written test cases for BOLA, broken authentication, and excessive data exposure. Those tests are precise and fast, but they only catch what you thought to write a test for. OWASP ZAP, the Zed Attack Proxy, takes a different approach, actively probing an API for a much broader set of known vulnerability patterns automatically. This post covers wiring it into a pipeline as a regression gate, and just as importantly, how to keep it from becoming noise nobody reads.\nBaseline Scan: Fast and Passive ZAP\u0026rsquo;s baseline scan is the lighter of its two main modes. It spiders the API, passively observes traffic, and flags obvious issues like missing security headers or verbose error messages, without sending anything that could actually mutate data. That makes it safe to run against a real environment, including one with real data in it.\ndocker run -t owasp/zap2docker-stable zap-baseline.py \\ -t https://payment-api-staging.internal \\ -r baseline-report.html This is fast enough to run on every pull request against a staging deployment, and it is a reasonable default gate for a team just getting started with automated security scanning.\nFull Scan: Active and Slower ZAP\u0026rsquo;s full scan goes further, actively attempting known attack patterns, including basic injection and fuzzing attempts against parameters it discovers. This is meaningfully slower and it does send requests capable of mutating state, which means it belongs against a dedicated test environment, never production, and ideally an environment where a reset between runs is cheap.\ndocker run -t owasp/zap2docker-stable zap-full-scan.py \\ -t https://payment-api-test.internal \\ -r full-scan-report.html Running the full scan nightly, rather than on every pull request, is a reasonable middle ground for most teams, since the slower runtime and larger blast radius make it less suited to blocking every single merge.\nWiring the Baseline Scan Into a Pipeline jobs: security-baseline: runs-on: ubuntu-latest steps: - name: Run ZAP baseline scan run: | docker run -t owasp/zap2docker-stable zap-baseline.py \\ -t ${{ vars.STAGING_URL }} \\ -r baseline-report.html \\ -x baseline-report.xml - name: Upload report uses: actions/upload-artifact@v4 with: name: zap-baseline-report path: baseline-report.html By default, ZAP\u0026rsquo;s baseline scan exits with a warning status rather than failing the build outright, which is a deliberate choice worth keeping in mind rather than fighting.\nTriaging Findings Without Drowning in Noise This is the part that determines whether a ZAP scan is actually useful or just something everyone learns to ignore. A fresh baseline scan against a real API often returns dozens of findings, many of them low severity or genuinely not applicable, like a missing header on an endpoint that was never meant to be called directly by a browser.\nThe fix is a maintained rules file that explicitly suppresses findings the team has reviewed and accepted, rather than starting from zero every time.\n- Missing Anti-clickjacking Header: ignore: true reason: \u0026#34;API only, no browser rendering, X-Frame-Options not applicable\u0026#34; - X-Content-Type-Options Header Missing: ignore: false zap-baseline.py -t ${{ vars.STAGING_URL }} -c zap-rules.conf -r baseline-report.html Building this rules file takes an initial time investment, sitting down with the first real scan report and making a genuine call on each finding. After that, new findings on subsequent runs are actually new, which is what makes the scan worth someone\u0026rsquo;s attention going forward instead of a report nobody opens.\nFailing the Build on High Severity Findings Once the noise is under control, it becomes reasonable to fail the pipeline specifically on high severity findings, while leaving lower severity ones as visible but non-blocking.\nzap-baseline.py -t ${{ vars.STAGING_URL }} -c zap-rules.conf -r report.html EXIT_CODE=$? if [ $EXIT_CODE -eq 1 ]; then echo \u0026#34;High severity findings detected, failing build\u0026#34; exit 1 fi What ZAP Does Not Replace ZAP is a strong complement to the hand written tests from the previous two posts, not a substitute for them. It is good at finding generic patterns, missing headers, obvious injection points, common misconfigurations, but it does not understand your specific business logic, which is exactly why the earlier BOLA test, checking one specific user cannot see another specific user\u0026rsquo;s data, still matters and ZAP will not reliably find that particular class of issue on its own.\nThe final post in this series covers a different layer entirely, dependency and secrets scanning, which catches risks that neither hand written API tests nor a ZAP scan against a running API will ever see.\n","permalink":"https://abygeorgea.com/blog/2025/06/17/automating-security-scans-with-owasp-zap/","summary":"\u003cp\u003eThe last two posts covered specific, hand written test cases for BOLA, broken authentication, and excessive data exposure. Those tests are precise and fast, but they only catch what you thought to write a test for. OWASP ZAP, the Zed Attack Proxy, takes a different approach, actively probing an API for a much broader set of known vulnerability patterns automatically. This post covers wiring it into a pipeline as a regression gate, and just as importantly, how to keep it from becoming noise nobody reads.\u003c/p\u003e","title":"Automating Security Regression with OWASP ZAP in CI/CD"},{"content":"In the previous post, we focused entirely on BOLA and object ownership checks. This post covers two more categories from the OWASP API Security Top 10 that tend to show up together in practice, broken authentication and excessive data exposure. Both are less about a single missing check and more about a general habit of trusting the client too much.\nBroken Authentication: Token Expiry A surprising number of APIs issue tokens correctly but never quite get around to enforcing their expiry properly. Testing this directly is simple once you have a way to generate an expired token.\n@Test void rejectsExpiredToken() { String expiredToken = tokenFactory.createExpired(\u0026#34;user-a@example.com\u0026#34;); given() .header(\u0026#34;Authorization\u0026#34;, \u0026#34;Bearer \u0026#34; + expiredToken) .when() .get(\u0026#34;/accounts/1042/transactions\u0026#34;) .then() .statusCode(401); } It is worth also testing the boundary directly, a token that expired one second ago, rather than only testing a token generated with an expiry far in the past. Clock skew and off by one errors in expiry checks are common enough to be worth a dedicated test.\n@Test void rejectsTokenExpiredOneSecondAgo() { String token = tokenFactory.createWithExpiry(Instant.now().minusSeconds(1)); given() .header(\u0026#34;Authorization\u0026#34;, \u0026#34;Bearer \u0026#34; + token) .when() .get(\u0026#34;/accounts/1042/transactions\u0026#34;) .then() .statusCode(401); } Broken Authentication: Credential Stuffing Resistance Login endpoints are a common target for credential stuffing, automated attempts at large numbers of username and password combinations. Testing full scale resistance is beyond a normal test suite\u0026rsquo;s scope, but confirming basic protections exist is not.\n@Test void locksAccountAfterRepeatedFailedLogins() { for (int i = 0; i \u0026lt; 5; i++) { given() .body(Map.of(\u0026#34;email\u0026#34;, \u0026#34;user-a@example.com\u0026#34;, \u0026#34;password\u0026#34;, \u0026#34;wrong-password\u0026#34;)) .when() .post(\u0026#34;/auth/login\u0026#34;) .then() .statusCode(401); } given() .body(Map.of(\u0026#34;email\u0026#34;, \u0026#34;user-a@example.com\u0026#34;, \u0026#34;password\u0026#34;, \u0026#34;correct-password\u0026#34;)) .when() .post(\u0026#34;/auth/login\u0026#34;) .then() .statusCode(423); } That last assertion, a 423 locked status even with the correct password after repeated failures, confirms the lockout genuinely blocks further attempts rather than only logging them.\nExcessive Data Exposure This risk shows up when an API returns its full internal object rather than a response shaped specifically for the client, relying on the client application to only display the fields it needs. That habit works fine until a different client, or someone inspecting network traffic directly, sees the entire object.\n@GetMapping(\u0026#34;/accounts/{accountId}\u0026#34;) public Account getAccount(@PathVariable Long accountId) { return accountRepository.findById(accountId).orElseThrow(); } If Account is the JPA entity itself, this endpoint likely serializes every column, including things like an internal risk score, a full card number if one is stored, or a hashed password field that should never leave the service at all.\n@Test void accountResponseDoesNotExposeInternalFields() { Response response = given() .header(\u0026#34;Authorization\u0026#34;, \u0026#34;Bearer \u0026#34; + tokenForUserA) .when() .get(\u0026#34;/accounts/{id}\u0026#34;, userAAccountId); response.then() .body(\u0026#34;$\u0026#34;, not(hasKey(\u0026#34;passwordHash\u0026#34;))) .body(\u0026#34;$\u0026#34;, not(hasKey(\u0026#34;internalRiskScore\u0026#34;))) .body(\u0026#34;$\u0026#34;, not(hasKey(\u0026#34;fullCardNumber\u0026#34;))); } This test does not care what fields the response should contain, only that specific sensitive ones are absent. That framing is deliberate. A response DTO evolves over time, and a test asserting the full shape of a response breaks constantly for unrelated reasons. A test asserting specific sensitive fields never appear stays focused on the actual risk and stays stable through unrelated changes.\nThe Underlying Habit to Fix Both of these issues come from the same root cause, trusting the client to behave responsibly with what the server gives it, or trusting a token\u0026rsquo;s presence without checking its validity thoroughly. The fix in both cases is the same instinct applied consistently. Build a response object deliberately, with only the fields a client actually needs, and validate every property of a token, not just whether it exists and is signed correctly.\nThe next post moves from targeted test cases like these to automated scanning, wiring OWASP ZAP into a CI pipeline to catch a broader class of these issues without writing a dedicated test for each one individually.\n","permalink":"https://abygeorgea.com/blog/2025/06/03/testing-authentication-and-data-exposure-in-apis/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2025/05/20/testing-broken-object-level-authorization/\"\u003eprevious post\u003c/a\u003e, we focused entirely on BOLA and object ownership checks. This post covers two more categories from the OWASP API Security Top 10 that tend to show up together in practice, broken authentication and excessive data exposure. Both are less about a single missing check and more about a general habit of trusting the client too much.\u003c/p\u003e\n\u003ch2 id=\"broken-authentication-token-expiry\"\u003eBroken Authentication: Token Expiry\u003c/h2\u003e\n\u003cp\u003eA surprising number of APIs issue tokens correctly but never quite get around to enforcing their expiry properly. Testing this directly is simple once you have a way to generate an expired token.\u003c/p\u003e","title":"Testing Authentication and Excessive Data Exposure in REST APIs"},{"content":"In the previous post, we walked through why the OWASP API Security Top 10 deserves attention from a QE team directly, not just a security specialist. Broken Object Level Authorization sits at the top of that list, and for good reason. It is one of the easiest vulnerabilities to introduce by accident, and one of the easiest to test for once you know to look.\nWhat BOLA Actually Is BOLA happens when an API checks that a user is authenticated, but does not check that the authenticated user is actually allowed to access the specific object they are requesting. The classic example, a transaction history endpoint.\nGET /accounts/1042/transactions Authorization: Bearer \u0026lt;valid token for user A\u0026gt; That request correctly returns user A\u0026rsquo;s transactions. The vulnerability shows up the moment someone tries this instead.\nGET /accounts/1043/transactions Authorization: Bearer \u0026lt;same valid token for user A\u0026gt; If the API returns user 1043\u0026rsquo;s transactions instead of a 403, it authenticated the request correctly and then completely failed to check whether user A actually owns account 1043. The token was valid. The authorization was missing.\nWhy It Happens So Easily This bug rarely comes from a careless developer skipping an obvious check. It usually comes from a query that is technically correct and silently trusts the ID in the URL.\n@GetMapping(\u0026#34;/accounts/{accountId}/transactions\u0026#34;) public List\u0026lt;Transaction\u0026gt; getTransactions(@PathVariable Long accountId) { return transactionRepository.findByAccountId(accountId); } This code does exactly what it looks like it does. It fetches transactions for whatever account ID is in the URL, with no check that the account belongs to the authenticated caller. It compiles, it passes a happy path test with the right account ID, and it ships.\nThe fix is a single added check, but someone has to think to write it, and more importantly, someone has to write a test that fails without it.\n@GetMapping(\u0026#34;/accounts/{accountId}/transactions\u0026#34;) public List\u0026lt;Transaction\u0026gt; getTransactions( @PathVariable Long accountId, @AuthenticationPrincipal AppUser caller) { if (!accountService.isOwnedBy(accountId, caller.getId())) { throw new AccessDeniedException(\u0026#34;Not authorized for this account\u0026#34;); } return transactionRepository.findByAccountId(accountId); } Writing the Test The test itself is straightforward once you frame it correctly. Authenticate as one user, then attempt to access an object belonging to a different user, and assert the request is rejected.\n@Test void userCannotAccessAnotherUsersTransactions() { String tokenForUserA = authenticateAs(\u0026#34;user-a@example.com\u0026#34;); given() .header(\u0026#34;Authorization\u0026#34;, \u0026#34;Bearer \u0026#34; + tokenForUserA) .when() .get(\u0026#34;/accounts/{accountId}/transactions\u0026#34;, userBAccountId) .then() .statusCode(403); } This one test, run against every object level endpoint in the API, catches a surprising number of real issues, precisely because the underlying mistake, trusting an ID from the URL without an ownership check, tends to repeat itself across an API rather than appearing once.\nMaking This Systematic Rather Than One-Off Writing this test for a single endpoint is useful. Writing it as a pattern applied to every endpoint that takes a resource ID is what actually closes the gap. A practical approach is a parameterized test that runs against a list of known object level endpoints, using a second test user\u0026rsquo;s ID for each.\n@ParameterizedTest @ValueSource(strings = { \u0026#34;/accounts/{id}/transactions\u0026#34;, \u0026#34;/accounts/{id}/statements\u0026#34;, \u0026#34;/accounts/{id}/beneficiaries\u0026#34; }) void enforcesObjectLevelAuthorization(String endpointTemplate) { given() .header(\u0026#34;Authorization\u0026#34;, \u0026#34;Bearer \u0026#34; + tokenForUserA) .when() .get(endpointTemplate, userBAccountId) .then() .statusCode(403); } As new object level endpoints get added to the API, adding one line to this list is a small cost for meaningful, ongoing coverage against the single most common API vulnerability category.\nWhat This Does Not Catch This kind of test only covers what you think to list. It does not catch a BOLA vulnerability in an endpoint nobody added to the parameterized list, and it does not catch more subtle variants, like an ID exposed indirectly through a nested object in a response rather than a URL path parameter. That gap is part of why automated scanning, which the OWASP ZAP post later in this series covers, is a useful complement rather than a replacement for tests like these.\nThe next post covers two more categories from the list together, broken authentication and excessive data exposure, both of which show up constantly in how an API handles tokens and shapes its JSON responses.\n","permalink":"https://abygeorgea.com/blog/2025/05/20/testing-broken-object-level-authorization/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2025/05/06/owasp-api-security-top-10-overview/\"\u003eprevious post\u003c/a\u003e, we walked through why the OWASP API Security Top 10 deserves attention from a QE team directly, not just a security specialist. Broken Object Level Authorization sits at the top of that list, and for good reason. It is one of the easiest vulnerabilities to introduce by accident, and one of the easiest to test for once you know to look.\u003c/p\u003e\n\u003ch2 id=\"what-bola-actually-is\"\u003eWhat BOLA Actually Is\u003c/h2\u003e\n\u003cp\u003eBOLA happens when an API checks that a user is authenticated, but does not check that the authenticated user is actually allowed to access the specific object they are requesting. The classic example, a transaction history endpoint.\u003c/p\u003e","title":"Testing for Broken Object Level Authorization (BOLA)"},{"content":"Most testers have at least heard of the OWASP Top 10, the well known list of the most critical web application security risks. Fewer have spent real time with its sibling list, the OWASP API Security Top 10, which exists because APIs fail in ways that are genuinely different from traditional web applications, and a checklist built for server rendered pages with form submissions misses most of what actually goes wrong in a REST API sitting behind a mobile app or a partner integration.\nThis post is an overview, framing why the API specific list matters, before the next several posts get hands on with testing for specific risks against a real Spring Boot service.\nWhy APIs Are a Different Attack Surface A traditional web application usually has a browser in front of it, rendering pages, handling cookies, and enforcing some baseline behavior through the browser itself. An API has no such intermediary. It exposes its full surface directly, often to a mobile client, another internal service, or a third party integration, none of which behave like a browser and none of which can be trusted to only send well formed requests.\nThat difference means risks like broken object level authorization, mass assignment through unexpected fields, and excessive data exposure in a JSON response show up constantly in APIs and comparatively rarely in the exact same form in a browser rendered page.\nThe List, Briefly The current OWASP API Security Top 10 covers ten categories. The ones we will spend the most time on in this series, because they show up constantly in payment and account focused APIs specifically, are:\nBroken Object Level Authorization, where an authenticated user can access or modify data belonging to another user simply by changing an ID in the request. Broken Authentication, covering weak token handling, missing expiry, and credential stuffing resistance. Excessive Data Exposure, where an API returns more fields than the client actually needs, relying on the client to filter, which leaks data the moment a different client consumes the same endpoint. Lack of Resources and Rate Limiting, where nothing stops a single client from exhausting a shared resource or brute forcing an endpoint. Security Misconfiguration, the broad category covering everything from verbose error messages leaking stack traces to permissive CORS settings nobody meant to ship. Why This Belongs in a QE Practice, Not Just Security There is a temptation to treat this list as something a separate security or penetration testing team owns exclusively. That is a mistake for two reasons. First, most of these issues are functional in nature, testable with the same tools and mindset used for any other API test, no specialized security tooling required to at least catch the obvious cases. Second, waiting for a periodic security audit means these issues sit in production, potentially for months, between audits.\nA BOLA vulnerability, for example, is really just a missing authorization check on an endpoint that otherwise works perfectly. Any API test suite already exercising that endpoint with valid data is one small addition away from also exercising it with another user\u0026rsquo;s ID, which is exactly the kind of test a QE team is well positioned to own directly.\nWhat This Series Actually Builds Rather than staying abstract, the next four posts each take one or two of these categories and build real, runnable tests against a sample payment API. We cover BOLA testing directly, authentication and data exposure together since they tend to overlap in practice, automating a security scan with OWASP ZAP inside a CI pipeline, and closing with dependency and secrets scanning, which catches an entirely different class of risk that API level testing alone does not touch.\nThe goal by the end is not a security certification. It is a concrete, automated first layer of defense that runs on every pull request, catching the mistakes that are common enough to be worth catching mechanically, before anything reaches a dedicated security review.\n","permalink":"https://abygeorgea.com/blog/2025/05/06/owasp-api-security-top-10-overview/","summary":"\u003cp\u003eMost testers have at least heard of the OWASP Top 10, the well known list of the most critical web application security risks. Fewer have spent real time with its sibling list, the OWASP API Security Top 10, which exists because APIs fail in ways that are genuinely different from traditional web applications, and a checklist built for server rendered pages with form submissions misses most of what actually goes wrong in a REST API sitting behind a mobile app or a partner integration.\u003c/p\u003e","title":"Inside the OWASP API Security Top 10"},{"content":"In the previous post, everything ran embedded, inside a single test class\u0026rsquo;s lifecycle. That is the right default for unit and component level tests. It stops working once several services in a pipeline all need to talk to the same stubbed dependency, or once integration tests running outside the JVM, a Postman collection for example, need to hit the same mock. That is what WireMock\u0026rsquo;s standalone mode is for.\nRunning WireMock Standalone The same WireMock artifact that ran embedded can run as its own process, listening on a real port like any other service.\ndocker run -d --name wiremock-inventory \\ -p 8089:8080 \\ -v $(pwd)/stubs:/home/wiremock/mappings \\ wiremock/wiremock:3.9.1 Stub definitions now live as JSON files in a stubs directory rather than inline Java code, which is exactly what makes this shareable across languages and tools, not just a single Java test suite.\n{ \u0026#34;request\u0026#34;: { \u0026#34;method\u0026#34;: \u0026#34;GET\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;/inventory/SKU-1042\u0026#34; }, \u0026#34;response\u0026#34;: { \u0026#34;status\u0026#34;: 200, \u0026#34;jsonBody\u0026#34;: { \u0026#34;sku\u0026#34;: \u0026#34;SKU-1042\u0026#34;, \u0026#34;available\u0026#34;: 37 }, \u0026#34;headers\u0026#34;: { \u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34; } } } Recording Real Responses Writing every stub by hand for a service with dozens of endpoints gets tedious fast, and it is easy to drift from what the real service actually returns. WireMock\u0026rsquo;s recording mode solves that by sitting in front of the real service and capturing genuine responses as stub files.\ndocker run -d --name wiremock-recorder \\ -p 8089:8080 \\ wiremock/wiremock:3.9.1 \\ --proxy-all=\u0026#34;https://inventory-staging.internal\u0026#34; \\ --record-mappings Point your client at localhost:8089 as usual, exercise the real flows you care about against staging, and WireMock forwards every request through to the real service while saving both the request and the actual response as a stub file. Stop the container afterward, and those recorded stubs become your starting point, edited by hand from there for the failure cases the real service will not reliably reproduce on demand.\nWiring It Into a Pipeline In CI, start WireMock as a service container before the tests that depend on it run.\njobs: integration-tests: runs-on: ubuntu-latest services: wiremock: image: wiremock/wiremock:3.9.1 ports: - 8089:8080 steps: - uses: actions/checkout@v4 - name: Load stub mappings run: | curl -X POST http://localhost:8089/__admin/mappings/import \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d @stubs/inventory-mappings.json - name: Run integration tests run: mvn verify -Dinventory.base-url=http://localhost:8089 Every job in the pipeline that needs the inventory dependency now points at the same WireMock container, configured identically, which keeps behavior consistent whether the tests are running on a developer\u0026rsquo;s machine or in CI.\nKeeping Stubs From Going Stale The one real risk with standalone stub files is drift. Nothing forces stubs/inventory-mappings.json to stay in sync with what the real inventory service actually returns once that service changes. A stub file checked in six months ago can happily keep passing tests long after the real API has moved on.\nThe most reliable mitigation is treating recorded stubs as something to refresh periodically against staging, on a schedule, rather than writing them once and assuming they stay accurate forever. This is also exactly the gap consumer-driven contract testing closes more rigorously, which is worth keeping in mind. WireMock and Pact are not competitors, they solve different problems well. WireMock gives you full control over a dependency\u0026rsquo;s behavior, failure modes included, for testing your own service in isolation. Pact gives you a guarantee that your assumptions about a dependency\u0026rsquo;s behavior are still true. A mature test strategy usually wants both, not one instead of the other.\nThat distinction is a good place to end this series, since it is the exact seam between what we just built with WireMock and what the earlier Pact series covered.\n","permalink":"https://abygeorgea.com/blog/2025/04/15/running-wiremock-in-ci/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2025/04/01/simulating-failures-with-wiremock/\"\u003eprevious post\u003c/a\u003e, everything ran embedded, inside a single test class\u0026rsquo;s lifecycle. That is the right default for unit and component level tests. It stops working once several services in a pipeline all need to talk to the same stubbed dependency, or once integration tests running outside the JVM, a Postman collection for example, need to hit the same mock. That is what WireMock\u0026rsquo;s standalone mode is for.\u003c/p\u003e\n\u003ch2 id=\"running-wiremock-standalone\"\u003eRunning WireMock Standalone\u003c/h2\u003e\n\u003cp\u003eThe same WireMock artifact that ran embedded can run as its own process, listening on a real port like any other service.\u003c/p\u003e","title":"Running WireMock in CI as a Standalone Server"},{"content":"In the previous post, we stubbed a happy path response from an inventory service and verified our client built the right request. Happy paths are the easy part. What actually determines whether a service survives production is how it behaves when a downstream dependency does not cooperate, and that is much harder to test against a real dependency, since you cannot exactly ask another team\u0026rsquo;s service to time out on demand. WireMock\u0026rsquo;s fault simulation is built for exactly this.\nSimulating a Slow Response Timeouts and circuit breakers are only worth having if something has actually tested that they trigger correctly. WireMock\u0026rsquo;s fixed delay makes that possible without waiting on a genuinely slow network.\nwireMock.stubFor(get(urlEqualTo(\u0026#34;/inventory/SKU-1042\u0026#34;)) .willReturn(aResponse() .withStatus(200) .withBody(\u0026#34;{\\\u0026#34;sku\\\u0026#34;: \\\u0026#34;SKU-1042\\\u0026#34;, \\\u0026#34;available\\\u0026#34;: 37}\u0026#34;) .withFixedDelay(3000))); assertThrows(TimeoutException.class, () -\u0026gt; client.getStock(\u0026#34;SKU-1042\u0026#34;)); If your client is configured with a two second timeout, this test proves that configuration actually does something, rather than just existing as a number in a properties file nobody has verified.\nSimulating Connection Failures Beyond slow responses, WireMock can simulate the connection itself misbehaving, which is a different failure mode your resilience code needs to handle separately.\nwireMock.stubFor(get(urlEqualTo(\u0026#34;/inventory/SKU-1042\u0026#34;)) .willReturn(aResponse() .withFault(Fault.CONNECTION_RESET_BY_PEER))); Other fault types are available for different scenarios, including EMPTY_RESPONSE for a connection that closes with nothing sent back at all, and MALFORMED_RESPONSE_CHUNK for a response that starts arriving and then breaks mid-stream. Each of these exercises a distinct code path in a well written HTTP client, and each one is close to impossible to reproduce reliably against a real service on demand.\nSimulating Malformed but Valid Responses Not every bad response is a network level failure. Sometimes a service is up, responds successfully at the HTTP level, and just returns something your code does not expect.\nwireMock.stubFor(get(urlEqualTo(\u0026#34;/inventory/SKU-1042\u0026#34;)) .willReturn(aResponse() .withStatus(200) .withHeader(\u0026#34;Content-Type\u0026#34;, \u0026#34;application/json\u0026#34;) .withBody(\u0026#34;{\\\u0026#34;sku\\\u0026#34;: \\\u0026#34;SKU-1042\\\u0026#34;}\u0026#34;))); InventoryLevel level = client.getStock(\u0026#34;SKU-1042\u0026#34;); assertEquals(0, level.available()); That response is missing the available field entirely. This test is really checking one specific decision in your deserialization logic, does a missing field default sensibly or does it throw an unhandled exception that takes down the calling thread. Either answer might be correct depending on the service, but it should be a decision your test suite actually verifies, not something you discover the first time the real service has a partial outage.\nChaining Failure Into Recovery WireMock supports stateful stubs through scenarios, which let you test a full sequence, a failure followed by a successful retry, rather than just a single failure in isolation.\nwireMock.stubFor(get(urlEqualTo(\u0026#34;/inventory/SKU-1042\u0026#34;)) .inScenario(\u0026#34;retry-then-succeed\u0026#34;) .whenScenarioStateIs(STARTED) .willReturn(aResponse().withStatus(503)) .willSetStateTo(\u0026#34;retried-once\u0026#34;)); wireMock.stubFor(get(urlEqualTo(\u0026#34;/inventory/SKU-1042\u0026#34;)) .inScenario(\u0026#34;retry-then-succeed\u0026#34;) .whenScenarioStateIs(\u0026#34;retried-once\u0026#34;) .willReturn(aResponse() .withStatus(200) .withBody(\u0026#34;{\\\u0026#34;sku\\\u0026#34;: \\\u0026#34;SKU-1042\\\u0026#34;, \\\u0026#34;available\\\u0026#34;: 37}\u0026#34;))); The first call to this endpoint returns a 503. The second call, after the scenario state changes, returns success. If your client has retry logic, this is how you prove it actually retries and actually succeeds on the second attempt, rather than just trusting the retry annotation is configured correctly.\nWhy This Matters More Than the Happy Path Ever Did A payment or inventory service that only ever gets tested against cooperative dependencies will have resilience code that has never actually run in a test. Timeouts, retries, circuit breakers, and fallback logic are the parts of a system most likely to have a bug precisely because they are hardest to exercise naturally. Simulating the failure directly, rather than hoping it eventually happens in staging, is what turns that code from theoretical to actually verified.\nThe next post moves this out of individual test classes and into CI, running WireMock as a standalone server so integration tests across a whole pipeline can share the same stubbed environment.\n","permalink":"https://abygeorgea.com/blog/2025/04/01/simulating-failures-with-wiremock/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2025/03/18/getting-started-with-wiremock/\"\u003eprevious post\u003c/a\u003e, we stubbed a happy path response from an inventory service and verified our client built the right request. Happy paths are the easy part. What actually determines whether a service survives production is how it behaves when a downstream dependency does not cooperate, and that is much harder to test against a real dependency, since you cannot exactly ask another team\u0026rsquo;s service to time out on demand. WireMock\u0026rsquo;s fault simulation is built for exactly this.\u003c/p\u003e","title":"Simulating Failures and Latency with WireMock"},{"content":"Lately, it feels like we’ve crossed into a totally new era with AI tools. Over the past few months, my focus has shifted from simple code auto-completions to playing around with Autonomous Agentic Workflows.\nInstead of just having an assistant that finishes a line of code, an agent acts more like a goal-oriented script. You give it a high-level target, and it can plan out multi-step actions, check the results, and adjust its approach on the fly.\nWhat I’ve Been Tinkering With 1. Autonomous End-to-End Test Generation Instead of writing Playwright scripts step-by-step, I’ve been experimenting with feeding high-level user journey specs into autonomous agents.\nThe agent inspects the application, navigates a live local build, generates the Playwright code, verifies that the test actually passes, and even prepares a clean Git commit—all with barely any manual intervention. Seeing a script map out its own execution path in real time is pretty incredible.\n2. Automated Accessibility Checks \u0026amp; Fixes Accessibility compliance is always one of those things that can feel tedious to audit by hand. Lately, I’ve been pairing traditional accessibility scanning tools like axe-core with LLM agents. When a component fails WCAG guidelines, the agent doesn\u0026rsquo;t just flag the error—it actually suggests specific code fixes to make the component accessible.\nThe Big Headache: Code Churn \u0026amp; Bloat The biggest problem I’ve run into with autonomous agents is that they write code at blistering speed, which quickly leads to codebase bloat.\nBecause agents don\u0026rsquo;t have a global memory of every utility script in a project, they tend to recreate redundant helper functions and duplicate existing tests. To keep my personal repos from turning into a chaotic mess, I’ve had to enforce strict linting rules, set code complexity thresholds, and make sure I carefully review every single agent-generated change before merging it.\n","permalink":"https://abygeorgea.com/blog/2025/03/25/rise-of-agentic-testing-framework/","summary":"\u003cp\u003eLately, it feels like we’ve crossed into a totally new era with AI tools. Over the past few months, my focus has shifted from simple code auto-completions to playing around with \u003cstrong\u003eAutonomous Agentic Workflows\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eInstead of just having an assistant that finishes a line of code, an agent acts more like a goal-oriented script. You give it a high-level target, and it can plan out multi-step actions, check the results, and adjust its approach on the fly.\u003c/p\u003e","title":"Rise Of Agentic Testing Framework"},{"content":"In the previous post, we looked at why WireMock\u0026rsquo;s embedded mode is a strong fit for a Java test suite specifically. This post gets into the actual setup, adding WireMock to a Spring Boot project and writing a real stub against a downstream service.\nAdding the Dependency \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.wiremock\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;wiremock-standalone\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.9.1\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; The standalone artifact bundles everything needed to run WireMock either embedded in a test or as its own process later, which keeps things simple while we are just getting started.\nStarting WireMock in a Test WireMock\u0026rsquo;s JUnit 5 extension manages the server\u0026rsquo;s lifecycle for you, starting it before each test class and stopping it afterward.\n@ExtendWith(WireMockExtension.class) class InventoryClientTest { @RegisterExtension static WireMockExtension wireMock = WireMockExtension.newInstance() .options(wireMockConfig().port(8089)) .build(); private final InventoryClient client = new InventoryClient(\u0026#34;http://localhost:8089\u0026#34;); } Point your real client\u0026rsquo;s base URL at localhost:8089, the same way you would point it at any other environment. Nothing about the client code itself needs to know it is talking to a mock.\nWriting a Stub A stub tells WireMock what request to expect and what response to return when it sees one that matches.\n@Test void returnsAvailableStock() { wireMock.stubFor(get(urlEqualTo(\u0026#34;/inventory/SKU-1042\u0026#34;)) .willReturn(aResponse() .withStatus(200) .withHeader(\u0026#34;Content-Type\u0026#34;, \u0026#34;application/json\u0026#34;) .withBody(\u0026#34;{\\\u0026#34;sku\\\u0026#34;: \\\u0026#34;SKU-1042\\\u0026#34;, \\\u0026#34;available\\\u0026#34;: 37}\u0026#34;))); InventoryLevel level = client.getStock(\u0026#34;SKU-1042\u0026#34;); assertEquals(37, level.available()); } Run that test and the client makes a real HTTP call, just against a mock server instead of the actual inventory service. WireMock matches the incoming request against every stub it knows about and returns the first one that fits.\nMatching on More Than the URL Real requests carry headers, query parameters, and bodies, and WireMock can match on all of them, which matters once you need different stubs for slightly different inputs.\nwireMock.stubFor(post(urlEqualTo(\u0026#34;/inventory/reserve\u0026#34;)) .withHeader(\u0026#34;Authorization\u0026#34;, matching(\u0026#34;Bearer .*\u0026#34;)) .withRequestBody(matchingJsonPath(\u0026#34;$.sku\u0026#34;, equalTo(\u0026#34;SKU-1042\u0026#34;))) .withRequestBody(matchingJsonPath(\u0026#34;$.quantity\u0026#34;, equalTo(\u0026#34;5\u0026#34;))) .willReturn(aResponse() .withStatus(200) .withBody(\u0026#34;{\\\u0026#34;reservationId\\\u0026#34;: \\\u0026#34;RES-9981\\\u0026#34;}\u0026#34;))); matchingJsonPath is worth calling out specifically. Rather than matching the entire request body as one exact string, which breaks the moment field order or whitespace changes, it lets you assert on individual fields within a JSON body, which is a much more resilient way to define a stub.\nVerifying What Your Client Actually Sent Stubbing the response is half the value. WireMock also lets you verify, after the fact, exactly what your client sent, which turns a stub into a genuine test of your client\u0026rsquo;s request building logic, not just its response parsing.\nwireMock.verify(postRequestedFor(urlEqualTo(\u0026#34;/inventory/reserve\u0026#34;)) .withRequestBody(matchingJsonPath(\u0026#34;$.quantity\u0026#34;, equalTo(\u0026#34;5\u0026#34;)))); If your client has a bug that sends the wrong field name or the wrong content type, this verification catches it, even though the stubbed response would have returned successfully regardless.\nWhere This Leaves Us At this point WireMock is standing in cleanly for a happy path dependency, matching requests precisely and letting us verify outbound calls. Real dependencies do not only return happy paths though. They time out, they return malformed responses, and they occasionally just fall over. The next post covers simulating exactly those failure conditions, since that is where service virtualization earns its keep the most.\n","permalink":"https://abygeorgea.com/blog/2025/03/18/getting-started-with-wiremock/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2025/03/04/wiremock-vs-mountebank/\"\u003eprevious post\u003c/a\u003e, we looked at why WireMock\u0026rsquo;s embedded mode is a strong fit for a Java test suite specifically. This post gets into the actual setup, adding WireMock to a Spring Boot project and writing a real stub against a downstream service.\u003c/p\u003e\n\u003ch2 id=\"adding-the-dependency\"\u003eAdding the Dependency\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026lt;dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;groupId\u0026gt;\u003c/span\u003eorg.wiremock\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/groupId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;artifactId\u0026gt;\u003c/span\u003ewiremock-standalone\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/artifactId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;version\u0026gt;\u003c/span\u003e3.9.1\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/version\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;scope\u0026gt;\u003c/span\u003etest\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/scope\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe standalone artifact bundles everything needed to run WireMock either embedded in a test or as its own process later, which keeps things simple while we are just getting started.\u003c/p\u003e","title":"Getting Started with WireMock: Your First Stub"},{"content":"I have written a fair amount here already about Mountebank for service virtualization, and it has served me well across a few different projects. But Mountebank is not the only serious option, and on a Java heavy stack in particular, WireMock tends to come up just as often, sometimes more. This post is not about replacing Mountebank, it is about knowing when WireMock is the better fit, since the two tools solve overlapping problems in genuinely different ways.\nSame Goal, Different Origin Both tools exist to stand in for a real dependency your service talks to, so you can test how your code behaves without needing that dependency actually running. Where they differ is in what world they were built for.\nMountebank is language agnostic by design. It runs as a standalone process, configured over HTTP with JSON, and it does not care what language is calling it. That makes it a strong pick when a test suite spans multiple languages, or when the team maintaining stubs is not necessarily the same team writing the service under test.\nWireMock grew up inside the Java ecosystem specifically. It can run standalone exactly like Mountebank, but it can also run embedded directly inside a JUnit test as a library, no separate process required. For a Spring Boot service where the whole test suite is already Java, that embedded mode removes a layer of infrastructure entirely.\nEmbedded Mode Is the Real Differentiator This is the single biggest practical difference. With WireMock, a test class can start and stop a mock server as part of its own lifecycle, in process, with no Docker container or separate binary to manage.\n@RegisterExtension static WireMockExtension wireMock = WireMockExtension.newInstance() .options(wireMockConfig().port(8089)) .build(); @Test void authorizesPaymentSuccessfully() { wireMock.stubFor(post(\u0026#34;/payments/authorize\u0026#34;) .willReturn(okJson(\u0026#34;{\\\u0026#34;status\\\u0026#34;: \\\u0026#34;AUTHORIZED\\\u0026#34;}\u0026#34;))); PaymentResponse response = paymentClient.authorize(request); assertEquals(\u0026#34;AUTHORIZED\u0026#34;, response.status()); } Mountebank can absolutely be run in CI, but it needs to exist as a running process before your tests start, usually via Docker or a background service, which is a small but real amount of extra pipeline plumbing that WireMock\u0026rsquo;s embedded mode sidesteps for pure Java test suites.\nWhere Mountebank Still Wins If your architecture genuinely spans multiple languages, a Node service calling a .NET service calling a Java service, Mountebank\u0026rsquo;s protocol agnostic design starts to matter more than WireMock\u0026rsquo;s convenience inside a single JVM test. Mountebank also has strong support for protocols beyond HTTP, including TCP and SMTP, which WireMock does not attempt to cover in the same way.\nIf you already have a working Mountebank setup and it is not causing friction, this post is not an argument to rip it out. It is here because the next few posts focus on WireMock specifically, and it is worth being upfront about why, rather than presenting it as the only option.\nWhy This Series Uses WireMock Given the Java and Spring Boot focus running through most of what I write, WireMock\u0026rsquo;s embedded mode is a genuinely better day to day fit. No separate process to start before running tests locally, no Docker dependency for the common case, and stub definitions that live directly next to the test code they belong to, in the same language, checked into the same pull request.\nThe next post gets hands on, standing up WireMock inside a Spring Boot test suite and writing the first real stub against a downstream dependency.\n","permalink":"https://abygeorgea.com/blog/2025/03/04/wiremock-vs-mountebank/","summary":"\u003cp\u003eI have written a fair amount here already about \u003ca href=\"/categories/mountebank/\"\u003eMountebank\u003c/a\u003e for service virtualization, and it has served me well across a few different projects. But Mountebank is not the only serious option, and on a Java heavy stack in particular, WireMock tends to come up just as often, sometimes more. This post is not about replacing Mountebank, it is about knowing when WireMock is the better fit, since the two tools solve overlapping problems in genuinely different ways.\u003c/p\u003e","title":"WireMock vs Mountebank: Choosing a Service Virtualization Tool"},{"content":"In the previous post, we walked through what happens when a contract genuinely breaks, and how tagging and versioning give both teams a precise way to reason about it. This last post in the series pulls every piece we have built, consumer tests, the broker, provider verification, and can-i-deploy, into one coherent pipeline view, so it is clear how this actually runs day to day rather than as a sequence of separate manual steps.\nThe Consumer Pipeline Every time checkout opens a pull request or merges to main, its pipeline runs Pact tests as part of the normal test suite, then publishes the resulting contract to the broker, tagged with the branch name.\nname: checkout-service CI on: [push, pull_request] jobs: test-and-publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run tests including Pact run: mvn test - name: Publish contracts to broker run: mvn pact:publish -Dpact.tag=${{ github.ref_name }} env: PACT_BROKER_URL: ${{ secrets.PACT_BROKER_URL }} Nothing here is different from a normal test and publish step, contract publishing is just one more artifact of a green build, same as a JAR file or a coverage report.\nThe Provider Pipeline Payment\u0026rsquo;s pipeline runs on the same triggers, but its job is verification rather than publishing. It pulls checkout\u0026rsquo;s latest contract from the broker, tagged main, and verifies it against payment\u0026rsquo;s own real implementation.\nname: payment-service CI on: [push, pull_request] jobs: verify-contracts: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Start application run: mvn spring-boot:run \u0026amp; - name: Run Pact provider verification run: mvn test -Dtest=PaymentServiceProviderPactTest env: PACT_BROKER_URL: ${{ secrets.PACT_BROKER_URL }} This runs on every payment commit, not just when checkout changes something, which is exactly the point. Payment finds out immediately if their own change breaks a contract, without needing to know checkout exists as a dependency in any conscious way. The broker is doing that tracking on their behalf.\nThe Deploy Gate The piece that ties it together sits right before either service actually deploys.\ndeploy: needs: verify-contracts runs-on: ubuntu-latest steps: - name: Can I deploy? run: | pact-broker can-i-deploy \\ --pacticipant payment-service \\ --version ${{ github.sha }} \\ --to-environment production \\ --broker-base-url ${{ secrets.PACT_BROKER_URL }} - name: Deploy run: ./deploy.sh - name: Record deployment if: success() run: | pact-broker record-deployment \\ --pacticipant payment-service \\ --version ${{ github.sha }} \\ --environment production Deploy only runs if can-i-deploy passes, and a successful deploy immediately tells the broker what is now live, which keeps future can-i-deploy checks, on either side, accurate.\nWhat This Buys the Team Put together, this is a closed loop. Checkout\u0026rsquo;s client code changes trigger a new contract. Payment finds out about that change on their own pipeline, without a Slack message or a shared calendar invite. A break gets caught as a failing build on whichever side owns the mismatch. And neither service can deploy past a broken contract, because can-i-deploy sits directly in the path to production, not as a dashboard someone has to remember to check.\nWhere This Fits Against Everything Else None of this replaces unit tests, and it deliberately does not replace end to end tests either. It fills the specific gap between them, the place where two services agree on paper but drift apart in practice, and it does that without needing a shared staging environment or a slow, brittle integration suite. For a system built from services owned by different teams, each shipping on their own schedule, that gap is usually where the most expensive production incidents actually come from, which is exactly why it is worth the setup cost this series walked through.\n","permalink":"https://abygeorgea.com/blog/2025/02/25/wiring-pact-into-cicd/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2025/02/18/versioning-and-breaking-changes-in-pact/\"\u003eprevious post\u003c/a\u003e, we walked through what happens when a contract genuinely breaks, and how tagging and versioning give both teams a precise way to reason about it. This last post in the series pulls every piece we have built, consumer tests, the broker, provider verification, and \u003ccode\u003ecan-i-deploy\u003c/code\u003e, into one coherent pipeline view, so it is clear how this actually runs day to day rather than as a sequence of separate manual steps.\u003c/p\u003e","title":"Wiring Pact Into CI/CD: The Full Contract Testing Pipeline"},{"content":"In the previous post, can-i-deploy gave checkout an automated gate that blocks a deploy when payment has not verified the current contract. That check is only as good as the version history behind it. This post looks at how Pact tracks that history, and what actually happens in the broker when a contract changes in a way that breaks an existing consumer.\nEvery Contract Publish Is a New Version Each time checkout runs pact:publish, it does not overwrite the previous contract. It adds a new version, tied to whatever identifier you passed in, normally a git commit hash. The broker keeps every version, along with which provider versions have verified each one. This is what makes can-i-deploy meaningful. It is not asking \u0026ldquo;has this contract ever passed,\u0026rdquo; it is asking \u0026ldquo;has this specific version passed.\u0026rdquo;\nTagging by Branch Commit hashes are precise but not very readable, and they do not tell you anything about where a version came from. Tags solve that.\nmvn pact:publish -Dpact.tag=main mvn pact:publish -Dpact.tag=feature/split-payment-currency A common pattern is publishing every branch\u0026rsquo;s contracts tagged with the branch name, then having can-i-deploy on the provider side check specifically against contracts tagged main, so a provider is never blocked by a consumer\u0026rsquo;s half finished feature branch.\npact-broker can-i-deploy \\ --pacticipant payment-service \\ --version $GIT_COMMIT \\ --to-environment production \\ --broker-base-url http://pact-broker:9292 can-i-deploy automatically resolves the right consumer versions to check against based on what is currently deployed in the target environment, which is why tagging by environment, alongside branch, tends to matter more as a system grows.\nWhat a Breaking Change Looks Like Say the payment team decides to rename authCode to authorizationCode in their response, as part of cleaning up naming across their API. They ship the change, their own unit tests pass, and their build goes green, because nothing in payment\u0026rsquo;s own test suite references the old field name anymore.\nThe very next time payment\u0026rsquo;s provider verification runs against checkout\u0026rsquo;s published contract, it fails.\nVerifying a pact between checkout-service and payment-service a request to authorize a payment returns a response which has a matching body (FAILED) $.body.authCode: Expected \u0026#39;AB1234\u0026#39; but got no value This is contract testing doing exactly its job. The change is objectively fine from payment\u0026rsquo;s own perspective and objectively breaking from checkout\u0026rsquo;s. Neither team is wrong in isolation, which is precisely the kind of disagreement that used to only surface once checkout was already failing in production.\nHandling It Properly The fix is not to weaken the contract or delete the failing assertion. It is a conversation, made necessary by a failing build instead of optional. Either payment supports both field names for a transition period, or checkout gets advance notice and updates its client before payment removes the old field, with both sides re-verifying before the removal actually ships.\n// payment-service, during the transition response.put(\u0026#34;authCode\u0026#34;, authCode); // deprecated, remove after checkout migrates response.put(\u0026#34;authorizationCode\u0026#34;, authCode); Once checkout has updated its client and republished a contract that only expects authorizationCode, payment can safely drop the deprecated field, verify again, and this time see a clean pass.\nWhy This Beats Documentation None of this depended on anyone reading an API changelog or a deprecation notice in a wiki. The contract, generated from real consumer usage and continuously re-verified, caught the mismatch mechanically, the same day the change was made, on the team that introduced it. That is a meaningfully different guarantee than \u0026ldquo;we told people in the release notes,\u0026rdquo; and it is one of the strongest arguments for contract testing on any team that owns services other teams depend on.\nThe final post in this series pulls everything together into one CI/CD pipeline view, from a consumer test running on a pull request through to a gated production deploy.\n","permalink":"https://abygeorgea.com/blog/2025/02/18/versioning-and-breaking-changes-in-pact/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2025/02/11/gating-releases-with-pact-can-i-deploy/\"\u003eprevious post\u003c/a\u003e, \u003ccode\u003ecan-i-deploy\u003c/code\u003e gave checkout an automated gate that blocks a deploy when payment has not verified the current contract. That check is only as good as the version history behind it. This post looks at how Pact tracks that history, and what actually happens in the broker when a contract changes in a way that breaks an existing consumer.\u003c/p\u003e\n\u003ch2 id=\"every-contract-publish-is-a-new-version\"\u003eEvery Contract Publish Is a New Version\u003c/h2\u003e\n\u003cp\u003eEach time checkout runs \u003ccode\u003epact:publish\u003c/code\u003e, it does not overwrite the previous contract. It adds a new version, tied to whatever identifier you passed in, normally a git commit hash. The broker keeps every version, along with which provider versions have verified each one. This is what makes \u003ccode\u003ecan-i-deploy\u003c/code\u003e meaningful. It is not asking \u0026ldquo;has this contract ever passed,\u0026rdquo; it is asking \u0026ldquo;has this specific version passed.\u0026rdquo;\u003c/p\u003e","title":"Versioning Contracts and Catching Breaking Changes in Pact"},{"content":"In the previous post, both checkout and payment started publishing contracts and verification results to a shared Pact Broker. The broker knows exactly which version of payment has verified exactly which version of checkout\u0026rsquo;s contract. What it does not do on its own is stop anyone from deploying a version that has not been verified. That is what can-i-deploy is for.\nThe Question It Answers Before checkout deploys a new version to production, there is one question worth asking automatically rather than trusting someone to remember it. Has every provider checkout depends on already verified this exact version of the contract. If the answer is no, the deploy should not happen, full stop.\ncan-i-deploy is a CLI command, part of the Pact Broker client tooling, that asks the broker exactly that question and returns a pass or fail result you can wire straight into a pipeline.\npact-broker can-i-deploy \\ --pacticipant checkout-service \\ --version $GIT_COMMIT \\ --to-environment production \\ --broker-base-url http://pact-broker:9292 Reading the Result When every provider has verified the contract for this version, you get a clean pass.\nComputer says yes \\o/ CONSUMER | C.VERSION | PROVIDER | P.VERSION | SUCCESSFUL? checkout-service | a1b2c3d | payment-service | f9e8d7c | true All required verification results are published and successful When they have not, the same command fails, with a message that tells you exactly which provider is missing a passing verification.\nComputer says no CONSUMER | C.VERSION | PROVIDER | P.VERSION | SUCCESSFUL? checkout-service | a1b2c3d | payment-service | (none) | (no verification found) There is no verified pact between version a1b2c3d of checkout-service and a suitable version of payment-service That second output is the whole point. It catches exactly the situation where checkout\u0026rsquo;s contract changed, payment has not run verification against the new version yet, and someone is about to deploy anyway.\nWiring It Into the Pipeline Add this as a step right before the actual deploy step in checkout\u0026rsquo;s pipeline, and treat a non-zero exit code the same way you would treat a failing test.\n- name: Verify contracts before deploy run: | pact-broker can-i-deploy \\ --pacticipant checkout-service \\ --version ${{ github.sha }} \\ --to-environment production \\ --broker-base-url ${{ secrets.PACT_BROKER_URL }} - name: Deploy to production if: success() run: ./deploy.sh The deploy step now literally cannot run unless the previous step exited zero, which only happens when every dependency payment relates to has a passing verification recorded against this exact commit.\nEnvironments Matter Here Notice --to-environment production in the command. The broker tracks which version of each service is currently deployed where, which lets can-i-deploy ask a more precise question than just \u0026ldquo;has this ever been verified.\u0026rdquo; It asks whether this version is safe to deploy against whatever is actually running in production right now, which is a meaningfully different and more useful question than checking against the latest contract in isolation.\nThis does mean the broker needs to be told what is deployed where, usually through a companion record-deployment call right after a successful deploy.\npact-broker record-deployment \\ --pacticipant checkout-service \\ --version $GIT_COMMIT \\ --environment production What This Buys You With this in place, a breaking change to payment\u0026rsquo;s API can no longer reach production through checkout\u0026rsquo;s pipeline without someone on the payment side explicitly verifying it first. The failure moves from a production incident, discovered by a customer, to a red step in CI, discovered by whoever pushed the change. That shift, catching the break before deploy instead of after, is the entire value proposition of consumer-driven contract testing in one pipeline step.\nThe next post looks at what happens once contracts start changing over time, and how Pact handles versioning so a breaking change gets flagged clearly instead of just quietly failing verification.\n","permalink":"https://abygeorgea.com/blog/2025/02/11/gating-releases-with-pact-can-i-deploy/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2025/02/04/publishing-contracts-to-a-pact-broker/\"\u003eprevious post\u003c/a\u003e, both checkout and payment started publishing contracts and verification results to a shared Pact Broker. The broker knows exactly which version of payment has verified exactly which version of checkout\u0026rsquo;s contract. What it does not do on its own is stop anyone from deploying a version that has not been verified. That is what \u003ccode\u003ecan-i-deploy\u003c/code\u003e is for.\u003c/p\u003e\n\u003ch2 id=\"the-question-it-answers\"\u003eThe Question It Answers\u003c/h2\u003e\n\u003cp\u003eBefore checkout deploys a new version to production, there is one question worth asking automatically rather than trusting someone to remember it. Has every provider checkout depends on already verified this exact version of the contract. If the answer is no, the deploy should not happen, full stop.\u003c/p\u003e","title":"Gating Releases with Pact's can-i-deploy"},{"content":"In the previous post, payment verified checkout\u0026rsquo;s contract by reading it from a local folder path. That works for a demo, but it does not scale. The provider team should not need a copy of the consumer\u0026rsquo;s build output sitting on disk to run their tests. This is what the Pact Broker exists to fix.\nWhat the Broker Actually Does A Pact Broker is a small standalone service that stores contracts, tracks which versions of which services have verified which contracts, and exposes that history through a web UI and an API. Instead of checkout emailing a JSON file to payment, checkout publishes its contract to the broker after every build, and payment pulls the latest version from the broker when it runs verification.\nThe broker becomes the single source of truth for who depends on whom, and whether that dependency is currently healthy.\nRunning a Broker For local development or a small team, the official Docker image is the fastest way to get one running.\ndocker run -d --name pact-broker \\ -e PACT_BROKER_DATABASE_ADAPTER=sqlite \\ -e PACT_BROKER_DATABASE_NAME=pact_broker.sqlite3 \\ -p 9292:9292 \\ pactfoundation/pact-broker For anything beyond local experimentation, point it at a real Postgres instance instead of sqlite, and run it somewhere both the consumer and provider pipelines can reach, since this needs to be a shared, always available service rather than something on someone\u0026rsquo;s laptop.\nPublishing From the Consumer Side Checkout\u0026rsquo;s build needs one extra step after its Pact tests run, publishing the generated contract to the broker.\n\u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;au.com.dius.pact.provider\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.6.5\u0026lt;/version\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;pactBrokerUrl\u0026gt;http://pact-broker:9292\u0026lt;/pactBrokerUrl\u0026gt; \u0026lt;projectVersion\u0026gt;${git.commit.id}\u0026lt;/projectVersion\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; mvn pact:publish Using the actual git commit hash as the version, rather than a static string, matters a lot here. It is what lets the broker later answer a very specific question. Was the contract published by this exact build ever verified by payment.\nPulling Contracts on the Provider Side Payment\u0026rsquo;s verification test now points at the broker instead of a local folder.\n@Provider(\u0026#34;payment-service\u0026#34;) @PactBroker(url = \u0026#34;http://pact-broker:9292\u0026#34;) class PaymentServiceProviderPactTest { // verification logic unchanged from the previous post } Run mvn test as before. Pact fetches the latest contracts for payment-service from the broker, verifies each one, and publishes the verification result back to the broker automatically. That last part matters just as much as the fetch. The broker now has a record showing exactly which version of payment successfully verified exactly which version of checkout\u0026rsquo;s contract.\nReading the Broker\u0026rsquo;s Network Diagram Open the broker\u0026rsquo;s web UI and you get a visual dependency graph, built entirely from published contracts and verification results, no manual documentation required. For a system with a handful of services this is convenience. For a system with thirty services calling each other in ways nobody has fully diagrammed in over a year, this becomes the most accurate architecture diagram the team has, because it is generated from what services actually do, not what someone remembers deciding.\nThe Question We Still Cannot Answer Right now, both teams can see contracts and verification results, but there is no automated gate stopping checkout from deploying a version that payment has never actually verified. Someone still has to manually check the broker before hitting deploy. That manual step is exactly what the next post removes, with Pact\u0026rsquo;s can-i-deploy check wired directly into the release pipeline.\n","permalink":"https://abygeorgea.com/blog/2025/02/04/publishing-contracts-to-a-pact-broker/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2025/01/28/verifying-pact-contracts-on-the-provider-side/\"\u003eprevious post\u003c/a\u003e, payment verified checkout\u0026rsquo;s contract by reading it from a local folder path. That works for a demo, but it does not scale. The provider team should not need a copy of the consumer\u0026rsquo;s build output sitting on disk to run their tests. This is what the Pact Broker exists to fix.\u003c/p\u003e\n\u003ch2 id=\"what-the-broker-actually-does\"\u003eWhat the Broker Actually Does\u003c/h2\u003e\n\u003cp\u003eA Pact Broker is a small standalone service that stores contracts, tracks which versions of which services have verified which contracts, and exposes that history through a web UI and an API. Instead of checkout emailing a JSON file to payment, checkout publishes its contract to the broker after every build, and payment pulls the latest version from the broker when it runs verification.\u003c/p\u003e","title":"Publishing and Sharing Contracts with a Pact Broker"},{"content":"In the previous post, checkout\u0026rsquo;s test suite generated a real contract file describing two interactions with the payment service, an authorized payment and a declined one. That file sitting in target/pacts proves nothing about payment\u0026rsquo;s actual behavior yet. This post covers the other half of Pact, taking that same file and replaying it against payment\u0026rsquo;s real implementation.\nAdding the Provider Dependency On the payment service side, add Pact\u0026rsquo;s provider JVM module.\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;au.com.dius.pact.provider\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit5\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.6.5\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; Pointing Pact at Your Real Service A provider verification test starts your actual Spring Boot application, or at least the controller layer, and lets Pact fire the recorded requests at it directly.\n@Provider(\u0026#34;payment-service\u0026#34;) @PactFolder(\u0026#34;../checkout-service/target/pacts\u0026#34;) class PaymentServiceProviderPactTest { @BeforeEach void setUp(PactVerificationContext context) { context.setTarget(new HttpTestTarget(\u0026#34;localhost\u0026#34;, 8080)); } @TestTemplate @ExtendWith(PactVerificationInvocationContextProvider.class) void pactVerificationTestTemplate(PactVerificationContext context) { context.verifyInteraction(); } } @PactFolder points at wherever the contract file lives. In a real setup this usually points at a Pact Broker instead of a local folder path, which we get to in the next post, but a local folder is the simplest way to see verification working end to end first.\nProvider States Notice the contract includes a providerState of \u0026ldquo;a valid card is presented\u0026rdquo; for the authorized interaction, and \u0026ldquo;a card with insufficient funds is presented\u0026rdquo; for the declined one. Pact calls these provider states, and they exist because payment\u0026rsquo;s real behavior depends on data that does not exist by default. There is no card with insufficient funds sitting in a database unless you put one there.\nA @State method handles that setup before the matching interaction runs.\n@State(\u0026#34;a valid card is presented\u0026#34;) void validCardPresented() { testCardRepository.save(new TestCard(\u0026#34;4111111111111111\u0026#34;, Status.VALID)); } @State(\u0026#34;a card with insufficient funds is presented\u0026#34;) void insufficientFundsCard() { testCardRepository.save(new TestCard(\u0026#34;4111111111111111\u0026#34;, Status.INSUFFICIENT_FUNDS)); } This is the piece that trips people up first. Provider states are not decorative text, they are an instruction to the provider test suite, telling it exactly what data or system state needs to exist for the following interaction to make sense.\nRunning Verification mvn test -Dtest=PaymentServiceProviderPactTest When this passes, Pact prints a summary confirming both interactions matched, request and response shape included. When it fails, the output tells you precisely which field did not match and how.\nVerifying a pact between checkout-service and payment-service a request to authorize a payment returns a response which has status code 200 (OK) has a matching body (FAILED) Failures: 1) Verifying a pact between checkout-service and payment-service - a request to authorize a payment Actual: {\u0026#34;result\u0026#34;: \u0026#34;AUTHORIZED\u0026#34;, \u0026#34;authCode\u0026#34;: \u0026#34;AB1234\u0026#34;} Expected: {\u0026#34;status\u0026#34;: \u0026#34;AUTHORIZED\u0026#34;, \u0026#34;authCode\u0026#34;: \u0026#34;AB1234\u0026#34;} $.body.status: Expected \u0026#39;AUTHORIZED\u0026#39; but got no value That specific failure, result instead of status, is exactly the kind of drift that would otherwise sit undetected until checkout deploys and its response parsing quietly breaks. Here it fails payment\u0026rsquo;s own build, on payment\u0026rsquo;s own pipeline, before it ever ships.\nWhere This Still Falls Short Right now both teams have to manually copy contract files around, or point at a shared local folder, which does not scale past a single pair of services talking directly to each other. The next post fixes that by introducing a Pact Broker, a shared service both consumer and provider publish to and verify against, so this whole exchange happens automatically as part of each team\u0026rsquo;s own CI pipeline.\n","permalink":"https://abygeorgea.com/blog/2025/01/28/verifying-pact-contracts-on-the-provider-side/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2025/01/21/writing-your-first-pact-consumer-test/\"\u003eprevious post\u003c/a\u003e, checkout\u0026rsquo;s test suite generated a real contract file describing two interactions with the payment service, an authorized payment and a declined one. That file sitting in \u003ccode\u003etarget/pacts\u003c/code\u003e proves nothing about payment\u0026rsquo;s actual behavior yet. This post covers the other half of Pact, taking that same file and replaying it against payment\u0026rsquo;s real implementation.\u003c/p\u003e\n\u003ch2 id=\"adding-the-provider-dependency\"\u003eAdding the Provider Dependency\u003c/h2\u003e\n\u003cp\u003eOn the payment service side, add Pact\u0026rsquo;s provider JVM module.\u003c/p\u003e","title":"Verifying Pact Contracts on the Provider Side"},{"content":"In the previous post, we added the Pact JVM dependency and sketched out a contract definition for authorizing a payment. That definition on its own does not generate anything yet. We still need a test method that actually exercises it, by calling real client code against Pact\u0026rsquo;s mock server. That is what turns a description of an interaction into a generated contract file on disk.\nConnecting the Test to the Mock Pact\u0026rsquo;s JUnit 5 extension injects a MockServer into your test method, giving you the actual host and port the mock is running on. Your job is to point your real HTTP client at that address instead of the real payment service.\n@Test @PactTestFor(pactMethod = \u0026#34;authorizePayment\u0026#34;) void authorizesAPaymentSuccessfully(MockServer mockServer) { PaymentClient client = new PaymentClient(mockServer.getUrl()); PaymentResponse response = client.authorize( new PaymentRequest(4999, \u0026#34;AUD\u0026#34;) ); assertEquals(\u0026#34;AUTHORIZED\u0026#34;, response.status()); assertEquals(\u0026#34;AB1234\u0026#34;, response.authCode()); } PaymentClient here is the same class checkout uses in production to call the payment service, the only difference is the base URL, which now points at Pact\u0026rsquo;s mock instead of a real environment. If that client class does not exist yet, this is a good forcing function to write it, since the whole point of contract testing is exercising real client code, not a stand in.\nRunning the Test Run this the same way you run any other JUnit test.\nmvn test -Dtest=PaymentServiceConsumerPactTest Assuming the assertions pass, look in target/pacts. You should see a file named something like checkout-service-payment-service.json, containing exactly the interaction you defined, request and response both, in Pact\u0026rsquo;s standard contract format.\n{ \u0026#34;consumer\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;checkout-service\u0026#34; }, \u0026#34;provider\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;payment-service\u0026#34; }, \u0026#34;interactions\u0026#34;: [ { \u0026#34;description\u0026#34;: \u0026#34;a request to authorize a payment\u0026#34;, \u0026#34;providerState\u0026#34;: \u0026#34;a valid card is presented\u0026#34;, \u0026#34;request\u0026#34;: { \u0026#34;method\u0026#34;: \u0026#34;POST\u0026#34;, \u0026#34;path\u0026#34;: \u0026#34;/payments/authorize\u0026#34;, \u0026#34;body\u0026#34;: { \u0026#34;amount\u0026#34;: 4999, \u0026#34;currency\u0026#34;: \u0026#34;AUD\u0026#34; } }, \u0026#34;response\u0026#34;: { \u0026#34;status\u0026#34;: 200, \u0026#34;body\u0026#34;: { \u0026#34;status\u0026#34;: \u0026#34;AUTHORIZED\u0026#34;, \u0026#34;authCode\u0026#34;: \u0026#34;AB1234\u0026#34; } } } ], \u0026#34;metadata\u0026#34;: { \u0026#34;pactSpecification\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;2.0.0\u0026#34; } } } Keeping Contracts Focused It is tempting to write one enormous test that covers every field and every status code payment might return. Resist that. A contract should describe interactions checkout genuinely relies on, not a full specification of payment\u0026rsquo;s API. If checkout never reads a processedAt timestamp from the response, do not assert on it in the contract, since that just gives payment one more thing they cannot change without breaking a consumer that never actually cared.\nA second, separate test method for a declined card is worth adding now, since it is a real path checkout has to handle differently.\n@Pact(consumer = \u0026#34;checkout-service\u0026#34;) public RequestResponsePact declinedPayment(PactDslWithProvider builder) { return builder .given(\u0026#34;a card with insufficient funds is presented\u0026#34;) .uponReceiving(\u0026#34;a request to authorize a declined payment\u0026#34;) .path(\u0026#34;/payments/authorize\u0026#34;) .method(\u0026#34;POST\u0026#34;) .body(\u0026#34;{\\\u0026#34;amount\\\u0026#34;: 4999, \\\u0026#34;currency\\\u0026#34;: \\\u0026#34;AUD\\\u0026#34;}\u0026#34;) .willRespondWith() .status(402) .body(\u0026#34;{\\\u0026#34;status\\\u0026#34;: \\\u0026#34;DECLINED\\\u0026#34;, \\\u0026#34;reason\\\u0026#34;: \\\u0026#34;INSUFFICIENT_FUNDS\\\u0026#34;}\u0026#34;) .toPact(); } Two interactions in one contract file now, a happy path and a realistic failure path, both driven from how checkout actually behaves.\nWhat This Contract Does Not Prove Yet Right now, this contract only proves that checkout\u0026rsquo;s client code correctly builds the request and correctly parses the response, against a mock that returns whatever we told it to. It says nothing about whether the real payment service actually behaves this way. That is the gap the next post closes, by taking this exact file and replaying it against payment\u0026rsquo;s real implementation.\n","permalink":"https://abygeorgea.com/blog/2025/01/21/writing-your-first-pact-consumer-test/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2025/01/14/setting-up-pact-with-spring-boot/\"\u003eprevious post\u003c/a\u003e, we added the Pact JVM dependency and sketched out a contract definition for authorizing a payment. That definition on its own does not generate anything yet. We still need a test method that actually exercises it, by calling real client code against Pact\u0026rsquo;s mock server. That is what turns a description of an interaction into a generated contract file on disk.\u003c/p\u003e\n\u003ch2 id=\"connecting-the-test-to-the-mock\"\u003eConnecting the Test to the Mock\u003c/h2\u003e\n\u003cp\u003ePact\u0026rsquo;s JUnit 5 extension injects a \u003ccode\u003eMockServer\u003c/code\u003e into your test method, giving you the actual host and port the mock is running on. Your job is to point your real HTTP client at that address instead of the real payment service.\u003c/p\u003e","title":"Writing Your First Pact Consumer Test"},{"content":"In the previous post, we covered why contract testing exists and where Pact fits between unit tests and full integration tests. Before we can write an actual contract, we need Pact wired into the project. This post is the setup step, getting a Spring Boot service ready to generate its first contract.\nAdding the Dependency Pact JVM ships a JUnit 5 module that plugs straight into the test framework you are probably already using. For a Maven project, add this to the checkout service\u0026rsquo;s pom.xml.\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;au.com.dius.pact.consumer\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit5\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.6.5\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; If you are on Gradle, the equivalent is a single line in your test dependencies.\ntestImplementation \u0026#39;au.com.dius.pact.consumer:junit5:4.6.5\u0026#39; Nothing else needs to change in the main source set. Pact only touches your test code, which is one of the reasons it is such a low friction addition to an existing service.\nProject Layout Pact generates contract files into a pacts directory at the root of your build output by default, usually target/pacts for Maven or build/pacts for Gradle. You do not need to create this directory yourself, Pact creates it the first time a consumer test runs.\nIt is worth deciding early where these generated files eventually live long term. For now, keep them local and out of version control. Once we introduce the Pact Broker in a later post, that becomes the shared home for contracts instead of a folder in your repo.\n.gitignore target/pacts/ The Test Skeleton A Pact consumer test looks close to a normal JUnit 5 test, with two extra pieces. A Pact extension that manages a mock provider server for you, and an annotation that defines what the mock should return.\n@ExtendWith(PactConsumerTestExt.class) @PactTestFor(providerName = \u0026#34;payment-service\u0026#34;) class PaymentServiceConsumerPactTest { @Pact(consumer = \u0026#34;checkout-service\u0026#34;) public RequestResponsePact authorizePayment(PactDslWithProvider builder) { return builder .given(\u0026#34;a valid card is presented\u0026#34;) .uponReceiving(\u0026#34;a request to authorize a payment\u0026#34;) .path(\u0026#34;/payments/authorize\u0026#34;) .method(\u0026#34;POST\u0026#34;) .body(\u0026#34;{\\\u0026#34;amount\\\u0026#34;: 4999, \\\u0026#34;currency\\\u0026#34;: \\\u0026#34;AUD\\\u0026#34;}\u0026#34;) .willRespondWith() .status(200) .body(\u0026#34;{\\\u0026#34;status\\\u0026#34;: \\\u0026#34;AUTHORIZED\\\u0026#34;, \\\u0026#34;authCode\\\u0026#34;: \\\u0026#34;AB1234\\\u0026#34;}\u0026#34;) .toPact(); } } Nothing here talks to a real payment service. Pact spins up a mock HTTP server that returns exactly what you defined in willRespondWith, and the test method you attach to this contract, which we will write next, calls your actual client code against that mock server instead of the real one.\nWhy the Mock Server Matters The mock server is the part that makes this different from a plain unit test with a stubbed HTTP client. Because Pact is the one serving the response, it also records exactly what request your client sent, and that recorded interaction is what becomes the contract file. You are not writing the contract by hand, you are writing a test the normal way and letting Pact capture the shape of the conversation for you.\nThat is the piece we build next, an actual test method that calls your checkout service\u0026rsquo;s payment client and asserts against the mock, generating a real contract file in the process.\n","permalink":"https://abygeorgea.com/blog/2025/01/14/setting-up-pact-with-spring-boot/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2025/01/07/consumer-driven-contract-testing-explained/\"\u003eprevious post\u003c/a\u003e, we covered why contract testing exists and where Pact fits between unit tests and full integration tests. Before we can write an actual contract, we need Pact wired into the project. This post is the setup step, getting a Spring Boot service ready to generate its first contract.\u003c/p\u003e\n\u003ch2 id=\"adding-the-dependency\"\u003eAdding the Dependency\u003c/h2\u003e\n\u003cp\u003ePact JVM ships a JUnit 5 module that plugs straight into the test framework you are probably already using. For a Maven project, add this to the checkout service\u0026rsquo;s \u003ccode\u003epom.xml\u003c/code\u003e.\u003c/p\u003e","title":"Setting Up Pact for a Spring Boot Consumer"},{"content":"Every team running more than a handful of microservices eventually hits the same wall. Integration tests that spin up three or four real services are slow and flaky. End-to-end tests that go through the whole system catch real problems, but only after everything is already deployed, and a single unrelated service being down fails the whole suite. Somewhere in between those two extremes sits a much cheaper question. Does my service still honor what the other services expect from it.\nThat question is what contract testing answers, and Pact is the tool I want to spend the next few posts on.\nThe Problem With Testing Microservices in Isolation Say you own a checkout service that calls a payment service to authorize a transaction. You write unit tests for checkout with the payment call mocked out. The payment team writes their own unit tests for their service. Both suites go green. Both services deploy. And then checkout starts failing in production because the payment team renamed a field in their response, or changed a status code they return on a declined card.\nNobody lied. Nobody skipped testing. The two services just drifted apart quietly, because nothing was checking that the mock checkout used still matched what payment actually returns.\nWhat a Contract Actually Is A contract, in this context, is a recorded set of expectations. The consumer, checkout in this example, states what requests it will send and what responses it expects back. That expectation gets captured as a contract file. The provider, payment, then replays those exact requests against its own real implementation and checks the responses still match.\nThis is the part that makes it consumer-driven. The contract is not written by the provider team guessing what consumers might need. It comes directly from how consumers actually use the service, which means it only ever covers real usage, not a full specification of every endpoint.\nWhere Pact Fits Pact is the most common tool for this pattern, with solid support across Java, JavaScript, .NET, and several other languages. It gives you two halves of the workflow.\nOn the consumer side, you write a test that defines the interaction you expect, and Pact generates a contract file from it, usually as JSON.\nOn the provider side, Pact takes that same contract file and replays it against your real service, failing the build if the actual response does not match what the consumer expects.\n{ \u0026#34;consumer\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;checkout-service\u0026#34; }, \u0026#34;provider\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;payment-service\u0026#34; }, \u0026#34;interactions\u0026#34;: [ { \u0026#34;description\u0026#34;: \u0026#34;a request to authorize a payment\u0026#34;, \u0026#34;request\u0026#34;: { \u0026#34;method\u0026#34;: \u0026#34;POST\u0026#34;, \u0026#34;path\u0026#34;: \u0026#34;/payments/authorize\u0026#34;, \u0026#34;body\u0026#34;: { \u0026#34;amount\u0026#34;: 4999, \u0026#34;currency\u0026#34;: \u0026#34;AUD\u0026#34; } }, \u0026#34;response\u0026#34;: { \u0026#34;status\u0026#34;: 200, \u0026#34;body\u0026#34;: { \u0026#34;status\u0026#34;: \u0026#34;AUTHORIZED\u0026#34;, \u0026#34;authCode\u0026#34;: \u0026#34;string\u0026#34; } } } ] } That file is the whole point. It is small, readable, and it lives independently of either service\u0026rsquo;s source code, which means it can be shared, versioned, and checked in CI on both sides.\nWhy Not Just Use Integration Tests Integration tests that stand up real dependencies are not wrong, they just answer a different question and cost more to run. A contract test does not need payment\u0026rsquo;s database, its downstream fraud checks, or its message queue. It only needs to know, does payment still return what checkout expects for this one interaction. That narrower scope is exactly what makes contract tests fast enough to run on every single commit, on both sides, without anyone waiting on a shared staging environment to be free.\nWhat Is Coming Next Over the next several posts we are going to build this out properly against a Java Spring Boot service. We will write a consumer test, generate a real contract, verify it on the provider side, publish contracts to a Pact Broker so both teams can see them, and finally wire the whole thing into a CI pipeline with Pact\u0026rsquo;s can-i-deploy check gating releases. By the end, the goal is a setup where a breaking change to an API gets caught in CI, on the provider\u0026rsquo;s own pipeline, before it ever reaches the consumer team as a production incident.\n","permalink":"https://abygeorgea.com/blog/2025/01/07/consumer-driven-contract-testing-explained/","summary":"\u003cp\u003eEvery team running more than a handful of microservices eventually hits the same wall. Integration tests that spin up three or four real services are slow and flaky. End-to-end tests that go through the whole system catch real problems, but only after everything is already deployed, and a single unrelated service being down fails the whole suite. Somewhere in between those two extremes sits a much cheaper question. Does my service still honor what the other services expect from it.\u003c/p\u003e","title":"Consumer-Driven Contract Testing Explained"},{"content":"As I’ve been leaning heavier into AI-assisted coding and scripting for my own side projects over the past year, my API token usage shot up fast—it went from a cheap experiment to an invoice that made me do a double-take. Lately, my focus has been all about Token Optimization, Context Caching, and keeping my setup lean.\nThings I’ve Been Tweaking 1. Prompt Caching \u0026amp; Smart Context Truncation When OpenAI and Anthropic rolled out prompt caching, I jumped on it immediately for my personal scripts. By caching heavy system prompts, common schemas, and reusable code patterns, I managed to slash my monthly LLM API spend by over 45% while actually speeding up execution times.\n2. Building Safety \u0026amp; Reliability Checks for My Own Tools As I\u0026rsquo;ve built out more LLM-powered side projects, I\u0026rsquo;ve had to get serious about testing the models themselves. I\u0026rsquo;ve been tinkering with personal evaluation harnesses to check my scripts for hallucinations, prompt injection vulnerabilities, and weird edge-case failures before relying on them.\nWhat I’ve Learned from the Whole Journey Looking back, this was the year using AI moved from throwing random prompts at a chat window to building actual structured, reliable tools. But if there\u0026rsquo;s one takeaway from playing around with all this stuff, it\u0026rsquo;s that human intuition and solid code structure are still non-negotiable.\nLeft unchecked, letting AI generate everything leads to messy repos, duplicate code, and an unnecessary cloud bill. Moving forward, keeping things modular, well-architected, and cost-effective is way more satisfying than just writing clever prompts.\n","permalink":"https://abygeorgea.com/blog/2024/12/17/token-optimization-and-prompt-caching/","summary":"\u003cp\u003eAs I’ve been leaning heavier into AI-assisted coding and scripting for my own side projects over the past year, my API token usage shot up fast—it went from a cheap experiment to an invoice that made me do a double-take. Lately, my focus has been all about \u003cstrong\u003eToken Optimization, Context Caching, and keeping my setup lean\u003c/strong\u003e.\u003c/p\u003e\n\u003ch3 id=\"things-ive-been-tweaking\"\u003eThings I’ve Been Tweaking\u003c/h3\u003e\n\u003ch4 id=\"1-prompt-caching--smart-context-truncation\"\u003e1. Prompt Caching \u0026amp; Smart Context Truncation\u003c/h4\u003e\n\u003cp\u003eWhen OpenAI and Anthropic rolled out prompt caching, I jumped on it immediately for my personal scripts. By caching heavy system prompts, common schemas, and reusable code patterns, I managed to slash my monthly LLM API spend by over 45% while actually speeding up execution times.\u003c/p\u003e","title":"Token Optimization and prompt caching"},{"content":"With multimodal models like GPT-4 Vision and Claude 3 Opus getting so much sharper, test automation is finally moving beyond fighting with the HTML DOM and into actual Visual and Spatial Verification.\nThings I’ve Been Experimenting With 1. Screenshot-Based Visual Assertions In the past, visual testing meant wrestling with rigid pixel-by-pixel diff tools that would blow up your build over a tiny font rendering shift. Lately, I’ve been feeding raw UI screenshots directly to vision-capable LLMs along with plain English questions: \u0026ldquo;Is the checkout button properly aligned below the order summary? Is any text overlapping?\u0026rdquo;\nIt actually evaluates layout aesthetics and obvious layout bugs almost like a human reviewer would, which is pretty mind-blowing to watch in real time.\n2. Tackling Horrible Dynamic Monoliths (Salesforce, ServiceNow) Platforms like Salesforce have always been my least favorite thing to test—deep shadow DOMs, nested iFrames, and randomized element IDs make writing stable locators nearly impossible.\nUsing vision models alongside intent-based prompts lets me automate flows across these enterprise tools visually, completely bypassing the need to maintain fragile, deeply nested CSS selectors.\nThe Catch: Cost and False Positives As cool as vision testing is, it comes with two big drawbacks: cost and speed.\nMultimodal API calls are way more expensive and noticeably slower than text-only endpoints. Plus, visual models can sometimes be a bit too sensitive, flagging tiny, non-functional padding tweaks as layout issues. Because of that, I’ve learned to be selective—reserving visual LLM checks for critical, high-value user journeys rather than slapping them onto every single PR.\n","permalink":"https://abygeorgea.com/blog/2024/09/24/multimodal-vision-testing/","summary":"\u003cp\u003eWith multimodal models like GPT-4 Vision and Claude 3 Opus getting so much sharper, test automation is finally moving beyond fighting with the HTML DOM and into actual \u003cstrong\u003eVisual and Spatial Verification\u003c/strong\u003e.\u003c/p\u003e\n\u003ch3 id=\"things-ive-been-experimenting-with\"\u003eThings I’ve Been Experimenting With\u003c/h3\u003e\n\u003ch4 id=\"1-screenshot-based-visual-assertions\"\u003e1. Screenshot-Based Visual Assertions\u003c/h4\u003e\n\u003cp\u003eIn the past, visual testing meant wrestling with rigid pixel-by-pixel diff tools that would blow up your build over a tiny font rendering shift. Lately, I’ve been feeding raw UI screenshots directly to vision-capable LLMs along with plain English questions: \u003cem\u003e\u0026ldquo;Is the checkout button properly aligned below the order summary? Is any text overlapping?\u0026rdquo;\u003c/em\u003e\u003c/p\u003e","title":"Multimodal Vision Testing"},{"content":"Flaky tests caused by shifting DOM element IDs have been the bane of my existence for as long as I’ve been writing test automation. I got tired of constant pipeline failures over minor UI tweaks, so lately I’ve been tinkering with Runtime Self-Healing Mechanisms directly inside my Playwright test runners.\nWhat I’ve Been Building Recently 1. Dynamic Selector Healing I set up dynamic error interceptors inside my test suites. When a locator lookup fails (say, a TimeoutError: element '#submit-order-v2' not found), the test runner catches the exception, snags a snapshot of the surrounding DOM tree, and hands it off to an LLM to request an alternative CSS or XPath selector.\nIf the new selector works, the test keeps chugging along without blowing up the build, and it drops a warning log so I can clean up the selector later.\n[Test Execution] -\u0026gt; Locator Failed (#submit-btn) │ ▼ [Catch Interceptor] -\u0026gt; Capture DOM Snapshot │ ▼ [LLM Evaluator] -\u0026gt; Propose Healed Locator (#submit-checkout) │ ▼ [Resume Run] -\u0026gt; Test Passes (Log PR for Selector Repair)\nThe Trade-Off: Dealing with Latency Self-healing feels like a total superpower when you first see it work, but it comes with a catch: latency.\nPausing mid-test to make an LLM call adds 3 to 5 seconds every single time a locator fails. If a suite has a dozen broken locators, the total execution time explodes real fast. It’s been a great reminder that self-healing is just a temporary safety net to keep CI green—not an excuse to ignore clean, robust locator strategies in the first place!\n","permalink":"https://abygeorgea.com/blog/2024/06/30/building-run-self-healing/","summary":"\u003cp\u003eFlaky tests caused by shifting DOM element IDs have been the bane of my existence for as long as I’ve been writing test automation. I got tired of constant pipeline failures over minor UI tweaks, so lately I’ve been tinkering with \u003cstrong\u003eRuntime Self-Healing Mechanisms\u003c/strong\u003e directly inside my Playwright test runners.\u003c/p\u003e\n\u003ch3 id=\"what-ive-been-building-recently\"\u003eWhat I’ve Been Building Recently\u003c/h3\u003e\n\u003ch4 id=\"1-dynamic-selector-healing\"\u003e1. Dynamic Selector Healing\u003c/h4\u003e\n\u003cp\u003eI set up dynamic error interceptors inside my test suites. When a locator lookup fails (say, a \u003ccode\u003eTimeoutError: element '#submit-order-v2' not found\u003c/code\u003e), the test runner catches the exception, snags a snapshot of the surrounding DOM tree, and hands it off to an LLM to request an alternative CSS or XPath selector.\u003c/p\u003e","title":"Building Run Self healing"},{"content":"In the previous post, we got a large suite running fast in CI. This is the last post in the series, and it covers something that matters more the longer a framework lives. Keeping it healthy after the initial build is done.\nA framework that looks clean on day one can turn into a mess after six months of multiple people adding tests under deadline pressure. Inconsistent formatting, selectors that break constantly, tests nobody trusts anymore. None of this is really a Playwright problem. It is a team habits problem, and it is solvable with the right tooling in place from early on.\nESLint and Prettier Consistent formatting removes an entire category of pointless pull request comments. Nobody should be leaving a review comment about spacing when a tool can just fix it automatically. Install both alongside the TypeScript ESLint tooling.\nnpm install --save-dev eslint prettier @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-config-prettier A reasonable starting ESLint config for a Playwright TypeScript project looks like this.\n// .eslintrc.js module.exports = { parser: \u0026#39;@typescript-eslint/parser\u0026#39;, plugins: [\u0026#39;@typescript-eslint\u0026#39;], extends: [ \u0026#39;eslint:recommended\u0026#39;, \u0026#39;plugin:@typescript-eslint/recommended\u0026#39;, \u0026#39;prettier\u0026#39;, ], rules: { \u0026#39;@typescript-eslint/no-unused-vars\u0026#39;: \u0026#39;error\u0026#39;, \u0026#39;@typescript-eslint/explicit-function-return-type\u0026#39;: \u0026#39;off\u0026#39;, \u0026#39;no-console\u0026#39;: \u0026#39;warn\u0026#39;, }, env: { node: true, es2022: true, }, }; And a Prettier config, kept deliberately small.\n// .prettierrc { \u0026#34;singleQuote\u0026#34;: true, \u0026#34;trailingComma\u0026#34;: \u0026#34;all\u0026#34;, \u0026#34;printWidth\u0026#34;: 100 } Add a couple of scripts to package.json so these are easy to run manually and in CI.\n{ \u0026#34;scripts\u0026#34;: { \u0026#34;lint\u0026#34;: \u0026#34;eslint . --ext .ts\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;prettier --write .\u0026#34; } } Enforcing It With Husky and lint-staged Having linting available is one thing. Having it actually run before broken code gets committed is another. Husky and lint-staged together give you exactly that, without slowing down every commit by re-checking the entire codebase.\nnpm install --save-dev husky lint-staged npx husky init Configure lint-staged in package.json to only touch files that are actually staged for commit.\n{ \u0026#34;lint-staged\u0026#34;: { \u0026#34;*.ts\u0026#34;: [\u0026#34;eslint --fix\u0026#34;, \u0026#34;prettier --write\u0026#34;] } } Then wire it into the pre-commit hook Husky just created.\n# .husky/pre-commit npx lint-staged Now every commit automatically lints and formats only the files being committed, and fixes what it can fix automatically. Anyone on the team gets this for free the moment they clone the repo and run npm install, since Husky\u0026rsquo;s setup hooks into npm install itself.\nIt is worth going a step further and also running the test suite itself, or at least a fast subset of it, in a pre-push hook rather than pre-commit, since a full Playwright run is too slow to sit in front of every single commit.\n# .husky/pre-push npm run test:smoke Where test:smoke is a small, fast tagged subset of your suite, not the full three hundred test run. This catches an obviously broken change before it even reaches a pull request, without making every commit feel painfully slow.\nSelector Governance Tooling handles formatting and syntax, but it cannot stop someone from writing a brittle selector. This has to be a team agreement, backed by a bit of review discipline.\nThe rule I push hardest on any team is preferring dedicated test attributes over anything tied to styling or DOM structure.\n// Fragile. Breaks the moment a class name changes for styling reasons. page.locator(\u0026#39;.btn.btn-primary.submit-btn\u0026#39;); // Fragile. Breaks if the DOM structure shifts even slightly. page.locator(\u0026#39;div \u0026gt; form \u0026gt; div:nth-child(3) \u0026gt; button\u0026#39;); // Stable. Survives styling and structural changes. page.locator(\u0026#39;[data-testid=\u0026#34;submit-order-button\u0026#34;]\u0026#39;); A data-testid attribute exists for exactly one purpose, and nobody refactoring styles or restructuring markup has a reason to touch it. This one habit alone prevents most of the selector breakage that causes maintenance headaches down the line. It does mean getting the front end team on board with adding these attributes, which is worth raising early rather than working around with fragile selectors indefinitely.\nReviewing Flaky Tests on Purpose The last piece of governance worth setting up deliberately is a recurring, scheduled look at flaky tests. It is easy for a team to develop a habit of just hitting rerun when a test fails intermittently, without ever circling back to actually fix it. Over months, this quietly erodes trust in the whole suite, to the point where a real failure gets dismissed as \u0026ldquo;probably just flaky\u0026rdquo; without anyone checking.\nA simple habit that works well is a short recurring review, maybe every couple of weeks, where someone looks specifically at which tests needed a retry to pass over that period. Most of the time this points at one of a small number of root causes. A missing wait for a genuinely async operation that auto-waiting cannot see, like a background job that finishes seconds after the UI stops loading. A shared piece of test data that occasionally collides, which we covered back in part three. Or a selector that matches more than one element under specific conditions. Treating this as a regular, expected part of maintaining the framework, rather than something that only gets attention when it becomes a crisis, is what keeps a suite trustworthy for the long haul.\nWrapping Up the Series Over these ten posts we went from an empty folder to a framework with a clean architecture, a proper Page Object Model, solid test data handling, fixtures for setup and teardown, reliable waiting behavior, a hybrid API and UI testing approach, rich failure diagnostics, a working CI pipeline, fast parallel execution across shards, and now the governance habits to keep all of it healthy over time.\nNone of this needs to happen all at once on a real project. Pick it up in roughly this order, and each piece builds cleanly on the one before it, the same way we walked through it here.\n","permalink":"https://abygeorgea.com/blog/2024/04/27/keeping-code-clean-playwright-linting-hooks-and-governance/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2024/04/18/maximizing-speed-playwright-workers-and-sharding/\"\u003eprevious post\u003c/a\u003e, we got a large suite running fast in CI. This is the last post in the series, and it covers something that matters more the longer a framework lives. Keeping it healthy after the initial build is done.\u003c/p\u003e\n\u003cp\u003eA framework that looks clean on day one can turn into a mess after six months of multiple people adding tests under deadline pressure. Inconsistent formatting, selectors that break constantly, tests nobody trusts anymore. None of this is really a Playwright problem. It is a team habits problem, and it is solvable with the right tooling in place from early on.\u003c/p\u003e","title":"Keeping Code Clean: Linting, Hooks, and Long-Term Governance"},{"content":"In the previous post, we got our suite running automatically in GitHub Actions. That works fine when you have thirty tests. It starts to feel slow once you have three hundred. Today we look at getting that feedback loop back down to something reasonable.\nThere are two separate levers here, and it is worth understanding the difference. Workers control how many tests run at once on a single machine. Sharding controls splitting the whole suite across multiple separate machines entirely. You usually want both.\nTuning Workers By default, Playwright picks a sensible number of workers based on the CPU cores available on the machine running the tests. You can override this directly in your config, which we touched on briefly back in part one.\nexport default defineConfig({ fullyParallel: true, workers: process.env.CI ? 4 : undefined, }); Leaving workers undefined locally lets Playwright use its own default based on your machine, which is usually the right call for local development. In CI, it is worth setting an explicit number, since CI runners often report more cores than they can actually give you a fair share of, and an overly high worker count there can cause tests to slow down instead of speeding up, purely from resource contention.\nfullyParallel: true matters here too. Without it, Playwright only parallelizes across different test files, and every test within a single file still runs one after another. With it enabled, tests within the same file can run across different workers as well, which matters a lot if you have a handful of large spec files rather than lots of small ones.\nWorth being honest about a tradeoff here too. More workers means more browser instances running at once, and if those tests are hitting a shared staging environment or a shared test database, too much concurrency can cause its own problems, like exhausting connection pools or hitting rate limits. Tune the worker count against what your target environment can actually handle, not just what your CI runner\u0026rsquo;s core count allows.\nSplitting the Suite With Sharding Workers help you use one machine efficiently. Sharding lets you split the entire test suite across several machines running at the same time, which is where the real wall clock time savings come from once a suite gets large.\nPlaywright supports this directly through a command line flag.\nnpx playwright test --shard=1/4 This tells Playwright to run only the first quarter of the test suite. Run the same command four times with 1/4, 2/4, 3/4, and 4/4, and between them, every test in the suite gets covered exactly once, split across four separate processes that can run on four separate machines simultaneously.\nIn GitHub Actions, the clean way to do this is with a matrix strategy.\njobs: test: timeout-minutes: 30 runs-on: ubuntu-latest strategy: fail-fast: false matrix: shard: [1, 2, 3, 4] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - name: Install dependencies run: npm ci - name: Install Playwright browsers run: npx playwright install --with-deps - name: Run Playwright tests run: npx playwright test --shard=${{ matrix.shard }}/4 - name: Upload blob report if: always() uses: actions/upload-artifact@v4 with: name: blob-report-${{ matrix.shard }} path: blob-report/ retention-days: 14 This spins up four separate jobs, each running a quarter of the suite, all in parallel. A suite that took forty minutes on one runner can drop to something closer to ten minutes across four runners, assuming the suite splits reasonably evenly.\nfail-fast: false is worth calling out specifically. Without it, GitHub Actions will cancel the other shards the moment any single shard fails, which means you lose visibility into whether the other three shards would have passed. Setting it to false lets every shard finish and report its own result independently.\nMerging Reports From Multiple Shards One side effect of sharding is that you end up with separate report data from each shard, rather than one unified HTML report. Playwright has a blob reporter built for exactly this situation, which is why the config above uses blob-report as the upload path instead of the usual playwright-report.\nAdd a small follow up job that downloads every shard\u0026rsquo;s blob report and merges them into a single HTML report.\nmerge-reports: if: always() needs: [test] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - name: Install dependencies run: npm ci - name: Download blob reports uses: actions/download-artifact@v4 with: path: all-blob-reports pattern: blob-report-* merge-multiple: true - name: Merge into HTML report run: npx playwright merge-reports --reporter html ./all-blob-reports - name: Upload merged report uses: actions/upload-artifact@v4 with: name: merged-html-report path: playwright-report/ retention-days: 14 Now anyone reviewing a pull request gets one single report to look at, covering the whole suite, regardless of how many shards it actually ran across.\nWrapping Up Workers make good use of a single machine\u0026rsquo;s resources. Sharding spreads a large suite across several machines at once. Together they turn a slow, single threaded test run into something that finishes in a fraction of the time, without touching the tests themselves.\nNext time, in our final post of this series, we look at keeping the framework itself healthy over months of active development, through linting, pre-commit hooks, and some practical rules around selector governance.\n","permalink":"https://abygeorgea.com/blog/2024/04/18/maximizing-speed-playwright-workers-and-sharding/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2024/04/09/automating-quality-playwright-in-github-actions/\"\u003eprevious post\u003c/a\u003e, we got our suite running automatically in GitHub Actions. That works fine when you have thirty tests. It starts to feel slow once you have three hundred. Today we look at getting that feedback loop back down to something reasonable.\u003c/p\u003e\n\u003cp\u003eThere are two separate levers here, and it is worth understanding the difference. Workers control how many tests run at once on a single machine. Sharding controls splitting the whole suite across multiple separate machines entirely. You usually want both.\u003c/p\u003e","title":"Maximizing Speed: Worker Optimization and CI Sharding"},{"content":"In the previous post, we set up reporting and traces so failures are easy to diagnose. None of that matters much if tests only run on your own laptop. Today we wire everything into a CI pipeline, so every pull request gets tested automatically before it can be merged.\nWe will use GitHub Actions here, since it is what most teams already have available, and Playwright has solid first party support for it.\nA Basic Workflow If you answered yes to the GitHub Actions prompt back when you ran npm init playwright@latest in part one, you already have a starting workflow file at .github/workflows/playwright.yml. Here is a version close to what I actually use on real projects, with a bit more structure around artifact uploads.\nname: Playwright Tests on: push: branches: [main] pull_request: branches: [main] jobs: test: timeout-minutes: 30 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - name: Install dependencies run: npm ci - name: Install Playwright browsers run: npx playwright install --with-deps - name: Run Playwright tests run: npx playwright test - name: Upload HTML report if: always() uses: actions/upload-artifact@v4 with: name: playwright-report path: playwright-report/ retention-days: 14 A couple of details worth calling out. The if: always() on the upload step means the report gets uploaded whether the tests passed or failed. You want the report from a failing run more than anything, so this cannot be conditional on success.\nnpx playwright install --with-deps installs both the browser binaries and any system level dependencies those browsers need on a fresh Ubuntu runner. Skipping the --with-deps flag is a common cause of tests failing in CI with confusing browser launch errors that never happen locally.\nUsing the Official Docker Image Instead Installing browsers fresh on every CI run works, but it adds time to every single job, and there is always a small risk of a subtle difference between your local OS and the CI runner\u0026rsquo;s rendering behavior. Playwright publishes an official Docker image with browsers and all their dependencies already baked in, which avoids both problems.\njobs: test: timeout-minutes: 30 runs-on: ubuntu-latest container: image: mcr.microsoft.com/playwright:v1.42.0-jammy steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - name: Install dependencies run: npm ci - name: Run Playwright tests run: npx playwright test - name: Upload HTML report if: always() uses: actions/upload-artifact@v4 with: name: playwright-report path: playwright-report/ retention-days: 14 Notice there is no separate browser install step here at all. The container already has everything Playwright needs, matched to the exact Playwright version in the image tag. This is worth pinning to a specific version rather than using latest, so an unrelated image update never quietly changes browser behavior out from under your pipeline.\nArchiving Traces Alongside Reports Reports alone are useful, but if a test fails, you want the trace file too, since that gives you the full step by step DOM and network detail we covered in the last post. Add a second upload step scoped to just the trace output.\n- name: Upload traces if: failure() uses: actions/upload-artifact@v4 with: name: playwright-traces path: test-results/ retention-days: 14 This one uses if: failure() instead of always(), since there is no point archiving trace files for a run that had nothing to investigate. Playwright writes trace files into the test-results directory automatically whenever the trace: 'on-first-retry' setting from our config triggers a capture.\nWith both of these artifacts in place, anyone on the team can go to a failed workflow run, download the report and the trace, and start debugging immediately without needing to reproduce the failure locally first.\nWrapping Up At this point every push and pull request against main runs the full suite automatically, using the same browser environment every time, with a report and traces waiting for anyone who needs them.\nNext time, we look at what happens once this suite grows large enough that a single CI run starts taking too long, and how worker tuning and test sharding keep feedback loops fast even as the number of tests keeps climbing.\n","permalink":"https://abygeorgea.com/blog/2024/04/09/automating-quality-playwright-in-github-actions/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2024/03/31/debugging-with-confidence-playwright-traces-and-html-reports/\"\u003eprevious post\u003c/a\u003e, we set up reporting and traces so failures are easy to diagnose. None of that matters much if tests only run on your own laptop. Today we wire everything into a CI pipeline, so every pull request gets tested automatically before it can be merged.\u003c/p\u003e\n\u003cp\u003eWe will use GitHub Actions here, since it is what most teams already have available, and Playwright has solid first party support for it.\u003c/p\u003e","title":"Automating Quality: Running Playwright in GitHub Actions"},{"content":"In the previous post, we sped up tests by mixing in API calls. Today we cover something every team hits eventually. A test fails overnight in CI, nobody was watching it run, and now someone has to figure out why.\nThis used to mean staring at a stack trace and a screenshot, if you were lucky enough to have a screenshot at all, and guessing at what the page must have looked like. Playwright gives you a lot more to work with than that, and it is worth setting all of it up before you actually need it.\nThe Built-In HTML Reporter We already set reporter: 'html' back in our config in part one. After any test run, this produces a report you can open locally.\nnpx playwright show-report This opens an interactive report in your browser, listing every test, its status, and how long it took. Click into a failed test and you get the full error message, the exact line of code that failed, and a timeline of every step Playwright took along the way. For anything beyond a trivial failure, this is always the first thing I look at.\nCapturing Screenshots and Video Automatically Static evidence of what the browser actually looked like at the moment of failure is worth a lot. Playwright can capture this automatically, without you writing any extra code in your tests.\nuse: { screenshot: \u0026#39;only-on-failure\u0026#39;, video: \u0026#39;retain-on-failure\u0026#39;, }, With these two settings in your config, a screenshot and a short video recording get saved automatically whenever a test fails, and both show up as attachments in the HTML report. Passing tests do not generate this extra data, so you are not filling up disk space or CI storage for runs that already worked fine.\nYou can also grab a screenshot manually at any point in a test, which is handy for debugging a specific step while you are writing a new test.\nawait page.screenshot({ path: \u0026#39;debug-checkout-step.png\u0026#39;, fullPage: true }); The Trace Viewer Screenshots and video tell you what the page looked like. The trace viewer tells you everything else. It is, in my experience, the single most useful debugging tool Playwright gives you.\nTurn it on in your config like this.\nuse: { trace: \u0026#39;on-first-retry\u0026#39;, }, This setting records a trace only when a test fails on its first attempt and then gets retried, which keeps the overhead low while still capturing exactly the runs you need. Once you have a trace file from a failed run, either from CI artifacts or a local run, open it like this.\nnpx playwright show-trace trace.zip This opens an interactive viewer where you can step through the test action by action. For every single step, you get a DOM snapshot exactly as it existed at that moment, a screenshot, the network requests that were in flight, and the browser console output. You can click on any action in the timeline and see precisely what the page looked like right before and right after it happened.\nThis turns debugging a remote CI failure from a guessing game into something closer to actually watching the test run happen. You are not reconstructing the failure from a single error message anymore. You are looking at the exact state of the page at the exact moment things went wrong.\nGenerating Traces On Demand Sometimes you want a trace for a specific local run, outside of the retry mechanism, especially while you are actively debugging something. You can do that directly from the command line.\nnpx playwright test checkout.spec.ts --trace on This forces every test in that run to record a trace, regardless of whether it passes or fails, which is useful when you want to inspect a passing test\u0026rsquo;s behavior in detail too, not just chase down a failure.\nArchiving Reports and Traces From CI None of this helps much if the report and trace files disappear the moment the CI job finishes. Make sure your pipeline uploads them as artifacts so anyone on the team can pull them down later. We will look at the exact GitHub Actions configuration for this in the next post, but the important habit to build now is treating the HTML report and any trace files as first class outputs of every CI run, not an afterthought.\nWrapping Up Between the HTML reporter, automatic screenshots and video on failure, and the trace viewer, you rarely need to guess what went wrong in a failed test. The evidence is already sitting there waiting for you.\nNext time, we take everything we have built so far and wire it into a GitHub Actions pipeline, so every pull request gets a full test run automatically, with reports and traces archived for anyone who needs to look at them later.\n","permalink":"https://abygeorgea.com/blog/2024/03/31/debugging-with-confidence-playwright-traces-and-html-reports/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2024/03/22/speeding-up-execution-playwright-api-and-ui-hybrid-testing/\"\u003eprevious post\u003c/a\u003e, we sped up tests by mixing in API calls. Today we cover something every team hits eventually. A test fails overnight in CI, nobody was watching it run, and now someone has to figure out why.\u003c/p\u003e\n\u003cp\u003eThis used to mean staring at a stack trace and a screenshot, if you were lucky enough to have a screenshot at all, and guessing at what the page must have looked like. Playwright gives you a lot more to work with than that, and it is worth setting all of it up before you actually need it.\u003c/p\u003e","title":"Debugging with Confidence: Traces, Screenshots, and HTML Reports"},{"content":"Entering 2024, it feels like the whole AI tooling ecosystem has leveled up dramatically over the last year. We’re finally moving past hacky string parsing and regex workarounds—OpenAI’s native Function Calling has completely changed how I build scripts that interact with LLMs.\nWhat I’ve Been Experimenting With Lately 1. Reliable, Deterministic Scripting via Function Calling Instead of asking the model for raw code or text and hoping for the best, I’ve been defining strict JSON schemas for core test actions—things like click_element(selector), fill_input(selector, text), and assert_text(expected).\nNow, I can pass a page’s state representation to the LLM and get back structured, executable function calls every time. It completely eliminates the random pipeline crashes caused by rogue Markdown formatting or unexpected conversational text.\n2. Trying Out First-Gen Test Copilots I’ve also been taking a look at dedicated QA copilots from platforms like Tricentis and Testim. Unlike general-purpose coding assistants that just guess, these specialized tools actually understand wait strategies, test assertions, and environment configs right out of the box.\nThe Tricky Part: Hallucinations Inside Tool Calls Even with structured function calling, the models still pull some weird moves. Every so often, an LLM will invent non-existent parameters or try to execute actions out of order—like attempting to hit a submit button before filling out the required form fields.\n","permalink":"https://abygeorgea.com/blog/2024/03/26/structured-output-function-calling-and-autonomous-test-authoring/","summary":"\u003cp\u003eEntering 2024, it feels like the whole AI tooling ecosystem has leveled up dramatically over the last year. We’re finally moving past hacky string parsing and regex workarounds—OpenAI’s native \u003cstrong\u003eFunction Calling\u003c/strong\u003e has completely changed how I build scripts that interact with LLMs.\u003c/p\u003e\n\u003ch3 id=\"what-ive-been-experimenting-with-lately\"\u003eWhat I’ve Been Experimenting With Lately\u003c/h3\u003e\n\u003ch4 id=\"1-reliable-deterministic-scripting-via-function-calling\"\u003e1. Reliable, Deterministic Scripting via Function Calling\u003c/h4\u003e\n\u003cp\u003eInstead of asking the model for raw code or text and hoping for the best, I’ve been defining strict JSON schemas for core test actions—things like \u003ccode\u003eclick_element(selector)\u003c/code\u003e, \u003ccode\u003efill_input(selector, text)\u003c/code\u003e, and \u003ccode\u003eassert_text(expected)\u003c/code\u003e.\u003c/p\u003e","title":"Structured Output, Function Calling, and Autonomous Test Authoring"},{"content":"In the previous post, we looked at why Playwright tests do not need manual sleeps. Today we look at a different kind of speed problem. Tests that spend most of their time on setup steps that have nothing to do with what they are actually testing.\nSay you are testing the checkout flow. To get there, a real user has to log in, search for a product, add it to the cart, and then go to checkout. If every single checkout test drives all of that through the UI first, you are spending most of your test run clicking through steps you already tested thoroughly somewhere else. That adds up fast across a whole suite.\nPlaywright gives you a clean way around this. It ships with a built in API testing client, so you can set up state directly through your backend, and only use the browser for the part you actually care about.\nThe Built-In Request Context Every test automatically gets access to an APIRequestContext through the request fixture. You do not need to install a separate HTTP client.\ntest(\u0026#39;check product details via API\u0026#39;, async ({ request }) =\u0026gt; { const response = await request.get(\u0026#39;/api/products/42\u0026#39;); expect(response.ok()).toBeTruthy(); const product = await response.json(); expect(product.name).toBe(\u0026#39;Wireless Mouse\u0026#39;); }); This works for POST, PUT, and DELETE too, and it handles headers, query params, and JSON bodies naturally.\nconst response = await request.post(\u0026#39;/api/cart/items\u0026#39;, { data: { productId: 42, quantity: 2, }, headers: { Authorization: `Bearer ${authToken}`, }, }); Because this is a real HTTP client under the hood, it is fast. There is no browser rendering involved, no waiting for a page to load, just a direct call to your backend and a response.\nSeeding State Before the UI Takes Over This is where things get genuinely useful. Instead of driving the browser through login, search, and add to cart, we can do all of that through direct API calls, and only open the browser once we are ready for checkout.\n// utils/api-client.ts import { APIRequestContext } from \u0026#39;@playwright/test\u0026#39;; export async function loginViaApi(request: APIRequestContext, username: string, password: string) { const response = await request.post(\u0026#39;/api/auth/login\u0026#39;, { data: { username, password }, }); const { token } = await response.json(); return token; } export async function addProductToCartViaApi(request: APIRequestContext, token: string, productId: number) { await request.post(\u0026#39;/api/cart/items\u0026#39;, { data: { productId, quantity: 1 }, headers: { Authorization: `Bearer ${token}` }, }); } And the test itself becomes short and focused entirely on checkout, which is the part we are actually validating.\ntest(\u0026#39;user can complete checkout with an item already in cart\u0026#39;, async ({ page, request }) =\u0026gt; { const token = await loginViaApi(request, \u0026#39;testuser\u0026#39;, \u0026#39;Password123\u0026#39;); await addProductToCartViaApi(request, token, 42); // Hand the authenticated session to the browser context await page.context().addCookies([ { name: \u0026#39;auth_token\u0026#39;, value: token, url: \u0026#39;https://example.com\u0026#39; }, ]); const checkoutPage = new CheckoutPage(page); await checkoutPage.goto(); await checkoutPage.completeOrder(); await expect(checkoutPage.confirmationBanner).toBeVisible(); }); The login and add to cart steps that used to take several seconds of clicking through the UI now happen in a couple of fast API calls. The browser only opens for the part of the test that actually matters, checkout itself.\nVerifying Backend State After a UI Action The same idea works in reverse too. Sometimes you want to confirm that an action a user took in the browser actually changed something correctly on the backend, beyond what is visible on the screen.\ntest(\u0026#39;placing an order updates inventory count\u0026#39;, async ({ page, request }) =\u0026gt; { const checkoutPage = new CheckoutPage(page); await checkoutPage.goto(); await checkoutPage.completeOrder(); await expect(checkoutPage.confirmationBanner).toBeVisible(); const response = await request.get(\u0026#39;/api/products/42\u0026#39;); const product = await response.json(); expect(product.stockCount).toBe(17); }); This gives you a level of confidence that a pure UI check cannot. The confirmation banner showing up tells you the user experience worked. Checking the API afterward tells you the backend state is actually correct too.\nWhen to Use the UI and When Not To The rule I follow is simple. If a step is not the thing the test is actually verifying, it is a candidate for the API. Logging in, creating prerequisite records, cleaning up test data, all of that belongs in API calls wherever your backend supports it. The UI is reserved for the actual behavior under test.\nThis does not replace end to end coverage entirely. You still want some tests that walk through a full real user journey from start to finish, UI only, because that is the only way to catch integration issues between steps. But for the bulk of your suite, hybrid tests like the ones above will run noticeably faster and fail less often, because you have fewer UI interactions that could hit a rendering quirk or a slow network call.\nWrapping Up Playwright\u0026rsquo;s request context turns API setup into a first class part of your test suite, without needing a separate HTTP library. Use it to skip repetitive UI steps and to verify backend state directly, and save full UI journeys for the parts of your application where the user interface itself is what you are testing.\nNext time, we look at what happens when a test does fail. Screenshots, videos, and the Playwright trace viewer, and how they turn a frustrating overnight CI failure into something you can diagnose in a couple of minutes.\n","permalink":"https://abygeorgea.com/blog/2024/03/22/speeding-up-execution-playwright-api-and-ui-hybrid-testing/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2024/03/13/eliminating-flakiness-playwright-auto-waiting-and-web-first-assertions/\"\u003eprevious post\u003c/a\u003e, we looked at why Playwright tests do not need manual sleeps. Today we look at a different kind of speed problem. Tests that spend most of their time on setup steps that have nothing to do with what they are actually testing.\u003c/p\u003e\n\u003cp\u003eSay you are testing the checkout flow. To get there, a real user has to log in, search for a product, add it to the cart, and then go to checkout. If every single checkout test drives all of that through the UI first, you are spending most of your test run clicking through steps you already tested thoroughly somewhere else. That adds up fast across a whole suite.\u003c/p\u003e","title":"Speeding Up Execution: Combining API Setup with UI Validation"},{"content":"In the previous post, we used fixtures to clean up test setup. Today we talk about the thing that probably causes more wasted engineering hours than anything else in test automation. Flakiness.\nIf you have worked with older browser automation tools, you know the pattern. A test fails intermittently. Someone adds sleep(2000) right before the failing step. The test passes for a while. Then it starts failing again on a slower CI runner, so someone bumps it to sleep(5000). Now your test suite takes twenty minutes longer to run and it is still not fully reliable.\nPlaywright takes a genuinely different approach, and it is worth understanding why it works so well.\nHow Auto-Waiting Actually Works Every action Playwright performs on a locator, like click, fill, or check, automatically waits for a set of conditions before it does anything. The element has to be attached to the DOM. It has to be visible. It has to be stable, meaning it is not in the middle of a CSS transition or animation. It has to receive events, meaning nothing else is covering it. And for things like inputs, it has to actually be enabled.\nPlaywright checks all of this before every action, and it keeps retrying those checks until they pass or the timeout is reached. You do not write any of this logic yourself. You just call the action.\nawait page.locator(\u0026#39;#submit-button\u0026#39;).click(); Behind that one line, Playwright is silently waiting for the button to exist, become visible, stop moving, and become clickable, before the click actually happens. If a loading spinner is covering the button for half a second, Playwright waits it out instead of throwing an error immediately.\nThis is the single biggest reason Playwright tests tend to be more stable than tests written with tools that click immediately and let you deal with the fallout.\nWeb-First Assertions The same philosophy carries over into assertions. A traditional assertion checks a condition once, right now, and either passes or fails immediately. A web-first assertion in Playwright keeps checking, on a short interval, until the condition is true or the timeout runs out.\nawait expect(page.locator(\u0026#39;.welcome-banner\u0026#39;)).toBeVisible(); This single line will keep polling for up to the configured timeout, checking whether that banner has become visible. If the banner takes three hundred milliseconds to render after some API call finishes, this assertion just waits for it naturally. No manual wait needed anywhere.\nThere are a lot of these built in matchers, and it is worth knowing the common ones.\nawait expect(page.locator(\u0026#39;.error-message\u0026#39;)).not.toBeVisible(); await expect(page.locator(\u0026#39;#order-status\u0026#39;)).toHaveText(\u0026#39;Confirmed\u0026#39;); await expect(page.locator(\u0026#39;.cart-count\u0026#39;)).toHaveText(\u0026#39;3\u0026#39;); await expect(page.locator(\u0026#39;input#email\u0026#39;)).toHaveValue(\u0026#39;test@example.com\u0026#39;); await expect(page).toHaveURL(/.*\\/checkout\\/success/); await expect(page).toHaveTitle(/Order Confirmation/); Every one of these retries automatically. You are not writing polling loops. You are describing the end state you expect, and letting Playwright handle the timing.\nWhat This Replaces It is worth being explicit about what you should no longer be reaching for. page.waitForTimeout() exists in the API, and it is tempting to use it the way you might have used a hard sleep in an older tool. Resist that urge. It almost always means there is a specific condition you should be waiting for instead, and Playwright almost certainly has a way to wait for that exact condition already.\n// Avoid this await page.waitForTimeout(3000); await page.locator(\u0026#39;.results-list\u0026#39;).click(); // Prefer this await expect(page.locator(\u0026#39;.results-list\u0026#39;)).toBeVisible(); await page.locator(\u0026#39;.results-list\u0026#39;).click(); The second version waits exactly as long as it needs to and no longer. On a fast day it might only wait fifty milliseconds. On a slow day it might wait two seconds. Either way, the test only proceeds once the real condition is met.\nSensible Timeouts and Retries Auto-waiting handles most timing issues on its own, but it is still worth configuring sane defaults for the situations that genuinely are slower, like a flaky third party network call in a staging environment. Two settings matter here, both of which we touched on back in our config file in part one.\nexport default defineConfig({ timeout: 30 * 1000, expect: { timeout: 5000, }, retries: process.env.CI ? 2 : 0, }); The expect.timeout setting controls how long any single web-first assertion will keep retrying before it gives up. The top level timeout controls how long an entire test is allowed to run. And retries gives you a safety net in CI specifically, where shared infrastructure and network variance are more likely to cause a one-off failure that would not reproduce on a second attempt.\nRetries are a safety net, though, not a fix for a genuinely broken test. If a test only passes on the second or third attempt consistently, that is a sign something in the test or the application needs attention, not a sign to increase the retry count further.\nWrapping Up Auto-waiting and web-first assertions remove almost every reason to reach for a manual sleep. Your tests wait exactly as long as they need to, and no longer, which makes both fast and slow days behave consistently.\nNext time, we look at combining Playwright\u0026rsquo;s API testing capabilities with UI testing, so we can skip repetitive UI setup steps entirely and let the API do the boring parts.\n","permalink":"https://abygeorgea.com/blog/2024/03/13/eliminating-flakiness-playwright-auto-waiting-and-web-first-assertions/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2024/03/04/supercharging-tests-playwright-native-fixtures/\"\u003eprevious post\u003c/a\u003e, we used fixtures to clean up test setup. Today we talk about the thing that probably causes more wasted engineering hours than anything else in test automation. Flakiness.\u003c/p\u003e\n\u003cp\u003eIf you have worked with older browser automation tools, you know the pattern. A test fails intermittently. Someone adds \u003ccode\u003esleep(2000)\u003c/code\u003e right before the failing step. The test passes for a while. Then it starts failing again on a slower CI runner, so someone bumps it to \u003ccode\u003esleep(5000)\u003c/code\u003e. Now your test suite takes twenty minutes longer to run and it is still not fully reliable.\u003c/p\u003e","title":"Eliminating Flakiness: Auto-Waiting and Web-First Assertions"},{"content":"In the previous post, we sorted out static and dynamic test data. This time we tackle something that quietly bloats a lot of test suites. Setup code.\nIf you have written more than a handful of Playwright tests, you have probably written a beforeEach block that logs a user in, or sets up a page object, or seeds some starting state. Do that across twenty spec files and you end up with the same boilerplate copied everywhere, and a small change to the login flow means touching every single file.\nPlaywright\u0026rsquo;s fixture system, through test.extend, solves this properly. Think of it as dependency injection for your tests. You describe what a test needs, and Playwright hands it to you already set up.\nCreating a Custom Fixture A fixture in Playwright is just a function that sets something up, hands it to the test, and optionally cleans up afterward. Let\u0026rsquo;s start by turning our LoginPage object into a fixture, so any test can just ask for it directly as a parameter.\n// fixtures/pages.fixture.ts import { test as base } from \u0026#39;@playwright/test\u0026#39;; import { LoginPage } from \u0026#39;../pages/login.page\u0026#39;; type PageFixtures = { loginPage: LoginPage; }; export const test = base.extend\u0026lt;PageFixtures\u0026gt;({ loginPage: async ({ page }, use) =\u0026gt; { const loginPage = new LoginPage(page); await use(loginPage); }, }); export { expect } from \u0026#39;@playwright/test\u0026#39;; Now instead of importing test from @playwright/test directly in your spec files, you import your own extended version.\nimport { test, expect } from \u0026#39;../fixtures/pages.fixture\u0026#39;; test(\u0026#39;user can log in with valid credentials\u0026#39;, async ({ loginPage }) =\u0026gt; { await loginPage.goto(); await loginPage.login(\u0026#39;testuser\u0026#39;, \u0026#39;Password123\u0026#39;); await expect(loginPage.welcomeBanner).toBeVisible(); }); The test never has to construct a LoginPage itself. It just declares that it needs one, and Playwright builds it for you before the test body runs.\nFixtures for Authenticated State This pattern really shines once you apply it to authentication. Logging in through the UI for every single test that needs to be logged in is slow, and it repeats the same three or four steps constantly. A better approach is to authenticate once, save the browser storage state, and reuse it.\nFirst, set up a small script that logs in and saves the state to a file.\n// fixtures/auth.setup.ts import { test as setup } from \u0026#39;@playwright/test\u0026#39;; import { LoginPage } from \u0026#39;../pages/login.page\u0026#39;; const authFile = \u0026#39;playwright/.auth/user.json\u0026#39;; setup(\u0026#39;authenticate\u0026#39;, async ({ page }) =\u0026gt; { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login(\u0026#39;testuser\u0026#39;, \u0026#39;Password123\u0026#39;); await page.context().storageState({ path: authFile }); }); Then reference this in playwright.config.ts as a dedicated setup project, and have your other projects depend on it.\nprojects: [ { name: \u0026#39;setup\u0026#39;, testMatch: /auth\\.setup\\.ts/ }, { name: \u0026#39;chromium\u0026#39;, use: { ...devices[\u0026#39;Desktop Chrome\u0026#39;], storageState: \u0026#39;playwright/.auth/user.json\u0026#39;, }, dependencies: [\u0026#39;setup\u0026#39;], }, ], With this in place, every test in the chromium project starts already logged in, because the browser context loads the saved storage state before the test even begins. Login happens exactly once per test run, not once per test.\nContext Isolation, Automatically One thing worth appreciating here is something Playwright gives you for free. Every test gets its own browser context by default. That means cookies, local storage, and session data from one test never leak into another, even when tests run in parallel in the same worker process. You do not need to manually clear cookies between tests the way you might have in older tools. Playwright\u0026rsquo;s isolation model handles it at the context level, so each test genuinely starts from a clean slate, aside from whatever storage state you explicitly load.\nGlobal Setup Without the beforeEach Chains Fixtures also give you a cleaner way to handle setup that used to live in long chains of beforeEach and afterEach blocks. Say a test needs a freshly created order before it runs, and the order needs to be cleaned up afterward regardless of whether the test passes or fails.\n// fixtures/order.fixture.ts import { test as base } from \u0026#39;@playwright/test\u0026#39;; import { createOrderViaApi, deleteOrderViaApi } from \u0026#39;../utils/api-client\u0026#39;; type OrderFixtures = { existingOrder: { id: string }; }; export const test = base.extend\u0026lt;OrderFixtures\u0026gt;({ existingOrder: async ({}, use) =\u0026gt; { const order = await createOrderViaApi(); await use(order); await deleteOrderViaApi(order.id); }, }); Everything before use(order) is setup. Everything after it is teardown, and it runs even if the test itself fails, because Playwright wraps the fixture lifecycle around the whole test. Compare that to manually remembering to clean up inside an afterEach, and hoping nobody forgets when they add a new test to the file.\nWrapping Up Fixtures turn setup and teardown into something declarative. A test asks for what it needs, whether that is a page object, an authenticated session, or a piece of freshly created data, and Playwright handles the rest behind the scenes.\nNext time, we look at what might be Playwright\u0026rsquo;s biggest quality of life improvement over older automation tools. Auto-waiting and web-first assertions, and why they get rid of most of the flakiness that used to plague UI test suites.\n","permalink":"https://abygeorgea.com/blog/2024/03/04/supercharging-tests-playwright-native-fixtures/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2024/02/23/decoupling-data-playwright-test-data-management/\"\u003eprevious post\u003c/a\u003e, we sorted out static and dynamic test data. This time we tackle something that quietly bloats a lot of test suites. Setup code.\u003c/p\u003e\n\u003cp\u003eIf you have written more than a handful of Playwright tests, you have probably written a \u003ccode\u003ebeforeEach\u003c/code\u003e block that logs a user in, or sets up a page object, or seeds some starting state. Do that across twenty spec files and you end up with the same boilerplate copied everywhere, and a small change to the login flow means touching every single file.\u003c/p\u003e","title":"Supercharging Tests with Native Playwright Fixtures"},{"content":"In the previous post, we cleaned up how our tests interact with the UI using the Page Object Model. This time we look at a different kind of mess. Test data.\nHere is a scenario that plays out on almost every team at some point. Two tests both try to register a new account using the email test@example.com. Run them one at a time and everything is fine. Run them at the same time in a parallel CI job, and one of them fails because the account already exists. Nobody touched the test code. The problem is the data.\nA good Playwright framework treats test data as its own concern, separate from test logic. That means static reference data lives in files, and anything that needs to be unique gets generated fresh for every run.\nExternalizing Static Data Some data genuinely does not change between runs. Country codes, product categories, a set of known test accounts that live permanently in a test environment. This kind of data belongs in a plain JSON file, not scattered across test files as string literals.\n// data/users.json { \u0026#34;standardUser\u0026#34;: { \u0026#34;username\u0026#34;: \u0026#34;standard_user\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;SecretPass1!\u0026#34; }, \u0026#34;adminUser\u0026#34;: { \u0026#34;username\u0026#34;: \u0026#34;admin_user\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;AdminPass1!\u0026#34; } } Loading it in a test is direct. TypeScript will even give you type checking on the shape of the file if you import it properly.\nimport users from \u0026#39;../data/users.json\u0026#39;; test(\u0026#39;admin can access the settings page\u0026#39;, async ({ page }) =\u0026gt; { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login(users.adminUser.username, users.adminUser.password); // continue the test }); This works well for anything that is genuinely fixed. It falls apart the moment two tests need their own unique version of that data at the same time.\nGenerating Dynamic Data With Faker For anything that needs to be unique per test run, like a new user registration or a new order, you want data generated on the fly. This is exactly what @faker-js/faker is built for. Install it alongside your other dev dependencies.\nnpm install --save-dev @faker-js/faker Then use it right inside your test, or better, inside a small helper function.\n// utils/data-factory.ts import { faker } from \u0026#39;@faker-js/faker\u0026#39;; export interface NewUser { firstName: string; lastName: string; email: string; password: string; } export function createNewUser(): NewUser { return { firstName: faker.person.firstName(), lastName: faker.person.lastName(), email: faker.internet.email(), password: faker.internet.password({ length: 12 }), }; } Now a registration test looks like this.\nimport { createNewUser } from \u0026#39;../utils/data-factory\u0026#39;; test(\u0026#39;a new user can register\u0026#39;, async ({ page }) =\u0026gt; { const newUser = createNewUser(); const registerPage = new RegisterPage(page); await registerPage.goto(); await registerPage.register(newUser); await expect(registerPage.successMessage).toBeVisible(); }); Run this test a hundred times in a hundred parallel workers, and every single one gets its own unique email address. No collisions, no cleanup needed between runs, and no more flaky failures caused by a duplicate account.\nYou can lean on Faker for a lot more than names and emails too. Addresses, phone numbers, company names, even realistic looking transaction amounts, all with sensible formats out of the box.\nconst orderAmount = faker.finance.amount({ min: 10, max: 500, dec: 2 }); const shippingAddress = { street: faker.location.streetAddress(), city: faker.location.city(), postcode: faker.location.zipCode(), }; Scoping Data So Parallel Tests Do Not Collide Faker solves the uniqueness problem for brand new data. But some tests need to work with existing records in a shared environment, and that brings its own risk. If two parallel tests both grab \u0026ldquo;the first product in the catalog\u0026rdquo; to add to a cart, you can end up with race conditions around stock counts or shared state.\nA few practical rules I follow here.\nNever hardcode an id or a record that another test might also be using. If a test needs an existing product, either seed one specifically for that test through the API, or query for one dynamically and use whatever comes back, rather than assuming record id 1 will always be free.\nPrefer creating fresh data per test over reusing shared fixtures whenever the workflow allows it. A test that registers its own new user and then acts on that user\u0026rsquo;s own data cannot collide with anything else running in parallel.\nWhere you truly must share a fixed data set, like a list of countries or currencies, treat it as read only. Nothing should ever be a test that mutates shared reference data, because the next parallel test relies on that data staying exactly as it was.\nWrapping Up Static data belongs in JSON files. Anything unique belongs in Faker. And anything shared across parallel tests should be treated as read only unless you have a very good reason not to.\nNext time, we move on to fixtures. Playwright\u0026rsquo;s test.extend gives you a clean way to inject page objects, authenticated sessions, and test data directly into your tests, and it removes a lot of repetitive setup code in the process.\n","permalink":"https://abygeorgea.com/blog/2024/02/23/decoupling-data-playwright-test-data-management/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2024/02/14/scaling-maintenance-playwright-page-object-model/\"\u003eprevious post\u003c/a\u003e, we cleaned up how our tests interact with the UI using the Page Object Model. This time we look at a different kind of mess. Test data.\u003c/p\u003e\n\u003cp\u003eHere is a scenario that plays out on almost every team at some point. Two tests both try to register a new account using the email \u003ccode\u003etest@example.com\u003c/code\u003e. Run them one at a time and everything is fine. Run them at the same time in a parallel CI job, and one of them fails because the account already exists. Nobody touched the test code. The problem is the data.\u003c/p\u003e","title":"Decoupling Data: Managing Test Inputs and Dynamic States"},{"content":"In the previous post, we scaffolded a Playwright project and set up a folder structure with an empty pages/ directory sitting there waiting to be used. Today we fill it in properly.\nHere is a problem I see all the time. A test file has ten tests in it. Every single one of them repeats the same selector for the login button. Then the front end team renames a CSS class, and suddenly all ten tests break at once. You end up doing a find and replace across a dozen files just to fix one small UI change.\nThe Page Object Model fixes this. It is not a Playwright specific idea. It has been around test automation for years. But Playwright and TypeScript make it especially clean to implement.\nKeeping Tests Focused on Behavior The core idea is simple. Your test files should read like a description of user behavior, not a list of CSS selectors. Compare these two approaches.\nWithout a page object, a login test tends to look like this.\ntest(\u0026#39;user can log in with valid credentials\u0026#39;, async ({ page }) =\u0026gt; { await page.goto(\u0026#39;/login\u0026#39;); await page.fill(\u0026#39;#username\u0026#39;, \u0026#39;testuser\u0026#39;); await page.fill(\u0026#39;#password\u0026#39;, \u0026#39;Password123\u0026#39;); await page.click(\u0026#39;button[type=\u0026#34;submit\u0026#34;]\u0026#39;); await expect(page.locator(\u0026#39;.welcome-banner\u0026#39;)).toBeVisible(); }); It works, but the test is mixing two concerns. It describes what the user does, and it describes exactly how the page is built. With a page object, the same test looks like this instead.\ntest(\u0026#39;user can log in with valid credentials\u0026#39;, async ({ page }) =\u0026gt; { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login(\u0026#39;testuser\u0026#39;, \u0026#39;Password123\u0026#39;); await expect(loginPage.welcomeBanner).toBeVisible(); }); Now the test reads like a sentence. Go to the login page, log in, check the welcome banner. All the messy selector detail moved somewhere else, and that somewhere else is a page object class.\nBuilding a Base Page Class Most pages in an application share some common behavior. Waiting for the page to be ready, grabbing the page title, that sort of thing. It makes sense to put that shared logic in one base class that every other page object extends.\n// pages/base.page.ts import { Page } from \u0026#39;@playwright/test\u0026#39;; export class BasePage { constructor(protected readonly page: Page) {} async waitForPageLoad(): Promise\u0026lt;void\u0026gt; { await this.page.waitForLoadState(\u0026#39;networkidle\u0026#39;); } async getTitle(): Promise\u0026lt;string\u0026gt; { return this.page.title(); } } Now the login page can extend this and add its own locators and actions.\n// pages/login.page.ts import { Page, Locator } from \u0026#39;@playwright/test\u0026#39;; import { BasePage } from \u0026#39;./base.page\u0026#39;; export class LoginPage extends BasePage { readonly usernameInput: Locator; readonly passwordInput: Locator; readonly submitButton: Locator; readonly welcomeBanner: Locator; constructor(page: Page) { super(page); this.usernameInput = page.locator(\u0026#39;#username\u0026#39;); this.passwordInput = page.locator(\u0026#39;#password\u0026#39;); this.submitButton = page.locator(\u0026#39;button[type=\u0026#34;submit\u0026#34;]\u0026#39;); this.welcomeBanner = page.locator(\u0026#39;.welcome-banner\u0026#39;); } async goto(): Promise\u0026lt;void\u0026gt; { await this.page.goto(\u0026#39;/login\u0026#39;); await this.waitForPageLoad(); } async login(username: string, password: string): Promise\u0026lt;void\u0026gt; { await this.usernameInput.fill(username); await this.passwordInput.fill(password); await this.submitButton.click(); } } Notice that locators are declared once, as readonly properties, and set up in the constructor. Every method after that just uses this.usernameInput instead of repeating the selector string. If the front end team changes the id from #username to #user-email, you update one line in one file. Every test that uses LoginPage keeps working without a single change.\nReusing Shared Components A lot of applications have UI pieces that show up on many pages. A header with a search bar. A footer with links. A data table used across three different admin screens. Copying the same locators into every page object that touches these pieces gets messy fast.\nThe fix is to treat these shared pieces as components of their own, separate from any single page.\n// pages/components/header.component.ts import { Page, Locator } from \u0026#39;@playwright/test\u0026#39;; export class HeaderComponent { readonly searchInput: Locator; readonly cartIcon: Locator; readonly accountMenu: Locator; constructor(private readonly page: Page) { this.searchInput = page.locator(\u0026#39;[data-testid=\u0026#34;header-search\u0026#34;]\u0026#39;); this.cartIcon = page.locator(\u0026#39;[data-testid=\u0026#34;header-cart\u0026#34;]\u0026#39;); this.accountMenu = page.locator(\u0026#39;[data-testid=\u0026#34;header-account\u0026#34;]\u0026#39;); } async search(term: string): Promise\u0026lt;void\u0026gt; { await this.searchInput.fill(term); await this.searchInput.press(\u0026#39;Enter\u0026#39;); } } Any page object that needs the header just creates an instance of it in its constructor.\n// pages/product-listing.page.ts import { Page } from \u0026#39;@playwright/test\u0026#39;; import { BasePage } from \u0026#39;./base.page\u0026#39;; import { HeaderComponent } from \u0026#39;./components/header.component\u0026#39;; export class ProductListingPage extends BasePage { readonly header: HeaderComponent; constructor(page: Page) { super(page); this.header = new HeaderComponent(page); } } Now a test can do productListingPage.header.search('running shoes') and it just works, without the product listing page object needing to know anything about how the header is built.\nWrapping Up We now have a base page, a real page object, and a reusable component pattern for shared UI. This alone removes most of the maintenance pain that comes from front end changes.\nNext time, we will look at test data. Hardcoded usernames and product ids look fine at first, but they cause real headaches once you start running tests in parallel against a shared environment. We will cover static data files and dynamic data generation with Faker, and how to keep parallel tests from stepping on each other.\n","permalink":"https://abygeorgea.com/blog/2024/02/14/scaling-maintenance-playwright-page-object-model/","summary":"\u003cp\u003eIn the \u003ca href=\"/blog/2024/02/05/laying-the-foundation-playwright-architecture-and-setup/\"\u003eprevious post\u003c/a\u003e, we scaffolded a Playwright project and set up a folder structure with an empty \u003ccode\u003epages/\u003c/code\u003e directory sitting there waiting to be used. Today we fill it in properly.\u003c/p\u003e\n\u003cp\u003eHere is a problem I see all the time. A test file has ten tests in it. Every single one of them repeats the same selector for the login button. Then the front end team renames a CSS class, and suddenly all ten tests break at once. You end up doing a find and replace across a dozen files just to fix one small UI change.\u003c/p\u003e","title":"Scaling Maintenance: Implementing the Page Object Model (POM)"},{"content":"Every solid test automation framework starts the same way. You pick a clean folder structure. You get your config right once, early, before you have hundreds of tests depending on it. Playwright makes this part refreshingly easy. TypeScript support is built in from day one. You do not need to bolt on extra plugins or wrestle with a transpiler.\nThis post walks through setting up a Playwright project the way I would actually set one up for a real team. Not a toy demo. Something that can grow to hundreds of tests without turning into a mess.\nThis is part one of a ten part series on building a proper Playwright framework. By the end of it, you will have a setup that handles page objects, test data, fixtures, CI, reporting, and more. Today we just lay the foundation.\nStarting With the Official CLI Playwright ships its own scaffolding tool, and it saves a lot of manual setup. Open a terminal in an empty folder and run this.\nnpm init playwright@latest You will get a short list of prompts. Answer them like this for a TypeScript project.\n✔ Do you want to use TypeScript or JavaScript? · TypeScript ✔ Where to put your end-to-end tests? · tests ✔ Add a GitHub Actions workflow? (y/N) · y ✔ Install Playwright browsers (can be done manually via \u0026#39;npx playwright install\u0026#39;)? (Y/n) · y Once it finishes, you get a working project with a sample test, a config file, and browser binaries installed locally. Run the sample test right away just to confirm everything is wired up.\nnpx playwright test If that green summary shows up in your terminal, you are ready to start shaping the project into something real.\nA Directory Layout That Scales The default scaffold gives you a single tests folder and a playwright.config.ts file. That is enough for a handful of tests. It is not enough once you have real page objects, shared fixtures, and test data files. Here is the layout I use on most projects.\n. ├── tests/ │ ├── login.spec.ts │ ├── checkout.spec.ts │ └── search.spec.ts ├── pages/ │ ├── base.page.ts │ ├── login.page.ts │ └── checkout.page.ts ├── fixtures/ │ └── test-options.ts ├── data/ │ ├── users.json │ └── products.json ├── utils/ │ ├── api-client.ts │ └── date-helpers.ts ├── playwright.config.ts ├── package.json └── tsconfig.json A quick word on what each folder is for, since this comes up in every code review I do.\ntests/ holds only spec files. Each file describes a user journey or a feature. No selectors, no low level logic here. pages/ holds Page Object Model classes. These wrap up the selectors and actions for a single page or component. fixtures/ holds custom Playwright fixtures. This is where you wire up authenticated sessions, shared setup, and anything injected into your tests. data/ holds static test data. Think reference lookups, seed users, and configuration values that do not change at runtime. utils/ holds small reusable helpers. API wrappers, date formatting, string generation, that kind of thing. Keeping this separation from day one means a new team member can open the repo and know exactly where to look for something. We will fill in pages/ and fixtures/ properly in the next two posts.\nConfiguring playwright.config.ts The config file is where you set the rules for your whole test run. Base URL, timeouts, which browsers to test against, and how many workers to use all live here. Below is a config close to what I use as a starting point on a real project.\nimport { defineConfig, devices } from \u0026#39;@playwright/test\u0026#39;; export default defineConfig({ testDir: \u0026#39;./tests\u0026#39;, timeout: 30 * 1000, expect: { timeout: 5000, }, fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 2 : undefined, reporter: \u0026#39;html\u0026#39;, use: { baseURL: process.env.BASE_URL || \u0026#39;https://example.com\u0026#39;, trace: \u0026#39;on-first-retry\u0026#39;, screenshot: \u0026#39;only-on-failure\u0026#39;, headless: true, }, projects: [ { name: \u0026#39;chromium\u0026#39;, use: { ...devices[\u0026#39;Desktop Chrome\u0026#39;] }, }, { name: \u0026#39;firefox\u0026#39;, use: { ...devices[\u0026#39;Desktop Firefox\u0026#39;] }, }, { name: \u0026#39;webkit\u0026#39;, use: { ...devices[\u0026#39;Desktop Safari\u0026#39;] }, }, ], }); A few points worth calling out here.\nbaseURL means every page.goto('/login') in your tests resolves against the right environment. You just swap the BASE_URL environment variable between local, staging, and production runs.\nfullyParallel: true tells Playwright it can run tests within the same file in parallel, not just across files. This matters a lot once your suite grows.\nretries is set to zero locally and two in CI. Flaky network conditions in a shared CI runner are a different problem than a genuinely broken test on your laptop, and this setting reflects that difference.\nThe projects array is what gives you cross browser coverage almost for free. Every test you write runs against Chromium, Firefox, and WebKit without any extra code.\nWrapping Up At this point you have a project that installs cleanly, runs a sample test, and has a folder structure ready for real page objects and fixtures. That is a solid place to stop for one post.\nNext time, we will tackle the Page Object Model properly. We will build a base page class, a couple of real page objects, and talk about why keeping selectors out of your test files saves you so much pain later.\n","permalink":"https://abygeorgea.com/blog/2024/02/05/laying-the-foundation-playwright-architecture-and-setup/","summary":"\u003cp\u003eEvery solid test automation framework starts the same way. You pick a clean folder structure. You get your config right once, early, before you have hundreds of tests depending on it. Playwright makes this part refreshingly easy. TypeScript support is built in from day one. You do not need to bolt on extra plugins or wrestle with a transpiler.\u003c/p\u003e\n\u003cp\u003eThis post walks through setting up a Playwright project the way I would actually set one up for a real team. Not a toy demo. Something that can grow to hundreds of tests without turning into a mess.\u003c/p\u003e","title":"Laying the Foundation: Architecture and Setup for a Playwright Project"},{"content":"Lately, it feels like we’ve hit a real tipping point with web automation. Selenium had a great decade-plus run, but for me, Microsoft Playwright has officially taken over as the go-to tool for modern web testing. It’s just so much faster and smoother to work with.\nAt the same time, I’ve been keeping an eye on the flood of new commercial \u0026ldquo;low-code\u0026rdquo; AI test tools popping up everywhere, trying to see if they actually deliver on bridging the gap between manual testing and automated pipelines.\nThings I’ve Been Experimenting With 1. Combining Playwright Codegen + Copilot Playwright’s built-in auto-waiting and network interception alone have made a massive dent in test flakiness, but the real fun has been tweaking the workflow.\nLately, I’ve been using Playwright’s native codegen tool to quickly record the core steps of a user journey, and then handing that raw output over to Copilot to refactor into a clean Page Object Model. Pairing the two turns what used to be a tedious recording into production-ready code in minutes.\n2. Taking a Look at AI \u0026ldquo;Low-Code\u0026rdquo; Builders It feels like every week a new vendor is pitching an \u0026ldquo;AI-driven visual test builder.\u0026rdquo; The pitch is always the same: let non-technical team members or product folks record flows, while AI automatically handles selector fixes and self-healing in the background. It’s an interesting space, but the reality is a bit more nuanced.\nMy Take: Code-First Wins Over Vendor Lock-In While these low-code platforms promise crazy fast onboarding, I’m still pretty skeptical. The biggest dealbreaker for me is how many of them store tests in proprietary formats, which totally breaks standard Git workflows and PR reviews.\nI’m much more interested in a code-first approach: keeping everything in solid open-source frameworks (like Playwright ) and using AI to speed up writing the code, rather than getting boxed into a black-box commercial platform.\n","permalink":"https://abygeorgea.com/blog/2023/09/18/playwrights-dominance-and-the-low-code-ai-influx/","summary":"\u003cp\u003eLately, it feels like we’ve hit a real tipping point with web automation. Selenium had a great decade-plus run, but for me, Microsoft Playwright has officially taken over as the go-to tool for modern web testing. It’s just so much faster and smoother to work with.\u003c/p\u003e\n\u003cp\u003eAt the same time, I’ve been keeping an eye on the flood of new commercial \u0026ldquo;low-code\u0026rdquo; AI test tools popping up everywhere, trying to see if they actually deliver on bridging the gap between manual testing and automated pipelines.\u003c/p\u003e","title":"Playwright's Dominance And the low code AI Influx"},{"content":"The initial novelty of pasting prompts into web browser windows has pretty much worn off. Lately, I\u0026rsquo;ve been way more interested in how AI fits directly into the everyday coding workflow—mostly tinkering with GitHub Copilot and inline code completions right inside the editor.\nAt the same time, my own approach to test automation has been shifting. I\u0026rsquo;ve been leaning much harder into API and microservice testing lately, mostly because keeping heavy UI tests reliable as apps change fast is a constant uphill battle.\nThings I’ve Been Experimenting With Lately 1. In-IDE Autocomplete for Quick Test Scaffolding GitHub Copilot has become a daily staple in my setup. I’ve been trying out a habit of writing a descriptive comment or function name first—something like // Test standard checkout flow with invalid credit card—and letting Copilot fill in the function body.\nIt’s surprisingly good at standard unit test structures in frameworks like Jest or PyTest. Where it stumbles, though, is when you try to get it to work with custom project wrappers or highly specific helper functions—it tends to guess wrong or fall back to generic pattern matching.\n2. Doubling Down on API Automation Over Brittle UI Tests UI tests are notorious for flaking out at the worst times. To keep things cleaner, I’ve been shifting more focus toward API testing using Postman, RestAssured, and a few custom Python scripts.\nOne really neat trick I stumbled across: AI tools are remarkably good at parsing OpenAPI / Swagger specs. Feeding a spec into an LLM and asking it to build out edge-case coverage (like 4xx validation errors and 5xx handling) generates surprisingly solid starting points.\nThe Real Headache: Hitting Token Limits The biggest bump in the road I\u0026rsquo;ve run into lately is dealing with context window limitations.\nWhenever I try to feed full HTML pages or complex DOM trees into a model for analysis, I run straight into GPT-3.5’s 4,000-token ceiling. To get around this, I ended up writing a small helper script using BeautifulSoup to strip away \u0026lt;style\u0026gt;, \u0026lt;script\u0026gt;, and inline SVG tags before passing the HTML along. It’s a hacky workaround, but it’s been essential for fitting everything into the token budget!\n","permalink":"https://abygeorgea.com/blog/2023/06/21/ide-assistants-and-shift-towards-api-automation/","summary":"\u003cp\u003eThe initial novelty of pasting prompts into web browser windows has pretty much worn off. Lately, I\u0026rsquo;ve been way more interested in how AI fits directly into the everyday coding workflow—mostly tinkering with GitHub Copilot and inline code completions right inside the editor.\u003c/p\u003e\n\u003cp\u003eAt the same time, my own approach to test automation has been shifting. I\u0026rsquo;ve been leaning much harder into API and microservice testing lately, mostly because keeping heavy UI tests reliable as apps change fast is a constant uphill battle.\u003c/p\u003e","title":"IDE Assistants and shift towards API Automation"},{"content":"If your group chats look anything like mine lately, they’re probably full of screenshots of ChatGPT spitting out code. Ever since the public launch blew up, I’ve been down a rabbit hole trying to figure out what’s actually useful versus what’s just hype.\nI wanted to cut through the noise and see how this stuff actually holds up in real life. Is AI going to write whole test suites for us overnight? Definitely not. But can it make the tedious, repetitive stuff a lot less painful right now? 100%.\nA Few Things I’ve Been Playing With 1. Quick-Starting Page Object Models (POM) The coolest trick I\u0026rsquo;ve found so far is using it to bypass the boring setup phase when building a new test suite. I’ll throw a chunk of raw HTML or a messy DOM snippet into ChatGPT (sticking with GPT-3.5) and ask it to sketch out a basic Page Object class in Python or TypeScript Playwright.\nIt’s definitely not perfect—it loves to hallucinate weird locators or make up fake async methods that don\u0026rsquo;t exist—but as a starter scaffold, it easily shaves off 30–40% of the initial setup time. It’s like having a rough draft ready the second you start.\n","permalink":"https://abygeorgea.com/blog/2023/03/28/what-chatgpt-means-for-our-test-automation-strategy/","summary":"\u003cp\u003eIf your group chats look anything like mine lately, they’re probably full of screenshots of ChatGPT spitting out code. Ever since the public launch blew up, I’ve been down a rabbit hole trying to figure out what’s actually useful versus what’s just hype.\u003c/p\u003e\n\u003cp\u003eI wanted to cut through the noise and see how this stuff actually holds up in real life. Is AI going to write whole test suites for us overnight? Definitely not. But can it make the tedious, repetitive stuff a lot less painful right now? 100%.\u003c/p\u003e","title":"What Chatgpt Means for Our Test Automation Strategy"},{"content":"ReadyAPI has inbuilt support for various test management tools. File \u0026raquo; Preferences will list down all integration with tools like Jira, Zephyr etc. However, at the time of writing this post, Zephyr Integration is available only with Zephyr Squad and not Zephyr Scale. If your team is using Zephyr Scale, there is no inbuilt integration. I hope this will change in future since both ReadyAPI and Zephyr Scale is owned by same company.\nAs of now, if you need to integrate the test execution result back to Zephyr Scale, then custom scripting has to be done. The main steps involved are below\nDefine ProjectID and TestCycleID at the project level. This can be done as Custom Project Properties. Select the Project folder and enter the details in custom project properties section Specify Jira Test Case ID for each test case. This can be done by custom test case properties. Create a new custom property for test case called ID and specify value as the Jira Key.\nAdd an event to run after every test case run. Click on event and select TestSuiteRunListener.afterTestcase. This will make sure that once we run a test suite, the code written in after test case will run after each test case. Please note that, it will run only if we execute test suite. Running a single test case will not trigger this.\nEnter the below code in after test case event. It does below actions. Retrieve test case , test suite and project object from test runner. Get details like test name, jira test case id, test cycle id and project code ( as defined in step 1 \u0026amp; 2) Identify whether test case is pass or fail Post the result into Zephyr. This will need a token id for Zephyr , which you can create in Zephyr ( provided you have access) Note:\nBelow code updates only one step of test case. Post body content has an element called testScriptResults which is an array. If there are multiple steps, it should have more number of elements in an array . The count should match number of steps. Zephyr Scale API currently doesnt support adding attachment. Hence if you need to have evidence of test execution in Zephyr, it has to be done either by actualResult or comment fields. def tcobject = testCaseRunner.getTestCase() def tsobject = testCaseRunner.getTestCase().testSuite def projobject = testCaseRunner.getTestCase().testSuite.project def stepList = tcobject.getTestStepList(); // Get all test cases’ names from the suite def testCaseName = tcobject.name; def testCaseID = tcobject.getPropertyValue(\u0026#34;ID\u0026#34;); def testCycleID = projobject.getPropertyValue(\u0026#34;TestCycleID\u0026#34;); def projectKey = projobject.getPropertyValue(\u0026#34;projectID\u0026#34;); log.info \u0026#34;projectKey : $projectKey , TestCaseName : $testCaseName , TestCaseIID : $testCaseID, TestCycleID: $testCycleID , \u0026#34;; if (testCaseID == null || testCaseID.length() == 0 || testCycleID == null || testCycleID.length() == 0 || projectKey == null || projectKey.length() == 0) { log.info \u0026#34;MANDATORY FIELDS NOT AVAILABLE. Please check Ready API script to confirm they have all fields defined\u0026#34; return 0; } def comment = \u0026#34;Comments For Jira Execution\u0026#34; // Check whether the case has failed if (testcaseStatus == \u0026#39;FAIL\u0026#39;) { // Log failed cases and test steps’ resulting messages log.info \u0026#34;$testCaseName has failed\u0026#34; for (testStepResult in testCaseRunner.getResults()) { testStepResult.messages.each() { msg -\u0026gt; log.info msg } } postToZephyrScale(projectKey, testCaseID, testCycleID, \u0026#34;Fail\u0026#34;, comment) } else if (testcaseStatus == \u0026#39;PASS\u0026#39;) { postToZephyrScale(projectKey, testCaseID, testCycleID, \u0026#34;Pass\u0026#34;, comment) log.info \u0026#34;$testCaseName Test Passed\u0026#34;; } log.info \u0026#34;Results updation to Jira is complete\u0026#34; def postToZephyrScale(String projectKey, String testcaseKey, String testcycleKey, String status, String comment) { def today = new Date() def formattedDate = today.format(\u0026#34;yyyy-MM-dd\u0026#39;T\u0026#39;HH:mm:ssZ\u0026#34;) def postmanPost = new URL(\u0026#39;https://api.zephyrscale.smartbear.com/v2/testexecutions\u0026#39;) def postConnection = postmanPost.openConnection() postConnection.setRequestProperty(\u0026#34;Content-Type\u0026#34;, \u0026#34;application/json\u0026#34;) postConnection.setRequestProperty(\u0026#34;Authorization\u0026#34;, \u0026#34;ENTER YOUR API TOKEN HERE\u0026#34;) postConnection.requestMethod = \u0026#39;POST\u0026#39; def form = \u0026#34; { \u0026#34; + \u0026#34; \\\u0026#34;projectKey\\\u0026#34;: \\\u0026#34;\u0026#34; + projectKey + \u0026#34;\\\u0026#34;, \u0026#34; + \u0026#34; \\\u0026#34;testCaseKey\\\u0026#34;: \\\u0026#34;\u0026#34; + testcaseKey + \u0026#34;\\\u0026#34;, \u0026#34; + \u0026#34; \\\u0026#34;testCycleKey\\\u0026#34;: \\\u0026#34;\u0026#34; + testcycleKey + \u0026#34;\\\u0026#34;, \u0026#34; + \u0026#34; \\\u0026#34;statusName\\\u0026#34;: \\\u0026#34;\u0026#34; + status + \u0026#34;\\\u0026#34;, \u0026#34; + \u0026#34; \\\u0026#34;testScriptResults\\\u0026#34;: [ \u0026#34; + \u0026#34; { \u0026#34; + \u0026#34; \\\u0026#34;statusName\\\u0026#34;: \\\u0026#34;\u0026#34; + status + \u0026#34;\\\u0026#34;, \u0026#34; + \u0026#34; \\\u0026#34;actualEndDate\\\u0026#34;: \\\u0026#34;$formattedDate\\\u0026#34;, \u0026#34; + \u0026#34; \\\u0026#34;actualResult\\\u0026#34;: \\\u0026#34;\u0026#34; + status + \u0026#34;\\\u0026#34; \u0026#34; + \u0026#34;} \u0026#34; + \u0026#34; ], \u0026#34; + \u0026#34; \\\u0026#34;comment\\\u0026#34;: \\\u0026#34;$comment\\\u0026#34; \u0026#34; + \u0026#34; } \u0026#34; log.info form postConnection.doOutput = true def text postConnection.with { outputStream.withWriter { outputStreamWriter -\u0026gt; outputStreamWriter \u0026lt;\u0026lt; form } text = content.text } if (postConnection.responseCode == 200 || postConnection.responseCode == 201) { } else { log.info \u0026#34;Posting to Jira failed\u0026#34; + postConnection.responseCode } } ","permalink":"https://abygeorgea.com/blog/2022/03/29/readyapi-how-to-integrate-readyapi-with-zephyr-scale/","summary":"\u003cp\u003eReadyAPI has inbuilt support for various test management tools. File \u0026raquo; Preferences will list down all integration with tools like Jira, Zephyr etc. However, at the time of writing this post,  Zephyr Integration is available only with Zephyr Squad and not Zephyr Scale.\nIf your team is using Zephyr Scale, there is no inbuilt integration. I hope this will change in future since both ReadyAPI and Zephyr Scale is owned by same company.\u003c/p\u003e","title":"How to integrate ReadyAPI with Zephyr Scale"},{"content":"In the previous blog post here and here, we saw how to create a functional test case and add assertions to validate the result. ReadyAPI provides many inbuilt assertion methods which will help to easily validate the output response without any coding. However, in real-world usage for test automation, it may not be enough. Consider the scenario where we need to validate the response content has proper values from an expected list. In this example, let us make an assertion to validate, that the status field in the response for making the order is either placed or notplaced.\nStep 1: Open up the assertion tab and click on +. Select the Script assertion. This will bring up a new window to write the code. ReadyAPI supports writing code in groovy or javascript. In this example, I am using groovy scripting. This can be defined at project level properties.\n//Check Valid values for Status is one of the below : placed , notplaced def jsonResponse = messageExchange.getResponse().contentAsString log.info \u0026#34;Recevied JSON String : \u0026#34; + jsonResponse def jsonSlurper = new groovy.json.JsonSlurper(); def actualobject = jsonSlurper.parseText(jsonResponse) log.info \u0026#34;Current Value of Status : \u0026#34; + actualobject.status; assert actualobject.status == \u0026#34;placed\u0026#34; || actualobject.status == \u0026#34;notplaced\u0026#34; Script assertion window provides access to some default objects like log, context and message exchange. They contain information about the request and response made. Details of available methods can be found in Javadoc defined at here or in here\nIn the first line, we are reading the response of the current step by using the getResponse() method of messageExchange object. Once we have a string, which is in JSON format, we can use JsonSlurper to parse it into an object. JSON slurper parses text or reader content into a data structure of lists and maps Once we have an object, we can easily assert whether the value belongs to the expected list\nWe can directly run the scripts from this editor and see output logs\nStep2 : We can expand above assertion to do additional validations. If there is a need to check response from current test step against previous steps, we can make use of context object.\n//Check Valid values for Status is one of the below : placed , notplaced def jsonResponse = messageExchange.getResponse().contentAsString log.info \u0026#34;Recevied JSON String : \u0026#34; + jsonResponse def jsonSlurper = new groovy.json.JsonSlurper(); def currentResponseObject = jsonSlurper.parseText(jsonResponse) log.info \u0026#34;Current Value of Status : \u0026#34; + currentResponseObject.status; assert currentResponseObject.status == \u0026#34;placed\u0026#34; || currentResponseObject.status == \u0026#34;notplaced\u0026#34; def tcobject = context.getTestCase() //print name of test case log.info \u0026#34;Testcase name is : \u0026#34;+ tcobject.getName() //Get list of Test steps in current Test case def stepList = tcobject.getTestStepList(); log.info \u0026#34;Number of test step is : \u0026#34;+ stepList.size() stepList.each { steps -\u0026gt; if(steps.getLabel()== \u0026#34;REST Request- Get details by PetID\u0026#34;){ log.info steps.getLabel() log.info steps.getPropertyValue(\u0026#34;Endpoint\u0026#34;) log.info steps.getPropertyValue(\u0026#34;Response\u0026#34;) def slurper = new groovy.json.JsonSlurper(); def previousResponseObject = slurper.parseText(steps.getPropertyValue(\u0026#34;Response\u0026#34;)) assert currentResponseObject.id == previousResponseObject.id } } In the above code, we are trying to compare the output of the current response with the response from the previous step. This can be achieved through Context objects. From the context object, get details of the test case object. Once we have access to the test case object, then it is easier to expand to the step list We can easily identify previous steps with a specific name and then extract its response. Parse it again to an object and then make an assertion\n","permalink":"https://abygeorgea.com/blog/2022/03/10/readyapi-how-to-use-script-assertions/","summary":"\u003cp\u003eIn the previous blog post \u003ca href=\"/blog/2022/01/19/readyapi-getting-started-with-readyapi/\"\u003ehere\u003c/a\u003e and \u003ca href=\"/blog/2022/02/01/readyapi-how-to-make-full-use-of-readyapi-features/\"\u003ehere\u003c/a\u003e,  we saw how to create a functional test case and add assertions to validate the result.\nReadyAPI provides many inbuilt assertion methods which will help to easily validate the output response without any coding. However, in real-world usage for test automation, it may not be enough. Consider the scenario where we need to validate the response content has proper values from an expected list.\nIn this example, let us make an assertion to validate, that the status field in the response for making the order is either placed or notplaced.\u003c/p\u003e","title":"ReadyAPI_How to use Script Assertions"},{"content":"ReadyAPI can be used for testing both SOAP and REST services. Output format of them is mainly XML / JSON. Hence it is important to know how to parse them into corresponding objects.\nParsing XML Consider a scenario where output for a SOAP service or JDBC call is returning a XML containing list of person information, which we need to convert to objects.\nXML format is like below, which available as a response content of a ReadyAPI Step\n\u0026lt;Results\u0026gt; \u0026lt;ResultSet fetchSize=\u0026#34;10\u0026#34;\u0026gt; \u0026lt;Row rowNumber=\u0026#34;1\u0026#34;\u0026gt; \u0026lt;FIRSTNAME\u0026gt;Tom\u0026lt;/FIRSTNAME\u0026gt; \u0026lt;LASTNAME\u0026gt;CITIZEN\u0026lt;/LASTNAME\u0026gt; \u0026lt;AGE\u0026gt;20\u0026lt;/AGE\u0026gt; \u0026lt;/Row\u0026gt; \u0026lt;Row rowNumber=\u0026#34;2\u0026#34;\u0026gt; \u0026lt;FIRSTNAME\u0026gt;Jerry\u0026lt;/FIRSTNAME\u0026gt; \u0026lt;LASTNAME\u0026gt;CITIZEN\u0026lt;/LASTNAME\u0026gt; \u0026lt;AGE\u0026gt;15\u0026lt;/AGE\u0026gt; \u0026lt;/Row\u0026gt; \u0026lt;/ResultSet\u0026gt; \u0026lt;/Results\u0026gt; Steps to parse them is as below.\nCreate a person object User XMLSlurper to parse the response content to XML document Iterate over the rows and create an object and add to a list //Create an object class Person { String FirstName String LastName Integer Age } //Parse response content string to XML object def Results = new XmlSlurper().parseText(messageExchange.responseContent) log.info \u0026#34;Number of records :\u0026#34; + Results.ResultSet.Row.size(); // Iterate over each row and convert to obhect def DBRecordList = new ArrayList\u0026lt;Person\u0026gt;(); for(record in Results.ResultSet.Row ){ def obj = new Person(); obj.FirstName = \u0026#34;${record.FIRSTNAME}\u0026#34; ; obj.LastName = \u0026#34;${record.LastName}\u0026#34; ; obj.Age = ${record.Age}; DBRecordList.add(obj); } ","permalink":"https://abygeorgea.com/blog/2022/02/24/readyapi-how-to-work-with-json-in-groovy-scripts/","summary":"\u003cp\u003eReadyAPI can be used for testing both SOAP and REST services. Output format of them is mainly XML / JSON. Hence it is important to know how to parse them into corresponding objects.\u003c/p\u003e\n\u003ch3 id=\"parsing-xml\"\u003eParsing XML\u003c/h3\u003e\n\u003cp\u003eConsider a scenario where output for a SOAP service or JDBC call is returning a XML containing list of person information, which we need to convert to objects.\u003c/p\u003e\n\u003cp\u003eXML format is like below, which available as a response content of a ReadyAPI Step\u003c/p\u003e","title":"How to work with XML in groovy Scripts"},{"content":"In my previous post here, I mentioned how to do a basic functional test. Sometimes, we might have to do complex flows where we need to call multiple APIs and also iterate the tests with various sets of data.\nLet us look at one scenario, which involves the below steps.\nGet details of pet based on PetID\nIf the pet details are retrieved, place an order\nIterate the above scenario for different Pet ID\nLet us look at how we can implement this\nStep 1: Get Details of Pet based on Pet ID. A glance of API functions in ready API or at the swagger link shows that there is an API endpoint to retrieve details of Pets based on pet ID. So let us get started\nCreate a new test case in ready API and add the API request to it. This can be done by first navigating to APIs \u0026raquo;SwaggerPetStore\u0026raquo;/pet/{petID}\u0026raquo;getPetByID and right-click on the request and add to test case.\nNow add the second API request to make an order. The API can be found at APIs \u0026raquo;SwaggerPetStore\u0026raquo;/store/order\u0026raquo;placeorder, right-click on the request and add to the test case.\nStep 2: Look at the test case now and rename the tests to reflect what each request is doing. This can be done by right-clicking on the request and selecting rename option\nStep 3: Test out already added requests. For the first request, enter a pet ID and test. Repeat the same for the Place order. Place order is a POST call and it needs a few inputs like PetID, quantity etc. Manually fill in this step for now and test it. We will look at how to transfer the details from one API to another later.\nStep 4: To make an order for a pet, we need to pass details from one step to another. This can be achieved by using the property Transfer step. Right-click on test case name and select Add Steps and select Property Transfer\nBy default, it adds a new test step at the end. Drag it to between the previous steps. Select the property transfer step and click on the + button and give a name for the value we need to pass between API requests. Let us start with PetId. ReadyAPI will automatically select the source and target steps. In this example, we need to transfer the pet id from the response of get details By PetID to the request to Place order Since the request and response is JSON, we can select JSONPath as the path language. We can then either manual key in JsonPath or use the icon to select the values\nIf we run the test now, it will both test and do the property transfer\nStep 5: To iterate the test across various data sources, DataSource \u0026amp; DataSourceLoop test steps should be used. Right-click on the test case name and select Add Steps and select DataSource and DataSourceLoop. Rearrange the test steps in such a way that Datasource is the first step and Datasource loop is the last step\nSelect Datasource and select source type as Grid. It also supports multiple other options like JSON, Excel, XML, JDBC connection, and even a data generator. For simplicity let us use Grid\nSelect Datasource loop . It will ask to select the data source and target step. Make sure to unselect the check box so that details of passed test cases are also saved\nStep 6: Run the test case by selecting the test case and clicking on the green Run button. This will trigger the test using all specified data sources. Details of the test can be found in Transaction log\nStep 7: If we need to make an order only if pets are available, it can be done by using the Conditional Go to Test step. Add a conditional Go To Test step and select details like below. Make sure to manually add the conditions as highlighted below\nStep 8: From the results, we can see that the property transfer is not executed.\n","permalink":"https://abygeorgea.com/blog/2022/02/01/readyapi-how-to-make-full-use-of-readyapi-features/","summary":"\u003cp\u003eIn my previous post \u003ca href=\"/blog/2022/01/19/readyapi-getting-started-with-readyapi/\"\u003ehere\u003c/a\u003e, I mentioned how to do a basic functional test. Sometimes, we might have to do complex flows where we need to call multiple APIs and also iterate the tests with various sets of data.\u003c/p\u003e\n\u003cp\u003eLet us look at one scenario, which involves the below steps.\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003eGet details of pet based on PetID\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eIf the pet details are retrieved, place an order\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eIterate the above scenario for different Pet ID\u003c/p\u003e","title":"How to make full use of readyAPI features"},{"content":"Ready API is the rebranded SOAPUI Pro, which can be used for testing both SOAP services and REST services. This is a licensed tool , which offer 14 days free trial for all features. ReadyAPI provides lot for upgrades compared to free open source SoapUI . Major advantages to support functional testing of REST APIs are dynamic data sources, assertion groups, scripting , advanced property transfer etc. Full list of differences between ReadyAPI and Soap UI can be found here .\nBelow blog post , shows step by step process to do basic REST API test.\nStart a new project : This can be done via FILE \u0026raquo; New Empty Project . Once the project is created, save it somewhere.\nImport API Definition: For this tutorial, I am using the petstore API . The json file with specs is available at https://petstore.swagger.io/v2/swagger.json . In order to import, right click on API folder in ReadyAPI project and select Import API Definition. It provides different tabs on the popup. Select URL tab and paste the json path. Click Import API.\nAs you can see , it list out all available methods . We can expand the drop down and reach the request . Fill in required details and execute this. For eg: if we need to need to execute findPetsByStatus, we can navigate to that , fill in a valid Status and click Send.\nNote: If we need to add authorisations, it can be done in \u0026ldquo;Auth\u0026rdquo; tab. Details of Header can be specified in Header tab.\nNow let us see how we can create a test cases for this. Right click on the request and select Add to test case. This will show a new popup where we can select existing test case or create a new one. It also provides 2 basic assertions to be added. One assertion is to check response status code is 200 and other one is to check response time is within 200 millisecond. After adding test case, it will look like below. Test cases are added under Functional Tests folder.\nRun the test by clicking Send Button . We can also run the test by selecting the Testcase folder or test suite folder and clicking on Run . As a rule of thumb, it runs all request coming under that folder. For eg, running a test suite runs all test cases under it . Running a test case run, all steps under it.\nAbove shows that we received a response and one assertion failed. The response time was 1733 millisecond instead of expected 200.\nAny test automation is only as good as the assertions added to it. Let us look in detail about the steps to add assertions. Click on the + button on assertions tab. That will provide a list of possible assertion Smart Assertion : This is a new feature which allows to automatically assert on all received data and meta data. It allows to selectively ignore few data and also to make assertion case insensitive if needed. Use this assertion with caution since it is heavily depended on server returning same json response everytime ( including order, timestamp etc).If there is a possibility for data to be added/ deleted/modified, then it is better to avoid this assertion to reduce flaky test cases.\nJSONPathcount : This allows to do validation on count of data returned.\nContains : This allows to check the response content has a string we specify.\nJSONPath Match assertion : This allows to check specific fields in JSON response. There is an icon which allows to select the field to validate and create JSON Path automatically.\nScript Assertion : This can be used for complex assertions which is not available readily from ReadyAPI. I will create another blog post for the same\n","permalink":"https://abygeorgea.com/blog/2022/01/19/readyapi-getting-started-with-readyapi/","summary":"\u003cp\u003eReady API is the rebranded SOAPUI Pro, which can be used for testing both SOAP services and REST services. This is a licensed tool , which offer 14 days free trial for all features. ReadyAPI provides lot for upgrades compared to free open source SoapUI . Major advantages to support functional testing of REST APIs are dynamic data sources, assertion groups, scripting , advanced property transfer etc. Full list of differences between ReadyAPI and Soap UI can be found \u003ca href=\"https://www.soapui.org/tools/readyapi/soapui-vs-readyapi/\"\u003ehere\u003c/a\u003e .\u003c/p\u003e","title":"Getting Started with ReadyAPI"},{"content":"Reflection is the process of describing the metadata of types, methods and fields in a code. It helps to get information about loaded assemblies and elements within it like classes, methods etc. According to microsoft documentation, Reflection provides objects (of type Type) that describe assemblies, modules, and types. You can use reflection to dynamically create an instance of a type, bind the type to an existing object, or get the type from an existing object and invoke its methods or access its fields and properties. If you are using attributes in your code, reflection enables you to access them.\nWhile creating test automation framework, we will come across scenarios where we need to create instance of an objects on run time, need to examine and instantiate types in an assembly, access attributes etc. One of the common usecase is when we create a generic framework, which will allow users to specify class name in feature files and handle it without doing any further modification. Let us look at how we can achieve those.\n##Examples of Reflection##\n###How to get Type of an object###\n// Using GetType to obtain type information: string i = \u0026#34;Hello World\u0026#34;; Type type = i.GetType(); Console.WriteLine(type); This will print System.String\n###How to get Details loaded assembly###\n// Using Reflection to get information of an Assembly: Assembly info = typeof(string).Assembly; Console.WriteLine(info); This will print System.Private.CoreLib, Version=4.0.0.0, Culture=neutral\n###How to create instance of a Class###\nCreating an instance of inbuilt class can be done like below.\n// create instance of class DateTime DateTime dateTime =dqqw(DateTime)Activator.CreateInstance(typeof(DateTime)); Creating an instance of custom class is a multistep process\n// 1. Load the dll having the class Assembly testAssembly = Assembly.LoadFile(@\u0026#34;PathToDll\\Test.dll\u0026#34;); // get type of class from just loaded assembly Type classType = testAssembly.GetType(\u0026#34;Test.CustomClass\u0026#34;); // create instance of class object classInstance = Activator.CreateInstance(classType); Activator.CreateInstance has detailed explanation of various constructors here\n###How to call a method from created instance###\n// fullNameOfClass is the complete name of the class including all namespaces. It is also assumed that , it is in same assembly, there is a default constructor and method doesn\u0026#39;t need any parameters. var classHandle = Activator.CreateInstance(null, fullNameOFClass, true, 0, null, null, null, null); var classInstance = (className)classHandle.Unwrap(); // Unwrap in above activate an object in another AppDomain, retrieve a proxy to it with the Unwrap method, and use the proxy to access the remote object. More details [here](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.remoting.objecthandle.unwrap?view=net-5.0) //If it is in same Appdomain, we will not need to cast Type t = classInstance.GetType(); //Invoking Method without parameters. If parameters are needed, pass them as an object array MethodInfo method = t.GetMethod(methodName); method.Invoke(p, null); //Getting Field value FieldInfo field = t.GetField(fieldName); return field.GetValue(p); Seeing this in an example will help to make our understanding clear.\nCreate SimpleCalculatorClass as below namespace SimpleConsoleApp.ReflectionExample { public class Calculator { public Calculator() { } public double Add(double FirstValue, double SecondValue) { return FirstValue + SecondValue; } } } Use reflection to create an instance of above calculator class We will start with creating a class Handle by passing full name of teh class. Then we get details of Add method. Then we call Add method by passing parameters as an object array. It will return the value as defined in the class\nstatic void Main(string[] args) { var classHandle = Activator.CreateInstance(null, \u0026#34;SimpleConsoleApp.ReflectionExample.Calculator\u0026#34;); var calculatorObjectCreated = classHandle.Unwrap(); Type t = calculatorObjectCreated.GetType(); MethodInfo method = t.GetMethod(\u0026#34;Add\u0026#34;); var result = method.Invoke(calculatorObjectCreated,new object[] { 2,5}); Console.ReadLine(); } } ","permalink":"https://abygeorgea.com/blog/2021/07/31/creating-classes-on-run-time-using-reflection-in-c-number/","summary":"\u003cp\u003eReflection is the process of describing the metadata of types, methods and fields in a code. It helps to get information about loaded assemblies and elements within it like classes, methods etc. According to microsoft documentation, Reflection provides objects (of type Type) that describe assemblies, modules, and types. You can use reflection to dynamically create an instance of a type, bind the type to an existing object, or get the type from an existing object and invoke its methods or access its fields and properties. If you are using attributes in your code, reflection enables you to access them.\u003c/p\u003e","title":"Dynamically create instance of a type on run time using Reflection in C#"},{"content":"In the previous post here we saw how to read external files in Cypress using cy.readFile. Cypress also provides another way to read files. In this post, I will show how to Fixtures to do data driven testing in Cypress.\n####Syntax####\nAccording to documentation, syntax is as below.\ncy.fixture(filePath) cy.fixture(filePath, encoding) cy.fixture(filePath, options) cy.fixture(filePath, encoding, options) ####Comparison with cy.readFile####\nMain difference between cy.readFile and cy.Fixture is that former one starts looking for the files from project root folder and later looks for files under Fixture folder. cy.Fixture supports a wide range of file types/extensions like json, txt, html,jpeg,gif,png etc. If file name is not specified it looks for all supported filetypes in specific order starting with JSON. We can even assign alias to this , which will help to reuse this later on.\n####Example####\nTo demonstrate this, let\u0026rsquo;s revisit old example of logging into SauceDemo website. This time, instead of using hardcoded values in feature file, I will move credentials into a separate JSON file and keep it inside Fixtures\\TestDataFiles folder . File should look like below. This has two set of login credentials.\n[ { \u0026#34;UserType\u0026#34; :\u0026#34;LockedOutUser\u0026#34;, \u0026#34;UserName\u0026#34; : \u0026#34;locked_out_user\u0026#34;, \u0026#34;Password\u0026#34; : \u0026#34;secret_sauce\u0026#34; }, { \u0026#34;UserType\u0026#34; :\u0026#34;StandardUser\u0026#34;, \u0026#34;UserName\u0026#34; : \u0026#34;standard_user\u0026#34;, \u0026#34;Password\u0026#34; : \u0026#34;secret_sauce\u0026#34; } ] ####Feature File####\nCreate a new scenario to login to ECommerce site by using Fixtures. In below scenario we just specify type of User which we need to use for this scenario. I am specifying a user type here since there are different types of users and we can have different scenarios for them.\n@focus Scenario: Logging into ECommerce Site as Standard User Given I Login to Demo shopping page as \u0026#39;StandardUser\u0026#39; Then I should see products listed Step Definition Step definition file for this will look like below. In Step definition files, below steps are performed.\nReading test data file by using cy.fixture() by passing relative path from Fixture folder and then alias it into a variable loginCredentials. The hierarchy is separated by forward slashes. Get the JSON object ( which is an array of objects) from above step and identify the specific object which we need to use for this step based on the UserType specified in feature file. This is achieved by using filter method. This is mostly like LINQ in C# . More details can be found here . This filter will return an array of object which satisfy filter criteria specified. In this case, there will be only one object in array. Login to the demo website by using first user in the above array. //Navigate to URl and login using credentials from Fixture const url = \u0026#39;https://www.saucedemo.com/index.html\u0026#39; Given(\u0026#39;I Login to Demo shopping page as {string}\u0026#39;, (UserTypeValue) =\u0026gt; { cy.visit(url); //Reading Json file and then alias it cy.fixture(\u0026#39;TestDataFiles/LoginCredentials.json\u0026#39;).as(\u0026#39;loginCredentials\u0026#39;); cy.log(\u0026#39;Value passed in\u0026#39; +UserTypeValue); //Use alias and identify the object which matched to the information passed from feature file cy.get(\u0026#39;@loginCredentials\u0026#39;).then((user) =\u0026gt; { // Find the object corresponding to UserType passed in var data = user.filter(item =\u0026gt; (item.UserType == UserTypeValue)); //printout details var propValue; cy.log(\u0026#39;filtered data :\u0026#39;+data[0]); for(var propName in data[0]) { propValue = data[0][propName] cy.log(propName,propValue); } //Login cy.get(\u0026#39;#user-name\u0026#39;).type(data[0].UserName); cy.get(\u0026#39;#password\u0026#39;).type(data[0].Password,{log:false}); }); cy.get(\u0026#39;#login-button\u0026#39;).click(); }); Test Output Now it is time to run above scenario. Open Cypress by running command npx cypress open\nRun the scenario on cypress UI and result will look like below. We can clearly see that assertions are passed.\n","permalink":"https://abygeorgea.com/blog/2020/12/19/data-driven-testing-in-cypress-using-fixtures/","summary":"\u003cp\u003eIn the previous post \u003ca href=\"/blog/2020/12/19/reading-external-files-in-cypress-using-readfile\"\u003ehere\u003c/a\u003e we saw how to read external files in Cypress using cy.readFile. Cypress also provides another way to read files. In this post, I will show how to Fixtures to do data driven testing in Cypress.\u003c/p\u003e\n\u003cp\u003e####Syntax####\u003c/p\u003e\n\u003cp\u003eAccording to \u003ca href=\"https://docs.cypress.io/api/commands/fixture.html#Syntax\"\u003edocumentation\u003c/a\u003e, syntax is as below.\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ecy.fixture(filePath)\ncy.fixture(filePath, encoding)\ncy.fixture(filePath, options)\ncy.fixture(filePath, encoding, options)\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e####Comparison with cy.readFile####\u003c/p\u003e\n\u003cp\u003eMain difference between cy.readFile and cy.Fixture is that former one starts looking for the files from project root folder and later looks for files under \u003cem\u003eFixture\u003c/em\u003e folder. cy.Fixture supports a wide range of file types/extensions like json, txt, html,jpeg,gif,png \u003ca href=\"https://docs.cypress.io/api/commands/fixture.html#JSON\"\u003eetc\u003c/a\u003e. If file name is not specified it looks for all supported filetypes in specific order starting with JSON. We can even assign alias to this , which will help to reuse this later on.\u003c/p\u003e","title":"Data driven testing in Cypress using Fixtures"},{"content":"In the previous post here and here, we saw how to write BDD test cases using cucumber along with Cypress and to use datatables. As we saw in those examples, we are hard coding few data in feature files. This is not a best practise since we need to modify the feature file for every new set of data. Assuming we need to have different data in different environments, this will make it hard to run the tests across various test environment. Let us look at how we can make read these data from a file outside of feature file.\nCypress provides two options to read external files. They are readFile and Fixtures. In this blog post, let us look into readFile method and how to use it.\nreadFile According to documentation, the command syntax is as below\ncy.readFile(filePath) cy.readFile(filePath, encoding) cy.readFile(filePath, options) cy.readFile(filePath, encoding, options) cy.readFile() command look for the file to be present in default project root folder .Hence filepath should be specified relative to the root folder . For any files other than JSON format, this command yields the content of the file. For JSON files, the content is parsed into Javascript and returned.\nFeatureFile Let us write a new scenario to read both text file and json file. We then assert the content of the files.\nStep Definition Corresponding Step definition will look like below. Here we get information from datatable and assert the text file content as is. For JSON files, cypress yields a JSON object . Hence we convert the expected text to json object and assert on its properties.\nTest Output Now it is time to run above scenario. Open Cypress by running command npx cypress open\nRun the scenario on cypress UI and result will look like below. We can clearly see that assertions are passed.\n","permalink":"https://abygeorgea.com/blog/2020/12/19/reading-external-files-in-cypress-using-readfile/","summary":"\u003cp\u003eIn the previous post \u003ca href=\"/blog/2020/12/09/writing-cypress-tests-in-bdd-format\"\u003ehere\u003c/a\u003e and \u003ca href=\"/blog/2020/12/11/using-datatable-in-cypress-cucumber\"\u003ehere\u003c/a\u003e, we saw how to write BDD test cases using cucumber along with Cypress and to use datatables. As we saw in those examples, we are hard coding few data in feature files. This is not a best practise since we need to modify the feature file for every new set of data. Assuming we need to have different data in different environments, this will make it hard to run the tests across various test environment. Let us look at how we can make read these data from  a file outside of feature file.\u003c/p\u003e","title":"Reading external files  in Cypress using readFile"},{"content":"Normally every project will have some values to be read from config files like user name, connection strings, environment specific details etc.\nDotnet Framework In dotnet framework projects, this can be easily done by using Configuration Manager. This is available when we add System.Configuration assembly reference.\nLet us look at an example of a app config file like below\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;utf-8\u0026#34; ?\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;startup\u0026gt; \u0026lt;supportedRuntime version=\u0026#34;v4.0\u0026#34; sku=\u0026#34;.NETFramework,Version=v4.5\u0026#34; /\u0026gt; \u0026lt;/startup\u0026gt; \u0026lt;appSettings\u0026gt; \u0026lt;add key=\u0026#34;Url\u0026#34; value=\u0026#34;https://www.saucedemo.com/\u0026#34;/\u0026gt; \u0026lt;add key=\u0026#34;UserName\u0026#34; value=\u0026#34;standard_user\u0026#34;/\u0026gt; \u0026lt;add key=\u0026#34;Password\u0026#34; value=\u0026#34;secret_sauce\u0026#34;/\u0026gt; \u0026lt;/appSettings\u0026gt; \u0026lt;/configuration\u0026gt; Storing user name and password in app config is not a best practise. For sake of simplicity , let us keep it here. In dotnet framework , we can read above config values with below\nvar Url = ConfigurationManager.AppSettings[\u0026#34;Url\u0026#34;]; var UserName = ConfigurationManager.AppSettings[\u0026#34;UserName\u0026#34;]; var Password = ConfigurationManager.AppSettings[\u0026#34;Password\u0026#34;]; ##Dotnet Core##\nNow let us look how to read above config values in a dotnet core project .\nLet us look at a appsettings.json file\n{ \u0026#34;SauceDemoDetails\u0026#34;: { \u0026#34;Url\u0026#34;: \u0026#34;https://www.saucedemo.com/\u0026#34;, \u0026#34;UserName\u0026#34;: \u0026#34;standard_user\u0026#34;, \u0026#34;Password\u0026#34;: \u0026#34;secret_sauce\u0026#34; }, \u0026#34;MyKey\u0026#34;: \u0026#34;My appsettings.json Value\u0026#34;, \u0026#34;Logging\u0026#34;: { \u0026#34;LogLevel\u0026#34;: { \u0026#34;Default\u0026#34;: \u0026#34;Information\u0026#34;, \u0026#34;Microsoft\u0026#34;: \u0026#34;Warning\u0026#34;, \u0026#34;Microsoft.Hosting.Lifetime\u0026#34;: \u0026#34;Information\u0026#34; } }, \u0026#34;AllowedHosts\u0026#34;: \u0026#34;*\u0026#34; } There are different approaches to read the config file. Let us look at couple of them.\n##IConfiguration##\nOne of the approach to consume this is using IConfiguration Interface. We can inject this to the class constructor where we need to consume these config values.\n//This is not entire class file. It just show the main areas where we need to make changes. //Using statement using Microsoft.Extensions.Configuration; // Make sure to create a private variable and inject IConfiguration to constructor IConfiguration _configuration; public demo(IConfiguration configuration) { _configuration = configuration; } //Usage is as below. Use GetValue method and specify the type. //Also we need to give entire hierarchy staring with section value to the keyname separated by : var Url = _configuration.GetValue\u0026lt;string\u0026gt;(\u0026#34;SauceDemoDetails:Url\u0026#34;); var UserName = _configuration.GetValue\u0026lt;string\u0026gt;(\u0026#34;SauceDemoDetails:UserName\u0026#34;); var Password = _configuration.GetValue\u0026lt;string\u0026gt;(\u0026#34;SauceDemoDetails:Password\u0026#34;); However injecting IConfiguration is not a good practise since we are not sure what configuration is this class now depend on. Also class should know the entire hierarchy of configuration making it tightly coupled.\n##IOptions##\nAnother way to access these config values is by using Options Pattern . Documentation of that can be found here. The options pattern uses classes to provide strongly typed access to groups of related settings. It also provides way to validate details.\nIn this pattern, we need to create an options class corresponding to the config value\npublic class SauceDemoDetailsOptions { public const string SauceDemoDetails = \u0026#34;SauceDemoDetails\u0026#34;; public string Url { get; set; } public string UserName { get; set; } public string Password { get; set; } } According to documentation, options class should be non abstract class with public parameterless constructor. All public get - set properties of the type are bound and fields are not bound. Hence in above class, Url, UserName, Password are bound to config values and SauceDemoDetails are not bound.\nLet us see how we can bind the configuration file to above created class. In startup.cs file, do below\n// Use configuration builder to load the appsettings.json file public Startup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile(\u0026#34;appsettings.json\u0026#34;, optional: true, reloadOnChange: true) .AddJsonFile($\u0026#34;appsettings.{env.EnvironmentName}.json\u0026#34;, optional: true); if (env.IsDevelopment()) { builder.AddUserSecrets(); } builder.AddEnvironmentVariables(); Configuration = builder.Build(); } // Configure Services to bind //Need to give entire hierarchy separated by : . In below example, I use the constant string from the class to specify so that it doesn\u0026#39;t need to be hard coded here. public void ConfigureServices(IServiceCollection services) { services.Configure\u0026lt;SauceDemoDetailsOptions\u0026gt;(Configuration.GetSection(SauceDemoDetailsOptions. SauceDemoDetails)); } Inorder to use this, we need to inject IOptions to the consuming class.\n//This is not entire class file. It just show the main areas where we need to make changes. //Using statement using Microsoft.Extensions.Options; // Make sure to create a private variable and inject IOptions to constructor. IOptions expose Value property which contains the details of object. private SauceDemoDetailsOptions _ sauceDemoDetailsOptions; public demo(IOptions\u0026lt;SauceDemoDetailsOptions\u0026gt; sauceDemoDetailsOptions) { _sauceDemoDetailsOptions = sauceDemoDetailsOptions.Value; } //Usage is as below. : var Url = _sauceDemoDetailsOptions.Url; var UserName = _sauceDemoDetailsOptions.UserName; var Password = _sauceDemoDetailsOptions.Password; One important point to remember is Ioptions interface doesn\u0026rsquo;t support reading configuration data after app has started. Hence any changes to appsettings.json after the app has started will not be effective till next restart. If we need to recompute the values every-time, then we should use IOptionsSnapshot interface. This is a scoped interface which cannot be injected to Singleton service. The usage of this is same as IOptions interface. We also need to make sure that configuration source also supports reload on change.\n##Validations##\nLet us look into how to implement validations into this. In above examples, if there are any mistakes like typo ,missing fields etc, then those fields will not be bound. However it will not error out there and will continue execution , till it throws an exception where it require these missing fields. We can add validations from DataAnnotations library to identify these earlier.\nDataAnnotations provide various validator attributes like Required, Range, RegularExpression, String Length etc.\nLet us add [Required] to all properties as below.\nusing System.ComponentModel.DataAnnotations; public class SauceDemoDetailsOptions { public const string SauceDemoDetails = \u0026#34;SauceDemoDetails\u0026#34;; [Required] public string Url { get; set; } [Required] public string UserName { get; set; } [Required] public string Password { get; set; } } We will also have to change the startup class to do validations after we bind.\npublic void ConfigureServices(IServiceCollection services) { services.AddOptions\u0026lt;SauceDemoDetailsOptions\u0026gt;() .Bind(Configuration.GetSection(SauceDemoDetailsOptions. SauceDemoDetails) .ValidateDataAnnotations(); } ","permalink":"https://abygeorgea.com/blog/2020/12/17/reading-configuration-values-in-dotnet-core-using-ioptions-pattern/","summary":"\u003cp\u003eNormally every project will have some values to be read from config files like user name, connection strings, environment specific details etc.\u003c/p\u003e\n\u003ch2 id=\"dotnet-framework\"\u003eDotnet Framework\u003c/h2\u003e\n\u003cp\u003eIn dotnet framework projects, this can be easily done by using Configuration Manager. This is available when we add System.Configuration assembly reference.\u003c/p\u003e\n\u003cp\u003eLet us look at an example of a app config file like below\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;utf-8\u0026#34; ?\u0026gt;\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026lt;configuration\u0026gt;\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003e\u0026lt;startup\u0026gt;\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;supportedRuntime\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eversion=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;v4.0\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003esku=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;.NETFramework,Version=v4.5\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e/\u0026gt;\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003e\u0026lt;/startup\u0026gt;\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003e\u0026lt;appSettings\u0026gt;\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;add\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ekey=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Url\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003evalue=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;https://www.saucedemo.com/\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e/\u0026gt;\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;add\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ekey=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;UserName\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003evalue=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;standard_user\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e/\u0026gt;\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;add\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ekey=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Password\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003evalue=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;secret_sauce\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e/\u0026gt;\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003e\u0026lt;/appSettings\u0026gt;\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/configuration\u0026gt;\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eStoring user name and password in app config is not a best practise. For sake of simplicity , let us keep it here. In dotnet framework , we can read above config values with below\u003c/p\u003e","title":"Reading Configuration Values in Dotnet core using Options Pattern"},{"content":"In the previous post here, we saw how to use cucumber along with Cypress. The demo scenario we saw was a basic example. Now let us look into how we can use datartable in cypress cucumber.\n##Feature File##\nFeature: Demo for BDD in Cypress I want to demo using BDD in Cypress @focus Scenario: Logging into ECommerce Site Given I open Demo shopping page When I login as \u0026#39;standard_user\u0026#39; user Then I should see products listed Given I add below products to cart |ProductName |Qty| |Sauce Labs Backpack |1 | |Sauce Labs Fleece Jacket |1 | |Sauce Labs Onesie |1 | The step definition file will be as below\n// Import Given , When, then from cypress-cucumber-preprocessort steps import { Given, When, Then } from \u0026#34;cypress-cucumber-preprocessor/steps\u0026#34;; //Navigate to URl const url = \u0026#39;https://www.saucedemo.com/index.html\u0026#39; Given(\u0026#39;I open Demo shopping page\u0026#39;, () =\u0026gt; { cy.visit(url) }); //Type in user name and password. Username is passed from feature file. For demo purpose password is hard coded // We specify what is the type of variable in step ..See {string} //Password is not logged by providing the option When(\u0026#34;I login as {string} user\u0026#34;, (username) =\u0026gt; { cy.get(\u0026#39;#user-name\u0026#39;).type(username); cy.get(\u0026#39;#password\u0026#39;).type(\u0026#39;secret_sauce\u0026#39;,{log:false}); cy.get(\u0026#39;#login-button\u0026#39;).click(); }); //Get list of children and assert its length Then(\u0026#39;I should see products listed\u0026#39;, () =\u0026gt; { cy.get(\u0026#39;div.inventory_list\u0026#39;).children().should(\u0026#39;have.length\u0026#39;, 6); }); //Use datatable to click on each element //Identify the elements based on the product name and click corresponding add to cart button Given(\u0026#39;I add below products to cart\u0026#39;, (dataTable) =\u0026gt; { cy.log(\u0026#39;raw : \u0026#39; + dataTable.raw()); cy.log(\u0026#39;rows : \u0026#39; + dataTable.rows()); cy.log(\u0026#39;HASHES : \u0026#39; ); var propValue; dataTable.hashes().forEach(elem =\u0026gt;{ for(var propName in elem) { propValue = elem[propName] cy.log(propName,propValue); } }); dataTable.hashes().forEach(elem =\u0026gt; { cy.log(\u0026#34;Adding \u0026#34;+elem.ProductName); cy.get(\u0026#39;.inventory_item_name\u0026#39;).contains(elem.ProductName).parent().parent().next().find(\u0026#39;.btn_primary\u0026#39;).click(); }); }); datatable can be used in few different ways . Documentation of cucumberjs is here. I have used datatable.Hashes which return an array of hashes where column name is the key. Apart from datatable.Hashes, there are other methods like row( return a 2D array without first row) , raw( return table as 2D array) , rowsHash(where first column is key and second column is value) etc.\nRunning this in Cypress will result in below. we can see the values logged by cy.log\n","permalink":"https://abygeorgea.com/blog/2020/12/11/using-datatable-in-cypress-cucumber/","summary":"\u003cp\u003eIn the previous post \u003ca href=\"/blog/2020/12/09/writing-cypress-tests-in-bdd-format\"\u003ehere\u003c/a\u003e, we saw how to use cucumber along with Cypress. The demo scenario we saw was a basic example. Now let us look into how we can use datartable in cypress cucumber.\u003c/p\u003e\n\u003cp\u003e##Feature File##\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-Gherkin\" data-lang=\"Gherkin\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eFeature:\u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003e Demo for BDD in Cypress\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003e  I want to demo using BDD in Cypress\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003e  @focus\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003e  \u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003eScenario:\u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003e Logging into ECommerce Site\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003e    Given \u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003eI open Demo shopping page\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003eWhen \u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003eI login as \u0026#39;standard_user\u0026#39; user\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003eThen \u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003eI should see products listed\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003e    \u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003eGiven \u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003eI add below products to cart\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003e    |\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003eProductName\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003e                |\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003eQty\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003e|\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003e    |\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003eSauce Labs Backpack\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003e        |\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e1\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003e  |\u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003e    |\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003eSauce Labs Fleece Jacket\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003e   |\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e1\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003e  |\u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003e    |\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003eSauce Labs Onesie\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003e          |\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e1\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003e  |\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe step definition file will be as below\u003c/p\u003e","title":"Using Datatable in Cypress Cucumber"},{"content":"As discussed in previous posts here and here, we use Cypress for writing tests to validate GUI and API tests. In many companies we use behaviour driven development practises where the requirements or acceptance criteria are specified in Gherkin format. As a results, test automation scenarios are also written in Gherkin format. Cucumber , Specflow etc are some of such framework used extensively . In this post, let us examine how we can write BDD test cases in Cypress.\nFirst step is to identify , how we can run Gherkin sytanxed specs with Cypress. This is done with help of cypress-cucumber-preprocessor.\n##Installation## Installation of this package is straight forward\nnpm install --save-dev cypress-cucumber-preprocessor ##Configure## Once NPM package is installed, then next step is to configure Cypress to use it. It consist of 3 main steps.\nAdd below code to cypress/package/index.js file. const cucumber = require(\u0026#39;cypress-cucumber-preprocessor\u0026#39;).default module.exports = (on, config) =\u0026gt; { on(\u0026#39;file:preprocessor\u0026#39;, cucumber()) } Modify package.json to add below line \u0026#34;cypress-cucumber-preprocessor\u0026#34;: { \u0026#34;nonGlobalStepDefinitions\u0026#34;: true } nonGlobalStepDefinitions is set as true, which means Cypress Cucumber Preprocessor Style pattern will be used for Step definitions. Default value is false which means old cucumber format of everything global will be used.\nThere are some other configuration values which we can specify . Details are available in above project documentation link.\nAdd support for feature files in cypress.json. { \u0026#34;testFiles\u0026#34;: \u0026#34;**/*.{feature,features}\u0026#34; } Normally browser is relaunched for each feature files which will result in extended test execution time. cypress-cucumber-preprocessor provides a way to combine all files into a single features file. Above snippet shows that we can expected files named as .feature and .features.\nFeature Files Now it is time to add a feature file. Add a demo.feature file inside Integration/Feature folder. Add below Gherkin into that file\nFeature: Demo for BDD in Cypress I want to demo using BDD in Cypress @focus Scenario: Logging into ECommerce Site Given I open Demo shopping page When I login as \u0026#34;standard_user\u0026#34; user Then I should see products listed Step Definitions Recommended way is to create Step definiton files in a folder named as Feature and keep it inside where we have feature files. Let us create a demosteps.js file.\nLocation of feature file : Integration/Feature/demo.feature.\nLocation of StepDefinition : Integration/Feature/demo/demosteps.js\nNote: Recommendation from team is to avoid using older way of having Global step definitions defined in cypress/support/step_definitions. More about the rationale for that is available in github documentation. We have specified to use latest style in package.json earlier.\n// Import Given , When, then from cypress-cucumber-preprocessort steps import { Given, When, Then } from \u0026#34;cypress-cucumber-preprocessor/steps\u0026#34;; //Navigate to URl const url = \u0026#39;https://www.saucedemo.com/index.html\u0026#39; Given(\u0026#39;I open Demo shopping page\u0026#39;, () =\u0026gt; { cy.visit(url) }); //Type in user name and password. Username is passed from feature file. For demo purpose password is hard coded // We specify what is the type of variable in step ..See {string} //Password is not logged by providing the option When(\u0026#34;I login as {string} user\u0026#34;, (username) =\u0026gt; { cy.get(\u0026#39;#user-name\u0026#39;).type(username); cy.get(\u0026#39;#password\u0026#39;).type(\u0026#39;secret_sauce\u0026#39;,{log:false}); cy.get(\u0026#39;#login-button\u0026#39;).click(); }); //Get list of children and assert its length Then(\u0026#39;I should see products listed\u0026#39;, () =\u0026gt; { cy.get(\u0026#39;div.inventory_list\u0026#39;).children().should(\u0026#39;have.length\u0026#39;, 6); }); Now run above demo file. This can be done in Cypress UI.\nFolder structure is as below ","permalink":"https://abygeorgea.com/blog/2020/12/09/writing-cypress-tests-in-bdd-format/","summary":"\u003cp\u003eAs discussed in previous posts \u003ca href=\"/blog/2020/11/25/how-to-validate-xhr-in-cypress\"\u003ehere\u003c/a\u003e and \u003ca href=\"/blog/2020/11/19/how-to-read-browser-cookies-in-cypress\"\u003ehere\u003c/a\u003e, we use Cypress for writing tests to validate GUI and API tests. In many companies we use behaviour driven development practises where the requirements or acceptance criteria are specified in Gherkin format. As a results, test automation scenarios are also written in Gherkin format. Cucumber , Specflow etc are some of such framework used extensively . In this post, let us examine how we can write BDD test cases in Cypress.\u003c/p\u003e","title":"Writing Cypress tests in BDD format"},{"content":"In the previous post we saw about working with Cookies in Cypress. Now let us look at how we can work with XHR request in Cypress.\nXHR stands for XMLHttpRequest. It is an API in the form of an object whose methods transfer data between a web browser and a web server. Cypress provides inbuilt functionality to work with XHR request. It provides us with objects with information about request and response of these calls. It will help to do various assertions on header, url , body , status code etc as needed. It also help us to stub the response if needed.\nIn Cypress 5 , the XHR testing was done mainly using cy.server() and cy.route(). However they are deprecated in Cypress 6.0.0. In version 6, XHR testing can be done using cy.intercept(), which will help to manipulate behaviour of HTTP request made.\nUsage format as defined in cypress documentation is as below\ncy.intercept(url, routeHandler?) cy.intercept(method, url, routeHandler?) cy.intercept(routeMatcher, routeHandler?) As you can see in above, the last parameter is routeHandler , which defines what should happen if cypress is able to intercept a call matching initial parameters. We can specify the criteria as either a URL (either string or regular expression) , method (string) \u0026amp; url (string or regular exp) or various combinations using routeMatcher. RouteMatcher has a list of properties based on which we identify the network calls. Properties are like path, url, auth, headers etc. Cypress documentation has more details on this.\nNow let us look at a practical example of asserting XHR. In below example,\n//below line is added to get intellisense in visual studio IDE /// \u0026lt;reference types=\u0026#34;cypress\u0026#34; /\u0026gt; it(\u0026#39;Working with XHR In Cypress\u0026#39;,() =\u0026gt; { //I am using only one parameter for intercept , which is an object with property pathname. This is the third way above ( routeMatcher). Only possible chaining for intercept is an alias. Hence I assign an alias as \u0026#39;weather\u0026#39; for the intercepted call cy.intercept({ pathname: \u0026#39;/scripts/marketing.json\u0026#39; }).as(\u0026#39;weather\u0026#39;); //visit the webpage cy.visit(\u0026#34;http://www.bom.gov.au/nsw/forecasts/sydney.shtml\u0026#34;); //wait for the interception and once network calls are made, then proceed with remaining cy.wait(\u0026#39;@weather\u0026#39;).then((interception) =\u0026gt; { // \u0026#39;interception\u0026#39; is an object with properties \u0026#39;id\u0026#39;, \u0026#39;request\u0026#39; and \u0026#39;response\u0026#39; cy.log(interception.id); cy.log(interception.state); cy.log(\u0026#39;Status code is \u0026#39; + interception.response.statusCode); cy.log(\u0026#39;response body is \u0026#39;+interception.response.body); expect(interception.response.statusCode).to.eq(200); }) }) Below is the results from running above test. As you can see the response body is an object.\nStubbing an XHR request Now let us see how we can stub the response call. Add another parameter to intercept method , which matches the routeHandler.\n//below line is added to get intellisense in visual studio IDE /// \u0026lt;reference types=\u0026#34;cypress\u0026#34; /\u0026gt; it(\u0026#39;Working with XHR In Cypress\u0026#39;,() =\u0026gt; { //Add second parameter to stub the response cy.intercept({ pathname: \u0026#39;/scripts/marketing.json\u0026#39; },\u0026#39;{ body: \u0026#34;Response body is stubbed\u0026#34; }\u0026#39;).as(\u0026#39;weather\u0026#39;); cy.visit(\u0026#34;http://www.bom.gov.au/nsw/forecasts/sydney.shtml\u0026#34;); cy.wait(\u0026#39;@weather\u0026#39;).then((interception) =\u0026gt; { // \u0026#39;interception\u0026#39; is an object with properties \u0026#39;id\u0026#39;, \u0026#39;request\u0026#39; and \u0026#39;response\u0026#39; cy.log(interception.id); cy.log(interception.state); cy.log(\u0026#39;Status code is \u0026#39; + interception.response.statusCode); cy.log(\u0026#39;response body is \u0026#39;+interception.response.body); expect(interception.response.statusCode).to.eq(200); }) }) Below is the results from running above test. As you can see the respone body is now having stubbed value passed above\n","permalink":"https://abygeorgea.com/blog/2020/11/25/how-to-validate-xhr-in-cypress/","summary":"\u003cp\u003eIn the previous \u003ca href=\"/blog/2020/11/19/how-to-read-browser-cookies-in-cypress/\"\u003epost\u003c/a\u003e we saw about working with Cookies in Cypress. Now let us look at how we can work with XHR request in Cypress.\u003c/p\u003e\n\u003cp\u003eXHR stands for XMLHttpRequest. It is an API in the form of an object whose methods transfer data between a web browser and a web server. Cypress provides inbuilt functionality to work with XHR request. It provides us with objects with information about request and response of these calls. It will help to do various assertions on header, url , body , status code etc as needed. It also help us to stub the response if needed.\u003c/p\u003e","title":"How to validate XHR  in Cypress"},{"content":"Cypress has inbuild support to read browser cookies. There are two commands for this - GetCookie and GetCookies. Refer documentation for more details\n###GetCookie\nThis command get a cookie by its name.\ncy.getCookie(name) cy.getCookie(name, options) Note: name is the name of cookie . Options can be used to change default behaviour like logging , timeout Examples usage is as below\n//Below line is added to get intellisense while writing code in visual studio code /// \u0026lt;reference types=\u0026#34;cypress\u0026#34; /\u0026gt; it(\u0026#39;Read Cookies In Cypress\u0026#39;,() =\u0026gt; { cy.visit(\u0026#34;www.commbank.com.au\u0026#34;); //GetCookie returns an object with properties like name, domain, httpOnly, path, secure, value, expiry ( if provided), sameSite(if provided) //Checking for individual cookie property value cy.getCookie(\u0026#39;s_cc\u0026#39;).should(\u0026#39;have.property\u0026#39;,\u0026#39;value\u0026#39;,\u0026#39;true\u0026#39;); cy.getCookie(\u0026#39;s_cc\u0026#39;).should(\u0026#39;have.property\u0026#39;,\u0026#39;domain\u0026#39;,\u0026#39;.commbank.com.au\u0026#39;); // Checking multiple properties of a cookie. *cy.getCookie* will get an object. *Then* helps to work with object yielded from previous cy.getCookie(\u0026#39;s_cc\u0026#39;).then((cookie) =\u0026gt; { cy.log(cookie); cy.log(cookie.name); expect(cookie.domain).to.equal(\u0026#39;.commbank.com.au\u0026#39;); expect(cookie.name).to.equal(\u0026#39;s_cc\u0026#39;); expect(cookie.httpOnly).to.equal(false); expect(cookie.path).to.equal(\u0026#39;/\u0026#39;); expect(cookie).to.not.have.property(\u0026#39;expiry\u0026#39;); }) }) Results from test run will look like below\n","permalink":"https://abygeorgea.com/blog/2020/11/19/how-to-read-browser-cookies-in-cypress/","summary":"\u003cp\u003eCypress has inbuild support to read browser cookies. There are two commands for this - GetCookie and GetCookies. Refer \u003ca href=\"https://docs.cypress.io/api/commands/getcookie.html#Session-id\"\u003edocumentation\u003c/a\u003e for more details\u003c/p\u003e\n\u003cp\u003e###GetCookie\u003c/p\u003e\n\u003cp\u003eThis command get a cookie by its name.\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ecy.getCookie(name)\n\ncy.getCookie(name, options)\n\nNote: name is the name of cookie . Options can be used to change default behaviour like logging , timeout\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eExamples usage is as below\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-javascript\" data-lang=\"javascript\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e//Below line is added to get intellisense while writing code in visual studio code\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e/// \u0026lt;reference types=\u0026#34;cypress\u0026#34; /\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003eit\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#39;Read Cookies In Cypress\u0026#39;\u003c/span\u003e,() =\u0026gt;  {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003ecy\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003evisit\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;www.commbank.com.au\u0026#34;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#75715e\"\u003e//GetCookie returns an object with properties like name, domain, httpOnly, path, secure, value, expiry ( if provided), sameSite(if provided)\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#75715e\"\u003e//Checking for individual cookie property value\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003ecy\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003egetCookie\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#39;s_cc\u0026#39;\u003c/span\u003e).\u003cspan style=\"color:#a6e22e\"\u003eshould\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#39;have.property\u0026#39;\u003c/span\u003e,\u003cspan style=\"color:#e6db74\"\u003e\u0026#39;value\u0026#39;\u003c/span\u003e,\u003cspan style=\"color:#e6db74\"\u003e\u0026#39;true\u0026#39;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003ecy\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003egetCookie\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#39;s_cc\u0026#39;\u003c/span\u003e).\u003cspan style=\"color:#a6e22e\"\u003eshould\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#39;have.property\u0026#39;\u003c/span\u003e,\u003cspan style=\"color:#e6db74\"\u003e\u0026#39;domain\u0026#39;\u003c/span\u003e,\u003cspan style=\"color:#e6db74\"\u003e\u0026#39;.commbank.com.au\u0026#39;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e   \u003cspan style=\"color:#75715e\"\u003e// Checking multiple properties of a cookie. *cy.getCookie* will get an object. *Then* helps to work with object yielded from previous\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003ecy\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003egetCookie\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#39;s_cc\u0026#39;\u003c/span\u003e).\u003cspan style=\"color:#a6e22e\"\u003ethen\u003c/span\u003e((\u003cspan style=\"color:#a6e22e\"\u003ecookie\u003c/span\u003e) =\u0026gt; {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#a6e22e\"\u003ecy\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003elog\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003ecookie\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#a6e22e\"\u003ecy\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003elog\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003ecookie\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003ename\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#a6e22e\"\u003eexpect\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003ecookie\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003edomain\u003c/span\u003e).\u003cspan style=\"color:#a6e22e\"\u003eto\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eequal\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#39;.commbank.com.au\u0026#39;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#a6e22e\"\u003eexpect\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003ecookie\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003ename\u003c/span\u003e).\u003cspan style=\"color:#a6e22e\"\u003eto\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eequal\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#39;s_cc\u0026#39;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#a6e22e\"\u003eexpect\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003ecookie\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003ehttpOnly\u003c/span\u003e).\u003cspan style=\"color:#a6e22e\"\u003eto\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eequal\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003efalse\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#a6e22e\"\u003eexpect\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003ecookie\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003epath\u003c/span\u003e).\u003cspan style=\"color:#a6e22e\"\u003eto\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eequal\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#39;/\u0026#39;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#a6e22e\"\u003eexpect\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003ecookie\u003c/span\u003e).\u003cspan style=\"color:#a6e22e\"\u003eto\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003enot\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003ehave\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eproperty\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#39;expiry\u0026#39;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    })\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e   \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e})\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eResults from test run will look like below\u003c/p\u003e","title":"How to read browser cookies in Cypress"},{"content":"Modifying environment variables for a logged in user is straight forward. However there are some instances when we need to modify environment variables for a different user. One frequent usage which I have come across is when we need to modify PATH variables for a service account in a CI box. Service account may not have local logon rights to the machine Or we may not know its password. Hence I always end up using below method to update path variables\nIdentify Security Identifier(SID) Open command prompt and use below commands\nReplace USERNAME with actual username\nwmic useraccount where name=\u0026#34;USERNAME\u0026#34; get sid We can also find username based on SID Replace SIDNUMBER with actual value\nwmic useraccount where sid=\u0026#34;SIDNUMBER\u0026#34; get name Once we have the SID number of the next user, move on to next step\nModify Registry Open registry editore (Regedit.exe) in windows. Navigate to HKEY_USERS \u0026raquo; SID of USER\u0026raquo; ENVIRONMENT This should display all defined environment variables. We can modify all variables as we need ","permalink":"https://abygeorgea.com/blog/2020/10/14/how-to-modify-environment-variables-for-a-different-not-logged-in-user-in-windows/","summary":"\u003cp\u003eModifying environment variables for a logged in user is straight forward. However there are some instances when we need to modify environment variables for a different user. One frequent usage which I have come across is when we need to modify PATH variables for a service account in a CI box. Service account may not have local logon rights to the machine Or we may not know its password. Hence I always end up using below method to update path variables\u003c/p\u003e","title":"How to modify environment variables for a different not logged in user in windows"},{"content":"XML Schema Definition tool will help to generate classes that conform to a schema. Steps are as follows.\nOpen VS Command prompt . ( Start Menu \u0026raquo; Visual Studio 2019 \u0026raquo; Developer command prompt for VS2019)\nPass xml schema as an argument to xsd.exe . \\c at the end denotes to generate classes\nxsd.exe C:\\Temp\\sampleschema.xsd /c /o:C:\\Temp There are other options as well. Main ones are below.\nxsd.exe \u0026lt;schema.xsd\u0026gt; /classes|Dataset [/e:] [/l:] [/n:] [/o:] [/s:] /classes | Dataset : denotes whether to generate class or dataset /e: Element from schema to process /l: Language to use for generated ode . Choose from \u0026#39;CS\u0026#39;,\u0026#39;VB\u0026#39;,\u0026#39;JS\u0026#39;,\u0026#39;VJS\u0026#39;,\u0026#39;CPP\u0026#39;. Default is CS /n: Name os namespace /o: Output directory for generated classes There are other options as well. Details can be found on help \u0026ldquo;xsd /?\u0026rdquo;\nLocation of xsd.exe tools is under C:\\Program Files (x86)\\Microsoft SDKs\\Windows\u0026lt;VERSION\u0026gt;\\bin\\NETFX Tools\\xsd.exe. There are chances of having multiple versions of this tool . If you ever get \u0026ldquo;xsd is not recognised as n internal or external command\u0026rdquo; error, make sure the PATH variable is set to this. Else directly go to that location and run.\n","permalink":"https://abygeorgea.com/blog/2020/05/13/generating-c-number-classes-from-xsd/","summary":"\u003cp\u003eXML Schema Definition tool will help to generate classes that conform to a schema.\nSteps are as follows.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003eOpen VS Command prompt . ( Start Menu \u0026raquo; Visual Studio 2019 \u0026raquo; Developer command prompt for VS2019)\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003ePass xml schema as an argument to xsd.exe . \\c at the end denotes to generate classes\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003exsd.exe C:\\Temp\\sampleschema.xsd /c /o:C:\\Temp \n\u003c/code\u003e\u003c/pre\u003e\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eThere are other options as well. Main ones are below.\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003exsd.exe \u0026lt;schema.xsd\u0026gt; /classes|Dataset [/e:] [/l:] [/n:] [/o:] [/s:] \n\n/classes | Dataset : denotes whether to generate class or dataset\n/e: Element from schema to process\n/l: Language to use for generated ode . Choose from \u0026#39;CS\u0026#39;,\u0026#39;VB\u0026#39;,\u0026#39;JS\u0026#39;,\u0026#39;VJS\u0026#39;,\u0026#39;CPP\u0026#39;. Default is CS\n/n: Name os namespace\n/o: Output directory for generated classes\n\u003c/code\u003e\u003c/pre\u003e\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eThere are other options as well. Details can be found on help \u0026ldquo;xsd /?\u0026rdquo;\u003c/p\u003e","title":"Generating C# classes from xsd"},{"content":"Most crucial factor for effectively learning a programming language are below.\nHaving hands own experience Having a structured learning path Having a mentor to guide and review the code. I was recently looking to learn more about javascript and came across Exercism.io. Exercism.io offer a solution for all above factors and is free of cost\nAll language track will have a series of exercises starting with very basic hello world program and then moving to complex concepts. User should download the exercise , which will have a failing test suite. Once we implement the code and ensure test cases are now passing , we can submit the code for mentor review. Mentors review the code and suggest better ways of doing it, if any. Exercism.io will prevent us from jumping ahead and force to complete one exercise before moving to next one. This actually helps to follow a structured learning path .\nSome of the language track doesn\u0026rsquo;t support Mentored mode initially . For those tracks, user can join in practise mode and then move on to mentored mode.\n","permalink":"https://abygeorgea.com/blog/2020/01/09/exercism/","summary":"\u003cp\u003eMost crucial factor for effectively learning a programming language  are below.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eHaving hands own experience\u003c/li\u003e\n\u003cli\u003eHaving a structured learning path\u003c/li\u003e\n\u003cli\u003eHaving a mentor to guide and review the code.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eI was recently looking to learn more about javascript and came across \u003ca href=\"https://exercism.io/\"\u003eExercism.io.\u003c/a\u003e Exercism.io offer a solution for all above factors and is free of cost\u003c/p\u003e\n\u003cp\u003eAll language track will have a series of exercises starting with very basic hello world program and then moving to complex concepts. User should download the exercise , which will have a failing test suite. Once we implement the code and ensure test cases are now passing , we can submit the code for mentor review. Mentors review the code and suggest better ways of doing it, if any. Exercism.io will prevent us from jumping ahead and force to complete one exercise before moving to next one. This actually helps to follow a structured learning path .\u003c/p\u003e","title":"Exercism"},{"content":"Recently I was looking for some code for sending emails via SMTP in C#. Below are few links which I found with some reusable code. Overall it looks fine , but have to include multiple validations for error handling .\nhttps://gist.github.com/robertgreiner/1529127 https://gist.github.com/gzuri/2850914 https://gist.github.com/pranavq212/1cbecac15abb229d40f1ad0765aa4dce https://gist.github.com/TrailCoder502/6254bdfcfe71c4000600 In Nutshell, flow is as below\nDefine a function to send email which accepts an input Email object Validate the email object to ensure all mandatory fields are present and correct Create a new MailMessage object and SMTPClient Object and send email Above gist links have some reusable code to achieve step 3 of above.\n","permalink":"https://abygeorgea.com/blog/2019/04/09/sending-emails-through-smtp-in-c-number/","summary":"\u003cp\u003eRecently I was looking for some code for sending emails via SMTP in C#. Below are few links which I found with some reusable code. Overall it looks fine , but have to include multiple validations for error handling .\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://gist.github.com/robertgreiner/1529127\"\u003ehttps://gist.github.com/robertgreiner/1529127\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://gist.github.com/gzuri/2850914\"\u003ehttps://gist.github.com/gzuri/2850914\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://gist.github.com/pranavq212/1cbecac15abb229d40f1ad0765aa4dce\"\u003ehttps://gist.github.com/pranavq212/1cbecac15abb229d40f1ad0765aa4dce\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://gist.github.com/TrailCoder502/6254bdfcfe71c4000600\"\u003ehttps://gist.github.com/TrailCoder502/6254bdfcfe71c4000600\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eIn Nutshell, flow is as below\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eDefine a function to send email which accepts an input Email object\u003c/li\u003e\n\u003cli\u003eValidate the email object to ensure all mandatory fields are present and correct\u003c/li\u003e\n\u003cli\u003eCreate a new MailMessage object and SMTPClient Object and send email\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eAbove gist links have some reusable code to achieve step 3 of above.\u003c/p\u003e","title":"Sending Emails through SMTP in C#"},{"content":"Recently one of my colleague approached me asking to help on creating a utility tool using selenium web driver. The requirement was simple which includes accepting few arguments from the command line and then open a browser and complete some actions on browser based on inputs provided. Having worked on selenium web driver for a few years, I thought this is relatively simple and can be done quickly.\nIt is implemented as a C# console app which had reference to selenium web driver. It accepts few arguments from command line and based on the values it opens up chrome browser and completes the action. The initial version was already there on which I made some modifications. We gave a demo to the user and thought it is all done.\nAs in any normal software projects, it was far from over. There were additional requirements to support multiple browsers, flexibility to provide arguments in any required order, the requirement to display detailed help text so that end user will know how to use the utility tool. All of them was done and we shared the build output, which included the exe file, all dlls used, drivers for various browsers and configuration files. That\u0026rsquo;s when I had my next requirement to make it as a portable EXE with the single exe file. That is not something which I had done before. Hence I spent quite some time to google and read through various approaches.\n##Fody.Costura##\nCostura is an addin for Fody. It helps to embed all assembly references into the output assembly/exe. Details documentation and source code can be found in github. Usage was pretty easy.\nInstall nuget package Install-Package Costura.Fody. Create a FodyWeavers.xml ( modify if it exists) in the root folder of project . Update contents as below \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;utf-8\u0026#34; ?\u0026gt; \u0026lt;Weavers\u0026gt; \u0026lt;Costura/\u0026gt; \u0026lt;/Weavers\u0026gt; Make sure all required dlls are marked as \u0026ldquo;Copy Local\u0026rdquo;. Build the project This helped to combine all dlls like webdriver, newtonsoft dll etc into the utility exe file. The build output had only an exe file, config and other resources which I marked to copy to output.\nEmbedding Resources## Fody.Costura helped to combine exe files with required dlls and there by reducing number of files which needs to be distributed. However, I still had few text and json files which are resources for this tool. Initially, all required resources were copied to output and were accessed from there.\nThere is an option to embed all required files. It is done by changing BuildAction in properties to Embedded Resources. This will include files in output assembly which can be accessed in code.\nThis webpage has more details about how it can be done.\nBelow code snippet will show how it can be accessed. This shows how to read a resource file called Help.Txt\nvar assembly = Assembly.GetExecutingAssembly(); var resourceName = \u0026#34;NameSpaceName.SubFolderPathWhereResourceIsKept.Help.txt\u0026#34;; using (Stream stream = assembly.GetManifestResourceStream(resourceName)) using (StreamReader sr = new StreamReader(stream)) { var line = sr.ReadToEnd(); Console.WriteLine(line); } After completing above two steps, I was able to combine all dlls and other required files into the Utility Exe. Now I just had to distribute exe file and the drivers for various browsers.\n","permalink":"https://abygeorgea.com/blog/2018/10/03/creating-portable-exe/","summary":"\u003cp\u003eRecently one of my colleague approached me asking to help on creating a utility tool using selenium web driver. The requirement was simple which includes accepting few arguments from the command line and then open a browser and complete some actions on browser based on inputs provided. Having worked on selenium web driver for a few years, I thought this is relatively simple and can be done quickly.\u003c/p\u003e\n\u003cp\u003eIt is implemented as a C# console app which had reference to selenium web driver. It accepts few arguments from command line and based on the values it opens up chrome browser and completes the action. The initial version was already there on which I made some modifications. We gave a demo to the user and thought it is all done.\u003c/p\u003e","title":"Creating utility tool as portable EXE"},{"content":"I recently faced an issue where one utility tool created by me was not running properly on another machine which had different dot net version installed. During troubleshooting, I was looking for ways to identify the installed dotnet version. Most of the links in google suggested to look for release value in registry as specified here.\nBelow powershell script will list down installed dotnet version on a machine. This is based on dotnet version listed on https://docs.microsoft.com/en-us/dotnet/framework/migration-guide/how-to-determine-which-versions-are-installed. We may have to update below snippet as when new versions are released. Currently it supports upto dotnet 4.7.2\n$netRegKey = Get-Childitem \u0026#34;HKLM:\\SOFTWARE\\Microsoft\\NET Framework Setup\\NDP\\v4\\Full\u0026#34; $release = $netRegKey.GetValue(\u0026#34;Release\u0026#34;) Write-host $release $releases =@( @{id=\u0026#34;378389\u0026#34;;value=\u0026#34;.NET Framework 4.5\u0026#34;}, @{id=\u0026#34;378675\u0026#34;;value=\u0026#34;.NET Framework 4.5.1\u0026#34;}, @{id=\u0026#34;379893\u0026#34;;value=\u0026#34;.NET Framework 4.5.2\u0026#34;}, @{id=\u0026#34;393295\u0026#34;;value=\u0026#34;.NET Framework 4.6\u0026#34;}, @{id=\u0026#34;393297\u0026#34;;value=\u0026#34;.NET Framework 4.6\u0026#34;}, @{id=\u0026#34;394254\u0026#34;;value=\u0026#34;.NET Framework 4.6.1\u0026#34;}, @{id=\u0026#34;394271\u0026#34;;value=\u0026#34;.NET Framework 4.6.1\u0026#34;}, @{id=\u0026#34;394802\u0026#34;;value=\u0026#34;.NET Framework 4.6.2\u0026#34;}, @{id=\u0026#34;394806\u0026#34;;value=\u0026#34;.NET Framework 4.6.2\u0026#34;}, @{id=\u0026#34;460798\u0026#34;;value=\u0026#34;.NET Framework 4.7\u0026#34;}, @{id=\u0026#34;460805\u0026#34;;value=\u0026#34;.NET Framework 4.7\u0026#34;}, @{id=\u0026#34;461308\u0026#34;;value=\u0026#34;.NET Framework 4.7.1\u0026#34;}, @{id=\u0026#34;461310\u0026#34;;value=\u0026#34;.NET Framework 4.7.1\u0026#34;}, @{id=\u0026#34;461808\u0026#34;;value=\u0026#34;.NET Framework 4.7.2\u0026#34;}, @{id=\u0026#34;461814\u0026#34;;value=\u0026#34;.NET Framework 4.7.2\u0026#34;} # Update more if new versions are released ) foreach($framework in $releases) { if($framework.id -eq $release){ Write-Output $framework.value } } Note: This assumes user can run Powershell with admin access so that it does not go through constraint language Mode . If running above script result in error like \u0026ldquo;Method Invocation is supported only on core types in this language mode\u0026rdquo;, it means it is on constraint language mode. In that case, we can run just Get-Childitem \u0026quot;HKLM:\\SOFTWARE\\Microsoft\\NET Framework Setup\\NDP\\v4\\Full\u0026quot; and manually look for release key in above microsoft link\n","permalink":"https://abygeorgea.com/blog/2018/10/03/find-dotnet-version-using-powershell/","summary":"\u003cp\u003eI recently faced an issue where one utility tool created by me was not running properly on another machine which had different dot net version installed. During troubleshooting, I was looking for ways to identify the installed dotnet version. Most of the links in google suggested to look for release value in registry as specified \u003ca href=\"https://docs.microsoft.com/en-us/dotnet/framework/migration-guide/how-to-determine-which-versions-are-installed\"\u003ehere\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eBelow powershell script will list down installed dotnet version on a machine. This is based on dotnet version listed on \u003ca href=\"https://docs.microsoft.com/en-us/dotnet/framework/migration-guide/how-to-determine-which-versions-are-installed\"\u003ehttps://docs.microsoft.com/en-us/dotnet/framework/migration-guide/how-to-determine-which-versions-are-installed\u003c/a\u003e. We may have to update below snippet as when new versions are released.  Currently it supports upto dotnet 4.7.2\u003c/p\u003e","title":"Find DotNet Version Using Powershell"},{"content":"I was hosting my blog on github pages for past one year. Last week I decided to move hosting of my blog to AWS S3. There are obvious advantages of hosting a static site on S3. Moreover the cost of hosting is also minimal. There are many blogs in internet which explains the steps for hosting an octopress blog on S3.\nSince this is my first exposure to AWS world, I did had a learning curve to get this done. Below are highlevel steps involved in hosting in S3.\nCreate an AWS login . Free plan was enough for my sites usage and traffic. AWS recommends creating an IAM user for all activities instead of using root login. Hence I created an IAM user and gave permission to work on S3, Cloudfront, codecommit. Grab the AWS access KeyId and Secret Key from MyAccount \u0026gt; Security Credentials \u0026gt; Access Keys (for IAM user). Install and configure s3cmd for uploading to S3 bucket. s3cmd is a free commandline tool to manage upload and retrieval of data from S3 bucket. Below are steps on Mac using homebrew for installing. # Install Homebrew ruby -e \u0026#34;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)\u0026#34; # Install s3cmd brew install s3cmd # Verify installation s3cmd --version # Configure s3cmd s3cmd --configure # Configuration will require below information * Access Key (use the access key from previous steps) * Secret Key Create S3 Bucket through s3cmd. Tutorial suggest creating S3 bucket with same domain name as static site. # Create new S3 Bucket s3cmd mb s3://www.my-new-bucket-name.com Login to AWS management console and verify the bucket exist. Navigate to properties and enable Static Website Hosting . Select index.html as index document. This will give the direct link to blog once it is hosted. Also enable public access to read objects in Permissions tab. Now, we need to modify the rake files to deploy to S3 Bucket. I was previously using Github pages and deploy was deploying to github. Hence made below changes to Rake File to deploy to S3 Bucket . It can be done by adding below details to Rake File. #Modify Rake File with below details - Change Default_deploy and add new variable #deploy_default = \u0026#34;push\u0026#34; ( This is existing value. Hence commenting it) deploy_default = \u0026#34;s3\u0026#34; s3_bucket = \u0026#34;www.my-new-bucket-name.com\u0026#34; # Replace the bucket name in above line with actual name #Add below at end of rake file desc \u0026#34;Deploy website via s3cmd\u0026#34; task :s3 do puts \u0026#34;## Deploying website via s3cmd\u0026#34; ok_failed system(\u0026#34;s3cmd sync --acl-public --reduced-redundancy --skip-existing --cf-invalidate public/* s3://#{s3_bucket}/\u0026#34;) end #skip-existing allows to upload only changed files. This will help to reduce the contents pushed across to S3 and will help to reduce cost # However I still need to test out how the deleted files are refelected # Cf-Invalidate will help to push the latest changes to cloudfront Run rake deploy to deploy the website to AWS S3 bucket. Configure domain DNS to point to the AWS site( the link generated while enabling static website hosting) . We need to configure corresponding DNS entires in domain provider(in my case CrazyDomain). At this point we can even use AWS Cloud Front. I followed the steps mentioned here and here for setting up a cloudfront and corresponding SSL certificates. Process is straight forward and easy to follow. Only place I struggled is while configuring dns entries in crazy doman as part of dns validation step during SSL certificate generation. I could not find the place to enter NAME for CNAME field. It is named as Subdomain in crazydomain. Finally setup CNAME in domain provider to redirect to cloudfront url for our blog While playing around with AWS, I noticed that AWS codecommit is always free for normal user ( eventhough usage limitation apply ) . I found that usage limitation is pretty high and I may not have to worry about that. Hence I decided to use AWS code commit for keeping blog\u0026rsquo;s source repository . (Partly because Github doesn\u0026rsquo;t support private repo on free plan). Process was straight forward as we do with any other source control system like Bitbucket, gitlab or github. Only catch I found was , we need to seperately create Https Credentials/SSH keys for AWS codecommit in IAM. The user name and password is different from normal login. Once everything was setup, I just changed the remote repository details on my local and pushed it through.\n","permalink":"https://abygeorgea.com/blog/2018/09/23/deploying-octopress-to-aws/","summary":"\u003cp\u003eI was hosting my blog on github pages for past one year. Last week I decided to move hosting of my blog to AWS S3. There are obvious advantages of hosting a static site on S3. Moreover the cost of hosting is also minimal. There are many blogs in internet which explains the steps for hosting an octopress blog on S3.\u003c/p\u003e\n\u003cp\u003eSince this is my first exposure to AWS world, I did had a learning curve to get this done. Below are highlevel steps involved in hosting in S3.\u003c/p\u003e","title":"Deploying Octopress to AWS S3 and CloudFront"},{"content":"Recently I had to find a way for running a command line process in server. I had to spend fair bit of time googling for various approaches of doing it. Most of them are by using PSExec. However there is another approach of using WMI (Windows Management Instrumentation) . Below is one of the approach , which I found at msdn blog.\nBelow method can be accessed anywhere by\nProcessWMI p = new ProcessWMI(); p.ExecuteRemoteProcessWMI(remoteMachine, sBatFile, timeout); The solution has multiple parts as follows\nConnect to remote machine using remote machine Name, user name and password Start the remote process. Win32 process and pass the command to be run Find if the remote process is running and if it does, start an event monitor to wait for it to exit Once the process exits, retrieve its exit code Code below is taken from above MSDN link ( Just to make sure it is available for me even if original MSDN link is unavailable in future.\npublic class ProcessWMI { public uint ProcessId; public int ExitCode; public bool EventArrived; public ManualResetEvent mre = new ManualResetEvent(false); public void ProcessStoptEventArrived(object sender, EventArrivedEventArgs e) { if ((uint)e.NewEvent.Properties[\u0026#34;ProcessId\u0026#34;].Value == ProcessId) { Console.WriteLine(\u0026#34;Process: {0}, Stopped with Code: {1}\u0026#34;, (int)(uint)e.NewEvent.Properties[\u0026#34;ProcessId\u0026#34;].Value, (int)(uint)e.NewEvent.Properties[\u0026#34;ExitStatus\u0026#34;].Value); ExitCode = (int)(uint)e.NewEvent.Properties[\u0026#34;ExitStatus\u0026#34;].Value; EventArrived = true; mre.Set(); } } public ProcessWMI() { this.ProcessId = 0; ExitCode = -1; EventArrived = false; } public void ExecuteRemoteProcessWMI(string remoteComputerName, string arguments, int WaitTimePerCommand) { string strUserName = string.Empty; try { ConnectionOptions connOptions = new ConnectionOptions(); //Note: This will connect using below credentials. If not provided, it will be based on logged in user connOptions.Username = ConfigurationManager.AppSettings[\u0026#34;RemoteMachineLogonUser\u0026#34;]; connOptions.Password = ConfigurationManager.AppSettings[\u0026#34;RemoteMachineUserPassword\u0026#34;]; connOptions.Impersonation = ImpersonationLevel.Impersonate; connOptions.EnablePrivileges = true; ManagementScope manScope = new ManagementScope(String.Format(@\u0026#34;\\\\{0}\\ROOT\\CIMV2\u0026#34;, remoteComputerName), connOptions); try { manScope.Connect(); } catch (Exception e) { throw new Exception(\u0026#34;Management Connect to remote machine \u0026#34; + remoteComputerName + \u0026#34; as user \u0026#34; + strUserName + \u0026#34; failed with the following error \u0026#34; + e.Message); } ObjectGetOptions objectGetOptions = new ObjectGetOptions(); ManagementPath managementPath = new ManagementPath(\u0026#34;Win32_Process\u0026#34;); using (ManagementClass processClass = new ManagementClass(manScope, managementPath, objectGetOptions)) { using (ManagementBaseObject inParams = processClass.GetMethodParameters(\u0026#34;Create\u0026#34;)) { inParams[\u0026#34;CommandLine\u0026#34;] = arguments; using (ManagementBaseObject outParams = processClass.InvokeMethod(\u0026#34;Create\u0026#34;, inParams, null)) { if ((uint)outParams[\u0026#34;returnValue\u0026#34;] != 0) { throw new Exception(\u0026#34;Error while starting process \u0026#34; + arguments + \u0026#34; creation returned an exit code of \u0026#34; + outParams[\u0026#34;returnValue\u0026#34;] + \u0026#34;. It was launched as \u0026#34; + strUserName + \u0026#34; on \u0026#34; + remoteComputerName); } this.ProcessId = (uint)outParams[\u0026#34;processId\u0026#34;]; } } } SelectQuery CheckProcess = new SelectQuery(\u0026#34;Select * from Win32_Process Where ProcessId = \u0026#34; + ProcessId); using (ManagementObjectSearcher ProcessSearcher = new ManagementObjectSearcher(manScope, CheckProcess)) { using (ManagementObjectCollection MoC = ProcessSearcher.Get()) { if (MoC.Count == 0) { throw new Exception(\u0026#34;ERROR AS WARNING: Process \u0026#34; + arguments + \u0026#34; terminated before it could be tracked on \u0026#34; + remoteComputerName); } } } WqlEventQuery q = new WqlEventQuery(\u0026#34;Win32_ProcessStopTrace\u0026#34;); using (ManagementEventWatcher w = new ManagementEventWatcher(manScope, q)) { w.EventArrived += new EventArrivedEventHandler(this.ProcessStoptEventArrived); w.Start(); if (!mre.WaitOne(WaitTimePerCommand,false)) { w.Stop(); this.EventArrived = false; } else w.Stop(); } if (!this.EventArrived) { SelectQuery sq = new SelectQuery(\u0026#34;Select * from Win32_Process Where ProcessId = \u0026#34; + ProcessId); using (ManagementObjectSearcher searcher = new ManagementObjectSearcher(manScope, sq)) { foreach (ManagementObject queryObj in searcher.Get()) { queryObj.InvokeMethod(\u0026#34;Terminate\u0026#34;, null); queryObj.Dispose(); throw new Exception(\u0026#34;Process \u0026#34; + arguments + \u0026#34; timed out and was killed on \u0026#34; + remoteComputerName); } } } else { if (this.ExitCode != 0) throw new Exception(\u0026#34;Process \u0026#34; + arguments + \u0026#34;exited with exit code \u0026#34; + this.ExitCode + \u0026#34; on \u0026#34; + remoteComputerName + \u0026#34; run as \u0026#34; + strUserName); else Console.WriteLine(\u0026#34;process exited with Exit code 0\u0026#34;); } } catch (Exception e) { throw new Exception(string.Format(\u0026#34;Execute process failed Machinename {0}, ProcessName {1}, RunAs {2}, Error is {3}, Stack trace {4}\u0026#34;, remoteComputerName, arguments, strUserName, e.Message, e.StackTrace), e); } } } ","permalink":"https://abygeorgea.com/blog/2018/09/08/running-command-line-in-remote-machine-using-wmi/","summary":"\u003cp\u003eRecently I had to find a way for running a command line process in server. I had to spend fair bit of time googling for various approaches of doing it. Most of them are by using PSExec.  However there is another approach of using WMI (Windows Management Instrumentation) . Below is one of the approach , which I found at \u003ca href=\"https://blogs.msdn.microsoft.com/padmanr/2010/05/08/execute-a-process-on-remote-machine-wait-for-it-to-exit-and-retrieve-its-exit-code-using-wmi/\"\u003emsdn blog\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eBelow method can be accessed anywhere by\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eProcessWMI p = new ProcessWMI();\np.ExecuteRemoteProcessWMI(remoteMachine, sBatFile, timeout);\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eThe solution has multiple parts as follows\u003c/p\u003e","title":"Running Command Line in Remote Machine Using WMI"},{"content":"Code snippet for running power shell on a remote machine. Loosely based on blog post here and here\nCode below is based on the sample code given in above two links\nAdd reference to System.Management.Automation\nusing System.Management.Automation; using System.Management.Automation.Runspaces; internal void runPowershellRemotely(string location, string scriptToBeRun) { string userName = ConfigurationManager.AppSettings[\u0026#34;RemoteMachineLogonUser\u0026#34;]; string password = ConfigurationManager.AppSettings[\u0026#34;RemoteMachineUserPassword\u0026#34;]; var securestring = new SecureString(); foreach (Char c in password){ securestring.AppendChar(c); } PSCredential creds = new PSCredential(userName, securestring); // Remove logging if not needed log.Info(String.Format(\u0026#34;\\tPOWERSHEL : Running Powershell {0} at location {1}\u0026#34;, scriptToBeRun, location)); WSManConnectionInfo connectionInfo = new WSManConnectionInfo(); connectionInfo.ComputerName = ConfigurationManager.AppSettings[\u0026#34;RemoteMachine\u0026#34;]; connectionInfo.Credential = creds; Runspace runspace = RunspaceFactory.CreateRunspace(connectionInfo); runspace.Open(); using (PowerShell ps = PowerShell.Create()) { ps.Runspace = runspace; ps.AddScript(@\u0026#34;cd \u0026#34;+ location); ps.AddScript(scriptToBeRun); try { var results = ps.Invoke(); log.Info(\u0026#34;\\tPOWERSHEL : Results from Powershell Script is ---------------------------\u0026#34;); foreach(var x in results) { log.Info(x.ToString()); } log.Info(\u0026#34;\\tPOWERSHEL : End of results--------------------------------- ---------------------------\u0026#34;); } catch (Exception e) { log.Error(\u0026#34;\\tPOWERSHEL : Exception from running Powershell Script is\u0026#34; + e.ToString()); } } runspace.Close(); } ","permalink":"https://abygeorgea.com/blog/2018/09/05/running-powershell-remotely/","summary":"\u003cp\u003eCode snippet for running power shell on a remote machine. Loosely based on blog post \u003ca href=\"https://com2kid.wordpress.com/2011/09/22/remotely-executing-commands-in-powershell-using-c/\"\u003ehere\u003c/a\u003e and \u003ca href=\"https://www.codeproject.com/Articles/773685/Enable-Remote-PowerShell-Execution-in-Csharp\"\u003ehere\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eCode below is based on the sample code given in above two links\u003c/p\u003e\n\u003cp\u003eAdd reference to System.Management.Automation\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eusing\u003c/span\u003e System.Management.Automation; \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eusing\u003c/span\u003e System.Management.Automation.Runspaces;    \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#66d9ef\"\u003einternal\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003evoid\u003c/span\u003e runPowershellRemotely(\u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e location, \u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e scriptToBeRun)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e userName = ConfigurationManager.AppSettings[\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;RemoteMachineLogonUser\u0026#34;\u003c/span\u003e];\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e password = ConfigurationManager.AppSettings[\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;RemoteMachineUserPassword\u0026#34;\u003c/span\u003e]; \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e           \u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e securestring = \u003cspan style=\"color:#66d9ef\"\u003enew\u003c/span\u003e SecureString();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003eforeach\u003c/span\u003e (Char c \u003cspan style=\"color:#66d9ef\"\u003ein\u003c/span\u003e password){\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                securestring.AppendChar(c);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e           \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            PSCredential creds = \u003cspan style=\"color:#66d9ef\"\u003enew\u003c/span\u003e PSCredential(userName, securestring);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#75715e\"\u003e// Remove logging if not needed\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            log.Info(String.Format(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\\tPOWERSHEL : Running Powershell {0} at location {1}\u0026#34;\u003c/span\u003e, scriptToBeRun, location));\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            WSManConnectionInfo connectionInfo = \u003cspan style=\"color:#66d9ef\"\u003enew\u003c/span\u003e WSManConnectionInfo();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e           \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e           connectionInfo.ComputerName = ConfigurationManager.AppSettings[\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;RemoteMachine\u0026#34;\u003c/span\u003e];\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            connectionInfo.Credential = creds;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            Runspace runspace = RunspaceFactory.CreateRunspace(connectionInfo);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            runspace.Open();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003eusing\u003c/span\u003e (PowerShell ps = PowerShell.Create())\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                ps.Runspace = runspace;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                ps.AddScript(\u003cspan style=\"color:#e6db74\"\u003e@\u0026#34;cd \u0026#34;\u003c/span\u003e+ location);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                ps.AddScript(scriptToBeRun);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#66d9ef\"\u003etry\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    \u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e results = ps.Invoke();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    log.Info(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\\tPOWERSHEL : Results from Powershell Script is ---------------------------\u0026#34;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    \u003cspan style=\"color:#66d9ef\"\u003eforeach\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e x \u003cspan style=\"color:#66d9ef\"\u003ein\u003c/span\u003e results)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                        log.Info(x.ToString());\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    log.Info(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\\tPOWERSHEL : End of results--------------------------------- ---------------------------\u0026#34;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#66d9ef\"\u003ecatch\u003c/span\u003e (Exception e)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    log.Error(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\\tPOWERSHEL : Exception from running Powershell Script is\u0026#34;\u003c/span\u003e + e.ToString());\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            runspace.Close();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e","title":"Running Powershell Remotely"},{"content":"Below is code snippet for working with windows service. It helps to find status of service, start , stop and restart as required.\nWe need to pass in details of windows services name ( as shown i services.msc ) and machine name(should be in same network).\nusing System.ServiceProcess; internal string FindStatus(string service, string server) { var myService = new ServiceController(service, server); log.Info(String.Format(\u0026#34;\\tStatus of {0} service in {1} is {2}\u0026#34;, service, server, myService.Status.ToString())); return myService.Status.ToString(); } internal string StopService(string service, string server) { var myService = new ServiceController(service, server); if (myService.Status == ServiceControllerStatus.Running) { myService.Stop(); myService.WaitForStatus(ServiceControllerStatus.Stopped); log.Info(String.Format(\u0026#34;\\t{0} service Stopped in {1}. Current Status is {2}\u0026#34;, service, server, myService.Status.ToString())); } return myService.Status.ToString(); } internal string StartService(string service, string server) { var myService = new ServiceController(service, server); if (myService.Status == ServiceControllerStatus.Stopped) { myService.Start(); myService.WaitForStatus(ServiceControllerStatus.Running); log.Info(String.Format(\u0026#34;\\t{0} service Started in {1}. Current Status is {2}\u0026#34;, service, server, myService.Status.ToString())); } return myService.Status.ToString(); } ","permalink":"https://abygeorgea.com/blog/2018/09/03/working-with-windows-services/","summary":"\u003cp\u003eBelow is code snippet for working with windows service. It helps to find status of service, start , stop and restart as required.\u003c/p\u003e\n\u003cp\u003eWe need to pass in details of windows services name ( as shown i services.msc ) and machine name(should be in same network).\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eusing\u003c/span\u003e System.ServiceProcess;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#66d9ef\"\u003einternal\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e FindStatus(\u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e service, \u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e server)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e myService = \u003cspan style=\"color:#66d9ef\"\u003enew\u003c/span\u003e ServiceController(service, server);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            log.Info(String.Format(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\\tStatus of {0} service in {1} is {2}\u0026#34;\u003c/span\u003e, service, server, myService.Status.ToString()));\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e myService.Status.ToString();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003einternal\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e StopService(\u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e service, \u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e server)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e myService = \u003cspan style=\"color:#66d9ef\"\u003enew\u003c/span\u003e ServiceController(service, server);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (myService.Status == ServiceControllerStatus.Running)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                myService.Stop();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                myService.WaitForStatus(ServiceControllerStatus.Stopped);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                log.Info(String.Format(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\\t{0} service Stopped in {1}. Current Status is {2}\u0026#34;\u003c/span\u003e, service, server, myService.Status.ToString()));\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e myService.Status.ToString();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003einternal\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e StartService(\u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e service, \u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e server)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e myService = \u003cspan style=\"color:#66d9ef\"\u003enew\u003c/span\u003e ServiceController(service, server);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (myService.Status == ServiceControllerStatus.Stopped)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                myService.Start();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                myService.WaitForStatus(ServiceControllerStatus.Running);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                log.Info(String.Format(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\\t{0} service Started in {1}. Current Status is {2}\u0026#34;\u003c/span\u003e, service, server, myService.Status.ToString()));\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e myService.Status.ToString();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e","title":"Working With Windows Services"},{"content":"Code snippet for comparing two xml files without using xsd for validating their structure is same ( nodes and arguments should be same. Values of each node/argument can be different).\ninternal void VerifyMessageHaveSimilarStructureOfTemplate(string inputXml, string templateXml) { var docA = new XmlDocument(); var docB = new XmlDocument(); docA.LoadXml(inputXml); docB.LoadXml(templateXml); var isDifferent = DoTheyHaveDiferentStructure(docA.ChildNodes, docB.ChildNodes); log.Info(\u0026#34;Result of Checking for difference of Input xml with template is : \u0026#34; + isDifferent.ToString()); } private bool DoTheyHaveDiferentStructure(XmlNodeList xmlNodeListA, XmlNodeList xmlNodeListB) { if (xmlNodeListA.Count != xmlNodeListB.Count) return true; for (var i = 0; i \u0026lt; xmlNodeListA.Count; i++) { var nodeA = xmlNodeListA[i]; var nodeB = xmlNodeListB[i]; if (nodeA.Attributes == null) { if (nodeB.Attributes != null) return true; else continue; } if (nodeA.Attributes.Count != nodeB.Attributes.Count || nodeA.Name != nodeB.Name) return true; List\u0026lt;string\u0026gt; AttributeNameA = new List\u0026lt;string\u0026gt;(); List\u0026lt;string\u0026gt; AttributeNameB = new List\u0026lt;string\u0026gt;(); for (var j = 0; j \u0026lt; nodeA.Attributes.Count; j++) { AttributeNameA.Add(nodeA.Attributes[j].Name); AttributeNameB.Add(nodeB.Attributes[j].Name); // -- If attribute position should be same, then include below as well //var attrA = nodeA.Attributes[j]; //var attrB = nodeB.Attributes[j]; //if (attrA.Name != attrB.Name) return true; } AttributeNameA.Sort(); AttributeNameB.Sort(); if(! AttributeNameA.SequenceEqual(AttributeNameB)) return true; if (nodeA.HasChildNodes \u0026amp;\u0026amp; nodeB.HasChildNodes) { return HaveDiferentStructure(nodeA.ChildNodes, nodeB.ChildNodes); } else { return true; } } return false; } ","permalink":"https://abygeorgea.com/blog/2018/09/01/comparing-xml-file-structure-without-xsd/","summary":"\u003cp\u003eCode snippet for comparing two xml files without using xsd for validating their structure is same ( nodes and arguments should be same. Values of each node/argument can be different).\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003einternal\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003evoid\u003c/span\u003e VerifyMessageHaveSimilarStructureOfTemplate(\u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e inputXml, \u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e templateXml)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e docA = \u003cspan style=\"color:#66d9ef\"\u003enew\u003c/span\u003e XmlDocument();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e docB = \u003cspan style=\"color:#66d9ef\"\u003enew\u003c/span\u003e XmlDocument();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            docA.LoadXml(inputXml);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            docB.LoadXml(templateXml);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e isDifferent = DoTheyHaveDiferentStructure(docA.ChildNodes, docB.ChildNodes);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            log.Info(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Result of Checking for difference of Input xml with template is : \u0026#34;\u003c/span\u003e + isDifferent.ToString());\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#66d9ef\"\u003eprivate\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003ebool\u003c/span\u003e DoTheyHaveDiferentStructure(XmlNodeList xmlNodeListA, XmlNodeList xmlNodeListB)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (xmlNodeListA.Count != xmlNodeListB.Count) \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003etrue\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003efor\u003c/span\u003e (\u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e i = \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e; i \u0026lt; xmlNodeListA.Count; i++)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e nodeA = xmlNodeListA[i];\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e nodeB = xmlNodeListB[i];\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (nodeA.Attributes == \u003cspan style=\"color:#66d9ef\"\u003enull\u003c/span\u003e)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (nodeB.Attributes != \u003cspan style=\"color:#66d9ef\"\u003enull\u003c/span\u003e)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003etrue\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    \u003cspan style=\"color:#66d9ef\"\u003eelse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                        \u003cspan style=\"color:#66d9ef\"\u003econtinue\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (nodeA.Attributes.Count != nodeB.Attributes.Count || nodeA.Name != nodeB.Name) \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003etrue\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                List\u0026lt;\u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e\u0026gt; AttributeNameA = \u003cspan style=\"color:#66d9ef\"\u003enew\u003c/span\u003e List\u0026lt;\u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e\u0026gt;();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                List\u0026lt;\u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e\u0026gt; AttributeNameB = \u003cspan style=\"color:#66d9ef\"\u003enew\u003c/span\u003e List\u0026lt;\u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e\u0026gt;();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#66d9ef\"\u003efor\u003c/span\u003e (\u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e j = \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e; j \u0026lt; nodeA.Attributes.Count; j++)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    AttributeNameA.Add(nodeA.Attributes[j].Name);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    AttributeNameB.Add(nodeB.Attributes[j].Name);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    \u003cspan style=\"color:#75715e\"\u003e// -- If attribute position should be same, then include below as well\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    \u003cspan style=\"color:#75715e\"\u003e//var attrA = nodeA.Attributes[j];\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    \u003cspan style=\"color:#75715e\"\u003e//var attrB = nodeB.Attributes[j];\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    \u003cspan style=\"color:#75715e\"\u003e//if (attrA.Name != attrB.Name) return true;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                AttributeNameA.Sort();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                AttributeNameB.Sort();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e(! AttributeNameA.SequenceEqual(AttributeNameB)) \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003etrue\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (nodeA.HasChildNodes \u0026amp;\u0026amp; nodeB.HasChildNodes)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e HaveDiferentStructure(nodeA.ChildNodes, nodeB.ChildNodes);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#66d9ef\"\u003eelse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003etrue\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003efalse\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e","title":"Comparing XML file Structure without XSD"},{"content":"There are many cases where we will have to convert Dataset into list of objects. Below is a generic method using reflection to achieve that.\nBelow will work only if datatable column name and class property name are same and they match exactly.\nusing System.Reflection internal static List\u0026lt;T\u0026gt; ConvertDataTableToList\u0026lt;T\u0026gt;(DataTable dt) { List\u0026lt;T\u0026gt; data = new List\u0026lt;T\u0026gt;(); foreach (DataRow row in dt.Rows) { T item = GetItem\u0026lt;T\u0026gt;(row); data.Add(item); } return data; } internal static T GetItem\u0026lt;T\u0026gt;(DataRow dr) { Type temp = typeof(T); T obj = Activator.CreateInstance\u0026lt;T\u0026gt;(); foreach (DataColumn column in dr.Table.Columns) { foreach (PropertyInfo pro in temp.GetProperties()) { if (pro.Name == column.ColumnName) pro.SetValue(obj, dr[column.ColumnName], null); else continue; } } return obj; } Usage of this will be like below\norderDetailsList = ConvertDataTable\u0026lt; OrderDetails \u0026gt;(orderdetailDatatable); // orderdetailDataset.Table[0] can be used ","permalink":"https://abygeorgea.com/blog/2018/08/10/converting-datatable-to-list-of-objects-in-csharp/","summary":"\u003cp\u003eThere are many cases where we will have to convert Dataset into list of objects. Below is a generic method using reflection to achieve that.\u003c/p\u003e\n\u003cp\u003eBelow will work only if datatable column name and class property name are same and they match exactly.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-Csharp\" data-lang=\"Csharp\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eusing\u003c/span\u003e System.Reflection\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003einternal\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003estatic\u003c/span\u003e List\u0026lt;T\u0026gt; ConvertDataTableToList\u0026lt;T\u0026gt;(DataTable dt)  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e{  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    List\u0026lt;T\u0026gt; data = \u003cspan style=\"color:#66d9ef\"\u003enew\u003c/span\u003e List\u0026lt;T\u0026gt;();  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eforeach\u003c/span\u003e (DataRow row \u003cspan style=\"color:#66d9ef\"\u003ein\u003c/span\u003e dt.Rows)  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    {  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        T item = GetItem\u0026lt;T\u0026gt;(row);  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        data.Add(item);  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e data;  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003einternal\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003estatic\u003c/span\u003e T GetItem\u0026lt;T\u0026gt;(DataRow dr)  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e{  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    Type temp = \u003cspan style=\"color:#66d9ef\"\u003etypeof\u003c/span\u003e(T);  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    T obj = Activator.CreateInstance\u0026lt;T\u0026gt;();  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eforeach\u003c/span\u003e (DataColumn column \u003cspan style=\"color:#66d9ef\"\u003ein\u003c/span\u003e dr.Table.Columns)  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    {  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003eforeach\u003c/span\u003e (PropertyInfo pro \u003cspan style=\"color:#66d9ef\"\u003ein\u003c/span\u003e temp.GetProperties())  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        {  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (pro.Name == column.ColumnName)  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                pro.SetValue(obj, dr[column.ColumnName], \u003cspan style=\"color:#66d9ef\"\u003enull\u003c/span\u003e);  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003eelse\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#66d9ef\"\u003econtinue\u003c/span\u003e;  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        }  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e obj;  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e} \n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eUsage of this will be like below\u003c/p\u003e","title":"Converting Datatable to List of Objects in CSharp"},{"content":"Cypress is not just UI automation tool . It can be used for testing APIs as well . Even though we have other tools like Postman, Newman, Rest Assured, SOAP UI etc for testing APIs, I believe cypress is a good alternative for testing API. It will help to use same tool for both UI and API test automation.\nDemo Let us look at a sample API test case. In below example, we trigger a API call to http://services.groupkt.com/country/get/iso2code/AU and validate below in the response.\nStatus code of response is 200. Header include \u0026lsquo;application/json\u0026rsquo;. Body contain \u0026ldquo;Country found matching code [AU].\u0026rdquo; We can then extend this to do any further checks if needed.\nCreate a new file inside Integration folder of cypress and copy below code into that.\ndescribe(\u0026#39;API Testing with Cypress\u0026#39;, () =\u0026gt; { var result it(\u0026#39;Validate the header\u0026#39;, () =\u0026gt; { result = cy.request(\u0026#39;http://services.groupkt.com/country/get/iso2code/AU\u0026#39;) result.its(\u0026#39;headers\u0026#39;) .its(\u0026#39;content-type\u0026#39;) .should(\u0026#39;include\u0026#39;, \u0026#39;application/json\u0026#39;) }) it(\u0026#39;Validate the status\u0026#39;, () =\u0026gt; { result = cy.request(\u0026#39;http://services.groupkt.com/country/get/iso2code/AU\u0026#39;) result.its(\u0026#39;status\u0026#39;) .should(\u0026#39;equal\u0026#39;,200); }) it(\u0026#39;Validate the body \u0026#39;, () =\u0026gt; { result = cy.request(\u0026#39;http://services.groupkt.com/country/get/iso2code/AU\u0026#39;) result.its(\u0026#39;body\u0026#39;) .its(\u0026#39;RestResponse.messages\u0026#39;) .should(\u0026#39;include\u0026#39;, \u0026#39;Country found matching code [AU].\u0026#39;); }) }) Open Cypress by running node_modules/.bin/cypress open inside cypress root folder. This will open up Cypress.\nRun newly created test.\nResults of test execution will look like below.\nExpand each of them and right click on the asserts and inspect the element. This will open up chrome developer tool. Select the console tab , which will list down details of calls made, request received and assertions performed. It will help to write additional assertions, investigate any failure etc.\n","permalink":"https://abygeorgea.com/blog/2018/05/27/running-api-test-using-cypress/","summary":"\u003cp\u003eCypress is not just UI automation tool . It can be used for testing APIs as well . Even though we have other tools like Postman, Newman, Rest Assured, SOAP UI etc for testing APIs, I believe cypress is a good alternative for testing API. It will help to use same tool for both UI and API test automation.\u003c/p\u003e\n\u003ch2 id=\"demo\"\u003eDemo\u003c/h2\u003e\n\u003cp\u003eLet us look at a sample API test case. In below example, we trigger a API call to \u003ccode\u003ehttp://services.groupkt.com/country/get/iso2code/AU\u003c/code\u003e and validate below in the response.\u003c/p\u003e","title":"Running API Test using Cypress"},{"content":"When we talk about UI automation for browsers, the default tool which comes to mind is Selenium. There are different wrappers around selenium like protractor, Nightwatch , selenium webdriver etc. All of them are build on top of selenium and have all advantages /disadvantages of selenium. All of the control browser by executing remote commands through Network. We will most probably need additional libraries, framework etc to make full use of selenium.\nCypress.io is an open source UI automation tool which can be used for UI testing . Unlike others, this is not build on top of selenium . Instead is a complete new architecture and run in same run loop as browser. So it is running inside browser and have access to almost everything happening inside and outside browser. It is a complete set of tools that you will require to create and run E2E UI automation test cases. Team who developed cypress has made few design trade off which causes some disadvantages to cypress. There is no right tool for automation . It will depend on multiple factors.\nInstalling Cypress We can install cypress using npm. Run below command inside project folder to install cypress and all dependencies.\nnpm install cypress --save-dev Another way of using cypress is to download zip file from here . Just extract the file and start using it.\nOpening Cypress Cypress can be opened by running node_modules/.bin/cypress open command in terminal under {{project_location}}/cypress.\nIf you have downloaded the zip file, you can open cypress by double clicking on the cypress executable.\nWrite your first test Cypress already come with predefined example of KitchenSink application which will help you to identify various commands which can be used. It can be found under cypress\\integration\\example_spec.js.\nLet us look at how to write a new test .\nCreate a new test script file called demotest.js under {project_location}\\cypress\\integration. Open up the file and write below code into it.\nThis code will open browser, load google and search for cypress.io and open up the first link.\ndescribe(\u0026#39;My first test for cypress\u0026#39;, function() { it(\u0026#39;Visits google home page \u0026#39;, function() { cy.visit(\u0026#39;https://google.com\u0026#39;); }) it(\u0026#39;should load the Google Homepage\u0026#39;, () =\u0026gt; { cy.title().should(\u0026#39;eql\u0026#39;, \u0026#39;Google\u0026#39;); }) it(\u0026#39;should search and open cypress home page\u0026#39;, () =\u0026gt; { cy.get(\u0026#39;#lst-ib\u0026#39;).type(\u0026#39;cypress.io\u0026#39;); cy.get(\u0026#39;[value=\u0026#34;I\\\u0026#39;m Feeling Lucky\u0026#34;]\u0026#39;).focus().click(); }) }) Note: If cross origin policy error is shown, flow the workarounds mentioned.\nHow does cypress.io compare with Selenium As mentioned earlier, there is no right or wrong tool for automation. It all depends on suitability for the task on hand. Let us compare few features where cypress.io and selenium have differences.\nCross browser support - At this point selenium have more cross browser support that cypress. Cypress supports only chrome variants. You can read about them here Debugging capability - This is high in cypress. I found that error message are more details and infact provide some more details about how to fix it. Also you have full access to chrome dev tools. Keypress - As of now , cypress doesnt support pressing Tab key . You can read about it here. Since cypress is build on node.js, we can chain commands together Cypress.io have built in support for test framework and assertion libraries like mocha, chai etc Cypress.io test cases can be written in Javascript Cypress.io handles wait times better than selenium Cypress.io have hot reloading of test cases. When we make changes to test cases and save it , the test rerun by itself. This is very effective to reduce time spend on building and rerunning selenium based test cases. Cypress.io have built in time travel and screenshots which will help us to go back to failure points and debug. It also capture before and after state for all actions I will keep adding to this when I play more with cypress.\n","permalink":"https://abygeorgea.com/blog/2018/05/11/ui-automation-with-cypress/","summary":"\u003cp\u003eWhen we talk about UI automation for browsers, the default tool which comes to mind is Selenium. There are different wrappers around selenium like protractor, Nightwatch , selenium webdriver etc. All of them are build on top of selenium and have all advantages /disadvantages of selenium. All of the control browser by executing remote commands through Network. We will most probably need additional libraries, framework etc to make full use of selenium.\u003c/p\u003e","title":"UI Automation with cypress"},{"content":"In previous blog post, we saw how to use BDD format for writing test cases in postman. Most important part of writing tests in postman is understanding various features available. Let us explore various options available . The examples specified in postman documentation, have lot of information about how to setup postman bdd, use chai http assertions, create custom assertions and use before and after hooks. Please import them into postman and try that by yourself to familiarise with postman BDD. Below is only few examples from them.\nPostman BDD makes use of Chai Assertion Library and Chai-Http. We have access to both libray and postman scripting environment for writing test cases. Chai has two types of assertion styles.\nExpect/should for BDD Assert for TDD Both styles support chainable language to construct assertions. We can use both of them to write postman test assertions. If you need details of all chainable constructs, please refer to their documentation. Major ones which we may use in postman tests are\nChains\nto be been is that which and has have with at of same but does Not - Negates all conditions\nany -\nall -\ninlcude\nOK\ntrue\nfalse\nnull\nundefined\nexist\nempty\nmatch(re[, msg])\nChai-Http module provide various assertions. Read through their documentation here to know details. Below are main commands at our disposal for validation .\n.status(code) .header (key[, value]) .headers .ip .json / .text / .html .redirect .param .cookie Postman bdd provide response object on which we do most of assertions. It will have all information like response.text, response.body, response.status, response.ok , response.error. Postman BDD will automatically parse JSON and XML responses and hence there is no need to call JSON.parse() or xml2json(). response.text will have unparsed content. It also have automatic error handling , which will allow to continue with other test even if something fails.\nExamples for various assertions done on response object are below\n\\\\Verifying Header information expect(response).to.have.status(500); expect(response).to.have.header(\u0026#39;x-api-key\u0026#39;); expect(response).to.have.header(\u0026#39;content-type\u0026#39;, \u0026#39;text/plain\u0026#39;); expect(request).to.have.header(\u0026#39;content-type\u0026#39;, /^text/); expect(response).to.have.headers; expect(\u0026#39;127.0.0.1\u0026#39;).to.be.an.ip; \\\\Verifying Response body expect(response).to.be.json; expect(response).to.be.html; expect(response).to.be.text; response.should.have.status(200); response.body.should.not.be.empty; response.ok.should.be.true; // sucess with code 2XX response.error.should.be.true; //failures \\\\Verifying request expect(req).to.have.param(\u0026#39;orderby\u0026#39;, \u0026#39;date\u0026#39;); expect(req).to.not.have.param(\u0026#39;orderby\u0026#39;); expect(req).to.have.cookie(\u0026#39;session_id\u0026#39;, \u0026#39;1234\u0026#39;); expect(req).to.not.have.cookie(\u0026#39;PHPSESSID\u0026#39;); If we use above assertions in proper BDD format, it will look like below\neval(globals.postmanBDD); describe(\u0026#39;Example for Blog using SHOULD\u0026#39;, function(){ it(\u0026#34;Tests using SHOULD\u0026#34;, function() { response.should.have.status(200); response.should.not.be.empty; response.should.have.header(\u0026#39;content-type\u0026#39;, \u0026#39;application/json; charset=utf-8\u0026#39;); response.type.should.equal(\u0026#39;application/json\u0026#39;); var user = response.body.results[0]; user.name.should.be.an(\u0026#39;object\u0026#39;); user.name.should.have.property(\u0026#39;first\u0026#39;).and.not.empty; //user.name.should.have.property(\u0026#39;first\u0026#39;,\u0026#39;david\u0026#39;); user.should.have.property(\u0026#39;gender\u0026#39;,\u0026#39;male\u0026#39;); }) }) describe(\u0026#39;Example for Blog using Expect\u0026#39;, function(){ it(\u0026#34;Tests using EXPECT\u0026#34;, function() { expect(response).to.have.status(200); expect(response).not.empty; expect(response).to.be.json; expect(response).to.have.header(\u0026#39;content-type\u0026#39;, \u0026#39;application/json; charset=utf-8\u0026#39;); }) }) it(\u0026#39;should contain the un-parsed JSON text\u0026#39;, () =\u0026gt; { response.text.should.be.a(\u0026#39;string\u0026#39;).with.length.above(50); response.text.should.contain(\u0026#39;\u0026#34;results\u0026#34;:[\u0026#39;); }); ","permalink":"https://abygeorgea.com/blog/2018/05/03/writing-tests-in-postman/","summary":"\u003cp\u003eIn previous blog \u003ca href=\"/blog/2018/04/28/postman-bdd/\"\u003epost\u003c/a\u003e, we saw how to use BDD format for writing test cases in postman. Most important part of writing tests in postman is understanding various features available. Let us explore various options available . The examples specified in postman \u003ca href=\"https://documenter.getpostman.com/view/220187/postman-bdd-examples/6Z3uY71#30dfc9d2-5de4-b932-db3e-641c29fb0459\"\u003edocumentation\u003c/a\u003e, have lot of information about how to setup postman bdd, use chai http assertions, create custom assertions and use before and after hooks. Please import them into postman and try that by yourself to familiarise with postman BDD. Below is only few examples from them.\u003c/p\u003e","title":"Writing Tests in Postman"},{"content":"In Previous blog post,we discussed about how to use postman and how to use collections using newman and data file. If you haven\u0026rsquo;t read that , please have a read through first .\nIn previous examples, we discussed about writing tests/assertions in postman. We followed normal Javascript syntax for writing test cases including asserting various factors of response ( like content , status code etc). Eventhough this is a straightforward way of writing, many people would like to use existing javascript test library like Mocha. They can use postman - bdd libraries.\nLet us take a deep dive into how to use setup postman bdd.\nNote: It is assumed that user already have postman and newman installed on their machine along with their dependencies.\n##Installing Postman BDD##\nInstallation is done triggering a Get request and setting the response as Global environment variable.\nCreate a GET request to http://bigstickcarpet.com/postman-bdd/dist/postman-bdd.js Set Global environment variable by using below command in test tab. postman.setGlobalVariable('postmanBDD', responseBody); Once we trigger above get request, postman bdd will be available for use. We can make use of postman BDD features by below command eval(globals.postmanBDD);\n##Writing Tests## Postman bdd library provide us with flexibility to write tests and assertions using fluent asserts and have best features of Chai and Mocha. Inorder to demonstrate this, I am using sample Tutorial given with postman client.\nOpen up the sample Request in Postman Tutorial folder under collections. It will already have some test predefined in Test tab. Remove them and add below test to it.\neval(globals.postmanBDD) //eval(postman.getGlobalVariable(\u0026#39;postmanBDD\u0026#39;)); var jsonData = JSON.parse(responseBody); describe(\u0026#39;Testing Sample Request in Postman Tutorial\u0026#39;, function () { it(\u0026#39;CASE 1: Should respond with statusCode = 200\u0026#39;, function () { response.should.have.status(200); }); it(\u0026#39;CASE 2: Should response time less than 500 ms\u0026#39;, function () { pm.response.responseTime.should.be.below(500); }); it(\u0026#39;CASE 3: User ID should be 1\u0026#39;, function () { jsonData.userId === 1; }); }); Note: You can find more details of various type of asserts in http://www.chaijs.com/api/bdd/\nOnce it is done, trigger the request\n.\n","permalink":"https://abygeorgea.com/blog/2018/04/28/postman-bdd/","summary":"\u003cp\u003eIn Previous blog post,we discussed about \u003ca href=\"/blog/2017/08/05/postman-tutorial/\"\u003ehow to use postman\u003c/a\u003e and how to use \u003ca href=\"/blog/2017/08/07/running-postman-collection-using-newman/\"\u003ecollections using newman \u003c/a\u003eand \u003ca href=\"/blog/2017/08/13/postman-using-data-file\"\u003edata file\u003c/a\u003e. If you haven\u0026rsquo;t read that , please have a read through first .\u003c/p\u003e\n\u003cp\u003eIn previous examples, we discussed about writing tests/assertions in postman. We followed normal Javascript syntax for writing test cases including asserting various factors of response ( like content , status code etc). Eventhough this is a straightforward way of writing, many people would like to use existing javascript test library like Mocha. They can use \u003ca href=\"https://github.com/BigstickCarpet/postman-bdd/#installation\"\u003epostman - bdd\u003c/a\u003e libraries.\u003c/p\u003e","title":"Postman BDD"},{"content":"Gulp is a toolkit for automating painful or time-consuming task in your development workflow, so you can stop messing around and build something. Gulp can be used for creating a simple task to run automated test cases.\nFirstly, we will create package.json file for this project. This can be done by below command from project folder. It will prompt you to enter a list of information required for creating package.json file\nnpm init Once this is done, install gulp. It can be done by below command. This will add gulp as a dev dependency.\nnpm install --save-dev gulp-install In order to run acceptance test cases, we will need to install nunit/xunit test runners. It can be done by below command from the root folder.\nnpm install --save-dev gulp-nunit-runner OR npm install --save-dev gulp-xunit-runner Detailed usage of above test runners are available here.\nOnce above are installed, we need to create gulpfile.js inside root folder. This file will have details of various gulp tasks\nSample Usage of test runner is below. Insert this code into gulpfile.js\nvar gulp = require(\u0026#39;gulp\u0026#39;), nunit = require(\u0026#39;gulp-nunit-runner\u0026#39;); gulp.task(\u0026#39;unit-test\u0026#39;, function () { return gulp.src([\u0026#39;**/*.Test.dll\u0026#39;], {read: false}) .pipe(nunit({ executable: \u0026#39;C:/nunit/bin/nunit-console.exe\u0026#39;, options : { where : \u0026#39;cat == test\u0026#39; } })); }); {read: false} means, it will read only file names and not the entire file. Executable is the path to nunit console runner, which should be available. gulp.src is that path to acceptance test solution dll. Since we use wild character, we may have to modify this path to reflect the exact path of dll.( something like ./**/Debug/Project.acceptancetest.dll) Once we have above in gulpfile.js, it can be run by below command\ngulp unit-test Out of above command will be something like\nC:/nunit/bin/nunit-console.exe \u0026quot;C:\\full\\path\\to\\Database.Test.dll\u0026quot; \u0026quot;C:\\full\\path\\to\\Services.Test.dll\u0026quot; Note: If it complains about assembly missing, it means path to acceptance test solution is incorrect . Retry after fixing the path.\nGulp Nunit runner provide lot options to configure test run, like selecting test cases based on category, creating output files etc. Detailed options can be found here.\nBelow is an example with few options\nvar gulp = require(\u0026#39;gulp\u0026#39;), nunit = require(\u0026#39;gulp-nunit-runner\u0026#39;); gulp.task(\u0026#39;unit-test\u0026#39;, function () { return gulp.src([\u0026#39;**/*.Test.dll\u0026#39;], {read: false}) .pipe(nunit({ executable: \u0026#39;C:/nunit/bin/nunit-console.exe\u0026#39;, options : { where : \u0026#39;cat == test\u0026#39;, work : \u0026#39;TestResultsFolder\u0026#39;, result : \u0026#39;TestResults.xml\u0026#39;, config : \u0026#39;Debug\u0026#39; } })); }); Where - Selects the category which needs to be run Work - Create a folder with specified path/name for output files result - create test results in xml config - select the config which needs to be run If we run gulp unit-test now, it will execute only the test cases having category test. It will create a folder named TestResultsFolder and will have an xml report of the test run inside it . The folder will be created in root where we have gulpfile.js.\n","permalink":"https://abygeorgea.com/blog/2018/03/20/gulp-task-for-running-automated-test/","summary":"\u003cp\u003e\u003ca href=\"https://gulpjs.com/\"\u003eGulp\u003c/a\u003e is a toolkit for automating painful or time-consuming task in your development workflow, so you can stop messing around and build something. Gulp can be used for creating a simple task to run automated test cases.\u003c/p\u003e\n\u003cp\u003eFirstly, we will create package.json file for this project. This can be done by below command from project folder. It will prompt you to enter a list of information required for creating package.json file\u003c/p\u003e","title":"Gulp Task For Running Automated Test"},{"content":"TeamCity is a java based build management and continuous Integration server from JetBrains. Very often , we will have to extract various metrics from TeamCity for tracking and trend analysis. TeamCity provides versatile api for extracting various metrics which can then be manipulated or interpreted as we need.\nBelow are basic api calls which can be used for extracting mertics. Please note that TeamCity api is powerful enough to do much more than extraction of data. However, for this blog post, I am focussing on metrics extraction part alone. All of these are GET request to TeamCity api with a valid user credentials ( use any id/password which can access TeamCity)\nGet List of Projects - http://teamcityURL:9999/app/rest/projects Get details of a project - http://teamcityURL:9999/app/rest/projects/(projectlocator) Project locator can be either \u0026ldquo;id:projectID\u0026rdquo; or \u0026ldquo;name:projectName\u0026rdquo; Get List of Build configuration - http://teamcityURL:9999/app/rest/buildTypes Get List of Build configuration for a project- http://teamcityURL:9999/app/rest/projects/(projectLocator)buildTypes Get List of Build - http://teamcityURL:9999/app/rest/builds/?locator=(buildLocator) Get details of a specific Build - http://teamcityURL:9999/app/rest/builds/(buildLocator) Build locator can be \u0026ldquo;id:BuildId\u0026rdquo; or \u0026ldquo;number:buildNumber\u0026rdquo; Or a combination of these like \u0026ldquo;id:BuildId,number:buildNumber,dimension3:dimensionvalue\u0026rdquo;. We can use various different values for these dimension. Details can be found in TeamCity documentation Get List of tests in a build - http://teamcityURL:9999/app/rest/testOccurrences?locator=build:(buildLocator) Get individual test history - http://teamcityURL:9999/app/rest/testOccurrences?locator=test:(testLocator) Recently I created a Nodejs program to extract below metrics by chaining some of the above api calls.\nNumber of builds between any two given dates and their status Details of number of test cases and their status , pass percentage, fail percentage etc for each build Details as above for entire period. Trend of test progress, build failures etc between those dates Create an output JSON with cumulative counts of passed/failed/ignored builds, passed/failed/ignored test cases , percentage of sucessful builds, frequency of pull request and their success rates etc. ","permalink":"https://abygeorgea.com/blog/2018/03/01/extracting-metrics-from-teamcity/","summary":"\u003cp\u003eTeamCity is a java based build management and continuous Integration server from JetBrains. Very often , we will have to extract various metrics from TeamCity for tracking and trend analysis.  TeamCity provides versatile api for extracting various metrics which can then be manipulated or interpreted as we need.\u003c/p\u003e\n\u003cp\u003eBelow are basic api calls which can be used for extracting mertics. Please note that TeamCity api is powerful enough to do much more than extraction of data. However, for this blog post, I am focussing on metrics extraction part alone. All of these are GET request to TeamCity api with a valid user credentials ( use any id/password which can access TeamCity)\u003c/p\u003e","title":"Extracting Metrics from TeamCity"},{"content":"What is Accessibility testing ? It is a kind of testing performed to ensure application under test is usable by people with disabilities. One of the most common accessibility testing for web applications is to ensure it is easily usable by people with vision impairment. They normally use screen readers to read the screen and use key board to navigate.\nWeb Content Accessibility Guideline (WCAG) list down guidelines and rules for creating accessible website. There are various browser extensions and developer tools available for scanning web pages to find out obvious accessibility issues. aXe is one of the widely used extension. Details of aXe can be found here. Once browser extension is installed, you can analyze any web page to find out accessibility issues. They also have a javascript API for aXe core .\nI recently came across axe-selenium-csharp , which is a .NET wrapper around aXe. It is relatively very easy to setup and use. Below are the steps\nInstall Globant.Selenium.Axe nuget package for solution. This will add reference to dll Import namespace using Globant.Selenium.Axe Call \u0026ldquo;Analyze\u0026rdquo; method to run accessibility check on the current page. using Globant.Selenium.Axe public void PerformAccessbilityAudit(IWebDriver _driver) { private AxeResult _results; _results = _driver.Analyze(); foreach (var xyz in _results.Violations) { log.Info(xyz.Impact.ToString()); log.Info(xyz.Description.ToString()); log.Info(xyz.Id.ToString()); } Assert.True(_results.Violations.Length == 0, \u0026#34;There are accessibility violations. Please check log file\u0026#34;); } Automated accessibility testing is NOT a completely foolproof solution. We will still require someone to scan the page using screen reader software later. But this will help to move accessibility testing to left and have more frequent runs and reduce the need for regression.\n","permalink":"https://abygeorgea.com/blog/2018/03/01/automating-accessibility-testing/","summary":"\u003ch3 id=\"what-is-accessibility-testing-\"\u003eWhat is Accessibility testing ?\u003c/h3\u003e\n\u003cp\u003eIt is a kind of testing performed to ensure application under test is usable by people with disabilities. One of the most common accessibility testing for web applications is to ensure it is easily usable by people with vision impairment. They normally use screen readers to read the screen and use key board to navigate.\u003c/p\u003e\n\u003cp\u003eWeb Content Accessibility Guideline (WCAG) list down guidelines and rules for creating accessible website. There are various browser extensions and developer tools available for scanning web pages to find out obvious accessibility issues. aXe is one of the widely used extension. Details of aXe can be found \u003ca href=\"https://www.deque.com/products/axe/\"\u003ehere\u003c/a\u003e.  Once browser extension is installed, you can analyze any web page to find out accessibility issues. They also have a javascript API for aXe core .\u003c/p\u003e","title":"Automate Accessibility Testing using aXe"},{"content":"In previous blog post, I have explained about how to create a json response in mountebank. You can read about that here and here. Recently , I had to test a scenario about what will happen to application if downstream API response is delayed for some time . Let us have a look about how we can use mountebank to simulate this scenario.\nMountebank supports adding latency to response by adding a behaviour. You can read about that here . Let us try to implement the wait behaviour in one of the previous examples . This is a slight modification of the files used as part of examples mentioned here and here. You can clone my github repo and look at \u0026ldquo;ExamplesForWaitBehaviour\u0026rdquo; for the files.\nThe only change which we need is to add a behavior to the response. This is added in \u0026ldquo;CustomerFound.json\u0026rdquo; file. After injecting file, we need to add behavior for waiting 5000 milliseconds.\n\u0026#34;responses\u0026#34;: [ { \u0026#34;inject\u0026#34;: \u0026#34;\u0026lt;%-stringify(filename, \u0026#39;ResponseInjection\\\\GetCustomerFound.js\u0026#39;) %\u0026gt;\u0026#34;, \u0026#34;_behaviors\u0026#34;: { \u0026#34;wait\u0026#34;: 5000 } } ], \u0026#34;predicates\u0026#34;: [ { \u0026#34;matches\u0026#34;: { \u0026#34;method\u0026#34; : \u0026#34;GET\u0026#34;, \u0026#34;path\u0026#34; : \u0026#34;/Blog.Api/[0-9]+/CustomerView\u0026#34; } } ] Now run Mountebank. If you are using the GitHub repo, you can do this by running RunMounteBankStubsWithExampleForWait.bat file. Else run below command inside the directory where mountebank is available. If needed, modify the path to Imposter.ejs as required.\nmb --configfile ExamplesForWaitBehaviour/Imposter.ejs --allowInjection When we trigger a request via postman, we will get a response after specified delay + time for getting a response. Have a look at response time in below screenshot. Response time is more than 5000 ms.\n","permalink":"https://abygeorgea.com/blog/2017/11/16/mountebank-adding-delay-to-response/","summary":"\u003cp\u003eIn previous blog post, I have explained about how to create a json response in mountebank. You can read about that \u003ca href=\"/blog/2017/04/07/mountebank-creating-a-response-based-on-a-file-template-and-modifying-it-based-on-request-part-1/\"\u003ehere\u003c/a\u003e and \u003ca href=\"/blog/2017/04/07/mountebank-creating-a-response-based-on-a-file-template-and-modifying-it-based-on-request-part-2/\"\u003ehere\u003c/a\u003e. Recently , I had to test a scenario about what will happen to application if downstream API response is delayed for some time . Let us have a look about how we can use mountebank to simulate this scenario.\u003c/p\u003e\n\u003cp\u003eMountebank supports adding latency to response by adding a behaviour. You can read about that \u003ca href=\"http://www.mbtest.org/docs/api/behaviors\"\u003ehere\u003c/a\u003e . Let us try to implement the wait behaviour in one of the previous examples . This is a slight modification of the files used as part of examples mentioned \u003ca href=\"/blog/2017/04/07/mountebank-creating-a-response-based-on-a-file-template-and-modifying-it-based-on-request-part-1/\"\u003ehere\u003c/a\u003e and \u003ca href=\"/blog/2017/04/07/mountebank-creating-a-response-based-on-a-file-template-and-modifying-it-based-on-request-part-2/\"\u003ehere\u003c/a\u003e.  You can clone my github repo and look at \u0026ldquo;ExamplesForWaitBehaviour\u0026rdquo; for the files.\u003c/p\u003e","title":"Mountebank - Adding delay to response"},{"content":"I was not active in this blog over past one month due to multiple reasons - both personal and professional. Following are some highlights over past one month.\nISTQB Test Automation Engineer As mentioned in previous post, I had a chance to attend ANZTB SIGiST conference in August 2017. There were some good talks about various certifications offered by ISTQB/ANZTB. ISTQB Test Automation Engineer was one among them. I decided to give that a try . Spend some time over past one month to prepare based on the syllabus and I gave it a shot. Fortunately, I passed the exam with good marks\nTraffic to blog I had an interesting observation when I looked into google analytics for the blog. The traffic to this blog has grown over ten times compared to previous months. I analyzed the data and found that my blog and github repo for mountebank examples are mentioned in mountebank website. This resulted in having more traffic to the blog. I hope others are also benefitted from my experience with mountebank and the tutorial which I have in this blog. Hopefully, this will give me the motivation to write more.\n","permalink":"https://abygeorgea.com/blog/2017/10/24/blog-traffic/","summary":"\u003cp\u003eI was not active in this blog over past one month due to multiple reasons - both personal and professional. Following are some highlights over past one month.\u003c/p\u003e\n\u003ch3 id=\"istqb-test-automation-engineer\"\u003eISTQB Test Automation Engineer\u003c/h3\u003e\n\u003cp\u003eAs mentioned in previous \u003ca href=\"/blog/2017/08/29/anztb-sigist-conference/\"\u003epost\u003c/a\u003e, I had a chance to attend ANZTB SIGiST conference in August 2017. There were some good talks about various certifications offered by ISTQB/ANZTB.  ISTQB \u003ca href=\"http://www.anztb.org/advancedlevel.php\"\u003eTest Automation Engineer\u003c/a\u003e was one among them. I decided to give that a try . Spend some time over past one month to prepare based on the syllabus and I gave it a shot. Fortunately, I passed the exam with good marks\u003c/p\u003e","title":"Highlights of past one month"},{"content":"An agile retrospective is a meeting held at end of a sprint to analyse their ways of working over past sprint and identify how to become more effective and then adjust accordingly. This is a ritual which belongs to team and criticism is given for facts/output and not for people. Retrospective creates an environment where the team feels safe and comfortable, which will allow them to talk freely about their thoughts and let go their frustration.\nAs an agile team, our team was pretty matured. Everyone knows what to do and what not to do. They come with solutions for most of the impediments faced during the sprint . Even then, there will be few issues which are still not resolved. Often retrospective meetings end up being a place for the team to let out their frustration rather than focusing on identifying what worked well and what could have been done better. Also action items coming out of discussion may be already tried out during sprint and was not working as expected.\nLast week, I had a chance to run retrospective for the team. I wanted to focus more on proactive actions taken by the team while dealing with impediments. I decided to run a different retro in which I tried to keep emotions out and focus more on facts.\nGoals of this retrospective Hope below exercise will help to achieve :\nTake emotions out of discussion and focus on facts Review the actions taken by team during sprint and asses its effectiveness Identify improvements which were not tried out during sprint How to do it### Sprint Goals First step is to identify the sprint goals and write it down on board for everyone to see. This reminds the team about their every day work to achieve the goals. Negatives Next step is to identify the risk, issues and blockers which prevented the team from achieving it. It can be anything which team found as an impediment. Each team member has to write down a unique impediment on a card. Hence for a team of 10 members, you will have 10 unique impediments. Team will then rate the impediments on a scale of 1 - 10. Where 1 being least and 10 is a complete blocker. Once that is done, cards are exchanged with team members. Positives Next person will then have to think about what all good ideas happened for impediment on their card ( written by someone else). It can be anything which team has tried to overcome the blockers, any innovative ideas tried out, usage of extra time for learning and development etc. Team members are free to discuss this with others to find out all positives of that issue. Once it is written, team member will rate it on a scale of 1 - 10. On practical world, the rating for positives will be less than the rating for negatives. Else, it will not be an impediment to start with. Actions Now facilitator has to collect back all cards and look for three cards having a maximum difference between negative and positive ratings. By end of this, team will have three pressing impediments which they could not overcome in the sprint. It takes into account of all pro-active actions taken by the team while dealing with that specific impediments. It is also based on facts and collective feedback . Now it is time to discuss and come up with action items. Obviously new action items coming out of discussion should be new and was not tried earlier. ","permalink":"https://abygeorgea.com/blog/2017/09/22/taking-emotions-out-of-sprint-retrospective/","summary":"\u003cp\u003eAn agile retrospective is a meeting held at end of a sprint to analyse their ways of working over past sprint and identify how to become more effective and then adjust accordingly. This is a ritual which belongs to team and criticism is given for facts/output and not for people. Retrospective creates an environment where the team feels safe and comfortable, which will allow them to talk freely about their thoughts and let go their frustration.\u003c/p\u003e","title":"Taking emotions out of sprint retrospective"},{"content":"Today I had a discussion with a project manager about stake holder expectations about value delivered from regression test automation and how to manage stake holder expectation. The discussion soon spanned on to challenges in automating manual test cases and candidates for automation testing.\nExpectation Vs Reality Management Stakeholders always visualize test automation as a silver bullet for fixing all pain points.They envision automation tests to be quicker, cheaper and effective in identifying all defects. Automation test cases are expected to be run on a button click and with 100% pass rate (except for valid bugs). Needless to say, that expectation is about having complete test coverage for automation scripts. Thinking is always geared towards reducing manual testers based on automation progress rather than having focus on improved quality of final product, faster time to market etc.\nThe ground reality is different from above expectation. Automated test cases are only as good as how you script it to be. Automated checks will alert tester about problems that checks have been programmed to detect.It ignores all other problems outside of it. Cost, speed, and ROI will depend on the tool used and complexity of tests implemented. Having an automated test is not a replacement for doing exploratory test manually. We need to cater for manual exploratory testing since automated scripts can only do verification of already known check points( for which the coding is done) and miss out check points which are not automated. In other words, test automation frees up tester\u0026rsquo;s time to focus more on exploratory testing which adds value.\nChallenge in this specific case is to automate E2E manual regression test cases which are not existing. The testers are supposed to identify the regression test cases first by going to through existing application and then automate them. The expectation is that testers will identify all possible error scenarios and incorporate corresponding checks in automated scripts. This is going to be time consuming and expensive. It depends on domain knowledge of the person who creates automation test cases. There are chances that all existing bugs will be considered as an expected behaviour. More over the end to end test cases done at UI level is generally time consuming to develop, slow to execute and heavily depended on UI which makes it brittle.\nTesting Pyramid### Solution to improve quality of a product is to follow the testing pyramid and try to automate more at lower levels instead of focussing at E2E level. This also has to be done while the product/software is developed.\nBelow is a modified version of testing pyramid.\nAs you can see above, more emphasis is given to have automated test at Unit test level, followed by component level, integration test level, and finally E2E level through UI. It is relatively cheaper to implement automated test at the base of the pyramid and will get more expensive as we go up. Similarly, unit tests are faster to run, it can isolate issues immediately and are more stable. These characteristics will change adversely as we go up in test pyramid.\nAs obvious, it is not feasible to achieve this for an already existing system without having a significant investment in people, time and tools. This will impact ROI. Depending on situations, there is no right or wrong way to do test automation. Having something is always better than nothing. Hence when there is a need to automate regression test cases, it normally starts from the top. Significant investment is needed upfront to identify all critical regression test cases and corresponding validation that should be performed by automated tests. It is not feasible to automate all test or to have 100% coverage. Success rates of automation script run will depend on various factors like test data, environment stability etc. Everyone should understand that we are automating check point verifications and hence it does trigger alerts only for the checks which it is programmed to do. E2E regression through UI should be only a minimal subset of what is covered through other levels. We should be ready to invest in maintaining the automation assets over a period of time.\nIn this case, stakeholder expectation needs to be carefully managed. It is important to set right expectation about benefits offered by test automation for a successful project delivery. Automation testing can deliver benefits over a long period of time , provided proper planning was done upfront to automate at different levels of testing. Instead of considering it as solution for all pain points, we need to clearly articulate /set expectation about its limitations and long term benefits.\n","permalink":"https://abygeorgea.com/blog/2017/09/07/test-automation/","summary":"\u003cp\u003eToday I had a discussion with a project manager about stake holder expectations about value delivered from regression test automation and how to manage stake holder expectation. The discussion soon spanned on to challenges in automating manual test cases and candidates for automation testing.\u003c/p\u003e\n\u003ch3 id=\"expectation-vs-reality\"\u003eExpectation Vs Reality\u003c/h3\u003e\n\u003cp\u003eManagement Stakeholders always visualize test automation as a silver bullet for fixing all pain points.They envision automation tests to be quicker, cheaper and effective in identifying all defects. Automation test cases are expected to be run on a button click and with 100% pass rate (except for valid bugs). Needless to say, that expectation is about having complete test coverage for automation scripts. Thinking is always geared towards reducing manual testers based on automation progress rather than having focus on improved quality of final product, faster time to market etc.\u003c/p\u003e","title":"Setting Right expectation about benefits of Test automation"},{"content":"Today, I had a chance to attend SIGiST conference organised by ANZTB. It was a 2-hour session which includes a presentation, discussion, and networking opportunities. Being a first-time attendee to SIGiST, I was not sure what surprise I may have. Overall it was a fruitful session and I had a chance to meet people from other organisation and to understand what is happening at their end.\nToday\u0026rsquo;s presentation was about \u0026ldquo;Test Automation – What YOU need to know\u0026rdquo;. Over all, it was a good session even though I found the presentation is more geared towards uplifting manual testers and what steps they should take to stay relevant in today\u0026rsquo;s world. Going by crowd surrounding presenters after the session, it seems topic was well received and resonated with most of the people in the room. But those who have experience in automation / performance testing will find it basic. The presentation is expected to be uploaded here in few days.\nThe topic for discussion was \u0026ldquo;Carriers in Testing\u0026rdquo;. This was really engaging and people participated actively sharing their experiences in career progression, experience in getting jobs etc. There was pretty lengthy discussion about how to make your resume stand out in the crowd, importance of certification, soft skill, analyticall and debugging skills and how to market yourself. Few recruiters/managers shed thoughts on what they look in prospective employee\u0026rsquo;s resume and how they short list candidates for interview.\n","permalink":"https://abygeorgea.com/blog/2017/08/29/anztb-sigist-conference/","summary":"\u003cp\u003eToday, I had a chance to attend SIGiST conference organised by ANZTB. It was a 2-hour session which includes a presentation, discussion, and networking opportunities. Being a first-time attendee to SIGiST, I was not sure what surprise I may have. Overall it was a fruitful session and I had a chance to meet people from other organisation and to understand what is happening at their end.\u003c/p\u003e\n\u003cp\u003eToday\u0026rsquo;s presentation was about \u0026ldquo;Test Automation – What YOU need to know\u0026rdquo;. Over all, it was a good session even though I found the presentation is more geared towards uplifting manual testers and what steps they should take to stay relevant in today\u0026rsquo;s world. Going by crowd surrounding presenters after the session, it seems topic was well received and resonated with most of the people in the room. But those who have experience in automation / performance testing will find it basic. The presentation is expected to be uploaded \u003ca href=\"http://www.anztb.org/downloads.php\"\u003ehere\u003c/a\u003e in few days.\u003c/p\u003e","title":"ANZTB SIGiST Conference"},{"content":"One of the common requirement for automated testing is to run same test case against multiple test data. Luckily postman supports this by providing facility to use data files. This is available only when we run through postman collection runner or newman.\nFor this example, let us take a free public API http://services.groupkt.com/country/get/iso2code/AU . This API will return the name of the country depending on the 2 digit code passed. Let us assume that, we need to test this API with multiple country codes. For eg: AU, IN, GB etc. Let us take a look to see how this can be achieved using postman data files.\nEnvironment file First, create an enviornment Manage Environment option at top right. Create an entry for endpoint as below.\nCreate Collection Next step is to create a collection with a GET request and write tests to verify the response. GET request used here is {EndPoint}/country/get/iso2code/{countrycode}\nEndpoint is defined in environment file and countrycode will be in data file\nNow write some tests to check the results. The data coming from data file will be available under \u0026ldquo;data\u0026rdquo; dictionary ( similar to global/environment variable. It can be accessed as data.VARIABLENAME or data[\u0026quot;VARIABLENAME\u0026quot;] in both test and pre requisite scripts. Below screenshot shows the test which is for validating country name based on the data file.\nDateFile#### Postman supports both CSV and JSON format. For CSV files, the first row should be the variable names as the header. All subsequent rows are data row. JSON file should be an array of the keyvalue pair where the variable name is the key.\nData file used in this example is below. It has 3 column, where the first column is test case ID and the second one is country code which is used in the request and the third one is the country name, which is used for asserting the response received. In this example, I am looking for 3 different country codes.\nRunning Collections#### While running collections, we need to specify below inputs.\nCollection Name Environment file Data File Depending on number of records in the data file, iterations will be auto populated. The results will also show the details for each iteration using the data. Details of response can be found by expanding response body\nRunning through Newman We can run same collection through Newman as well\nnewman run PathToCollectionsFile -e PathToEnvironmentFiles -d PathToDataFile In this cases, I should run newman run DataDriven.postman_collection.json -e DataDrivenEnvironment.postman_environment.json -d data-article.csv.\nResults will be as below\n","permalink":"https://abygeorgea.com/blog/2017/08/13/postman-using-data-file/","summary":"\u003cp\u003eOne of the common requirement for automated testing is to run same test case against multiple test data. Luckily postman supports this by providing facility to use data files. This is available only when we run through postman collection runner or newman.\u003c/p\u003e\n\u003cp\u003eFor this example, let us take a free public API \u003ccode\u003ehttp://services.groupkt.com/country/get/iso2code/AU\u003c/code\u003e . This API will return the name of the country depending on the 2 digit code passed. Let us assume that, we need to test this API with multiple country codes. For eg: AU, IN, GB etc.  Let us take a look to see how this can be achieved using postman data files.\u003c/p\u003e","title":"Postman - Using Data File"},{"content":"In previous blog, I explained about how to create a GET request, analyze its response, write test cases for API and to save details to a collection for future use. In this blog, let me explain about how to run collections using Newman.\nWhat is Newman Newman is a command line collection runner for postman. Newman also has feature parity with Postman and it runs collection in the same way how it is run through Postman. Newman also makes it easier to integrate API test case execution with other systems like Jenkins.\nInstalling Newman Newman is built on Node.js and hence it requires Node.js to be installed as prerequisite. Newman can be installed from npm with below command\n$ npm install -g newman Running collection using Newman Collections are executed by calling run command in Newman. Basic command for executing collections is\nnewman run PathToCollectionFile -e PathToEnvironmentFileIfAny Below is an example of running collections created in previous blog post using newman. Command will look like below newman run /Users/abygeorgea/Projects/Postman/Postman\\ Tutorial.postman_collection.json -e /Users/abygeorgea/Projects/Postman/Test.postman_environment.json\nResults The result of API test case execution will look like below. It has a detailed report of number of iterations, number of request, test scripts, pre-requisites, assertions etc. As per standard, passed ones are shown in green and failed in red. The results look similar to details provided if collections are executed using postman.\nAdditional Options of run command Newman has various options to customize run. Different options can be found by running with -h flag\nnewman run -h Different options listed are below\nAbys-MacBook-Pro:~ abygeorgea$ newman run -h usage: newman run [-h] [-v VERSION] [--no-color] [--color] [--timeout-request TIMEOUT_REQUEST] [--ignore-redirects] [-k] [--ssl-client-cert SSL_CLIENT_CERT] [--ssl-client-key SSL_CLIENT_KEY] [--ssl-client-passphrase SSL_CLIENT_PASSPHRASE] [-e ENVIRONMENT] [-g GLOBALS] [--folder FOLDER] [-r REPORTERS] [-n ITERATION_COUNT] [-d ITERATION_DATA] [--export-environment [EXPORT_ENVIRONMENT]] [--export-globals [EXPORT_GLOBALS]] [--export-collection [EXPORT_COLLECTION]] [--delay-request DELAY_REQUEST] [--bail] [-x] [--silent] [--disable-unicode] [--global-var GLOBAL_VAR] collection The \u0026#34;run\u0026#34; command can be used to run Postman Collections Positional arguments: collection URL or path to a Postman Collection Optional arguments: -h, --help Show this help message and exit. -v VERSION, --version VERSION Display the newman version --no-color Disable colored output --color Force colored output (for use in CI environments) --timeout-request TIMEOUT_REQUEST Specify a timeout for requests (in milliseconds) --ignore-redirects If present, Newman will not follow HTTP Redirects -k, --insecure Disables SSL validations. --ssl-client-cert SSL_CLIENT_CERT Specify the path to the Client SSL certificate. Supports .cert and .pfx files. --ssl-client-key SSL_CLIENT_KEY Specify the path to the Client SSL key (not needed for .pfx files). --ssl-client-passphrase SSL_CLIENT_PASSPHRASE Specify the Client SSL passphrase (optional, needed for passphrase protected keys). -e ENVIRONMENT, --environment ENVIRONMENT Specify a URL or Path to a Postman Environment -g GLOBALS, --globals GLOBALS Specify a URL or Path to a file containing Postman Globals --folder FOLDER Run a single folder from a collection -r REPORTERS, --reporters REPORTERS Specify the reporters to use for this run. -n ITERATION_COUNT, --iteration-count ITERATION_COUNT Define the number of iterations to run. -d ITERATION_DATA, --iteration-data ITERATION_DATA Specify a data file to use for iterations (either json or csv) --export-environment [EXPORT_ENVIRONMENT] Exports the environment to a file after completing the run --export-globals [EXPORT_GLOBALS] Specify an output file to dump Globals before exiting --export-collection [EXPORT_COLLECTION] Specify an output file to save the executed collection --delay-request DELAY_REQUEST Specify the extent of delay between requests (milliseconds) --bail Specify whether or not to gracefully stop a collection run on encountering the first error -x, --suppress-exit-code Specify whether or not to override the default exit code for the current run --silent Prevents newman from showing output to CLI --disable-unicode Forces unicode compliant symbols to be replaced by their plain text equivalents --global-var GLOBAL_VAR Allows the specification of global variables via the command line, in a key=value format ","permalink":"https://abygeorgea.com/blog/2017/08/07/running-postman-collection-using-newman/","summary":"\u003cp\u003eIn previous \u003ca href=\"/blog/2017/08/05/postman-tutorial/\"\u003eblog\u003c/a\u003e, I explained about how to create a GET request, analyze its response, write test cases for API and to save details to a collection for future use. In this blog, let me explain about how to run collections using Newman.\u003c/p\u003e\n\u003ch3 id=\"what-is-newman\"\u003eWhat is Newman\u003c/h3\u003e\n\u003cp\u003eNewman is a command line collection runner for postman. Newman also has feature parity with Postman and it runs collection in the same way how it is run through Postman. Newman also makes it easier to integrate API test case execution with other systems like Jenkins.\u003c/p\u003e","title":"Running Postman collection using Newman"},{"content":"Recently one of my colleagues has asked me to train him on using postman and Newman for API testing. Below is a cut down version of training session which I took for him.\nWhat is Postman Postman is an Http client for testing web services. It has a friendly GUI for constructing request and analyzing the response. There is a command line tool called Newman for running the postman collections from command line. This will help to integrate postman to other testing tools.\nHow to Install Postman is available as both chrome extension and also as a native install. Native install files can be found here.\nExample - GET Request In order to trigger a get request, we need to identify below information\nURL of API Authentication details Header details For this example, let us look at a google finance API. API URL(including parameters) is http://www.google.com/finance/info?infotype=infoquoteall\u0026amp;q=NSE:BHEL There is no authentication details and header details that need to be passed with this. The params button will list down various parameters passed in a tabular format , which makes it easy to edit.\nIn postman, Select drop down as GET and enter the API Url. Screen will look like below Now hit Send button. This will trigger a call to API and get the response which will then displayed in UI. Screen will look like below\nHeaders returned are\nWriting Tests Above is an example of calling an API and analyzing its response. Postman also has a facility to write test cases to verify the response. Test cases are written in javascript. Tests are run after the request is sent and it will allow access to response objects. The editor also provides commonly used code snippets which make it easier to write test.\nThe Below example is written for calling one of free API mentioned here. In this example, we have test scripts for checking status code, values in the header, values in response, response time. We can even expand the test cases to complex verifications by writing javascript tests.\nWe notice following from above screenshot,\n6 test cases written on the top part to check for the status code, response time, header and response. The response received on the bottom part. Test tab shows that 6/6 test cases are passed ( in Green). Now let us dive into details of the test results. Below screenshot shows details of test cases and their status.\nCollections We can save the current request and it associated tests ( if any) for future use in postman. It can also be exported and shared with others. Select option as Save As from drop down next to Save. We can specify request name, provide a description and select a folder and sub folder to save the response.\nOnce saved, it will be available for use in collections.\nEnvironments Very frequently, we will have to run API test in different environments. Most of the time, there will be few differences in the requests, like different URL. In such cases, we can use environments in Postman.\nClick on the Settings button on top right corner and select Manage environments. This is open up a new pop up where we can add Environment or import an existing environment file. For this tutorial, we will use Add option.\nNow we can specify all unique parameter for each environment. In this case, I have given a key called \u0026ldquo;URL\u0026rdquo; and entered corresponding values and saved it as an environment named Test.\nNow let us run the request using environments. First step is to replace https://jsonplaceholder.typicode.com with url in double curly braces. Then select Test in the Environment drop down at the top. Now click send. This will execute the request and run all associated test cases. Postman will dynamically replace {{url}} with corresponding URL value specified in selected environment file. So assuming we have different environment files, each time the request will be sent to different URL based on environment selected. We can have any number of keys and values in one environment file.\nFrom above, we can see that one test case is failed. Let us have a look into failed test case.\nFailed test case is for the time taken for the response. Current request took 1491 ms which is higher than expected 200ms.\nExporting Collections and environment files Postman provides facility to export collections and environment files as JSON. This helps to share the details with other team members and also to use Newman for running postman collections. Let us have a look into how to export them.\nExporting Collections\nClick on Collections Tab.\nClick on ... next to Collections Name.\nClick on Export.\nSelect V2 option and save the file.\nExporting Environment File\nClick on Settings button on top right corner.\nClick on Manage environment.\nDownload the file.\nRunning Collections Using Postman Collection Runner Postman provides a feature to run collections using collection Runner.\nClick on Runner button on Top left to open collection runner\nSelect Collection name in drop down and select environment and then hit Start Run.\nThis will trigger execution of request and test cases mentioned in collection and results will be shown. Also note that collection runner has additional options like number of iteration, delay before sending request , input from data file etc .\nOnce execution is complete, result will be shown like below. It will have details of all assertions done and options to export results for future verification. What Next ? In this post, I have explain basic usage of postman for API testing . However the functionalities provided by postman is much more than above. We can also use Newman , which is command line collection runner , to execute collections. I will write another post about it sometime soon.\n","permalink":"https://abygeorgea.com/blog/2017/08/05/postman-tutorial/","summary":"\u003cp\u003eRecently one of my colleagues has asked me to train him on using postman and Newman for API testing. Below is a cut down version of training session which I took for him.\u003c/p\u003e\n\u003ch3 id=\"what-is-postman\"\u003eWhat is Postman\u003c/h3\u003e\n\u003cp\u003ePostman is an Http client for testing web services. It has a friendly GUI for constructing request and analyzing the response. There is a command line tool called Newman for running the postman collections from command line. This will help to integrate postman to other testing tools.\u003c/p\u003e","title":"Postman Tutorial"},{"content":"In previous blogs here , I have explained how we return a XML response using mountebank. However , most of the time, we will have to make some modification to the template response before returning a response. Say for example, we may have to replace details like timestamp, or use an input from request parameter and update that in response etc.\nOne of the easiest way to do this without using other frameworks like xml2js etc is to extract the substring between the node values and replace it . Below is a code snippet which will help to achieve this\nThe sample xml which we need to return is\n\u0026lt;Status\u0026gt;Added\u0026lt;/Status\u0026gt; \u0026lt;GeneratedID\u0026gt;12345\u0026lt;/GeneratedID\u0026gt; In above example, assume that we need to replace the inserted record value every time based on the request coming through . We can do that by below\nvar xmldata = \u0026#34;\u0026lt;Status\u0026gt;Added\u0026lt;/Status\u0026gt;\\r\\n\u0026lt;GeneratedID\u0026gt;12345\u0026lt;/GeneratedID\u0026gt;\u0026#34; var generatedId = xmldata.match(new RegExp(\u0026#34;\u0026lt;GeneratedID\u0026gt;\u0026#34;+\u0026#34;(.*)\u0026#34;+\u0026#34;\u0026lt;/GeneratedID\u0026gt;\u0026#34;)); console.log(generatedId); // Output will be as below. from Array we can extract the substring, index of its location etc /* [ \u0026#39;\u0026lt;GeneratedID\u0026gt;12345\u0026lt;/GeneratedID\u0026gt;\u0026#39;, \u0026#39;12345\u0026#39;, index: 24, input: \u0026#39;\u0026lt;Status\u0026gt;Added\u0026lt;/Status\u0026gt;\\r\\n\u0026lt;GeneratedID\u0026gt;12345\u0026lt;/GeneratedID\u0026gt;\u0026#39; ] */ //so extract data from first location to get substring generatedId = xmldata.match(new RegExp(\u0026#34;\u0026lt;GeneratedID\u0026gt;\u0026#34;+\u0026#34;(.*)\u0026#34;+\u0026#34;\u0026lt;/GeneratedID\u0026gt;\u0026#34;))[1]; console.log(generatedId); //Above will print \u0026#34;12345\u0026#34; , which is the expected value // This can be used for extracting value of xml nodes //if we need to replace this with another value ( possibly coming from request parameter) var result = xmldata.replace(generatedId, \u0026#34;99999\u0026#34;); console.log(result); ","permalink":"https://abygeorgea.com/blog/2017/07/21/extracting-substring-using-javascript/","summary":"\u003cp\u003eIn previous blogs \u003ca href=\"/blog/2017/04/27/stubbing-xml-responses-using-mountebank/\"\u003ehere\u003c/a\u003e , I have explained how we return a XML response using mountebank. However , most of the time, we will have to make some modification to the template response before returning a response. Say for example, we may have to replace details like timestamp, or use an input from request parameter and update that in response etc.\u003c/p\u003e\n\u003cp\u003eOne of the easiest way to do this without using other frameworks like xml2js etc is to extract the substring between the node values and replace it . Below is a code snippet which will help to achieve this\u003c/p\u003e","title":"Extracting Substring using Javascript"},{"content":"Predicates in Mountebank imposter files is a pretty powerful way to configure stubs. It helps us to return different responses based on the request parameters like type, query string , headers, body etc. Let us have some quick look at extracting values from request\nBased on Query String Below is an example of extracting the records based on query string. If the request is like path?customerId=123\u0026amp;customerId=456\u0026amp;email=abc.com Note: This is slightly modified version of code in mbtest.org\n{ \u0026#34;port\u0026#34;: 4547, \u0026#34;protocol\u0026#34;: \u0026#34;http\u0026#34;, \u0026#34;stubs\u0026#34;: [ { \u0026#34;predicates\u0026#34;: [{ \u0026#34;equals\u0026#34;: { \u0026#34;query\u0026#34;: { \u0026#34;customerId\u0026#34;: [\u0026#34;123\u0026#34;, \u0026#34;456\u0026#34;] } } }], \u0026#34;responses\u0026#34;: [{ \u0026#34;is\u0026#34;: { \u0026#34;body\u0026#34;: \u0026#34;Customer ID is either 123 or 456\u0026#34; } }] }, { \u0026#34;predicates\u0026#34;: [{ \u0026#34;equals\u0026#34;: { \u0026#34;query\u0026#34;: { \u0026#34;customerId\u0026#34;: \u0026#34;123\u0026#34;, \u0026#34;email\u0026#34; :\u0026#34;abc.com\u0026#34; } } }], \u0026#34;responses\u0026#34;: [{ \u0026#34;is\u0026#34;: { \u0026#34;body\u0026#34;: \u0026#34;Customer ID is 123 and email is abc.com\u0026#34; } }] } ] } Based on Header Content If input data is shared through values in header, that can be extracted. Below snippet is directly from mbtest.org\n{ \u0026#34;port\u0026#34;: 4545, \u0026#34;protocol\u0026#34;: \u0026#34;http\u0026#34;, \u0026#34;stubs\u0026#34;: [ { \u0026#34;responses\u0026#34;: [{ \u0026#34;is\u0026#34;: { \u0026#34;statusCode\u0026#34;: 400 } }], \u0026#34;predicates\u0026#34;: [ { \u0026#34;equals\u0026#34;: { \u0026#34;method\u0026#34;: \u0026#34;POST\u0026#34;, \u0026#34;path\u0026#34;: \u0026#34;/test\u0026#34;, \u0026#34;query\u0026#34;: { \u0026#34;first\u0026#34;: \u0026#34;1\u0026#34;, \u0026#34;second\u0026#34;: \u0026#34;2\u0026#34; }, \u0026#34;headers\u0026#34;: { \u0026#34;Accept\u0026#34;: \u0026#34;text/plain\u0026#34; } } }, { \u0026#34;equals\u0026#34;: { \u0026#34;body\u0026#34;: \u0026#34;hello, world\u0026#34; }, \u0026#34;caseSensitive\u0026#34;: true, \u0026#34;except\u0026#34;: \u0026#34;!$\u0026#34; } ] } ] } ","permalink":"https://abygeorgea.com/blog/2017/07/15/predicates-in-mountebank/","summary":"\u003cp\u003ePredicates in Mountebank imposter files is a pretty powerful way to configure stubs. It helps us to return different responses based on the request parameters like type, query string , headers, body etc. Let us have some quick look at extracting values from request\u003c/p\u003e\n\u003ch3 id=\"based-on-query-string\"\u003eBased on Query String\u003c/h3\u003e\n\u003cp\u003eBelow is an example of extracting the records based on query string.\nIf the request is like \u003ccode\u003epath?customerId=123\u0026amp;customerId=456\u0026amp;email=abc.com\u003c/code\u003e\nNote: This is slightly modified version of code in mbtest.org\u003c/p\u003e","title":"Predicates In Mountebank"},{"content":"In previous post, I mentioned that we can use Galen for automated lay out testing. Galen offers a simple solution to test location of objects relative to each other on the page. Galen is implemented using Selenium Web driver. Hence we can use it for normal functional automation testing as well.\nDocumentation of Galen Galen has its own domain specific language to define Specs. Detailed documentation can be found here. Galen has its own javascript API which provides a list of functions which make writing test cases easier. Detailed documentation can be found here. Galen pages javascript API is light weight javascript test framework. Details are available here. Details of galen test suite syntax are here. Galen framework has a detailed documentation of its usage and functions here. ###Installation###\nBelow are high-level steps to help you get started.\nEnsure Java is installed. Galen needs Java version above 1.8 Download binary from http://galenframework.com/download/ Extract the zip file Add the location of extracted files to PATH environment variables. A detailed guide for older versions of Windows is available here. Alternatively, on Windows , you can create a bat file to run Galen by changing Path on the fly. Details are in below steps. ###Setting up Galen Framework### There are different framework available for testing responsive design based on Galen. Galen bootstrap is one of such framework which can be reused.\nDownload and extract the project from Github. Keep relevant files only. You can remove Create an init.js file to load galen-bootstrap/galen-bootstrap.js script and configure all devices and a website URL for testing. URL mentioned below is an example of responsive web design template. load(\u0026#34;galen-bootstrap/galen-bootstrap.js\u0026#34;); //$galen.settings.website = \u0026#34;https://alistapart.com/d/responsive-web-design/ex/ex-site-FINAL.html\u0026#34;; //$galen.registerDevice(\u0026#34;mobile\u0026#34;, inLocalBrowser(\u0026#34;mobile emulation\u0026#34;, \u0026#34;450x800\u0026#34;, [\u0026#34;mobile\u0026#34;])); //$galen.registerDevice(\u0026#34;tablet\u0026#34;, inLocalBrowser(\u0026#34;tablet emulation\u0026#34;, \u0026#34;600x800\u0026#34;, [\u0026#34;tablet\u0026#34;])); //$galen.registerDevice(\u0026#34;desktop\u0026#34;, inLocalBrowser(\u0026#34;desktop emulation\u0026#34;, \u0026#34;1024x768\u0026#34;, [\u0026#34;desktop\u0026#34;])); Note: Uncomment the lines above. Octopress blog engine was throwing error when it tries generate post.\nRun galen config from the command line with the project directory. This will create Galen config file in the location where the command is run. Modify galen.config file to make chrome as default browser and add path to chrome driver. There are other useful configs like range approximation, screenshot, selenium grid etc in the config. galen.default.browser=chrome $.webdriver.chrome.driver=.\\\\..\\\\WebProject\\\\Driver\\\\chromedriver.exe Create a folder named Test for keeping test cases and create test files example.test.js. Copy below content to example.test.js. Make sure to update the relative location of the init.js file created in previous steps. Below content loads init.js file which lists out website URL, device sizes that need to be tested.It then calls a function to test on all devices. Check layout is one of the available javascript API function. load (\u0026#34;.\\\\..\\\\init.js\u0026#34;) testOnAllDevices(\u0026#34;Welcome page test\u0026#34;, \u0026#34;/\u0026#34;, function (driver, device) { checkLayout(driver, \u0026#34;specs/homepage.gspec\u0026#34;, device.tags, device.excludedTags); }); Create a folder named specs and create a spec file named homepage.gspec. We need to update the specs with layout checks . Below is the sample spec for checking image and section intro for the sample URL from init.js. First section defines the objects and its identifier. Second section says that on desktop, image will on left side of section intro and on mobile and tablet, it will be above section intro @objects image id logo menu css #page \u0026gt; div \u0026gt; div.mast \u0026gt; ul sectionintro css #page \u0026gt; div \u0026gt; div.section.intro = Main Section = image: @on desktop left-of sectionintro @on mobile, tablet above sectionintro Now create a bat file in the main folder to run the galen test cases. Make sure to give relative paths to test file, configs, reports correctly. Modify Path variable to include path location to galen bin. This is not needed if we manually set pah while installing. However, I prefer to have galen bin files as well in source control and point the path to that location so that we don\u0026rsquo;t have any specific dependency outside the project. SET PATH=%PATH%;.\\galen-bin galen test .\\\\test\\\\example.test.js --htmlreport .\\reports --jsonreport .\\jsonreports --config .\\galen.config once all files are created, folder structure will look like below run the bat file created above. This will ideally run example.test.js file which invokes chrome driver, navigate to the URl, resizes the browser and then check for the specs.It will list out the results in command prompt. Once it completes are all test execution, it creates both HTML report and JSON report in corresponding folder location mentioned in bat file. Below is a sample HTML report, which is self-explanatory. Main report If we expand the result for desktop emulation, it will look like below.It will list down each assertion made and indicate whether it is passed or failed. If we click on the assertion point, it will show the screenshot taken for that assertion by highlighting the objects which will help for easier verification. Below screenshot shows that image is on left side of section intro as defined in spec file. ","permalink":"https://abygeorgea.com/blog/2017/06/25/galen-framework-getting-started/","summary":"\u003cp\u003eIn previous \u003ca href=\"%7Bsite.root%7D%7Dblog/2017/06/21/automated-testing-of-responsive-web-design\"\u003epost\u003c/a\u003e, I mentioned that we can use Galen for automated lay out testing. Galen offers a simple solution to test location of objects relative to each other on the page. Galen is implemented using Selenium Web driver. Hence we can use it for normal functional automation testing as well.\u003c/p\u003e\n\u003ch3 id=\"documentation-of-galen\"\u003eDocumentation of Galen\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eGalen has its own domain specific language to define Specs. Detailed documentation can be found \u003ca href=\"http://galenframework.com/docs/reference-galen-spec-language-guide/\"\u003ehere\u003c/a\u003e.\u003c/li\u003e\n\u003cli\u003eGalen has its own javascript API which provides a list of functions which make writing test cases easier. Detailed documentation can be found \u003ca href=\"http://galenframework.com/docs/reference-galen-javascript-api/\"\u003ehere\u003c/a\u003e.\u003c/li\u003e\n\u003cli\u003eGalen pages javascript API is light weight javascript test framework. Details are available \u003ca href=\"http://galenframework.com/docs/reference-galenpages-javascript-api/\"\u003ehere\u003c/a\u003e.\u003c/li\u003e\n\u003cli\u003eDetails of galen test suite syntax are \u003ca href=\"http://galenframework.com/docs/reference-galen-test-suite-syntax/\"\u003ehere\u003c/a\u003e.\u003c/li\u003e\n\u003cli\u003eGalen framework has a detailed documentation of its usage and functions \u003ca href=\"http://galenframework.com/docs/all/\"\u003ehere\u003c/a\u003e.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e###Installation###\u003c/p\u003e","title":"Galen Framework - Getting started"},{"content":"In a world where mobile first seems to be the norm, testing of look and feel of websites on various mobile/tablet devices are essential. More businesses are now adopting Responsive Web designs for developing their web applications and sites.\n###What is Responsive Web Design###\nAccording to Wikipedia, Responsive web design (RWD) is an approach to web design aimed at allowing desktop webpages to be viewed in response to the size of the screen or web browser one is viewing with. In addition, it\u0026rsquo;s important to understand that Responsive Web Design tasks include offering the same support to a variety of devices for a single website. A site designed with RWD adapts the layout to the viewing environment by using fluid, proportion-based grids, flexible images, and CSS3 media queries, an extension of the @media rule, in the following ways:\nThe fluid grid concept calls for page element sizing to be in relative units like percentages, rather than absolute units like pixels or points. Flexible images are also sized in relative units, so as to prevent them from displaying outside their containing element. Media queries allow the page to use different CSS style rules based on characteristics of the device the site is being displayed on, most commonly the width of the browser How do we test responsiveness An ideal option for testing is to test on different physical devices of various screen size. However, it is impossible to get hold of all available mobile/tablet devices in the market. Even if we prioritize the devices using analytics, it is very expensive to buy enough number of devices. Along with this, we need to upgrade to newer version of devices frequently when apple/google/Samsung releases an upgraded version.\nNext possible option is to use device emulators like device mode in Chrome dev tools. As pointed out in their documentation, it is only a close approximation of how the website will look on a mobile device.It have its own limitations which are listed here\nBest approach will be to use emulators early in development cycle and once UX design is stabilized, then test it on physical device based on priority obtained by analytics.\n###Challenges in testing Responsive Websites###\nTesting of responsive websites has its own challenges.\nNumber of Options to be tested or number of breakpoints which needs to be validated are high\nDistinctive UI designs for different device screen sizes makes testing time consuming. This adds complexity to testing since it will require testing of below in various screen sizes\nAll UI elements like image, text, controls are aligned properly with each other and doesn\u0026rsquo;t overflow from screen display area Consistency in font size, color, shades , padding, display orientation etc Resizing of controls which take inputs (like text) to cater for long content typed in by users. Other CSS validation specific for mobile and tablet devices It is hard to test all of the above on every iteration manually.\nComparing UI \u0026amp; UX \u0026amp; Visual design will require more efforts.\nHard to keep track of every feature that needs to be tested and will have testing fatigue which will result in Non-obvious changes to UI\nAutomated Responsive Design testing - Galen Framework As mentioned above, one of the pain points in responsive design testing is the user fatigue happening over multiple iterations of testing. This can be easily avoided by having an automated test framework. I recently came across galen framework which is an open source framework to test layouts of webpages. You can read about Galen framework here. Galen framework can be used for automation of CSS testing easier. It has evolved over time and has its own Domain specific language and commands which can be used for CSS testing. I will go through galen framework in more details in next post\n","permalink":"https://abygeorgea.com/blog/2017/06/21/automated-testing-of-responsive-web-design/","summary":"\u003cp\u003eIn a world where mobile first seems to be the norm, testing of look and feel of websites on various mobile/tablet devices are essential. More businesses are now adopting Responsive Web designs for developing their web applications and sites.\u003c/p\u003e\n\u003cp\u003e###What is Responsive Web Design###\u003c/p\u003e\n\u003cp\u003eAccording to \u003ca href=\"https://en.wikipedia.org/wiki/Responsive_web_design\"\u003eWikipedia\u003c/a\u003e, Responsive web design (RWD) is an approach to web design aimed at allowing desktop webpages to be viewed in response to the size of the screen or web browser one is viewing with. In addition, it\u0026rsquo;s important to understand that Responsive Web Design tasks include offering the same support to a variety of devices for a single website. A site designed with RWD adapts the layout to the viewing environment by using fluid, proportion-based grids, flexible images, and CSS3 media queries, an extension of the @media rule, in the following ways:\u003c/p\u003e","title":"Automated  testing of CSS for Responsive Web Design"},{"content":"Very often we will be committing smaller pieces of work in our local machine as we go. However before we push them to a centralized repository, we may have to combine these small commits to single large commit, which makes sense for rest of the team. I will explain how this can be achieved by using interactive rebasing.\nTo start with, let us assume the initial commits history look like below. It have 4 minor commits done to the same file. Now we need to squash last for commits into a single commit. The command required for that is as below. This tells git to rebase head with previous 4 commits in an interactive mode.\n$ git rebase -i HEAD~4 This will pop up another editor with details of last 4 commits and some description about possible actions on this. Initially, all of them will have a default value of PICK. Since we are trying to squash commits together, we can select one of the commits as PICK and rest all needs to be changed as SQUASH. Save and close the editor once all changes are made.\nAfter this, another popup will appear with comments given for each of the commits. We can comment out unnecessary comments by using # and also modify required comments as we need. In below screen, I have modified comments for the first commit and commented out rest all. Save and close the editor once all changes are made.\nNow Git will continue rebasing and it will squash all commits as selected in the previous step.\nIf we look at commit history, we can see that commits are now squashed to single commit.\n","permalink":"https://abygeorgea.com/blog/2017/06/15/how-to-squash-commits-in-git/","summary":"\u003cp\u003eVery often we will be committing smaller pieces of work in our local machine as we go. However before we push them to a centralized repository, we may have to combine these small commits to single large commit, which makes sense for rest of the team. I will explain how this can be achieved by using interactive rebasing.\u003c/p\u003e\n\u003cp\u003eTo start with, let us assume the initial commits history look like below. It have 4 minor commits done to the same file.\n\u003cimg alt=\"Initial Commit Structure\" loading=\"lazy\" src=\"/images/2017/06/15/HowToSquashCommits_image1.png\"\u003e\u003c/p\u003e","title":"How to Squash Commits in Git"},{"content":"Over the past weekend, I noticed that my blog is not available since azure has disabled hosting of my WordPress blog. It happened because I ran out of my free credits for the current month. I started looking for alternate options for hosting WordPress. That\u0026rsquo;s when I came across (Static Generator is All a Blog Needs - Moving to Octopress). I decided to give it a try.\nBelow are the main steps which I followed for migrating to Octopress\nDocumentation Read documentation of Octopress here and Jekyll here Setup Install Chocolatey as mentioned in documentation here Below command can be run on cmd.exe open as administrator @powershell -NoProfile -ExecutionPolicy Bypass -Command \u0026#34;iex ((New-Object System.Net.WebClient).DownloadString(\u0026#39;https://chocolatey.org/install.ps1\u0026#39;))\u0026#34; \u0026amp;\u0026amp; SET \u0026#34;PATH=%PATH%;%ALLUSERSPROFILE%\\chocolatey\\bin\u0026#34; As mentioned in octopress documentation, ensure Git, ruby and devkit are installed. Cholocatey way of installation can be found in git, ruby , devkit. Below commands can be run on cmd.exe choco install git.install choco install ruby choco install ruby2.devkit By default, devkit is installed in C:\\tools\\. Move in devkit folder and run below commands ruby dk.rb init ruby dk.rb install gem install bundler Install Octopress Now install Octopress as per documentation git clone git://github.com/imathis/octopress.git octopress cd octopress bundle install rake install // Install default Octopress theme Install Octostrap3 theme \u0026amp; Customize# Since I didn\u0026rsquo;t like the default theme much, I installed Octostrap3 theme as mentioned here git clone https://github.com/kAworu/octostrap3.git .themes/octostrap3 rake \u0026#34;install[octostrap3]\u0026#34; Fix up all issues. The date displayed as \u0026ldquo;Ordinal\u0026rdquo; can be fixed by updating _config.yml file as mentioned in their blog. Below is the config which I used date_format: \u0026#34;%e %b, %Y\u0026#34; I made few more changes for changing the navigation header color, color of code blocks and also to include a side bar with categories. The changes are as below Changing color of code blocks is done by commenting below line in octopress\\sass\\custom\\_colors.scss\n\\\\$solarized: light; Navigation header color is changed by adding below to octopress\\sass\\custom\\_styles.scss\n.navbar-default { background-image: -webkit-gradient(linear,left top,left bottom,from(#263347),to(#263347)); } .navbar-default .navbar-brand { color: #fff; } .navbar-default .navbar-nav\u0026gt;li\u0026gt;a { color: #fff; } Adding category side bar is done by following steps mentioned in Category List Aside\nGoogle Analytics Integration# Next step was google analytics integration. Detailed steps for this is available on various blogs. Below is what I followed\nSign up for google analytics ID in here Update _config.yml with google analytics ID # Google Analytics google_analytics_tracking_id: UA-XXXXXXXX-1 Update google_analytics.html file with below \u0026lt;script\u0026gt; (function(i,s,o,g,r,a,m){i[\u0026#39;GoogleAnalyticsObject\u0026#39;]=r;i[r]=i[r]||function(){ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) })(window,document,\u0026#39;script\u0026#39;,\u0026#39;//www.google-analytics.com/analytics.js\u0026#39;,\u0026#39;ga\u0026#39;); ga(\u0026#39;create\u0026#39;, \u0026#39;UA-XXXXXXXX-1\u0026#39;, \u0026#39;auto\u0026#39;); ga(\u0026#39;send\u0026#39;, \u0026#39;pageview\u0026#39;); \u0026lt;/script\u0026gt; UA-XXXXXXXX-1 can be replaced with site.google_analytics_tracking_id enclosed in double braces/curly brackets Log in to Google Analytics site and navigate to Admin \u0026raquo; View \u0026raquo; Filters Add a new filter to exclude all traffice to hostname \u0026ldquo;localhost\u0026rdquo;. This will help to exclude all site visit done for development/ preview purpose. Sample Post Now create a Hello World post and check how it look rake new_post[\u0026#34;Hello World\u0026#34;] rake generate rake preview rake preview mounts a webserver at http://localhost:4000. By opening a browser window and navigating to http://localhost:4000 will preview the Hello World Post\nDeploying to GitHub Pages Detailed instructions can be found in Deploying to Github Pages. Below are high-level steps copied from there\nCreate a GitHub repository with name yourusername.github.io Run below command. It will prompt for GitHub URL, which needs to be filled in rake setup_github_pages // This does all configurations rake generate rake deploy Now we can commit the source git add . git commit -m \u0026#39;your message\u0026#39; git push origin source Custom Domain Create a file named CNAME in blog source Update it with custom domain name. It has to be a sub domain (www.examplesubdomain.com) Update the CNAME dns setting in your domain provider to point to https://username.github.io If top-level domains (exampletopdomain.com) are needed, then configure A record to point to IP address 192.30.252.153 or 192.30.252.154. Migrating Old blog Post from word press After completing above steps, a new octopress blog is ready to go . Below are the steps which I followed to migrate old blog posts from word press.\nClone Exitwp\nFollow the steps mentioned in readme.md.\nExport old wordpress blog using WordPress exporter in tools/export in WordPress admin Copy xml file to wordpress-xml directory Run python exitwp.py in the console from the same directory of unzipped archive All blogs will be created as separate directory under build directory Copy relevant folders to source folder of the blog Find broken redirection links and fix\nThe redirection links are now changed to something like {site.root}blog/2017/04/07/mountebank-creating-a-response-based-on-a-file-template-and-modifying-it-based-on-request-part-1/ Find broken image links and fix\nInorder to make it easier for migrating to another platform later, I created a new config value in _config.yml as below . images_dir: /images The image links are not pointing to {site.images_dir}/2017/04/27/Mountebank_XML_Response_Folder-Tree.jpg SEO Optimisation in Octopress In rake file, add below two lines post.puts \u0026quot;keywords: \u0026quot; and post.puts \u0026quot;description: \u0026quot; Final content will look like below post.puts \u0026#34;---\u0026#34; post.puts \u0026#34;layout: post\u0026#34; post.puts \u0026#34;title: \\\u0026#34;#{title.gsub(/\u0026amp;/,\u0026#39;\u0026amp;amp;\u0026#39;)}\\\u0026#34;\u0026#34; post.puts \u0026#34;date: #{Time.now.strftime(\u0026#39;%Y-%m-%d %H:%M:%S %z\u0026#39;)}\u0026#34; post.puts \u0026#34;comments: true\u0026#34; post.puts \u0026#34;categories: \u0026#34; post.puts \u0026#34;keywords: \u0026#34; post.puts \u0026#34;description: \u0026#34; post.puts \u0026#34;---\u0026#34; Add relevant Keyword and description to all pages ","permalink":"https://abygeorgea.com/blog/2017/05/20/migrating-to-octopress/","summary":"\u003cp\u003eOver the past weekend, I noticed that my blog is not available since azure has disabled hosting of my WordPress blog. It happened because I ran out of my free credits for the current month. I started looking for alternate options for hosting WordPress. That\u0026rsquo;s when I came across (\u003ca href=\"http://www.rahulpnath.com/blog/static-generator-is-all-a-blog-needs-moving-to-octopress/\"\u003eStatic Generator is All a Blog Needs - Moving to Octopress\u003c/a\u003e). I decided to give it a try.\u003c/p\u003e\n\u003cp\u003eBelow are the main steps which I followed for migrating to Octopress\u003c/p\u003e","title":"Migrating To Octopress"},{"content":"The PowerShell command to remove an entire directory and its contents ( including sub folders and files) is below\nrm -Rf pathToDirectoryToBeRemoved/ R flag denotes to run “rm” command recursively . “f” flag denotes to run in forcefully. We can even replace “f” with “v” for verbose mode and “i” for interactive mode.\nNote: Above command can also be used to delete files which have long path ( more than 260 characters)\n","permalink":"https://abygeorgea.com/blog/2017/05/02/powershell-remove-entire-directory-and-its-content/","summary":"\u003cp\u003eThe PowerShell command to remove an entire directory and its contents ( including sub folders and files) is below\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-plain\" data-lang=\"plain\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003erm -Rf pathToDirectoryToBeRemoved/\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eR flag denotes to run “rm” command recursively . “f” flag denotes to run in forcefully. We can even replace “f” with “v” for verbose mode and “i” for interactive mode.\u003c/p\u003e\n\u003cp\u003eNote: Above command can also be used to delete files which have long path ( more than 260 characters)\u003c/p\u003e","title":"Powershell - Remove entire directory and it's content"},{"content":"Previous two blog post talked about how we can use mountebank for stubbing where responses are in json format . They can be accessed (here) and (here). We can use same approach for stubbing SOAP services using XML as well. In this post, I will explain how we can provide XML response using Mountebank .\nLet us have a quick look into the files created. Before we begin, folder structure of various file as below\nImposter.ejs The main Imposter file is\n{ \u0026#34;imposters\u0026#34;: [ \u0026lt;% include Port4547.json %\u0026gt; ] } Port4547.json This file specifies which port number to use and what all stubs needs to be created is as below\n{ \u0026#34;port\u0026#34;: 4547, \u0026#34;protocol\u0026#34;: \u0026#34;http\u0026#34;, \u0026#34;stubs\u0026#34;: [ { \u0026lt;% include XMLStubGET.json %\u0026gt; }, { \u0026lt;% include XMLStubPOST.json %\u0026gt; } ] } XMLStubGET.json This is the first stub for this example and it looks for any request coming with the method \u0026ldquo;GET\u0026rdquo; and path \u0026ldquo;/Blog.Api/[0-9]+/CustomerView\u0026rdquo; , where [0-9]+ is regular expression of any numeric\n\u0026#34;responses\u0026#34;: [ { \u0026#34;inject\u0026#34;: \u0026#34;\u0026lt;%-stringify(filename, \u0026#39;ResponseInjection\\\\GetXMLStub.js\u0026#39;) %\u0026gt;\u0026#34; } ], \u0026#34;predicates\u0026#34;: [ { \u0026#34;matches\u0026#34;: { \u0026#34;method\u0026#34; : \u0026#34;GET\u0026#34;, \u0026#34;path\u0026#34; : \u0026#34;/Blog.Api/[0-9]+/CustomerView\u0026#34; } } ] XMLStubPOST.json This is the second stub for this example and it looks for any request coming with method \u0026ldquo;POST\u0026rdquo; and path \u0026ldquo;/Blog.Api/XMLexamplePOST/[0-9]+\u0026rdquo; , where [0-9]+ is regular expression of any numeric .It also needs a body as InsertCustomer1\nNote: If you have body in multi-line, then make sure to enter \u0026ldquo;\\n\u0026rdquo; for new line\n\u0026#34;responses\u0026#34;: [ { \u0026#34;inject\u0026#34;: \u0026#34;\u0026lt;%-stringify(filename, \u0026#39;ResponseInjection\\\\GetXMLStub-POST.js\u0026#39;) %\u0026gt;\u0026#34; } ], \u0026#34;predicates\u0026#34;: [ { \u0026#34;matches\u0026#34;: { \u0026#34;body\u0026#34; : \u0026#34;\u0026lt;Action\u0026gt;Insert\u0026lt;/Action\u0026gt;\u0026lt;Record\u0026gt;Customer1\u0026lt;/Record\u0026gt;\u0026#34;, \u0026#34;method\u0026#34; : \u0026#34;POST\u0026#34;, \u0026#34;path\u0026#34; : \u0026#34;/Blog.Api/XMLexamplePOST/[0-9]+\u0026#34; } } ] GetXMLStub.js Below js file create a response based on template mentioned and return the response with proper status. Please note that, we are not using \u0026ldquo;Json.Parse\u0026rdquo; here as we did for previous examples involving json.\nfunction GetTemplateResponse (request, state, logger) { response = \u0026#34;\u0026lt;%- stringify(filename, \u0026#39;StubTemplate\\\\CustomerDetails.xml\u0026#39;) %\u0026gt;\u0026#34; return { statusCode : 200, headers: { \u0026#39;Content-Type\u0026#39;: \u0026#39;application/xml; charset=utf-8\u0026#39; }, body: response }; } GetXMLStub-POST.js function GetTemplateResponse (request, state, logger) { response = \u0026#34;\u0026lt;%- stringify(filename, \u0026#39;StubTemplate\\\\RecordAdded.xml\u0026#39;) %\u0026gt;\u0026#34; return { statusCode : 200, headers: { \u0026#39;Content-Type\u0026#39;: \u0026#39;application/xml; charset=utf-8\u0026#39; }, body: response }; } CustomerDetails.XML This is the template for the first stub - GET example\n\u0026lt;customer\u0026gt; \u0026lt;FirstName\u0026gt;John\u0026lt;/FirstName\u0026gt; \u0026lt;LastName\u0026gt;Citizen\u0026lt;/LastName\u0026gt; \u0026lt;Address\u0026gt;Some St, Some State, Some Country\u0026lt;/Address\u0026gt; \u0026lt;Email\u0026gt;Test@test.com\u0026lt;/Email\u0026gt; \u0026lt;/customer\u0026gt; RecordAdded.xml This is the template for the second stub - POST example\n\u0026lt;Status\u0026gt;Added\u0026lt;/Status\u0026gt; \u0026lt;Record\u0026gt;Customer1\u0026lt;/Record\u0026gt; After creating above files and keeping them as per directory structure is shown above, it is time to start mountebank\nmb \u0026ndash;configfile SOAP-XMLStubExample/Imposter.ejs \u0026ndash;allowInjection\nNote: Give the right path to Imposter.ejs . If you need to debug Mountebank, you can use below command at the end \u0026quot; \u0026ndash;loglevel debug\u0026quot;\nNow trigger a get request to http://localhost:4547/Blog.Api/3123/CustomerView.\nThis should match with our first predicate and should return the response mentioned\nMountebank_XML_Response_\nNow trigger a POST request with a body . If predicates are matched, then it will respond with expected response as below\nIn Nut shell, creating a XML response is similar to creating json response. There are only minor differences in the js file which creates the response. The main difference is the omission of Json.Parse and also changing the response headers.\nAbove examples can be cloned from my GitHub repository here. After cloning the repository to local, just run RunMounteBankStubsWithSOAPXMLStubExampleData.bat file. Postman scripts can also be found inside PostmanCollections Folder to testing this\n","permalink":"https://abygeorgea.com/blog/2017/04/27/stubbing-xml-responses-using-mountebank/","summary":"\u003cp\u003ePrevious two blog post talked about how we can use mountebank for stubbing where responses are in json format . They can be accessed (\u003ca href=\"/blog/2017/04/07/mountebank-creating-a-response-based-on-a-file-template-and-modifying-it-based-on-request-part-1/\"\u003ehere\u003c/a\u003e) and (\u003ca href=\"/blog/2017/04/07/mountebank-creating-a-response-based-on-a-file-template-and-modifying-it-based-on-request-part-2/\"\u003ehere\u003c/a\u003e). We can use same approach for stubbing SOAP services using XML as well. In this post, I will explain how we can provide XML response using Mountebank .\u003c/p\u003e\n\u003cp\u003eLet us have a quick look into the files created. Before we begin, folder structure of various file as below\u003c/p\u003e","title":"Stubbing XML responses using Mountebank"},{"content":"On corporate world, most of the times, the access required for installing applications and connecting to internet will be limited. There can be scenarios where access to install Mountebank using npm will not be available. In those circumstances, we can just unzip the zip file downloaded from self contained archive links in mbtest.org\nBelow code snippet can be used for extracting the zip files on the fly , so that it can be used for running test cases on any machine.\n###Pre-Requisite###\n.Net 4.5 is needed Add reference to below dll to solution System.IO.Compression.dll System.IO.Compression.FileSystem.dll ###Example###\nBelow example is based on (copied from) [msdn](https://msdn.microsoft.com/en-us/library/hh485723(v=vs.110)\nPublic void ZipFile() { string startPath = @\u0026#34;c:\\example\\start\u0026#34;; string zipPath = @\u0026#34;c:\\example\\result.zip\u0026#34;; string extractPath = @\u0026#34;c:\\example\\extract\u0026#34;; ZipFile.CreateFromDirectory(startPath, zipPath); } Public void UnZipFile() { string startPath = @\u0026#34;c:\\example\\start\u0026#34;; string zipPath = @\u0026#34;c:\\example\\result.zip\u0026#34;; string extractPath = @\u0026#34;c:\\example\\extract\u0026#34;; ZipFile.ExtractToDirectory(zipPath, extractPath); } ","permalink":"https://abygeorgea.com/blog/2017/04/20/zip-and-extract-zip-files-using-csharp/","summary":"\u003cp\u003eOn corporate world, most of the times, the access required for installing applications and connecting to internet will be limited. There can be scenarios where access to install Mountebank using npm will not be available. In those circumstances, we can just unzip the zip file downloaded from self contained archive links in \u003ca href=\"http://www.mbtest.org/docs/install\"\u003embtest.org\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eBelow code snippet can be used for extracting the zip files on the fly , so that it can be used for running test cases on any machine.\u003c/p\u003e","title":"Zip and Extract zip files using Csharp"},{"content":"This is an extension to my previous blog about how we can use mountebank to create a stubbed response based on a template file . You can read about it here. In this step by step example, I will explain how we will use mountebank to modify the response based on the request . Before we start, please ensure you are familiar with Part1 of the excercise. If you need to know more about mountebank and how to use mountebank , please read through how to install mountebank and service virtualisation using mountebank.\nAs in previous example, let us create Imposter.ejs and 4547.json . Contents of the Imposter.ejs is as below\n{ \u0026#34;imposters\u0026#34;: [ \u0026lt;% include 4547.json %\u0026gt; ] } Contents of 4547.json is as below\n4547.json\n{ \u0026#34;port\u0026#34;: 4547, \u0026#34;protocol\u0026#34;: \u0026#34;http\u0026#34;, \u0026#34;stubs\u0026#34;: [ { \u0026lt;% include CustomerNotFound.json %\u0026gt; }, { \u0026lt;% include CustomerFound.json %\u0026gt; } ] } Now create CustomerFound.json\n\u0026#34;responses\u0026#34;: [ { \u0026#34;inject\u0026#34;: \u0026#34;\u0026lt;%-stringify(filename, \u0026#39;ResponseInjection\\\\GetCustomerFound.js\u0026#39;) %\u0026gt;\u0026#34; } ], \u0026#34;predicates\u0026#34;: [ { \u0026#34;matches\u0026#34;: { \u0026#34;method\u0026#34; : \u0026#34;GET\u0026#34;, \u0026#34;path\u0026#34; : \u0026#34;/Blog.Api/[0-9]+/CustomerView\u0026#34; } } ] As we can see from above, if there is request which matches the predicates , then response will be dictated by the GetCustomerFound javascript file kept inside directory ResponseInjection. Predicate used here is a GET request which have a matching path of /Blog.Api/[0-9]+/CustomerView.\nContents of GetCustomerFound.js is\nfunction GetTemplateResponse (request, state, logger) { response = JSON.parse(\u0026#34;\u0026lt;%- stringify(filename, \u0026#39;StubTemplate\\\\CustomerFoundView.json\u0026#39;) %\u0026gt;\u0026#34;); var ext =require(\u0026#39;../../../StubResponse/ResponseInjection/extractrequest\u0026#39;); var reqdata = ext.extractor(request); response.data.customerID=reqdata.CustomerID; return { statusCode : 200, headers: { \u0026#39;Content-Type\u0026#39;: \u0026#39;application/json; charset=utf-8\u0026#39; }, body: response }; } The javascript file have a single function , which reads the stubbed response kept in template file . Then it calls another Javascript function to called \u0026ldquo;extractrequest\u0026rdquo;. We will see the details of it soon. For now, it actually returns the customer number from the request . For eg, if request is \u0026ldquo;http://localhost:4547/Blog.Api/3123/CustomerView \u0026quot; then it return 3123 as customer ID. Once we extract the customer ID, then it will replace the customer ID in our template response with the value coming from request and return the response.\nLet us take a close look at the extractrequest function.\nmodule.exports = {extractor:function extractCIFAndPackageID (request) { if(request \u0026amp;\u0026amp; request.path) { var req = request.path.split(\u0026#39;/\u0026#39;); if(req.length \u0026gt;2 \u0026amp;\u0026amp; req[1]) { return { CustomerID: req[2] } } } return null; }} This method will take the input parameter as the request and split it at \u0026ldquo;/\u0026rdquo; to get a an array . Then we will return the array[2] which is the customer ID from the request\nFinally , the template response\nCustomerFoundView.json\n{ \u0026#34;status\u0026#34;: \u0026#34;success\u0026#34;, \u0026#34;code\u0026#34;: 0, \u0026#34;message\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;data\u0026#34;: { \u0026#34;customerID\u0026#34;: \u0026#34;123\u0026#34;, \u0026#34;firstName\u0026#34;: \u0026#34;John\u0026#34;, \u0026#34;lastName\u0026#34;: \u0026#34;Citizen\u0026#34;, \u0026#34;email\u0026#34;: \u0026#34;John.Citizen@abcabacas.com\u0026#34; } } Now let us fire up mountebank\nMake few request using postman, which have different request parameter\nAnother request\nIn above two examples,we can see the CustomerID field is response is updated with number extracted from request.\nNow let us try another example , where request is http://localhost:4547/Blog.Api/1234542323/CustomerView\nAs you can see, we are getting a customer Not found response. This is due to the order of predicates we use. In our 4547.json, the order of response are as below.\nCustomer Not found which has a predicate of \u0026ldquo;/Blog.Api/1[0-9]+/CustomerView\u0026rdquo;\nCustomer found which has a predicate of \u0026ldquo;/Blog.Api/[0-9]+/CustomerView\u0026rdquo;\nAs you can see from above order, when a request comes through , mountebank will first match with predicate of first response and if it matches, it returns the response. If not, mountebank will keep trying with next one followed by all others. In this particular example, since our request have a customer ID of 1234542323, it matches with regular expression of first one ( 1[0-9]+) and hence it return customer not found response.\nIn next blog post, I will provide more insights about how to extract request from different type of requests.\n","permalink":"https://abygeorgea.com/blog/2017/04/06/mountebank-creating-a-response-based-on-a-file-template-and-modifying-it-based-on-request-part-2/","summary":"\u003cp\u003eThis is an extension to my previous blog about how we can use mountebank to create a stubbed response based on a template file . You can read about it \u003ca href=\"/blog/2017/04/07/mountebank-creating-a-response-based-on-a-file-template-and-modifying-it-based-on-request-part-1/\"\u003ehere\u003c/a\u003e.  In this step by step example, I will explain how we will use mountebank to modify the response based on the request . Before we start, please ensure you are familiar with \u003ca href=\"/blog/2017/04/07/mountebank-creating-a-response-based-on-a-file-template-and-modifying-it-based-on-request-part-1/\"\u003ePart1 \u003c/a\u003eof the excercise. If you need to know more about mountebank and how to use mountebank , please read through \u003ca href=\"/blog/2017/02/13/service-virtualisation-using-mountebank/\"\u003ehow to install mountebank \u003c/a\u003e and \u003ca href=\"/blog/2017/03/03/mountebank-your-first-service-virtualisation/\"\u003eservice virtualisation using mountebank\u003c/a\u003e.\u003c/p\u003e","title":"Mountebank - Creating a response based on a file template and modifying it"},{"content":"In the previous two blog post, I have explained about how to setup mountebank (here) and how to create a virtualised respone(here) . Now coming to more detailed use cases which we might encounter in daily life. In this blog post, I will explain how we can use mountebank to create a virtualised response based on a template response stored in a file and modifying certain fields in response based on the request coming through.\nIn below Step by Step example , I will have two mock responses for searching for a customer details. First response is when customer is not available in back end systems and second response is when customer details are found.\nBefore we start, below is folder structure which I have and in this blog post we are discussing about only one stubbed response, which is the NOT FOUND scenario.\nLet us first create the imposter.ejs file\n{ \u0026#34;imposters\u0026#34;: [ \u0026lt;% include 4547.json %\u0026gt; ] } Now let us create the file which specifies the port number where it should run and order of responses. Below code tells mountebank that port which it needs to listen for incoming request is 4547 and protocol is http. There are two set of mock responses planned.\n{ \u0026#34;port\u0026#34;: 4547, \u0026#34;protocol\u0026#34;: \u0026#34;http\u0026#34;, \u0026#34;stubs\u0026#34;: [ { \u0026lt;% include CustomerNotFound.json %\u0026gt; }, { \u0026lt;% include CustomerFound.json %\u0026gt; } ] } In this example, let us look at first mock response.\n\u0026#34;responses\u0026#34;: [ { \u0026#34;inject\u0026#34;: \u0026#34;\u0026lt;%- stringify(filename, \u0026#39;ResponseInjection\\\\GetCustomerNotFound.js\u0026#39;) %\u0026gt;\u0026#34; } ], \u0026#34;predicates\u0026#34;: [ { \u0026#34;matches\u0026#34;: { \u0026#34;method\u0026#34; : \u0026#34;GET\u0026#34;, \u0026#34;path\u0026#34; : \u0026#34;/Blog.Api/1[0-9]+/CustomerView\u0026#34; } } ] From above response, we can infer below. When ever an http GET request come to port 4547 , with a path matching \u0026ldquo;/Blog.Api/1[0-9]+/CustomerView\u0026rsquo;, then we will call the Javascript function \u0026ldquo;GetCustomerNotFound.js\u0026rdquo; which is kept inside a directory \u0026ldquo;ResponseInjection\u0026rdquo; in same location. It is also good to notice that , predicate is a regular expression ( hence use matches) and all request where 1 followed by any number of numeric will be returned with this response\nThe javascript function listed here is responsible for reading the sample template response and sending it back .\nfunction GetTemplateResponse (request, state, logger) { response = JSON.parse(\u0026#34;\u0026lt;%- stringify(filename, \u0026#39;StubTemplate\\\\CustomerNotFoundView.json\u0026#39;) %\u0026gt;\u0026#34;); return { statusCode : 404, headers: { \u0026#39;Content-Type\u0026#39;: \u0026#39;application/json; charset=utf-8\u0026#39; }, body: response }; } Above function reads a json response kept inside directory \u0026ldquo;StubTemplate\u0026rdquo; and convert it to json and return to mountebank. Since this is for a scenario where customer records are not found,we set the status code as 404. We can also set the headers if needed\nThe stub template is as below\n{ \u0026#34;status\u0026#34;: \u0026#34;fail\u0026#34;, \u0026#34;code\u0026#34;: \u0026#34;CUSTOMER_NOT_FOUND\u0026#34;, \u0026#34;message\u0026#34;: \u0026#34;Customer details not found.\u0026#34; } Now let us run mountebank\nRequest through postman\nAs you can see , the GET request matching with predicate is returning the stubbed response with status 404.\nFor real time usage for testing any web application which needs to get a 404 message from back end API calls, just point the end point to this local host end point and fire a request which matches the predicate.\nDetails of second response will be shared in next blog post\n","permalink":"https://abygeorgea.com/blog/2017/04/06/mountebank-creating-a-response-based-on-a-file-template-and-modifying-it-based-on-request-part-1/","summary":"\u003cp\u003eIn the previous two blog post, I have explained about how to setup mountebank (\u003ca href=\"/blog/2017/02/13/service-virtualisation-using-mountebank/\"\u003ehere\u003c/a\u003e) and how to create a virtualised respone(\u003ca href=\"/blog/2017/03/03/mountebank-your-first-service-virtualisation/\"\u003ehere\u003c/a\u003e) . Now coming to more detailed use cases which we might encounter in daily life. In this blog post, I will explain how we can use mountebank to create a virtualised response based on a template response stored in a file and modifying certain fields in response based on the request coming through.\u003c/p\u003e","title":"Mountebank - Creating a response based on a file template and modifying it"},{"content":"How to create HTML report with details of test execution Very often , we will be required to create a report with details of test execution , so that it can be presented to various stakeholders. Specflow provides a feature to create HTML reports. Let us look into more details about how is this done\nRead through and understand details of reporting from specflow. Ensure packages for Specflow, Nunit, Nunit console runner are already installed. If you are using Nunit 3, install NUnit.Extension.NUnitV2ResultWriter package via nuget package manager. If this is not installed, we will get an error \u0026ldquo;Unknown result format: nunit2\u0026rdquo;. Follow setups required for running specflow test cases from command line. Details can be found here. Modify the bat file to create nunit2 reports. PathToNunitConsolerunner\\nunit3-console.exe --labels=All --out=TestResult.txt \u0026#34;--result=TestResult.xml;format=nunit2\u0026#34; PathTo\\AcceptanceTests.dll Add below command into Bat file. This will create HTML Report called \u0026ldquo;MyResult.html\u0026rdquo; PathToSpecfloPackage\\specflow.exe nunitexecutionreport PathTo\\AcceptanceTests.csproj /out:MyResult.html Final bat file will look like below. REM bat file to run test cases from console and create xml result file .\\..\\..\\..\\packages\\NUnit.ConsoleRunner.3.8.0\\tools\\nunit3-console.exe --labels=All --out=TestResult.txt \u0026#34;--result=TestResult.xml;format=nunit2\u0026#34; .\\AcceptanceTest.dll REM Generate html report from test output .\\..\\..\\..\\packages\\SpecFlow.2.1.0\\tools\\specflow.exe nunitexecutionreport .\\..\\..\\AcceptanceTest.csproj /out:MyResult.html EDITED: If it throws below error in newer version of Visual Studio then ensure MS Build tool 2013 is installed. It can be downloaded from https://www.microsoft.com/en-US/download/details.aspx?id=40760\nError : \u0026ldquo;The tools version \u0026ldquo;12.0\u0026rdquo; is unrecognized. Available tools versions are \u0026ldquo;2.0\u0026rdquo;, \u0026ldquo;3.5\u0026rdquo;, \u0026ldquo;4.0\u0026rdquo;. \u0026ldquo;,\n","permalink":"https://abygeorgea.com/blog/2017/03/05/creating-html-report-for-test-execution-result/","summary":"\u003ch3 id=\"how-to-create-html-report-with-details-of-test-execution\"\u003eHow to create HTML report with details of test execution\u003c/h3\u003e\n\u003cp\u003eVery often , we will be required to create a report with details of test execution , so that it can be presented to various stakeholders. Specflow provides a feature to create HTML reports. Let us look into more details about how is this done\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eRead through and understand details of reporting from \u003ca href=\"https://github.com/techtalk/SpecFlow/wiki/Reporting\"\u003especflow\u003c/a\u003e.\u003c/li\u003e\n\u003cli\u003eEnsure packages for Specflow, Nunit, Nunit console runner are already installed.\u003c/li\u003e\n\u003cli\u003eIf you are using Nunit 3, install NUnit.Extension.NUnitV2ResultWriter package via nuget package manager. If this is not installed, we will get an error \u0026ldquo;Unknown result format: nunit2\u0026rdquo;.\u003c/li\u003e\n\u003cli\u003eFollow setups required for running specflow test cases from command line. Details can be found \u003ca href=\"/blog/2017/03/04/running-specflow-test-from-command-line-using-nunit\"\u003ehere\u003c/a\u003e.\u003c/li\u003e\n\u003cli\u003eModify the bat file to create nunit2 reports.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ePathToNunitConsolerunner\\nunit3-console.exe --labels=All --out=TestResult.txt \u0026#34;--result=TestResult.xml;format=nunit2\u0026#34; PathTo\\AcceptanceTests.dll\n\u003c/code\u003e\u003c/pre\u003e\u003cul\u003e\n\u003cli\u003eAdd below command into Bat file. This will create HTML Report called \u0026ldquo;MyResult.html\u0026rdquo;\u003c/li\u003e\n\u003c/ul\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ePathToSpecfloPackage\\specflow.exe nunitexecutionreport PathTo\\AcceptanceTests.csproj /out:MyResult.html\n\u003c/code\u003e\u003c/pre\u003e\u003cul\u003e\n\u003cli\u003eFinal bat file will look like below.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eREM bat file to run test cases from console and create xml result file\n.\\..\\..\\..\\packages\\NUnit.ConsoleRunner.3.8.0\\tools\\nunit3-console.exe  --labels=All --out=TestResult.txt \u0026#34;--result=TestResult.xml;format=nunit2\u0026#34; .\\AcceptanceTest.dll\n\nREM Generate html report from test output\n.\\..\\..\\..\\packages\\SpecFlow.2.1.0\\tools\\specflow.exe nunitexecutionreport .\\..\\..\\AcceptanceTest.csproj /out:MyResult.html\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eEDITED: If it throws below error in newer version of Visual Studio  then ensure MS Build tool 2013 is installed. It can be downloaded from \u003ca href=\"https://www.microsoft.com/en-US/download/details.aspx?id=40760\"\u003ehttps://www.microsoft.com/en-US/download/details.aspx?id=40760\u003c/a\u003e\u003c/p\u003e","title":"Creating HTML report for test execution result"},{"content":"How to run specflow test cases from command line We can use nunit console runner for running specflow test cases from command line. Running specflow test cases through nunit console runner will help to create test results in xml file, which can then be used for creating html reports.\nProcedure for command line test execution are\nDefine Nunit as the test runner. This is done in config file \u0026lt;specFlow\u0026gt; \u0026lt;unitTestProvider name=\u0026#34;NUnit\u0026#34;/\u0026gt; \u0026lt;/specFlow\u0026gt; Include Nunit.Console.Runner package to solution via nuget package manager Run specflow test cases using below command. We can create a bat file with below command and execute them as required. pathToNunitConsoleRunner\\nunit3-console.exe PathToProject.dll example will be .\\..\\..\\..\\packages\\NUnit.ConsoleRunner.3.8.0\\tools\\nunit3-console.exe .\\BDDFramework.dll ","permalink":"https://abygeorgea.com/blog/2017/03/04/running-specflow-test-from-command-line-using-nunit/","summary":"\u003ch3 id=\"how-to-run-specflow-test-cases-from-command-line\"\u003eHow to run specflow test cases from command line\u003c/h3\u003e\n\u003cp\u003eWe can use nunit console runner for running specflow test cases from command line. Running specflow test cases through nunit console runner will help to create test results in xml file, which can then be used for creating html reports.\u003c/p\u003e\n\u003cp\u003eProcedure for command line test execution are\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eDefine Nunit as the test runner.  This is done in config file\u003c/li\u003e\n\u003c/ol\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;specFlow\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e          \u003cspan style=\"color:#f92672\"\u003e\u0026lt;unitTestProvider\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ename=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;NUnit\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;/specFlow\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003col start=\"2\"\u003e\n\u003cli\u003eInclude Nunit.Console.Runner package to solution via nuget package manager\u003c/li\u003e\n\u003cli\u003eRun specflow test cases using below command. We can create a bat file with below command and execute them as required.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e\npathToNunitConsoleRunner\\nunit3-console.exe  PathToProject.dll\n\n example will be\n .\\..\\..\\..\\packages\\NUnit.ConsoleRunner.3.8.0\\tools\\nunit3-console.exe   .\\BDDFramework.dll\n\u003c/code\u003e\u003c/pre\u003e","title":"Running Specflow Test from command line using Nunit"},{"content":"In current development world, there will be scenarios were both API and its consumers are developed in parallel. Inorder to decouple their dependencies, we can mock an api response using mountebank. In this example, I will explain how to get started with your first service virtualisation using mountebank. After installing mountebank as mentioned in here (Install Mountebank), we will proceed with configuring mountebank. It can be done in few ways. The method which I explain below is by using file based configuration. This involve setting up an imposter file and a stub response\nHow to Create a Stub Navigate to mountebank installation path\nCreate a folder and name it as \u0026ldquo;StubResponse\u0026rdquo;. ( You can name it whatever you want)\nCreate two json file using notepad and save it as \u0026ldquo;MockResponeForApiOne.json\u0026rdquo; and \u0026ldquo;MockResponeForApiTwo.json\u0026rdquo;( Or what ever you want).\nCopy paste below code to \u0026ldquo;MockResponeForApiOne.json\u0026rdquo; . Sample example only. Update the response and predicates to suite your need ( if required)\n\u0026#34;responses\u0026#34;: [ { \u0026#34;is\u0026#34;: { \u0026#34;statusCode\u0026#34;: 200, \u0026#34;body\u0026#34;: { \u0026#34;Text\u0026#34;:\u0026#34;Response ONE \u0026#34;,\u0026#34;token\u0026#34;:\u0026#34;username\u0026#34;,\u0026#34;expires_in\u0026#34;:90 } } } ], \u0026#34;predicates\u0026#34;: [ { \u0026#34;exists\u0026#34;: { \u0026#34;body\u0026#34; : { \u0026#34;username\u0026#34;: true,\u0026#34;password\u0026#34; : true }, \u0026#34;method\u0026#34; : \u0026#34;POST\u0026#34;, \u0026#34;path\u0026#34; : \u0026#34;/Apitesting/v1/test?type=ResponseOne\u0026#34; } } ] Copy paste below code to \u0026ldquo;MockResponeForApiTwo.json\u0026rdquo; . Sample example only. Update the response and predicates to suite your need ( if required) \u0026#34;responses\u0026#34;: [ { \u0026#34;is\u0026#34;: { \u0026#34;statusCode\u0026#34;: 200, \u0026#34;body\u0026#34;: { \u0026#34;Text\u0026#34;:\u0026#34;Response TWO \u0026#34;,\u0026#34;token\u0026#34;:\u0026#34;emailAddress\u0026#34;,\u0026#34;expires_in\u0026#34;:90 } } } ], \u0026#34;predicates\u0026#34;: [ { \u0026#34;exists\u0026#34;: { \u0026#34;body\u0026#34; : { \u0026#34;email\u0026#34;: true,\u0026#34;password\u0026#34; : true }, \u0026#34;method\u0026#34; : \u0026#34;POST\u0026#34;, \u0026#34;path\u0026#34; : \u0026#34;/Apitesting/v1/test?type=ResponseTwo\u0026#34; } } ] How to create an Imposter Create another file called test.json in same path as above\ncopy and paste below contents to it\n{ \u0026#34;imposters\u0026#34;: [ { \u0026#34;port\u0026#34;: 4547, \u0026#34;protocol\u0026#34;: \u0026#34;http\u0026#34;, \u0026#34;stubs\u0026#34;: [ { \u0026lt;% include MockResponseForApiOne.json %\u0026gt; }, { \u0026lt;% include MockResponseForApiTwo.json %\u0026gt; } ] } ] } Let us have a close look into Imposter and stubs Responses – Contains an array of responses expected to return for the defined stub. In the above scenario the response will include status code as 200 and response body. For more info, http://www.mbtest.org/docs/api/contracts Predicates – is an array of predicates which will be used during matching process. Predicate object can be quite complex, it supports lots of different matching techniques. For more info, http://www.mbtest.org/docs/api/predicates\nLet\u0026rsquo;s Mock it Once all required files are created and saved, mountebank can be started by following command in command prompt , after navigating to installation folder of mountebank\nmb --configfile StubResponse/test.json Once mountebank is started, we can verify it by navigating to path http://localhost:2525/imposters\nIt will list out all active ports and a list of stubs available\nTest It Once we complete above steps, mountebank is ready with stubs. Now comes the part to test it and use. You can use any api testing tool ( Postman, soapUi etc ) for testing this. Just send the request matching the predicates and look for the responses\nBelow are the screenshot of Postman request\nRequesting for First API.\nPredicate of response One says that , request has to be of type POST, body of request should have \u0026ldquo;username\u0026rdquo; and \u0026ldquo;password\u0026rdquo; . Path of the request should have /Apitesting/v1/test?type=ResponseOne\u0026quot;\nNow construct a postman request matching above and fire it\nRequest for second API\nPredicate of response One says that , request has to be of type POST, body of request should have \u0026ldquo;email\u0026rdquo; and \u0026ldquo;password\u0026rdquo; . Path of the request should have /Apitesting/v1/test?type=ResponseTwo\u0026quot;\nNow construct a postman request matching above and fire it\nAs you can see, both request has succesfully received expected response message\nFor actual development usage, just point your application to this localhost URL and start consuming virtualised API\n","permalink":"https://abygeorgea.com/blog/2017/03/03/mountebank-your-first-service-virtualisation/","summary":"\u003cp\u003eIn current development world, there will be scenarios were both API and its consumers are developed in parallel. Inorder to decouple their dependencies, we can mock an api response using mountebank. In this example, I will explain how to get started with your first service virtualisation using mountebank. After installing mountebank as mentioned in \u003ca href=\"/blog/2017/02/13/service-virtualisation-using-mountebank/\"\u003ehere (Install Mountebank)\u003c/a\u003e, we will proceed with configuring mountebank. It can be done in few ways. The method which I explain below is by using file based configuration. This involve setting up an imposter file and a stub response\u003c/p\u003e","title":"Mountebank - Your first service Virtualisation"},{"content":"What is Mountebank? In short mountebank is a open source service virtualisation tool . Mountebank uses imposters to act as on demand test doubles. Hence our test cases communicate to Mountebank and mountebank responds back with relevant stubs as defined.\nHow to Setup Mountebank ? Installation can be done via two methods\nnpm Mountebank can be installed as a npm package. Node.js should be installed for this option to work\n\u0026lt;code\u0026gt;npm install -g mountebank\u0026lt;/code\u0026gt; Self contained Installation file OS Specific installation file can be downloaded from Download\nNote: Please read through the windows path limitation mentioned in above link\n","permalink":"https://abygeorgea.com/blog/2017/02/13/service-virtualisation-using-mountebank/","summary":"\u003ch1 id=\"what-is-mountebank\"\u003eWhat is Mountebank?\u003c/h1\u003e\n\u003c!-- raw HTML omitted --\u003e\n\u003cp\u003eIn short mountebank is a open source service virtualisation tool . Mountebank uses imposters to act as on demand test doubles. Hence our test cases communicate to Mountebank and mountebank responds back with relevant stubs as defined.\u003c/p\u003e\n\u003ch1 id=\"how-to-setup-mountebank-\"\u003eHow to Setup Mountebank ?\u003c/h1\u003e\n\u003cp\u003eInstallation can be done via two methods\u003c/p\u003e\n\u003ch3 id=\"npm\"\u003enpm\u003c/h3\u003e\n\u003cp\u003eMountebank can be installed as a npm package. Node.js should be installed for this option to work\u003c/p\u003e","title":"Service Virtualisation Using Mountebank"},{"content":"The traditional approach for automating UI test cases is to create selenium web driver based ( or any UI testing tools) scripts for exercising complete end to end flow. However, it comes with its own challenges. It will have multiple steps as pre-requiste for reaching required UI page and hence it behaves as an E2E integration test rather than UI test.\nA typical web application architecture will have one or more front-end application, which will talk to multiple back-end services, API\u0026rsquo;s etc. They will, in turn, talk to other back-end services or to different databases. On High level , architecture looks like below On an enterprise world, all these will be developed and maintained by different teams. All of them will be working in parallel and will push in their code changes ( including occasional broken code) frequently. This will result in breakages since test automation scripts heavily depending on UI and its integration. Even if there is no broken code, a test can still fail due to multiple environmental issues for any of the backend services and other components. Hence it will become increasingly difficult for achieving a green build.\nHence UI based test cases are less robust due different reasons like\nTest depends on external factors which are outside of our control and not part of scope of testing\nFailing test may not pin point exact location of failure since it is trying to test too many things.\nThere are chances that all components will not be ready when we want to test UI. Hence testing it pushed to the end , which will increase cost of fixing defects.\nRe - running of test cases may pass (if failure is caused by environmental issues)\nUI test are brittle by nature since they will even fail due to timing issues because it is depending on data from back end services.\nThe solution for above is to adopt more unit test like structure for UI testing. We should be testing UI in isolation to other back-end services and their dependency. This allows testing as much as possible early in lifecycle without any dependency on other streams. We should replace all backend service calls with stubs\nMountebank is a tool which we can use for mocking the service calls. As per mbtest.org, mountebank is the first open source tool to provide cross-platform, multi-protocol test doubles over the wire. We can use mountebank for stubbing the back-end service calls and there by use it for decoupling UI from unpredictable back end.\n","permalink":"https://abygeorgea.com/blog/2017/02/12/ui-testing-in-isolation/","summary":"\u003cp\u003eThe traditional approach for automating UI test cases is to create selenium web driver based ( or any UI testing tools) scripts for exercising complete end to end flow. However, it comes with its own challenges. It will have multiple steps as pre-requiste for reaching required UI page and hence it behaves as an E2E integration test rather than UI test.\u003c/p\u003e\n\u003cp\u003eA typical web application architecture will have one or more front-end application, which will talk to multiple back-end services, API\u0026rsquo;s etc. They will, in turn, talk to other back-end services or to different databases. On High level , architecture looks like below\n\u003cimg loading=\"lazy\" src=\"/images/2017/02/02/UIinIsolation_1.png\"\u003e\u003c/p\u003e","title":"UI Testing- decoupling back end dependency"},{"content":"Two options for copying files are below.\nRobocopy - More details can be found at Robocopy Copy_Item cmdlet - More details can be found at Copy-Item Copying Folder structure Only $source = \u0026#34;C:\\tools\\DevKit2\\octopress-blog\\source\u0026#34; $dest = \u0026#34;D:\\delete\u0026#34; Copy-Item $source $dest -Filter {PSIsContainer} -Recurse -Force #OR robocopy $source $dest /e /xf *.* # /e denotes all folder including empty folders. /xf denotes all files except one of format *.* # /e can be replaced with /s for ignoring empty folders Flattening Folder structure - Copy all files from nested folders to a single folder $source = \u0026#34;C:\\tools\\DevKit2\\octopress-blog\\source\u0026#34; $dest = \u0026#34;D:\\delete\u0026#34; # Below is required only if we need to create destination folder. Uncomment below line if folder needs to be created #New-Item $dest -type directory Get-ChildItem $source -Recurse | ` Where-Object { $_.PSIsContainer -eq $False } | ` ForEach-Object {Copy-Item -Path $_.Fullname -Destination $dest -Force} Copy same folder structure $source = \u0026#34;C:\\tools\\DevKit2\\octopress-blog\\source\u0026#34; $dest = \u0026#34;D:\\delete\u0026#34; robocopy $source $dest /e ","permalink":"https://abygeorgea.com/blog/2016/10/10/powershell-copying-folders-and-files/","summary":"\u003cp\u003eTwo options for copying files are below.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eRobocopy - More details can be found at \u003ca href=\"https://technet.microsoft.com/en-us/library/cc733145.aspx\"\u003eRobocopy\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eCopy_Item cmdlet - More details can be found at \u003ca href=\"https://msdn.microsoft.com/en-us/powershell/reference/5.1/microsoft.powershell.management/copy-item\"\u003eCopy-Item\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"copying-folder-structure-only\"\u003eCopying Folder structure Only\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-plain\" data-lang=\"plain\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e$source = \u0026#34;C:\\tools\\DevKit2\\octopress-blog\\source\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e$dest = \u0026#34;D:\\delete\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eCopy-Item $source $dest -Filter {PSIsContainer} -Recurse -Force\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e#OR\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003erobocopy $source $dest /e /xf *.*\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e# /e denotes all folder including empty folders. /xf denotes all files except one of format *.*\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e# /e can be replaced with /s for ignoring empty folders\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"flattening-folder-structure---copy-all-files-from-nested-folders-to-a-single-folder\"\u003eFlattening Folder structure - Copy all files from nested folders to a single folder\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-plain\" data-lang=\"plain\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e$source = \u0026#34;C:\\tools\\DevKit2\\octopress-blog\\source\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e$dest = \u0026#34;D:\\delete\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e# Below is required only if we need to create destination folder. Uncomment below line if folder needs to be created\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e#New-Item $dest -type directory \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eGet-ChildItem $source -Recurse | `\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    Where-Object { $_.PSIsContainer -eq $False } | `\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    ForEach-Object {Copy-Item -Path $_.Fullname -Destination $dest -Force} \n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"copy-same-folder-structure\"\u003eCopy same folder structure\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-plain\" data-lang=\"plain\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e$source = \u0026#34;C:\\tools\\DevKit2\\octopress-blog\\source\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e$dest = \u0026#34;D:\\delete\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003erobocopy $source $dest /e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e","title":"Powershell - Copying Folders and Files"},{"content":"Git for windows is normally shipped with long path support disabled due to mysys not supporting file path/name greater than 260 character. While cloning repository with large nested directory structute may cause error \u0026ldquo;file name too long\u0026rdquo;. This can be fixed by below command. It can be executed using powershell or cmd directly in project ( or anywhere if git variable is available)\ngit config --system core.longpaths true ","permalink":"https://abygeorgea.com/blog/2016/09/23/git-how-to-solve-filename-too-long-error/","summary":"\u003cp\u003eGit for windows is normally shipped with long path support disabled due to mysys not supporting file path/name greater than 260 character. While cloning repository with large nested directory structute may cause error \u0026ldquo;file name too long\u0026rdquo;. This can be fixed by below command. It can be executed using powershell or cmd directly in project ( or anywhere if git variable is available)\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-plain\" data-lang=\"plain\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003egit config --system core.longpaths true\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e","title":"Git - How to solve filename too long error"},{"content":"Code Snippet\nprivate void RunCLIjobsOnLocal(string arguments, int WaitTimePerCommand) { var psi = new ProcessStartInfo(); psi.CreateNoWindow = true; //This hides the dos-style black window that the command prompt usually shows psi.FileName = @\u0026#34;cmd.exe\u0026#34;; psi.Arguments = \u0026#34;/C \u0026#34; + arguments; psi.RedirectStandardOutput = true; psi.RedirectStandardInput = true; psi.RedirectStandardError = true; psi.UseShellExecute = false; var sspw = new SecureString(); foreach (var c in password) { sspw.AppendChar(c); } psi.Domain = domain; psi.UserName = userName; psi.Password = sspw; psi.WorkingDirectory = @\u0026#34;C:\\\u0026#34;; using (Process process = new Process()) { try { process.StartInfo = psi; process.Start(); var procId = process.Id; string owner = GetProcessOwner(procId); // Synchronously read the standard output of the spawned process. StreamReader reader = process.StandardOutput; string output = reader.ReadToEnd(); reader = process.StandardError; string error = reader.ReadToEnd(); if(error.Length \u0026gt;0) process.WaitForExit(); } catch (Exception e) { log.Error(e.Message + \u0026#34;\\n\u0026#34; + e.StackTrace); } } } private string GetProcessOwner(int processId) { string query = \u0026#34;Select * From Win32_Process Where ProcessID = \u0026#34; + processId; ManagementObjectSearcher searcher = new ManagementObjectSearcher(query); ManagementObjectCollection processList = searcher.Get(); foreach (ManagementObject obj in processList) { string[] argList = new string[] { string.Empty, string.Empty }; int returnVal = Convert.ToInt32(obj.InvokeMethod(\u0026#34;GetOwner\u0026#34;, argList)); if (returnVal == 0) { return argList[1] + \u0026#34;\\\\\u0026#34; + argList[0]; } } return \u0026#34;NO OWNER\u0026#34;; } ","permalink":"https://abygeorgea.com/blog/2016/09/08/running-command-line-from-csharp/","summary":"\u003cp\u003eCode Snippet\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#66d9ef\"\u003eprivate\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003evoid\u003c/span\u003e RunCLIjobsOnLocal(\u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e arguments, \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e WaitTimePerCommand)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e psi = \u003cspan style=\"color:#66d9ef\"\u003enew\u003c/span\u003e ProcessStartInfo();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            psi.CreateNoWindow = \u003cspan style=\"color:#66d9ef\"\u003etrue\u003c/span\u003e; \u003cspan style=\"color:#75715e\"\u003e//This hides the dos-style black window that the command prompt usually shows\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            psi.FileName = \u003cspan style=\"color:#e6db74\"\u003e@\u0026#34;cmd.exe\u0026#34;\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            psi.Arguments = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;/C \u0026#34;\u003c/span\u003e + arguments;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            psi.RedirectStandardOutput = \u003cspan style=\"color:#66d9ef\"\u003etrue\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            psi.RedirectStandardInput = \u003cspan style=\"color:#66d9ef\"\u003etrue\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            psi.RedirectStandardError = \u003cspan style=\"color:#66d9ef\"\u003etrue\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            psi.UseShellExecute = \u003cspan style=\"color:#66d9ef\"\u003efalse\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e sspw = \u003cspan style=\"color:#66d9ef\"\u003enew\u003c/span\u003e SecureString();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003eforeach\u003c/span\u003e (\u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e c \u003cspan style=\"color:#66d9ef\"\u003ein\u003c/span\u003e password)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                sspw.AppendChar(c);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            psi.Domain = domain;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            psi.UserName = userName;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            psi.Password = sspw;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            psi.WorkingDirectory = \u003cspan style=\"color:#e6db74\"\u003e@\u0026#34;C:\\\u0026#34;\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003eusing\u003c/span\u003e (Process process = \u003cspan style=\"color:#66d9ef\"\u003enew\u003c/span\u003e Process())\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#66d9ef\"\u003etry\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    process.StartInfo = psi;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    process.Start();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    \u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e procId = process.Id;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    \u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e owner = GetProcessOwner(procId);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    \u003cspan style=\"color:#75715e\"\u003e// Synchronously read the standard output of the spawned process. \u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    StreamReader reader = process.StandardOutput;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    \u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e output = reader.ReadToEnd();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    reader = process.StandardError;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    \u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e error = reader.ReadToEnd();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e(error.Length \u0026gt;\u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                       process.WaitForExit();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#66d9ef\"\u003ecatch\u003c/span\u003e (Exception e)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    log.Error(e.Message + \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\\n\u0026#34;\u003c/span\u003e + e.StackTrace);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003eprivate\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e GetProcessOwner(\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e processId)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e query = \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Select * From Win32_Process Where ProcessID = \u0026#34;\u003c/span\u003e + processId;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            ManagementObjectSearcher searcher = \u003cspan style=\"color:#66d9ef\"\u003enew\u003c/span\u003e ManagementObjectSearcher(query);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            ManagementObjectCollection processList = searcher.Get();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003eforeach\u003c/span\u003e (ManagementObject obj \u003cspan style=\"color:#66d9ef\"\u003ein\u003c/span\u003e processList)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e[] argList = \u003cspan style=\"color:#66d9ef\"\u003enew\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e[] { \u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e.Empty, \u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e.Empty };\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e returnVal = Convert.ToInt32(obj.InvokeMethod(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;GetOwner\u0026#34;\u003c/span\u003e, argList));\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (returnVal == \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                   \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e argList[\u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e] + \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\\\\\u0026#34;\u003c/span\u003e + argList[\u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e];\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;NO OWNER\u0026#34;\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e","title":"Running Command Line from C#"},{"content":"In previous post, I have mentioned different ways of identifying web elements using XPath . Very often , we will have to identify child elements while automating using selenium. Let us consider below example . This is an HTML layout of table\n\u0026lt;table id=table1 style=\u0026#34;width:100%\u0026#34;\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;td\u0026gt;John\u0026lt;/th\u0026gt; \u0026lt;td\u0026gt;Smith\u0026lt;/th\u0026gt; \u0026lt;td\u0026gt;50\u0026lt;/th\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;td\u0026gt;Jill\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;Smith\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;50\u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;td\u0026gt;Eve\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;Jackson\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;94\u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/table\u0026gt; Assuming we need to iterate across all the rows to identify which row have the name \u0026ldquo;Eve\u0026rdquo; and then do some action on that row . This can be achieved by below\n// Identify the table header using ID IWebElement e = driver.FindElement(By.Id(\u0026#34;table1\u0026#34;)); /* Identify all child nodes for the webelement e (table) . Note the \u0026#34;.\u0026#34; in XPath which means to search within current node. */ IList\u0026lt;IWebElement\u0026gt; rowlist = e.FindElements(By.XPath(\u0026#34;.\\\\tr\u0026#34;)); foreach(var row in rowlist){ IList\u0026lt;IWebElement\u0026gt; collist = row.FindElements(By.XPath(\u0026#34;.\\\\td\u0026#34;)); // Now iterate over each value in collist to check whether it have \u0026#34;Eve\u0026#34;. } Important step here is to use \u0026ldquo;.\u0026rdquo; in XPath so that selenium limit its search for current node rather than everywhere on document. This will help to identify elements with respective to another element.\nFurther details of how to use XPath can be found in https://www.w3schools.com/xml/xpath_syntax.asp\nMain ones are\n/ - Search from root // - Search anywhere in document which match selection . - select current node .. - select parent of current node ","permalink":"https://abygeorgea.com/blog/2016/09/07/element-location-using-xpath-axis-part-2/","summary":"\u003cp\u003eIn previous post, I have mentioned different ways of identifying web elements using XPath . Very often , we will have to identify child elements while automating using selenium. Let us consider below example . This is an HTML layout of table\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-html\" data-lang=\"html\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u0026lt;\u003cspan style=\"color:#f92672\"\u003etable\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eid\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003etable1\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003estyle\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;width:100%\u0026#34;\u003c/span\u003e\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u0026lt;\u003cspan style=\"color:#f92672\"\u003etr\u003c/span\u003e\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u0026lt;\u003cspan style=\"color:#f92672\"\u003etd\u003c/span\u003e\u0026gt;John\u0026lt;/\u003cspan style=\"color:#f92672\"\u003eth\u003c/span\u003e\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u0026lt;\u003cspan style=\"color:#f92672\"\u003etd\u003c/span\u003e\u0026gt;Smith\u0026lt;/\u003cspan style=\"color:#f92672\"\u003eth\u003c/span\u003e\u0026gt; \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u0026lt;\u003cspan style=\"color:#f92672\"\u003etd\u003c/span\u003e\u0026gt;50\u0026lt;/\u003cspan style=\"color:#f92672\"\u003eth\u003c/span\u003e\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u0026lt;/\u003cspan style=\"color:#f92672\"\u003etr\u003c/span\u003e\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u0026lt;\u003cspan style=\"color:#f92672\"\u003etr\u003c/span\u003e\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u0026lt;\u003cspan style=\"color:#f92672\"\u003etd\u003c/span\u003e\u0026gt;Jill\u0026lt;/\u003cspan style=\"color:#f92672\"\u003etd\u003c/span\u003e\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u0026lt;\u003cspan style=\"color:#f92672\"\u003etd\u003c/span\u003e\u0026gt;Smith\u0026lt;/\u003cspan style=\"color:#f92672\"\u003etd\u003c/span\u003e\u0026gt; \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u0026lt;\u003cspan style=\"color:#f92672\"\u003etd\u003c/span\u003e\u0026gt;50\u0026lt;/\u003cspan style=\"color:#f92672\"\u003etd\u003c/span\u003e\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u0026lt;/\u003cspan style=\"color:#f92672\"\u003etr\u003c/span\u003e\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u0026lt;\u003cspan style=\"color:#f92672\"\u003etr\u003c/span\u003e\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u0026lt;\u003cspan style=\"color:#f92672\"\u003etd\u003c/span\u003e\u0026gt;Eve\u0026lt;/\u003cspan style=\"color:#f92672\"\u003etd\u003c/span\u003e\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u0026lt;\u003cspan style=\"color:#f92672\"\u003etd\u003c/span\u003e\u0026gt;Jackson\u0026lt;/\u003cspan style=\"color:#f92672\"\u003etd\u003c/span\u003e\u0026gt; \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u0026lt;\u003cspan style=\"color:#f92672\"\u003etd\u003c/span\u003e\u0026gt;94\u0026lt;/\u003cspan style=\"color:#f92672\"\u003etd\u003c/span\u003e\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u0026lt;/\u003cspan style=\"color:#f92672\"\u003etr\u003c/span\u003e\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u0026lt;/\u003cspan style=\"color:#f92672\"\u003etable\u003c/span\u003e\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eAssuming we need to iterate across all the rows to identify which row have the name \u0026ldquo;Eve\u0026rdquo; and then do some action on that row . This can be achieved by below\u003c/p\u003e","title":"Element Location Using XPath Axis Part 2"},{"content":"Very frequently testers will meet a situation where they need to take screenshot of webpage they are testing , either for base line or as a proof of test result. This is same with automated testing . Even though automated test cases have their own of way of publishing test results, it is always desirable to keep a proof of result.. Screenshot come to help in this regards. In this blog post , I will explain , how to take a screenshot with Selenium Web Driver with C#. In future I will add another couple of post to explain , how to consolidate the screenshots into a PDF document.\nIn .NET binding, we have an interface called ITakesScreenshot , which helps to capture screenshot of active window. Below code will help to take screenshot and save it in the path specified while calling the function\n// PathToFolder is the location where we need to save the screenshot // FileName is another string where PathToFolder is appended with timestamp public void TakeScreenShot(string PathToFolder) { string fileName = PathToFolder + DateTime.Now.ToString(\u0026#34;HHmmss\u0026#34;) +\u0026#34;.jpeg\u0026#34;; Screenshot cp = ((ITakesScreenshot)driver).GetScreenshot(); cp.SaveAsFile(fileName, System.Drawing.Imaging.ImageFormat.Jpeg); } ","permalink":"https://abygeorgea.com/blog/2016/09/06/how-to-take-screenshots-with-selenium-in-c/","summary":"\u003cp\u003eVery frequently testers will meet a situation where they need to take screenshot of webpage they are testing , either for base line or as a proof of test result. This is same with automated testing . Even though automated test cases have their own of way of publishing test results, it is always desirable to keep a proof of result.. Screenshot come to help in this regards. In this blog post , I will explain , how to take a screenshot with Selenium Web Driver with C#. In future I will add another couple of post to explain , how to consolidate the screenshots into a PDF document.\u003c/p\u003e","title":"How to take screenshots with Selenium in C#"},{"content":"During testing we will sometimes come up to situations where developers are not following best practises for testability . We will frequently come up situations where elements doesn\u0026rsquo;t have any unique identifiable property. XPath axis comes to help in those situations. We can identify elements using various XPath Properties\nList of various XPath Axis are available in https://developer.mozilla.org/en-US/docs/Web/XPath/Axes If you have well-defined properties to identify the element, use them as your locator. Please read locator strategy Using XPath and Other Parameters\nBelow are major one\u0026rsquo;s which we will frequently use\n1. ancestor This selects all ancestors of current node. That will include parent, grand parents etc Eg :\n//td[text()=\u0026#39;Product Desc\u0026#39;]/ancestor::tr 2. descendant This selects all children of current node. That will include child, grand child etc Eg:\n/table/descendant::td/input 3. followingis Th selects everything after the closing tag of current node Eg:\n//td[text()=\u0026#39;Product Desc\u0026#39;]/following::tr 4. following-sibling This selects all siblings after the closing tah of current node. Eg:\n//td[text()=\u0026#39;Product Desc\u0026#39;]/followingsibling::td 5. preceding This selects everything prior to current node Eg:\n//td[text()=\u0026#39;Add to cart\u0026#39;]/preceding::tr preceding-sibling This selects all siblings prior to current node Eg:\n//td[text()=\u0026#39;Add to cart\u0026#39;]/precedingsibling::td 7. child This selects all children of current node\n8. parent This select parent of current node\nAs usual , you can always use combinations of above in your test. Statements can be constructed in the same way as we traverse the XPath axis\nLast , but not least\u0026hellip; we can also use regular expression in XPath.\n","permalink":"https://abygeorgea.com/blog/2016/09/03/element-location-using-xpath-axis/","summary":"\u003cp\u003eDuring testing we will sometimes come up to situations where developers are not following best practises for testability . We will frequently come up situations where elements doesn\u0026rsquo;t have any unique identifiable property. XPath axis comes to help in those situations. We can identify elements using various XPath Properties\u003c/p\u003e\n\u003cp\u003eList of various XPath Axis are available in \u003ca href=\"https://developer.mozilla.org/en-US/docs/Web/XPath/Axes\"\u003ehttps://developer.mozilla.org/en-US/docs/Web/XPath/Axes\u003c/a\u003e\nIf you have well-defined properties to identify the element, use them as your locator. Please read  locator strategy  \u003ca href=\"/blog/2016/08/30/element-location-using-xpath/\"\u003eUsing XPath\u003c/a\u003e and \u003ca href=\"/blog/2016/08/17/identifying-elements-using-locators-in-selenium/\"\u003eOther Parameters\u003c/a\u003e\u003c/p\u003e","title":"Element location using XPath Axis"},{"content":"XPath is XML query language which can be used for selecting nodes in XML. Hence it can be used to identify elements from DOM since they are represented as XHTML documents. Selenium WebDriver also supports XPath for locating elements. They also help to look for elements in both direction and hence it is generally slow compared to all other locator strategy. We can use XPath with both absolute path and relative path.\nAbsolute XPath Absolute Path refers to specific location in DOM, by considering it\u0026rsquo;s complete hierarchy. However this is not an ideal locator strategy since it makes your test very brittle. The absolute path will change if there is any change/realignment etc in UI.\nExample of Xpath using absolute path is as below\nWebElement userId = driver.FindElement(By.XPath(html/body/div[2]/div/form/input[2])); Relative XPath With relative Path , we can find element directly without entire structure. It helps to look out for any elements which matches with specified relative path . Example for a relative path based locator strategy is as below.\nNote: Relative XPath starts with \u0026ldquo;//\u0026rdquo;\nWebElement userId = driver.FindElement(By.XPath(\u0026#34;//input\u0026#34;)); // This retrieve first element with input tag. WebElement userId = driver.FindElement(By.XPath(\u0026#34;//input[2]\u0026#34;)); // This retrieve second element with input tag. Relative XPath - With Attributes If we need to further narrow down our location strategy, we can use Attributes along with relative XPath. There may be situations where we need to multiple attributes to uniquely identify an element. We can also specify locators to identify for ANY attribute\nWebElement passwordField = driver.FindElement(By.XPath(\u0026#34;//input[@id=\u0026#39;password\u0026#39;]\u0026#34;)); // Above will identify first element with input tag which also has id as \u0026#34;password\u0026#34;. WebElement LoginButton = driver.FindElement(By.XPath(\u0026#34;//input[@type=\u0026#39;submit\u0026#39;and @value=\u0026#39;Login\u0026#39;]\u0026#34;)); //Note you can use \u0026#34;or\u0026#34; as well. WebElement someField = driver.FindElement(By.XPath(\u0026#34;//input[@*=\u0026#39;password\u0026#39;]\u0026#34;)); // Above will identify first element with input tag which also has any attribute as \u0026#34;password\u0026#34;. Relative XPath - Partial Match Sometimes there may be situations where element attributes like ID are dynamically generated. Those will generally have some unique part in attributes likeID and remaining will be generated dynamically , which will keep on changing. This will need a locator strategy which will help us to identify elements using partial match. Main types are\nstarts-with()\nends-with()\ncontains()\nWebElement passwordField1 = driver.FindElement(By.XPath(\u0026#34;//input[starts-with(@id,\u0026#39;password\u0026#39;)]\u0026#34;)); // Above will identify first element with input tag which also has id starting with \u0026#34;password\u0026#34;. WebElement passwordField2 = driver.FindElement(By.XPath(\u0026#34;//input[ends-with(@id,\u0026#39;password\u0026#39;)]\u0026#34;)); // Above will identify first element with input tag which also has id ending with \u0026#34;password\u0026#34;. WebElement passwordField3 = driver.FindElement(By.XPath(\u0026#34;//input[contains(@id,\u0026#39;password\u0026#39;)]\u0026#34;)); // Above will identify first element with input tag which also has id containing with \u0026#34;password\u0026#34;. ","permalink":"https://abygeorgea.com/blog/2016/08/30/element-location-using-xpath/","summary":"\u003cp\u003eXPath is XML query language which can be used for selecting nodes in XML. Hence it can be used to identify elements from DOM since they are represented as XHTML documents. Selenium WebDriver also supports XPath for locating elements. They also help to look for elements in both direction and hence it is generally slow compared to all other locator strategy. We can use XPath with both absolute path and relative path.\u003c/p\u003e","title":"Element Location using XPath"},{"content":"Assert.AreEqual vs Assert.AreSame Very frequently I use Assert.AreEqual and Assert.AreSame for doing assertions in the code. Below is high level difference between both.\nAssert.AreSame Assert.AreSame checks whether both comparing objects are exactly the same ( reference indicate same object in memory) .It is normally known as Reference Equality\nAssert.AreEqual Assert.AreEqual checks whether both objects contain same value. It is normally known as Value Equality. For primitive value types ( like int, bool) this is straight forward. But for other types ( especially user defined objects) , it is depends on how the type defines equality.\nHence Assert.AreEqual will fail ( most of the time) when we compare two objects. This link have some discussion point about the same.\n","permalink":"https://abygeorgea.com/blog/2016/08/22/nunit-assert/","summary":"\u003ch2 id=\"assertareequal-vs-assertaresame\"\u003eAssert.AreEqual vs Assert.AreSame\u003c/h2\u003e\n\u003cp\u003eVery frequently I use Assert.AreEqual and Assert.AreSame for doing assertions in the code. Below is high level difference between both.\u003c/p\u003e\n\u003ch3 id=\"assertaresame\"\u003eAssert.AreSame\u003c/h3\u003e\n\u003cp\u003eAssert.AreSame checks whether both comparing objects are exactly the same ( reference indicate same object in memory) .It is normally known as Reference Equality\u003c/p\u003e\n\u003ch3 id=\"assertareequal\"\u003eAssert.AreEqual\u003c/h3\u003e\n\u003cp\u003eAssert.AreEqual checks whether both objects contain same value. It is normally known as Value Equality. For primitive value types ( like int, bool) this is straight forward. But for other types ( especially user defined objects) , it is depends on how the type defines equality.\u003c/p\u003e","title":"Nunit Assert"},{"content":"Locators are html properties of a web element , which can be considered as an address of the element. An element will have various html properties. We can use Firebug extension or Chrome dev tools to identify different locators of an element.\nSelenium Web Driver provides two different methods for identifying html elements .\n_**FindElement **_for WebDriver and WebElement Class. When locating element matching specified criteria, it looks through DOM( Document Object Model) for matching element and return the first matching element. If there are no matching element, it will throw NoSuchElementFoundException\n_FindElements _for WebDriver and WebElement Class. When locating element matching specified criteria, it looks through DOM( Document Object Model) for matching element and return a list of all matching element. If there are no matching elements, then it will return an empty list .\nNote: Both of them doesn\u0026rsquo;t support regular expression for finding element. Simple way to do that will be to get list of all elements and then iterate to find a matching regular expression\nThere are multiple criteria which we can use for looking for an element. FindElement and FindElements work exactly same way except for above difference. Different critieria are\ndriver.FindElement(By.Id())\ndriver.FindElement(By.Name())\ndriver.FindElement(By.ClassName())\ndriver.FindElement(By.TagName())\ndriver.FindElement(By.LinkText())\ndriver.FindElement(By.PartialLinkText())\ndriver.FindElement(By.CssSelector())\ndriver.FindElement(By.XPath())\nExample:\nWebElement _firstElement = driver.findElement(By.id(\u0026#34;div1\u0026#34;)); WebElement _secondElementInsideFirstOne =_firstElement .findElement(By.linkText(\u0026#34;username\u0026#34;)); IList\u0026amp;lt;IWebElement\u0026amp;gt; elements = driverOne.FindElements(By.ClassName(\u0026amp;lt;span class=\u0026#34;pl-s\u0026#34;\u0026amp;gt;\u0026amp;lt;span class=\u0026#34;pl-pds\u0026#34;\u0026amp;gt;“\u0026amp;lt;/span\u0026amp;gt;green\u0026amp;lt;span class=\u0026#34;pl-pds\u0026#34;\u0026amp;gt;“\u0026amp;lt;/span\u0026amp;gt;\u0026amp;lt;/span\u0026amp;gt;)); Using any attributes other than XPath and CssSelector are straight forward. More about using XPath and CssSelector in next blog.\n","permalink":"https://abygeorgea.com/blog/2016/08/17/identifying-elements-using-locators-in-selenium/","summary":"\u003cp\u003eLocators are html properties of a web element , which can be considered as an address of the element. An element will have various html properties. We can use Firebug extension or Chrome dev tools to identify different locators of an element.\u003c/p\u003e\n\u003cp\u003eSelenium Web Driver provides two different methods for identifying html elements .\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e_**FindElement  **_for WebDriver and WebElement Class. When locating element matching specified criteria, it looks through DOM( Document Object Model) for matching element and return the first matching element. If there are no matching element, it will throw NoSuchElementFoundException\u003c/p\u003e","title":"Identifying elements using Locators in Selenium"},{"content":"In Specflow, Step definitions are global. So a scenario can have multiple step definitions which can be present in different classes. Sometimes, there arise a need to share the data between steps residing in different classes. How do we do it??\nThere are multiple ways to do it\nContext Injection\nFeature Context\nScenario Context\nLet us look into more details about how to store and retrieve data using Scenario Context .\nScenarioContext.Current\nHow do we add a key value pair to Scenario Context ? It is as simple as below\n[Given(@\u0026#34;I have entered (.*) and (.*) into the Login Page\u0026#34;)] public void GivenIHaveEnteredAndIntoTheLoginPage(string p0, string p1) { ScenarioContext.Current.Add(\u0026#34;username\u0026#34;, p0); ScenarioContext.Current.Add(\u0026#34;password\u0026#34;, p1); } How do we retrieve the value from ScenarioContext ?\nWhen(@\u0026#34;I press retrieve data\u0026#34;)] public void WhenIRetrieveData() { string username = (string)ScenarioContext.Current[\u0026#34;username\u0026#34;]; string password = (string)ScenarioContext.Current[\u0026#34;password\u0026#34;]; } Note: While retrieving , scenarioContext.Current always return an object . Hence we need use explicit casting while retrieving data from scenario context.\nIn Nut Shell,\n**Get a value of the key ( Retrieve data) ** var value =(Type) ScenarioContext.Current.[string Key];\nvar value = ScenarioContext.Current.Get(string Key);\nWe can use this for storing and passing objects as well\nScenarioContext.Current.Add(\u0026ldquo;driver1\u0026rdquo;, browser);\nIWebDriver driver2 = (IWebDriver)ScenarioContext.Current[\u0026ldquo;driver1\u0026rdquo;];\n","permalink":"https://abygeorgea.com/blog/2016/08/01/specflow-sharing-data-between-steps/","summary":"\u003cp\u003eIn Specflow, Step definitions are global. So a scenario can have multiple step definitions which can be present in different classes.  Sometimes, there arise a need to share the data between steps residing in different classes. How do we do it??\u003c/p\u003e\n\u003cp\u003eThere are multiple ways to do it\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003eContext Injection\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eFeature Context\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eScenario Context\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eLet us look into more details about how to store and retrieve data using Scenario Context .\u003c/p\u003e","title":"Specflow - Sharing data between steps"},{"content":"I am not going to explain what is data driven framework or what is its benefits. All of them are pretty well-known . If not, just google it.\nHere I am going to explain a sample code which can be used to read from xml data files. This will be helpful to implement a data driven frame work for BDD testing , using Specflow or Cucumber\nPre - requiste Code is written in Csharp . We need to add below reference to visual studio solution\nAdd reference to System.xml\nAdd reference to System.Xml.Linq\nXML Format [code language=\u0026ldquo;xml\u0026rdquo; ]\n[/code]\nNode names in above example are Scenario1, Scenario2 and Scenario3\nElement or Attribute name are username, password, Email .\nYou can add any number of nodes and attributes depending on the test scenario\nRead specific Value from XML Below code provides a solution to read values of existing Key/Attribute from a specific node.\npublic static string ReadDataFromXML(string FileName, string NodeName, string KeyName) { string _basePath = AppDomain.CurrentDomain.BaseDirectory.ToString(); string _datafilePath = _basePath + @\u0026#34;..\\..\\Data\\\u0026#34; + FileName; XDocument xmlDoc = XDocument.Load(_datafilePath); data = xmlDoc.Root.Element(NodeName).Attribute(KeyName).Value; return data; } Read All values for a Scenario from XML This code gives a solution for reading details of all attributes/key from a node. This comes very handy for reading all data required for a scenario and adding them to scenario context , so that data can be shared across specflow/cucumber step definitions\npublic static void ReadAllDataFromXml(string xFileName, string xNodeName) { string data = string.Empty; // Load path of xml file . Data Folder in below is folder name string _basePath = AppDomain.CurrentDomain.BaseDirectory.ToString(); string _datafilePath = _basePath + @\u0026#34;..\\..\\Data\\\u0026#34; + xFileName; XDocument xmlDoc = XDocument.Load(_datafilePath); var cols = xmlDoc.Descendants(xNodeName).First(); foreach (XAttribute xAtt in cols.Attributes()) { Console.WriteLine(\u0026#34;{0},{1}\u0026#34;, xAtt.Name, xAtt.Value); // --printing values -- string temp = xAtt.Name.ToString(); ScenarioContext.Current.Add(temp, xAtt.Value); // The values are added to scenario context as key value pair.can be modified } Write Data into XML Below function gives a solution to update value on an existing key/attribute in data sheet . Sometime the data sheet will be copied over to bin folder when we build the solution. That makes it necessary to update both original data sheet and the one in bin folder so that modified data can be used in same test without another rebuild.\npublic void WriteIntoXML(string xData, string xElement, string xAttribute, string xFileName) { // This solution updates value of an existing key. // Xdoc refers to the xmlsheet in the Solution explorer // xDoc_2 refers to the xmlsheet inside bin folder while running // This solution write them separately in below code. // Below is the path to xml file after building solution XDocument xDoc_2 = XDocument.Load(Path.Combine(Environment.CurrentDirectory, \u0026#34;Data Folder\u0026#34;, xFileName)); //Below should be the path of xml file XDocument xDoc = XDocument.Load(Path.Combine(Path.GetFullPath(@\u0026#34;../../Data Folder\u0026#34;), xFileName)); xDoc_2.Root.Element(xElement).Attribute(xAttribute).Value = data.ToString(); xDoc.Root.Element(xElement).Attribute(xAttribute).Value = data.ToString(); xDoc_2.Save(Path.Combine(Environment.CurrentDirectory, \u0026#34;Data Folder\u0026#34;, xFileName)); xDoc.Save(Path.Combine(Path.GetFullPath(@\u0026#34;../../Data Folder\u0026#34;), xFileName)); } ","permalink":"https://abygeorgea.com/blog/2016/07/29/data-driven-framework-xml/","summary":"\u003cp\u003eI am not going to explain what is data driven framework or what is its benefits.  All of them are pretty well-known . If not, just google it.\u003c/p\u003e\n\u003cp\u003eHere I am going to explain a sample code which can be used to read from xml data files. This will be helpful to implement a data driven frame work for BDD testing , using Specflow or Cucumber\u003c/p\u003e\n\u003ch2 id=\"pre---requiste\"\u003ePre - requiste\u003c/h2\u003e\n\u003cp\u003eCode is written in Csharp . We need to add below reference to visual studio solution\u003c/p\u003e","title":"Data Driven Framework - XML"},{"content":"In previous blog post, I have explained about how use XML for making a data driven framework for automation testing . It can be found here. I have also written about how to use jxl library for reading from excel and writing into Excel.\nBelow is another code snippet to read all values of a row and save it into a hash map for accessing later during automation test.\npublic HashMap GetAllDataForARow (String sheet, int Row){ HashMap DataMap = new HashMap(); try { Workbook wrk1 = Workbook.getWorkbook(new File(dataPath)); //Obtain the reference to the first sheet in the workbook Sheet sheet1 = wrk1.getSheet(sheet); int x =0; Cell colArow1 , colArow2; do { colArow1 = sheet1.getCell(x,0); colArow2 = sheet1.getCell(x,Row); DataMap.put(colArow1.getContents(), colArow2.getContents()); x=x+1; }while (colArow1.getContents() != \u0026#34;\u0026#34;); } catch (BiffException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }catch (IndexOutOfBoundsException e){ } return DataMap; } ","permalink":"https://abygeorgea.com/blog/2016/07/30/data-driven-framework-excel/","summary":"\u003cp\u003eIn previous blog post, I have explained about how use XML for making a data driven framework for automation testing . It can be found \u003ca href=\"/blog/2016/07/29/data-driven-framework-xml/\"\u003ehere\u003c/a\u003e. I have also written about how to use jxl library for \u003ca href=\"/blog/2014/06/01/java-reading-a-specific-cell-in-excel/\"\u003ereading from excel\u003c/a\u003e and \u003ca href=\"/blog/2014/06/01/java-writing-into-specific-cell-in-excel/\"\u003ewriting into Excel\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eBelow is another code snippet to read all values of a row and save it into a hash map for accessing later during automation test.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-Java\" data-lang=\"Java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003epublic\u003c/span\u003e  HashMap \u003cspan style=\"color:#a6e22e\"\u003eGetAllDataForARow\u003c/span\u003e  (String sheet, \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e Row){\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        HashMap DataMap \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003enew\u003c/span\u003e HashMap();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003etry\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            Workbook wrk1 \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e  Workbook.\u003cspan style=\"color:#a6e22e\"\u003egetWorkbook\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003enew\u003c/span\u003e File(dataPath));\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#75715e\"\u003e//Obtain the reference to the first sheet in the workbook\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            Sheet sheet1 \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e wrk1.\u003cspan style=\"color:#a6e22e\"\u003egetSheet\u003c/span\u003e(sheet);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e x \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e0;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            Cell colArow1 , colArow2;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003edo\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                colArow1 \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e sheet1.\u003cspan style=\"color:#a6e22e\"\u003egetCell\u003c/span\u003e(x,0);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                colArow2 \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e sheet1.\u003cspan style=\"color:#a6e22e\"\u003egetCell\u003c/span\u003e(x,Row);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                DataMap.\u003cspan style=\"color:#a6e22e\"\u003eput\u003c/span\u003e(colArow1.\u003cspan style=\"color:#a6e22e\"\u003egetContents\u003c/span\u003e(), colArow2.\u003cspan style=\"color:#a6e22e\"\u003egetContents\u003c/span\u003e());\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                x\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003ex\u003cspan style=\"color:#f92672\"\u003e+\u003c/span\u003e1;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            }\u003cspan style=\"color:#66d9ef\"\u003ewhile\u003c/span\u003e (colArow1.\u003cspan style=\"color:#a6e22e\"\u003egetContents\u003c/span\u003e() \u003cspan style=\"color:#f92672\"\u003e!=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u0026#34;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ecatch\u003c/span\u003e (BiffException e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            e.\u003cspan style=\"color:#a6e22e\"\u003eprintStackTrace\u003c/span\u003e();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        } \u003cspan style=\"color:#66d9ef\"\u003ecatch\u003c/span\u003e (IOException e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            e.\u003cspan style=\"color:#a6e22e\"\u003eprintStackTrace\u003c/span\u003e();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        }\u003cspan style=\"color:#66d9ef\"\u003ecatch\u003c/span\u003e (IndexOutOfBoundsException e){\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e DataMap;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e","title":"Data Driven Framework - Excel"},{"content":"This post is just for making notes during my learning of Node.js through various courses and online material. This will be always a work in progress blog post\nNode.js is an open source server side runtime environment, which is cross platform.It uses Javascript as its language\ncheck node version\nnode --version Making web request in Node we can make webrequest by using inbuilt http or by using \u0026lsquo;request\u0026rsquo; example : For making web request by http\nvar http = require(\u0026#39;http\u0026#39;); var req = http.request(\u0026#39;http://www.google.com/finance/info?infotype=infoquoteall\u0026amp;q=NSE:TCS\u0026#39;, function(response) { console.log(response.statusCode); response.pipe(process.stdout); }); req.end(); example: Please note that there is no space between key and : while defining options\nvar http = require(\u0026#39;http\u0026#39;); var options = { host: \u0026#39;www.google.com\u0026#39;, port: 80, path: \u0026#39;/finance/info?infotype=infoquoteall\u0026amp;q=NSE:TCS\u0026#39;, method: \u0026#39;GET\u0026#39; }; var req = http.request(options, function(response) { console.log(response.statusCode); response.pipe(process.stdout); }); req.end(); Example: We can simply by giving GET . There is no need to close request since we are not going to send any more information to request\nvar http = require(\u0026#39;http\u0026#39;); var options = { host: \u0026#39;www.google.com\u0026#39;, port: 80, path: \u0026#39;/finance/info?infotype=infoquoteall\u0026amp;q=NSE:TCS\u0026#39;, method: \u0026#39;GET\u0026#39; }; http.get(options, function(response) { console.log(response.statusCode); response.pipe(process.stdout); }); Another option is\nhttp.get(options, function(res){ var body = \u0026#39;\u0026#39;; res.on(\u0026#39;data\u0026#39;, function(chunk){ body += chunk; }); res.on(\u0026#39;end\u0026#39;, function(){ console.log(\u0026#34;Got a response: \u0026#34;, body); }); }).on(\u0026#39;error\u0026#39;, function(e){ console.log(\u0026#34;Got an error: \u0026#34;, e); }); ###Starting with Node.js and Express###\nnpm init Above command will create a package.json. Leave details are default or change accordingly\nnpm install express --save Above will install express and also add it as dependency in package.json\ntouch app.js Above will create a app.js file. In package.json add below\n\u0026#34;scripts\u0026#34;: { \u0026#34;test\u0026#34;: \u0026#34;echo \\\u0026#34;Error: no test specified\\\u0026#34; \u0026amp;\u0026amp; exit 1\u0026#34;, \u0026#34;start\u0026#34;: \u0026#34;node app.js\u0026#34; }, Now, if we run \u0026ldquo;npm start\u0026rdquo; from command line, it will run app.js\nBower Package manager for web/front end. It is installed with NPM and have flat package hierarchy(doesnt install dependency underneath one level) .It works similar to NPM and have Bower.json for dependency managament.\nCreate a .bowerrc file and have project specific settings. Now move the components from bower_component to public folder defined earlier. update .bowerrc with below\n{ \u0026#34;directory\u0026#34; : \u0026#34;public/lib\u0026#34; } now run below\nbower init \\\\ leave all settings as default bower install --save bootstrap \\\\ it will install bootstrap. It create a directory under public/lib and under that it will have bootstrap andjquery since jquery is a dependency for bootstrap Gulp It is a task manager for web projects. It have code based config. It is packaged base so that we can use different external packages\nMongo DB Install MongoDB from website mongoD is command for running server mongo is command for running another terminal for interacting with mongo db show dbs will show list of db Install MongoDB Node.js driver using NPM ","permalink":"https://abygeorgea.com/blog/2016/07/12/nodejs/","summary":"\u003cp\u003eThis post is just for making notes during my learning of Node.js through various courses and online material. This will be always a work in progress blog post\u003c/p\u003e\n\u003cp\u003eNode.js is an open source server side runtime environment, which is cross platform.It uses Javascript as its language\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003echeck node version\u003c/strong\u003e\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003enode --version\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e\u003cstrong\u003eMaking web request in Node\u003c/strong\u003e\nwe can make webrequest by using inbuilt \u003cem\u003ehttp\u003c/em\u003e or by using \u0026lsquo;\u003cem\u003erequest\u003c/em\u003e\u0026rsquo;\nexample : For making web request by http\u003c/p\u003e","title":"Nodejs"},{"content":"A couple of months back , I helped out to organize clinical examination for RACP. I created a tool ( Excel Macro) for finalizing exam schedules for all attendees. The roster should consider employee preference, examiner availability, timeslot, venue and other parameters. Below is what I got in return ( even though they misspelled my name).\n","permalink":"https://abygeorgea.com/blog/2015/10/16/volunteering-experience/","summary":"\u003cp\u003eA couple of months back , I helped out to organize clinical examination for RACP. I created a tool ( Excel Macro)  for finalizing exam schedules for all attendees. The roster should consider employee preference, examiner availability, timeslot, venue and other parameters. Below is what I got in return ( even though they misspelled my name).\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/2015/10/16/volunteer%20certificate.png\"\u003e\u003c/p\u003e","title":"Volunteering Experience"},{"content":"Difference between / and // in XPath / is used to create absolute XPath which starts from root. This is highly brittle since any changes to UI will change element locator.\n// is used to create relative XPath . The XPath is created relative to another element in UI\nDifference between driver.get() and driver.navigate().to() Both driver.get() and driver.navigate().to() will try to open a webpage and wait for the page to load. It means, it will wait till Onload event has fired. But it will not wait for all AJAX calls to trigger and process. Both of them are essentially the same. How ever Navigate() interface exposes the ability to move backward and forward in browser history.\ndriver.get(\u0026#34;http://www.google.com\u0026#34;); driver.navigate().to(\u0026#34;http://www.yahoo.com\u0026#34;); driver.navigate().forward(); driver.navigate().back(); Difference between driver.close() and driver.quit() driver.close() will close the window which is currently accessed by webdriver. driver.quit() will close all windows that are opened by the webdriver during current execution.\n","permalink":"https://abygeorgea.com/blog/2015/06/22/selenium/","summary":"\u003ch3 id=\"difference-between--and--in-xpath\"\u003eDifference between / and // in XPath\u003c/h3\u003e\n\u003cp\u003e/ is used to create absolute XPath which starts from root. This is highly brittle since any changes to UI will change element locator.\u003c/p\u003e\n\u003cp\u003e// is used to create relative XPath . The XPath is created relative to another element in UI\u003c/p\u003e\n\u003ch3 id=\"difference-between-driverget-and-drivernavigateto\"\u003eDifference between driver.get() and driver.navigate().to()\u003c/h3\u003e\n\u003cp\u003eBoth \u003ccode\u003edriver.get()\u003c/code\u003e and \u003ccode\u003edriver.navigate().to()\u003c/code\u003e will try to open a webpage and wait for the page to load. It means, it will wait till \u003ccode\u003eOnload\u003c/code\u003e event has fired. But it will not wait for all AJAX calls to trigger and process. Both of them are essentially the same. How ever Navigate() interface exposes the ability to move backward and forward  in browser history.\u003c/p\u003e","title":"Selenium"},{"content":"In my previous blog post , I have mentioned how to read from an excel file using jxl jar files in Java. It can be found here\nIn this post, I will explain how to write into an excel using same library. Below example will update the excel cell content with the value passed and also update its formatting . The color of the cell will change depending on value we pass. We can use similar functions for updating any other cell format.\nBelow is the import section\nimport jxl.Cell; import jxl.Sheet; import jxl.Workbook; import jxl.format.Colour; import jxl.read.biff.BiffException; import jxl.write.*; Below is the function for writing into excel\npublic void WriteDataIntoExcelCell (String sheet, String field_name, int Row, String input){ try { Workbook wrk1 = Workbook.getWorkbook(new File(dataPath)); //Obtain the reference to the first sheet in the workbook Sheet sheet1 = wrk1.getSheet(sheet); int x =0; int y =0; int Col=0; // Find Column number from excel by iteration first row and comparing the names Cell colArow1 = sheet1.getCell(x,y); do { colArow1 = sheet1.getCell(x,y); if (colArow1.getContents().equalsIgnoreCase(field_name) ){ Col = colArow1.getColumn(); break; } x=x+1; }while (colArow1.getContents() != \u0026#34;\u0026#34;); // write to file File exlFile = new File(dataPath); WritableWorkbook writableWorkbook = Workbook.createWorkbook(exlFile,wrk1); WritableSheet writableSheet = writableWorkbook.getSheet(sheet); //WritableCellFormat writableCell = writableWorkbook.getSheet(sheet). // Update cell content and format String Varcolour ; Label label; if (input.equalsIgnoreCase(\u0026#34;PASS\u0026#34;)){ label = new Label(Col,Row,input,getCellFormat(Colour.GREEN)); } else if (input.equalsIgnoreCase(\u0026#34;FAIL\u0026#34;)) { label = new Label(Col,Row,input,getCellFormat(Colour.RED)); } else { label = new Label(Col,Row,input); } //Label label = new Label(Col,Row,input); writableSheet.addCell(label); writableWorkbook.write(); writableWorkbook.close(); } catch (BiffException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (WriteException e) { e.printStackTrace(); } } private static WritableCellFormat getCellFormat(Colour colour) throws WriteException { WritableFont cellFont = new WritableFont(WritableFont.TAHOMA, 10); WritableCellFormat cellFormat = new WritableCellFormat(cellFont); cellFormat.setBackground(colour); return cellFormat; } ","permalink":"https://abygeorgea.com/blog/2014/06/10/java-writing-into-specific-cell-in-excel/","summary":"\u003cp\u003eIn my previous blog post , I have mentioned how to read from an excel file using jxl jar files in Java. It can be found \u003ca href=\"/blog/2014/06/01/java-reading-a-specific-cell-in-excel/\"\u003ehere\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eIn this post, I will explain how to write into an excel using same library. Below example will update the excel cell content with the value passed and also update its formatting . The color of the cell will change depending on value we pass. We can use similar functions for updating any other cell format.\u003c/p\u003e","title":"Java - Writing Into specific cell in Excel"},{"content":"Below is a code snippet for reading a specific cell from Excel using Java.\nIt is done by using importing jxl jar files which can be found here.\nImport below in class file\nimport jxl.Cell; import jxl.Sheet; import jxl.Workbook; import jxl.format.Colour; import jxl.read.biff.BiffException; import jxl.write.*; Below is function for reading value from specific cell in Excel\npublic String readexcel(String sheet, int intRow, int intCol){ try { //Create a workbook object from the file at specified location. //Change the path of the file as per the location on your computer. Workbook wrk1 = Workbook.getWorkbook(new File(dataPath)); //Obtain the reference to the first sheet in the workbook Sheet sheet1 = wrk1.getSheet(sheet); //Obtain reference to the Cell using getCell(int col, int row) method of sheet // Add \u0026#34; - 1\u0026#34; to both intRow , intCol depending how whether we consider excel start with row \u0026amp; column number as 0 or 1 Cell colArow1 = sheet1.getCell(intRow , intCol ); //Read the contents of the Cell using getContents() method, which will return //it as a String String strReturn = colArow1.getContents(); return strReturn; /* //Display the cell contents System.out.println(\u0026#34;Contents of cell Col A Row 1: \\\u0026#34;\u0026#34;+str_colArow1 + \u0026#34;\\\u0026#34;\u0026#34;); System.out.println(\u0026#34;Contents of cell Col B Row 1: \\\u0026#34;\u0026#34;+str_colBrow1 + \u0026#34;\\\u0026#34;\u0026#34;); System.out.println(\u0026#34;Contents of cell Col A Row 2: \\\u0026#34;\u0026#34;+str_colArow2 + \u0026#34;\\\u0026#34;\u0026#34;);*/ } catch (BiffException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return sheet; } Below is an example of how to read from a cell based on row number and Column NAME\npublic String GetDataBasedOnRowNumAndColName (String sheet, String field_name, int Row){ try { Workbook wrk1 = Workbook.getWorkbook(new File(dataPath)); //Obtain the reference to the first sheet in the workbook Sheet sheet1 = wrk1.getSheet(sheet); int x =0; int y =0; int Col=0; boolean FOUND = false; // Find corresponding Column number based on Name Cell colArow1 = sheet1.getCell(x,y); do { colArow1 = sheet1.getCell(x,y); if (colArow1.getContents().equalsIgnoreCase(field_name) ){ Col = colArow1.getColumn(); // System.out.println(Col); FOUND = true; break; } x=x+1; }while (colArow1.getContents() != \u0026#34;\u0026#34;); if (FOUND) { colArow1 = sheet1.getCell(Col,Row-1); // System.out.println(colArow1.getContents()); return colArow1.getContents(); } else { return \u0026#34; \u0026#34;; } } catch (BiffException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }catch (IndexOutOfBoundsException e){ return \u0026#34;\u0026#34;; } return sheet; } ","permalink":"https://abygeorgea.com/blog/2014/06/01/java-reading-a-specific-cell-in-excel/","summary":"\u003cp\u003eBelow is a code snippet for reading a specific cell from Excel using Java.\u003c/p\u003e\n\u003cp\u003eIt is done by using importing jxl jar files which can be found \u003ca href=\"https://sourceforge.net/projects/jxl/\"\u003ehere\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eImport below in class file\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eimport jxl.Cell;\nimport jxl.Sheet;\nimport jxl.Workbook;\nimport jxl.format.Colour;\nimport jxl.read.biff.BiffException;\nimport jxl.write.*;\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eBelow is function for reading value from specific cell in Excel\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-Java\" data-lang=\"Java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e \u003cspan style=\"color:#66d9ef\"\u003epublic\u003c/span\u003e  String \u003cspan style=\"color:#a6e22e\"\u003ereadexcel\u003c/span\u003e(String sheet, \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e intRow, \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e intCol){\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003etry\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#75715e\"\u003e//Create a workbook object from the file at specified location.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#75715e\"\u003e//Change the path of the file as per the location on your computer.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e          \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            Workbook wrk1 \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e  Workbook.\u003cspan style=\"color:#a6e22e\"\u003egetWorkbook\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003enew\u003c/span\u003e File(dataPath));\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#75715e\"\u003e//Obtain the reference to the first sheet in the workbook\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            Sheet sheet1 \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e wrk1.\u003cspan style=\"color:#a6e22e\"\u003egetSheet\u003c/span\u003e(sheet);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#75715e\"\u003e//Obtain reference to the Cell using getCell(int col, int row) method of sheet\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#75715e\"\u003e// Add \u0026#34; - 1\u0026#34; to both intRow , intCol depending how whether we consider excel start with row \u0026amp; column number as 0 or 1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            Cell colArow1 \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e sheet1.\u003cspan style=\"color:#a6e22e\"\u003egetCell\u003c/span\u003e(intRow , intCol );\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#75715e\"\u003e//Read the contents of the Cell using getContents() method, which will return\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#75715e\"\u003e//it as a String\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            String strReturn \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e colArow1.\u003cspan style=\"color:#a6e22e\"\u003egetContents\u003c/span\u003e();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e strReturn;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e         \u003cspan style=\"color:#75715e\"\u003e/*  //Display the cell contents\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e           System.out.println(\u0026#34;Contents of cell Col A Row 1: \\\u0026#34;\u0026#34;+str_colArow1 + \u0026#34;\\\u0026#34;\u0026#34;);\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e           System.out.println(\u0026#34;Contents of cell Col B Row 1: \\\u0026#34;\u0026#34;+str_colBrow1 + \u0026#34;\\\u0026#34;\u0026#34;);\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e           System.out.println(\u0026#34;Contents of cell Col A Row 2: \\\u0026#34;\u0026#34;+str_colArow2 + \u0026#34;\\\u0026#34;\u0026#34;);*/\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        } \u003cspan style=\"color:#66d9ef\"\u003ecatch\u003c/span\u003e (BiffException e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            e.\u003cspan style=\"color:#a6e22e\"\u003eprintStackTrace\u003c/span\u003e();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        } \u003cspan style=\"color:#66d9ef\"\u003ecatch\u003c/span\u003e (IOException e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            e.\u003cspan style=\"color:#a6e22e\"\u003eprintStackTrace\u003c/span\u003e();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e sheet;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eBelow is an example of how to read from a cell based on row number and Column NAME\u003c/p\u003e","title":"Java - Reading a specific cell in Excel"},{"content":"Redirect\n","permalink":"https://abygeorgea.com/blog/2014/05/21/hello-world/","summary":"\u003cp\u003e\u003ca href=\"/blog/2016/09/04/element-location-using-xpath-axis/\"\u003eRedirect\u003c/a\u003e\u003c/p\u003e","title":"Hello world"},{"content":"An engineer with cross-industry experience in banking, financial services, medtech, and retail (ResMed, ASX, Commonwealth Bank, Suncorp, AMEX, Woolworths), still hands-on with code every day. Recently focused on integrating AI-assisted software development and agentic development workflows.\nI’ve spent equal time in engineering leadership, building and forming teams, and in the code itself, which means I can design a strategy and also implement it.\nWhat I bring:\n🔹 Languages \u0026amp; frameworks: TypeScript, C#/.NET, Java, Python, Node.js 🔹 AI-assisted engineering: Claude Code, GitHub Copilot, MCP servers, GitHub Spec Kit — agentic development lifecycle 🔹 Infrastructure \u0026amp; cloud: Docker, Kubernetes, Terraform, AWS - defining infrastructure as code and setting up deployment pipelines 🔹 CI/CD \u0026amp; platform engineering: GitHub Actions, Jenkins, TeamCity - pipeline architecture, shift-left/shift-right quality gates, service virtualization, observability 🔹 Test architecture \u0026amp; tools: Playwright, Selenium, REST Assured, RestSharp, Karate, JMeter, Pact 🔹 Test strategy \u0026amp; management: across distributed, cross-institutional teams Certified: ISTQB Certified Test Automation Engineer, AWS Certified AI Practitioner, SAFe Agilist.\nOpen to conversations on hands-on automation engineering, platform engineering, technical leadership and delivery management roles where staying close to the code is part of the deal.\n","permalink":"https://abygeorgea.com/resume/","summary":"\u003cp\u003eAn engineer with cross-industry experience in banking, financial services, medtech, and retail (ResMed, ASX, Commonwealth Bank, Suncorp, AMEX, Woolworths), still hands-on with code every day. Recently focused on integrating AI-assisted software development and agentic development workflows.\u003c/p\u003e\n\u003cp\u003eI’ve spent equal time in engineering leadership, building and forming teams, and in the code itself, which means I can design a strategy and also implement it.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eWhat I bring:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e🔹 \u003cstrong\u003eLanguages \u0026amp; frameworks:\u003c/strong\u003e TypeScript, C#/.NET, Java, Python, Node.js\u003c/li\u003e\n\u003cli\u003e🔹 \u003cstrong\u003eAI-assisted engineering:\u003c/strong\u003e Claude Code, GitHub Copilot, MCP servers, GitHub Spec Kit — agentic development lifecycle\u003c/li\u003e\n\u003cli\u003e🔹 \u003cstrong\u003eInfrastructure \u0026amp; cloud:\u003c/strong\u003e Docker, Kubernetes, Terraform, AWS - defining infrastructure as code and setting up deployment pipelines\u003c/li\u003e\n\u003cli\u003e🔹 \u003cstrong\u003eCI/CD \u0026amp; platform engineering:\u003c/strong\u003e GitHub Actions, Jenkins, TeamCity - pipeline architecture, shift-left/shift-right quality gates, service virtualization, observability\u003c/li\u003e\n\u003cli\u003e🔹 \u003cstrong\u003eTest architecture \u0026amp; tools:\u003c/strong\u003e Playwright, Selenium, REST Assured, RestSharp, Karate, JMeter, Pact\u003c/li\u003e\n\u003cli\u003e🔹 \u003cstrong\u003eTest strategy \u0026amp; management:\u003c/strong\u003e across distributed, cross-institutional teams\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eCertified: ISTQB Certified Test Automation Engineer, AWS Certified AI Practitioner, SAFe Agilist.\u003c/p\u003e","title":"Resume"}]