The previous lesson handed you the brief. This lesson walks you through actually building it. Each step maps to a chapter you've already studied and produces one of the deliverables in the rubric. Work through them in order — every step depends on the one before. Pause Postman where you need to; the goal is a working collection at the end, not racing through. Plan two to three sessions of two to three hours each. Open Postman, open a terminal, open this lesson alongside; build as you read.
The build, end to end
Nine steps, one collection, one Git repo, one CI run.
Step 1 of 9
1. Collection structure
Create the PetStore collection with feature folders. Verb-first request names. ~30 minutes.
Total: roughly 5 hours of focused work. Every step ends in something runnable; if something breaks, you have the previous step's known-good state to fall back on.
Step 1 — Collection structure
Create the collection. Top-left New button → Collection → name it PetStore API Tests. In the right panel, paste a short markdown description:
# PetStore API Tests
End-to-end test suite for the PetStore REST API.
## Running locally
1. Import this collection.
2. Import the Local, Staging, or Production environment.
3. Set the active environment (top-right dropdown).
4. Run the **CRUD Chain** folder to verify setup.Add five folders by right-clicking the collection → Add folder: Auth, Categories, Pets, Orders, CRUD Chain. Inside each folder, create the requests listed in the brief. Names are verb-first: POST Register, GET All Pets, DELETE Order. Save every one with Cmd/Ctrl+S to its right folder.
For now, the URLs use {{baseUrl}} even though {{baseUrl}} doesn't resolve yet — we'll fix that in step 2.
Step 2 — Environments
In the Environments sidebar tab, click + three times. Create:
Local: variablesbaseUrl = http://localhost:3000,testEmail = qa@petstore.local,testPassword = QApass!1.Staging: variablesbaseUrl = https://petstore3.swagger.io/api/v3(or your local mock URL),testEmail,testPassword. MarktestPasswordas typesecret.Production: variablesbaseUrl = https://api.petstore.com, plustestEmail(a read-only smoke account). Used only for production smoke runs.
For each environment, use the Initial value column for placeholders and the Current value column for real values. The two are independent (Chapter 2 Lesson 2). Activate the Staging environment from the top-right dropdown.
Step 3 — Auth + auto-refresh
Right-click the collection → Edit → Authorization tab. Set:
- Type:
Bearer Token. - Token:
{{authToken}}.
Save. Now every request in the collection inherits Bearer auth. Open POST Register and POST Login and override their Authorization to No Auth — they need to run before a token exists.
In the same Edit Collection dialog, switch to the Pre-request Script tab. Paste the auto-refresh skeleton:
const tokenExpiry = pm.collectionVariables.get("tokenExpiry");
const now = Date.now();
if (!tokenExpiry || now > parseInt(tokenExpiry, 10)) {
console.log("Token missing or expired — refreshing");
pm.sendRequest({
url: pm.environment.get("baseUrl") + "/users/login",
method: "POST",
header: { "Content-Type": "application/json" },
body: {
mode: "raw",
raw: JSON.stringify({
email: pm.environment.get("testEmail"),
password: pm.environment.get("testPassword")
})
}
}, (err, res) => {
if (err) {
console.error("Login failed:", err);
return;
}
const data = res.json();
pm.collectionVariables.set("authToken", data.token);
pm.collectionVariables.set("tokenExpiry", (now + 29 * 60 * 1000).toString());
console.log("Refreshed authToken");
});
}Save. The collection now logs in automatically before any request fires when the token is missing or older than 29 minutes.
Step 4 — Request chaining
Open the CRUD Chain folder. The order is 1. POST Register, 2. POST Login, 3. POST Create Pet, 4. POST Create Order, 5. GET Verify Order, 6. DELETE Order. Each request's Tests tab does its bit:
// 1. POST Register — Tests
const r = pm.response.json();
pm.test("User registered", () => {
pm.expect(pm.response.code).to.be.oneOf([201, 200, 409]); // 409 if email already exists is OK
});// 2. POST Login — Tests
const r = pm.response.json();
pm.test("Login successful", () => {
pm.response.to.have.status(200);
pm.expect(r).to.have.property("token");
});
pm.collectionVariables.set("authToken", r.token);
pm.collectionVariables.set("tokenExpiry", (Date.now() + 29 * 60 * 1000).toString());
pm.collectionVariables.set("userId", r.user.id);// 3. POST Create Pet — Tests
const r = pm.response.json();
pm.test("Pet created", () => {
pm.response.to.have.status(201);
pm.expect(r).to.have.property("id");
});
pm.collectionVariables.set("petId", r.id);
console.log("Saved petId:", r.id);// 4. POST Create Order — Tests
const r = pm.response.json();
pm.test("Order created", () => {
pm.response.to.have.status(201);
pm.expect(r.petId).to.equal(parseInt(pm.collectionVariables.get("petId"), 10));
});
pm.collectionVariables.set("orderId", r.id);The body of POST Create Order references the chained value:
{ "petId": {{petId}}, "quantity": 1 }Run the chain via Collection Runner → right-click the folder → Run folder. Six green ticks expected. If anything fails, open the failed request, check the resolved URL in the Console, fix, re-run.
Step 5 — Test assertions
Now layer in the broader test set. The brief asks for at least 30 assertions across the collection. Distribution:
- ~5 assertions per happy-path request: status, content-type, body shape, response time, schema.
- ~3 assertions per negative test: status, error-message presence, no leaked sensitive fields.
- 5+ assertions in the chain on shape preservation between steps.
A happy-path template you can paste on most GETs:
const body = pm.response.json();
pm.test("Status is 2xx", () => {
pm.expect(pm.response.code).to.be.within(200, 299);
});
pm.test("Response time is acceptable", () => {
pm.expect(pm.response.responseTime).to.be.below(800);
});
pm.test("Content-Type is JSON", () => {
pm.expect(pm.response.headers.get("Content-Type")).to.include("application/json");
});
pm.test("Body has expected shape", () => {
pm.expect(body).to.have.property("id");
pm.expect(body).to.have.property("name");
});
pm.test("No password fields leak", () => {
pm.expect(body).to.not.have.property("password");
pm.expect(body).to.not.have.property("passwordHash");
});A schema-validation template (paste on GET Pet by ID):
const petSchema = {
type: "object",
required: ["id", "name", "categoryId", "status"],
properties: {
id: { type: "number" },
name: { type: "string", minLength: 1 },
categoryId: { type: "number" },
status: { type: "string", enum: ["available", "pending", "sold"] },
price: { type: "number", minimum: 0 }
}
};
pm.test("Pet matches schema", () => {
const valid = tv4.validate(pm.response.json(), petSchema);
pm.expect(valid, JSON.stringify(tv4.error)).to.be.true;
});Negative cases — make a Negative Cases folder and add at least five:
POST Login — wrong password— expect 401, no token in response.POST Create Pet — missing name— expect 400.GET Pet by ID — invalid id— expect 404 (or whatever the API returns).POST Create Pet — no auth— expect 401.DELETE Pet — wrong owner— expect 403.
Each negative test asserts on the failure status and the absence of unexpected fields (no token, no leaked debug info).
Run the full collection from the Collection Runner. Total assertion count appears in the summary — verify it's at least 30.
Step 6 — Data-driven CSV
Create pets-data.csv on disk:
name,categoryId,status,price,expectedStatus
Buddy,1,available,29.99,201
Whiskers,2,available,15,201
Rex,1,sold,40,201
,,available,0,400
Bigfoot,9999,available,5,400
Polly,3,pending,12.50,201
"Sir Reginald III",1,available,99.99,201
"Reggie ' OR 1=1 --",1,available,5,400
Shadow,2,unknown_status,10,400
Felix,2,available,-5,400Ten rows: six valid creations, four expected failures. Use it on the POST Create Pet request:
- Body:
{ "name": "{{name}}", "categoryId": {{categoryId}}, "status": "{{status}}", "price": {{price}} }. - Tests:
const expected = parseInt(pm.iterationData.get("expectedStatus"), 10); pm.test(`Iteration "${pm.iterationData.get("name") || "(empty)"}" → ${expected}`, () => { pm.response.to.have.status(expected); });
Right-click the request → Run request → in the Runner, click Select File under Data → pick pets-data.csv → Run. Ten iterations, ten dynamically-named test results. Take a screenshot of the report — it's a portfolio piece.
Step 7 — Newman setup
Make a directory petstore-tests/. Inside it:
mkdir postman reports
git init
npm init -y
npm install --save-dev newman newman-reporter-htmlextraExport the collection (right-click → Export → Collection v2.1 → save to postman/petstore.postman_collection.json). Export each environment (Environments tab → ⋯ → Export → save to postman/<env>.postman_environment.json). Add pets-data.csv to postman/ too.
Add to package.json:
{
"scripts": {
"test:api": "newman run postman/petstore.postman_collection.json -e postman/staging.postman_environment.json -r cli,htmlextra,junit --reporter-htmlextra-export reports/api-report.html --reporter-junit-export reports/results.xml --timeout-request 10000",
"test:api:smoke": "newman run postman/petstore.postman_collection.json --folder Smoke -e postman/production.postman_environment.json --bail",
"test:api:data": "newman run postman/petstore.postman_collection.json -e postman/staging.postman_environment.json -d postman/pets-data.csv --folder Pets"
}
}Run npm run test:api. Watch the CLI output. Confirm echo $? is 0 and the HTML report exists at reports/api-report.html. Open it in a browser.
Step 8 — HTML report
The htmlextra report has summary, per-folder breakdown, request/response details, and a "show failed only" filter. Validate that:
- Summary counts match the CLI output (same totals).
- Each folder's pass/fail distribution looks right.
- Click any failed request — the assertion message and the actual response should be visible.
To prove the report shows failures correctly, deliberately break one assertion (expect(...).to.equal(999)), re-export, re-run. Confirm a red row appears. Revert.
Step 9 — GitHub Actions
Add reports/ and node_modules/ to .gitignore. Stage the rest:
echo "reports/" >> .gitignore
echo "node_modules/" >> .gitignore
git add .
git commit -m "Add PetStore Postman test suite"Create a GitHub repo, push:
git remote add origin https://github.com/<you>/petstore-tests.git
git branch -M main
git push -u origin mainAdd .github/workflows/api-tests.yml:
name: API Tests
on:
push:
branches: [main]
pull_request:
jobs:
api-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- name: Run API tests
run: |
npx newman run postman/petstore.postman_collection.json \
-e postman/staging.postman_environment.json \
--env-var "testEmail=${{ secrets.STAGING_EMAIL }}" \
--env-var "testPassword=${{ secrets.STAGING_PASSWORD }}" \
-r cli,htmlextra,junit \
--reporter-htmlextra-export reports/api-report.html \
--reporter-junit-export reports/results.xml \
--timeout-request 10000
- if: always()
uses: actions/upload-artifact@v4
with:
name: api-test-report
path: reports/In GitHub repo settings → Secrets and variables → Actions, add STAGING_EMAIL and STAGING_PASSWORD. Commit and push the workflow file. Open the Actions tab in GitHub — the workflow should run on the push and (assuming the staging env is reachable) finish green. Download the api-test-report artifact, open the HTML, confirm it matches what you saw locally.
You're done
Nine steps, nine deliverables, one running CI pipeline. You've now built end-to-end:
- A collection that someone else can pick up and run locally.
- Three environments switchable from a dropdown.
- Auto-refreshing auth that survives long runs.
- A chained CRUD test that exercises the whole API.
- 30+ assertions covering positive and negative paths.
- A data-driven test set with ten input rows.
- A Newman command and an HTML report.
- A GitHub Actions workflow that runs on every PR.
- A Git repo other engineers can review.
That's the same skeleton most QA teams use in real projects. Take a screenshot of the green CI run; that goes on your CV. The next lesson is the rubric — a checklist to grade your own work, plus stretch goals if you want to go further.