Positive and Negative Testing

Positive testing checks that a system does what it should when it is used as intended: valid data, allowed actions, steps in the expected order. Negative testing, as the ISTQB glossary defines it, tests a system in a way it was not intended to be used: missing or invalid data, an action in the wrong state, a user without permission. Many teams also include error conditions, such as a service that does not answer.

At checkout, entering the current discount code SAVE20 and seeing the discount is a positive test. Entering RAMADAN24, which ended yesterday, and checking that it is refused with a clear message while the total stays the same is a negative test.

Why it matters

Users mistype, leave fields empty and press Back at the wrong moment; other systems time out. A product that works only on the happy path may lose a user's input after one mistake, answer an invalid request with a server error, or charge a customer twice. These are defects in the code that handles the unexpected, and they reach production when nobody designed a test for them.

Developers usually try the happy path before handing a feature over; the rejection paths are less likely to have been tried, so negative tests uncover much of the hidden risk. Positive tests stay essential: they prove that the feature serves its purpose.

Positive testing and the happy path

A positive test uses valid data and an allowed sequence of actions, and expects success: correct output, correctly stored data and the right side effects, such as a confirmation email. As in the lesson on functional testing, the expected result comes from the test basis, not from whatever the system happens to do.

The happy path is the main scenario in which everything goes as planned: typical data, default choices, no mistakes. Some teams call it the main success scenario. A feature has one happy path but usually several positive tests, because there is more than one valid way to succeed.

  • Happy path: a cart of 250.00 SAR and the code SAVE20 (20% off) give a discount of 50.00 SAR and a total of 200.00 SAR.
  • Valid variations: save20 in lower case works too, because codes are not case-sensitive; removing the code returns the total to 250.00 SAR.

Try it: Positive Test Generator

Negative testing: invalid input and unintended use

A negative test deliberately gives the system something it should not accept and expects a controlled rejection. The test passes when the system refuses exactly as specified. It fails when the system accepts the input, crashes, or refuses in a harmful way, such as clearing the whole form.

The test basis defines what is invalid; you will meet the same idea as invalid equivalence partitions in the test design module. Look for negative tests in these categories:

  • Missing data: an empty required field, an API request without a body.
  • Wrong format or type: letters in an amount, an email without @, the date 31/02/2026.
  • Out of range: a quantity of 0 or -1.
  • Wrong state or order: RAMADAN24 after its end date, paying for an order that is already paid.
  • No permission: opening another customer's order, only on systems you own or have written permission to test.
  • A failed dependency: the payment provider times out.
Discount code at checkout: the positive test passes when SAVE20 is applied, the negative test passes when the expired code RAMADAN24 is refused with a clear message and the total stays 250.00 SAR.

Positive test

Valid input, intended use

Question it answers:
Does it work when used as intended?
Typical input:
Valid data, allowed actions, the expected order
Example:
SAVE20 on a cart of 250.00 SAR
Expected result:
Discount 50.00, total 200.00, code shown as applied
The test passes when:
The action succeeds as specifiedPass
A defect shows as:
A wrong result, or valid input rejectedFail

Negative test

Invalid input or unintended use

Question it answers:
Does it refuse safely when used wrongly?
Typical input:
Missing data, wrong format, out of range, wrong state, no permission, a failed dependency
Example:
RAMADAN24, which ended yesterday
Expected result:
“This code has expired”, total still 250.00, no internal details shown
The test passes when:
The action is refused as specifiedPass
A defect shows as:
Invalid input accepted, a 500 error, a vague message or changed dataFail

Both are tests that should pass: “negative” describes the input and the intent, not the outcome.

Try it: Negative Test Generator Try it: Validation Test Generator

Expected error handling: what a correct rejection looks like

"An error is shown" is not an expected result, because almost any behaviour passes it. Write down the parts of a correct rejection:

At API level, the request below sends the expired code. The test checks the status and the body, and a second request, GET /api/cart, confirms that the total is still 250.00 SAR.

  • A specific message, in text next to the field, that says what to fix. Specific is not revealing: a login form says that the email or password is incorrect, not which one.
  • No other change: no record created, no money moved, no email sent, and the user's input kept.
  • No internal details: no stack trace or SQL error. The OWASP Error Handling Cheat Sheet recommends a generic message for the user and the details in the server log.
  • The status code the API contract defines. RFC 9110 uses the 4xx class for errors in the client's request and 5xx for server errors, so invalid data gets a 4xx such as 400 or 422, never a 500.
  • Recovery: once the input is corrected, the positive path works without starting again.
POST /api/cart/discount HTTP/1.1
Host: shop.example.com
Content-Type: application/json

{"code": "RAMADAN24"}

HTTP/1.1 422 Unprocessable Content
Content-Type: application/json

{"error": "code_expired", "message": "This discount code has expired."}

Try it: API Status Code Simulator Try it: Mock Error Response Generator

Worked example: tests for a discount code

Requirement: "At checkout, a customer can apply one discount code. A code works only between its start and end dates and once per customer, and it is not case-sensitive." The cart holds 250.00 SAR of eligible items.

Start with the happy path, then use one invalid condition per negative test, so that a failure points to exactly one rule. In DC-03 to DC-06 the total must stay 250.00 SAR. Designing negative tests also exposes gaps in the requirement, as DC-07 shows.

  • DC-01, positive (happy path): SAVE20 → discount 50.00, total 200.00.
  • DC-02, positive: save20 → the same result as DC-01.
  • DC-03, negative: Apply with the field empty → "Enter a discount code".
  • DC-04, negative: NOPE99, which does not exist → "This code is not valid".
  • DC-05, negative: RAMADAN24, which ended yesterday → "This code has expired".
  • DC-06, negative: SAVE20 a second time by the same customer → "You have already used this code".
  • DC-07, open question: WELCOME10 while SAVE20 is applied. Does it replace the first code or is it refused? Ask the product owner before writing the expected result.

Common mistakes

  • Testing only the happy path, because that is what the demo and the acceptance criteria show.
  • Writing "an error is shown" as the expected result, without the exact message or what must stay unchanged.
  • Putting several invalid values in one test: the first error hides the others.
  • Marking a negative test as failed because an error appeared, when that error was the expected result.
  • Checking only the screen: the message appears, but the order was still created.
  • Testing invalid input only through the form while the API behind it accepts the same data. Send it to the API too, on a system you are allowed to test.

The QA perspective

Negative testing starts in backlog refinement. For every acceptance criterion, ask "and what if not?": missing input, the wrong state, no permission, the payment service down. User stories often describe only the happy path, so these questions reveal error behaviour nobody has specified yet. Agree the messages with the product owner and the status codes with the developers before coding starts.

Mark each test case as positive or negative so suites can be filtered. Run the happy path first: if it is broken, report it at once, because it blocks most other tests. A bug report for a failed negative test names the exact input, the expected rejection, the actual behaviour and any side effect. Severity follows the consequence: an unclear message is usually minor, while invalid data saved or money moved is critical.

Practice exercise

Design positive and negative tests for a transfer

Your task

A banking app has this requirement: "A signed-in customer can transfer money between two of their own accounts. The amount must be greater than 0, have at most 2 decimal places and not exceed the available balance or 20,000.00 SAR per day in total. Source and destination must differ. After a successful transfer both balances update and a reference number is shown."

Design the tests on paper, then compare your list with the Positive Test Generator and the Negative Test Generator.

Steps
  1. Write the happy path with exact data and the expected result, then two more positive tests.
  2. Write at least one negative test per category from this lesson, with one invalid condition each.
  3. For every negative test, write the expected error handling.
  4. List the questions the requirement leaves open.
Show the model answer
Model answer

Positive: 500.00 SAR from Current (available 2,000.00) to Savings succeeds with a reference number; Current shows 1,500.00 and Savings is 500.00 higher. This is the happy path. Also: 125.50 SAR succeeds, and Savings to Current succeeds.

Negative: empty amount ("Enter an amount"); abc ("Enter a valid amount"); 0 and -100 ("The amount must be greater than 0"); 12.345 ("Use at most 2 decimal places"); 2,500.00 with 2,000.00 available ("Insufficient balance"); the same account on both sides ("Choose a different account"); 600.00 after 19,800.00 sent today ("Daily limit exceeded").

State, permission and dependency: the session expires before Confirm (no transfer); another customer's account number sent through the API in your test environment (refused with a 4xx, nothing moves); the core banking service does not respond (a clear "try again later" message, and money never leaves Current without reaching Savings).

Error handling for every negative test: both balances unchanged, no reference number, no history entry, the input kept, no technical details.

Questions: which time zone defines "per day"? Is the limit per customer or per account? What does the customer see if the connection drops after Confirm?

Frequently asked questions

If a negative test passes, did the system fail?

No. A test passes when the actual result matches the expected result. For a negative test the expected result is a correct rejection, so the test passes when the system refuses the input as specified. If the system accepts the expired code, the test fails and you have observed a failure, caused by a defect in the input validation.

How many negative tests should I write compared with positive ones?

There is no fixed ratio. There are usually more ways to use a feature wrongly than rightly, so negative tests tend to outnumber positive ones. Choose them by risk: anything that touches money, personal data or permissions first, then the mistakes real users make most often.

Is negative testing the same as testing edge cases?

No. Negative testing is about intent: checking that invalid input or unintended use is refused. An edge case is a value or condition at an extreme, such as the longest allowed name or an empty list, and it can be valid or invalid. A name one character over the limit is both an edge value and a negative test.

References

Practise with Mutqan tools

Generate positive (happy-path) test cases that confirm each requirement and valid input combination works.

Test Design

Generate negative test cases for form fields and APIs: empty, null, wrong type, too long, injection, invalid format.

Test Design

Generate validation test cases for form fields: required, format, length, range, pattern and cross-field rules.

Test Design

Get a public endpoint that returns any HTTP status code, with optional delay and custom JSON body, to test client error handling.

API Testing

Generate structured error responses (validation, auth, not found, rate limit, server error) in popular formats.

Mocking

Go deeper

Your progress on this topic

Part of the QA Roadmap