ProductHow it worksPricingBlogDocsLoginFind Your First Bug
Grey box testing shown as three cubes, white, grey and black, with a source code sheet above the grey cube and a browser window below it, both linked to the same point on its face
TestingGrey Box TestingTest Design

Why Grey Box Testing Was Never a Compromise

Tom Piaggio
Tom PiaggioCo-Founder at Autonoma

Grey box testing is testing performed with partial knowledge of a system's internals, verified by exercising the application through its ordinary external interface rather than reading the code directly. The textbook treats this as a compromise, less rigorous than white box testing, less representative of a real user than black box testing, because a person doing it can only hold one perspective at a time. A system that reads the codebase to decide what to check, then drives the running application to check it, holds both perspectives fully instead of partially. That inversion is the whole argument here.

This page is for the engineer being asked to define grey box testing cleanly, not the one deciding whether to adopt it. Maybe it's an interview question. Maybe you're briefing a QA hire who has only ever heard black box and white box named. Maybe a security auditor wants a paragraph they can cite back to you correctly. That's a different job than the one a QA lead has when assembling a testing strategy document, and a different job than a team trying to work out whether its AI-generated tests are checking anything real. This page does one thing: it defines the middle position honestly, then explains why that position's oldest limitation was never really about the technique.

What grey box testing is, and why the textbook called it a compromise

Every testing textbook draws the same three boxes, and the security literature draws them the same way: NIST SP 800-115 describes tests that use both white box and black box techniques as gray box testing, in exactly those words. White box testing gives the tester the source: every branch, every condition, every internal state, and the test is built by reading that structure directly. Black box testing gives the tester nothing but the interface: inputs go in, outputs come out, and the internals stay sealed. Grey box testing (or gray box testing, its American spelling variant, used interchangeably through the literature) sits between them on purpose. Stated plainly: grey box testing is a test methodology that assumes partial knowledge of a system's internal structure, applied through the system's external interface. That is close to the wording NIST uses, and it is the definition the rest of this page builds on. The tester gets partial knowledge, a data schema, an API contract, a sequence diagram, an architecture document, but verifies that knowledge the black box way, by driving the system through its actual interface rather than inspecting the code that implements it.

The textbook is honest about the tradeoff, and it's worth stating plainly rather than glossing over it. Grey box testing is not as rigorous as white box testing, because the tester never sees the actual conditionals and can only infer them from documentation that may be stale or incomplete. It is not as representative of a real user as black box testing, because knowing the schema at all changes which inputs a tester reaches for.

When to use grey box testing

The technique earns its place anyway in exactly the situations where partial internal knowledge is the most anyone realistically has: integration testing between two services where you know the contract but not the other team's implementation, security testing where a tester knows the database schema well enough to try a targeted case without having read every query, or a QA engineer handed API documentation and access to a staging environment but not the repository.

If you're in that seat right now, handed partial documentation and asked to verify behavior you can't fully see, the textbook definition above is doing its job, and nothing here changes what you should do today. What it changes is why the technique was ever called a compromise. Every one of the three boxes was named when running a test case meant a person sitting down and executing it by hand, so the real constraint was never how much a tester could see, it was how many cases that tester could afford to run. Grey box testing's compromise, partial knowledge held by one person, was a symptom of that constraint: a person has one set of eyes, so partial knowledge was the most that could be verified without either reading every line, too slow, or seeing nothing at all, too blind. Take away the assumption that a single person is doing the looking, and the compromise stops being necessary. It was never a fact about the technique's ceiling. It was a fact about who was allowed to hold two perspectives at once.

One person switching focus between reading source and driving the interface one view at a time, next to one system holding both views at once and feeding them into a single test
A person switches between two views. A system holds both at once.

Black box and white box were always a description of what a person was permitted, or willing, to look at during a specific engagement, not a description of what kind of system could exist. Grey box testing is the box where that fact is easiest to see, because it's the one built explicitly around a person splitting the difference on access. Remove the person, ask what a system, not a tester, could see and verify, and the three boxes stop being options on a spectrum of trust. They become a description of one thing, done two ways, at once.

Derive from structure, verify through behavior

Picture the requirements document for an ordinary marketing feature: a promo code that takes 10% off a cart. The document says the code needs to exist, needs to not be expired, and needs a cart total of at least some minimum. Three conditions, three test cases, done. Reading the actual validation function that enforces the rule surfaces two more branches nobody put in the requirements document, because nobody writing marketing copy for a discount campaign was thinking about them: a rule excluding certain product categories from the discount, and a check that this exact customer hasn't already redeemed this exact code before. That's a gray box test, spelled the American way this time, aimed at a single validator instead of an entire integration boundary.

A codebase read of the validator's five branches and a browser-driven run of the application both feeding into one derived test, which verifies the six outcomes the validator can produce
The structure decides what to check. The interface decides how it gets verified.

Here's that validator, five conditions checked in order, each one a branch a partition can be derived from directly:

function checkPromoCode(code, cart, now = new Date()) {
  if (!code) {
    return { valid: false, reason: "not_found" };
  }
  if (now >= code.expiresAt) {
    return { valid: false, reason: "expired" };
  }
  if (cart.subtotal < code.minCartTotal) {
    return { valid: false, reason: "below_minimum" };
  }
  if (cart.items.some((item) => code.excludedCategories.includes(item.category))) {
    return { valid: false, reason: "excluded_category" };
  }
  if (code.redeemedBy.includes(cart.userId)) {
    return { valid: false, reason: "already_redeemed" };
  }
  return { valid: true, reason: null };
}

module.exports = { checkPromoCode };

Read the branches in order and the partitions fall out on their own, no guessing required. The table below shows exactly what each branch derives, including one that settles a question the requirements document never actually answered: one-per-account, or one-per-cart enforced some other way? The branch settles it. Every partition here is derived from a condition that exists, not guessed from a sentence that might be incomplete.

Branch in the sourcePartition it derivesReason returned
Code lookup failsCode does not existnot_found
expiresAt in the pastExpired codeexpired
subtotal below minCartTotalCart under the minimumbelow_minimum
Item category in excludedCategoriesExcluded category in cartexcluded_category
userId present in redeemedByAlready redeemed by this accountalready_redeemed

The excluded-category branch is the one a black box tester working from the marketing brief would plausibly never test, because nothing in that brief mentions gift cards. It's the one a white box tester counting branch coverage would find instantly, and never think to ask why it isn't in anyone's written spec. Reading the structure to decide what to verify catches exactly the class a document-only reading misses.

Now change the internals without changing the rule. Instead of five sequential if statements, the same five checks become a rules table, each entry a predicate and a reason, walked in a loop until one fails:

const RULES = [
  {
    reason: "not_found",
    fails: (code) => !code,
  },
  {
    reason: "expired",
    fails: (code, cart, now) => now >= code.expiresAt,
  },
  {
    reason: "below_minimum",
    fails: (code, cart) => cart.subtotal < code.minCartTotal,
  },
  {
    reason: "excluded_category",
    fails: (code, cart) => cart.items.some((item) => code.excludedCategories.includes(item.category)),
  },
  {
    reason: "already_redeemed",
    fails: (code, cart) => code.redeemedBy.includes(cart.userId),
  },
];

function checkPromoCode(code, cart, now = new Date()) {
  for (const rule of RULES) {
    if (rule.fails(code, cart, now)) {
      return { valid: false, reason: rule.reason };
    }
  }
  return { valid: true, reason: null };
}

module.exports = { checkPromoCode };

Run both versions against the same six cases, a missing code, an expired one, a cart under minimum, a gift card in the cart, an already-redeemed code, and a valid one, and every outcome matches. The rule didn't change. Only its shape in the source did.

One refactor from an if chain to a rules table producing two outcomes: the structural test bound to the if chain fails, while the behavioral test driving the checkout page still passes because the visible rejection message never moved
One refactor, two outcomes: the structural test breaks, the behavioral test still reads the same rejection message.

A test written against the first version's structure, one that imported the individual condition checks directly, or asserted that a specific line inside the if chain executed, breaks the moment that chain becomes a loop over a rules table. Nothing about the promo code rule changed. That test was never checking the rule. It was checking a shape in the source that the refactor was free to discard.

A test written against the interface instead doesn't know or care which shape the validator takes. It fills in a promo code on the checkout page and asserts on what a shopper actually sees:

const { test, expect } = require("@playwright/test");

test("promo code SPRING10 is rejected when the cart contains a gift card", async ({ page }) => {
  await page.goto("/checkout");
  await page.getByLabel("Promo code").fill("SPRING10");
  await page.getByRole("button", { name: "Apply" }).click();

  await expect(page.getByText("This code can't be applied to gift cards")).toBeVisible();
});

test("promo code SPRING10 is accepted on an eligible cart", async ({ page }) => {
  await page.goto("/checkout?seed=eligible-cart");
  await page.getByLabel("Promo code").fill("SPRING10");
  await page.getByRole("button", { name: "Apply" }).click();

  await expect(page.getByText("10% off applied")).toBeVisible();
});

That test never imports checkPromoCode, never inspects a RULES array, never knows an if chain became a loop. It asserts on the rejection message a real customer would read, and that message didn't move. Derive the partition from the structure. Verify it through the behavior. The structure can be rewritten at will. The behavior is the part with a customer standing in front of it.

How Autonoma is grey box by construction

Everything above was done by hand, for one function, to make a point. A real codebase has many functions shaped like checkPromoCode, and no team derives partitions from every one of them by hand on every change. What actually happens looks more like the requirements-document version: partitions get guessed from a ticket, a handful of cases get written from memory, and the excluded-category branch, or whatever that codebase's equivalent turns out to be, gets covered by accident or not at all.

Autonoma closes that gap by doing, for a whole codebase, what this article just did for one function. Our agents read the routes, components, and validation logic that make up an application, and derive from that reading exactly the kind of partition this article walked through by hand: an equivalence class off a conditional, a boundary off a comparison operator, a branch a spec never mentioned. Verification then happens the same way the behavioral test above did, by driving the application's UI in a live, deployed preview environment and checking what a real user would see, not by asserting against the internal shape of the function that produced it. That combination, reading structure to decide what to check, checking it through the interface instead of the internals, is grey box testing done by construction rather than held together by one person's discipline. A person choosing that combination on purpose, every time, for a codebase too large to do it by hand, was always the compromise's actual bottleneck. It was never the technique itself.

Map it onto the worked example directly. The codebase reading is the part that finds an excluded-category branch without anyone writing it into a ticket. The behavioral execution is the part that survives the refactor from an if-chain to a rules table, because it was never watching the if-chain in the first place. When the code changes again next quarter, the same reading happens again against the new structure, rather than against a stale assumption about what the old one looked like.

What this architecture does not give you

None of this makes the other two boxes optional, and being plain about the boundary is the entire point of calling this a taxonomy instead of a pitch. Behavioral, browser-driven verification is not the same job as several other things a team still needs, and each of those things has a tool built for exactly that job.

Unit-level structural coverage, statement and branch coverage counted against a single function the way white box testing does, is a job for coverage instrumentation inside your own test runner, not for anything watching a browser. Contract verification between two services, confirming that what one team ships still matches what another team's client expects, belongs to a dedicated contract testing framework, not to a tool that only sees a rendered page. Load and performance behavior needs a load testing tool built to generate concurrent traffic, not a suite designed to check one user's flow at a time. Accessibility conformance needs an automated scanner paired with a manual audit: a scanner catches missing labels and contrast ratios, a human catches whether a screen reader user can actually finish the flow.

Five peer boxes on one row: unit coverage, contract testing, load testing, and accessibility scan in neutral grey, with a behavioral Autonoma layer in lime alongside them, none stacked above another
Four dedicated tools, one behavioral layer, all on the same level.

Autonoma is the behavioral, browser-driven layer sitting alongside all four, not instead of them. A team running proper branch coverage, contract tests, load tests, and an accessibility scanner still needs something driving the actual UI the way a customer would, and that's the specific job this architecture does.

Reading the two other boxes from here

Grey box testing only makes sense next to the other two. White box testing covers the coverage-criteria arithmetic this article deliberately left alone: what statement, branch, and path coverage actually count on a single function, and why structural coverage can be complete while still missing a requirement no one wrote down. Black box vs. white box testing covers the three-way comparison by access, by who performs each kind of testing, and by what each one actually detects, the table this article intentionally didn't repeat. And the seven test design techniques, equivalence partitioning and boundary value analysis among them, are the specific methods this article's worked example borrowed from, laid out individually with one worked example each. Separately, what agentic testing is, how it compares with traditional automation, and what AI-driven end-to-end testing looks like in practice cover the tooling category this article stayed out of; this one is about the technique, not the tools implementing it.

Grey box testing earned its name from a limitation that had nothing to do with the technique and everything to do with who was doing the looking. A person verifying a system through its interface, working from a document instead of the source, was never choosing to see less on purpose. It was the only view available to one set of eyes at a time. Once deriving from structure and verifying through behavior can happen inside a single system instead of a single person, the middle position stops being a compromise and starts being the most honest position about how testing should work. That's the position Autonoma is built from directly: read a codebase to decide what belongs in a test, then check it the way a person at a keyboard would.

Frequently Asked Questions

Grey box testing, also written gray box testing in American usage, is a software testing approach performed with partial knowledge of a system's internals, verified by exercising the application through its normal external interface rather than inspecting the code directly. A tester might know a data schema, an API contract, or an architecture diagram without having read the actual implementation, and uses that partial knowledge to design test cases that are then run the way an end user or a calling service would run them.

Black box testing assumes zero knowledge of internals: the tester only sees inputs and outputs. Grey box testing assumes partial knowledge, a schema, a contract, a state diagram, and uses that knowledge to target test cases more precisely, while still verifying behavior through the same external interface a black box tester would use. The difference is in how the test case gets chosen, not in how it gets run.

A common example is testing a promo code validator with knowledge of the database schema and business rules but not the source code itself: knowing that codes expire, have a minimum cart total, exclude certain product categories, and can be redeemed once per account, then verifying each of those rules by actually applying a code on a checkout page rather than calling the validation function directly.

Grey box testing fits situations where partial internal knowledge is genuinely all that's available or appropriate: integration testing between two services where you know the contract but not the other team's implementation, security testing where a tester knows enough of the schema to attempt a targeted case without a full code read, and QA testing performed against API documentation and a staging environment without repository access.

The advantage is targeting: partial internal knowledge lets a tester aim at conditions a pure black box tester would never think to try, while still verifying through the interface a real user touches, so the test survives internal refactoring. The disadvantages are that it is less rigorous than white box testing, because the tester infers conditions from documentation that may be stale rather than reading them, and less representative than black box testing, because knowing the schema changes which inputs the tester reaches for.

It can be. The technique itself doesn't specify who or what derives the test cases or runs them, only that the derivation draws on partial internal knowledge while the verification happens through the external interface. Automating the derivation, deriving partitions directly from source rather than from a person's partial understanding of it, is what removes the original limitation of the technique rather than the technique itself.

Yes, by construction rather than by label. Autonoma's agents read a codebase to derive what should be tested, the same partial-but-structural knowledge a grey box tester works from, and verify it by driving the running application's UI in a live preview environment, the same external-interface verification a grey box tester performs by hand. It doesn't replace unit test coverage, contract testing, load testing, or accessibility scanning. It's the behavioral layer that sits alongside them.

Related articles

Six test automation anti-pattern icons, including an inverted ice cream cone test pyramid, arranged around a single root-cause symbol

Test Automation Anti-Patterns: 6 Failures, 1 Root Cause

Six test automation anti-patterns, each with a diagnostic tell you can check today, and the one root cause behind every one of them.

Three vertical panels of the same application, each showing progressively more of the underlying implementation exposed, illustrating black box, grey box, and white box access

Black Box Testing: 3 Access Levels, Same System

Black box and white box testing differ in implementation access, not skill. The three-way comparison table everyone else drops the middle row from.

White box testing coverage as four concentric rings around a branching code structure, with unlit crescent tiles stranded outside every ring

What Is White Box Testing? 4 Coverage Criteria, Counted

White box testing gives you access to the implementation. See statement, branch, condition, and path coverage counted on one real function: 4, 4, 6, 8 tests.

A horizontal agent trajectory diagram showing a tool call passing a right-tool checkpoint but failing an argument-accuracy checkpoint

How to Test AI Agents That Take Actions (Tool Calls)

A runnable guide to testing tool-calling agents: right tool, right order, right arguments, mocked vs live calls, failure handling, and non-determinism.