This content originally appeared on DEV Community and was authored by Yacine Si Tayeb
Who this is for: platform and QA engineers, API owners, integration leads
What you’ll get: copy-paste Pact contracts, Postman smoke tests and monitors, a GitHub Actions workflow, a crisp backward-compat policy, and optional Pact Broker wiring. Everything below is runnable.
Why Open APIs still break (even when you follow the spec)
TM Forum Open APIs such as TMF620 Product Catalog and TMF622 Product Ordering give teams a common language. That still doesn’t prevent outages. The usual culprits:
- Version drift. A provider updates its implementation or CTK; consumers lag. “Optional” fields become practically required; responses change shape; integrations crack.
- Schema vs. reality. Specs allow many optional fields. Your consumer needs five of them. A provider “tidies” an enum or omits a field and production breaks. Contracts must reflect consumer expectations, not just a published schema.
- Faster release tempo. Teams ship more often; coordination thins; small changes leak.
CTK vs. CDC in one line: CTK proves spec conformance; consumer-driven contracts protect what your consumers actually rely on. You need both.
The fix in practice
Consumer-driven contracts (CDC). Each consumer codifies what it needs; the provider verifies those expectations before shipping. Pact turns that into runnable tests and shareable contracts.
“Contract tests check the shape of external service calls, not the exact data.” — paraphrasing Martin Fowler.
What we’ll build
- Pact consumer tests for TMF620 and TMF622 that produce pact files.
- Pact provider verification against your mock or service.
- Postman collection for smoke tests and a Monitor on a schedule.
- GitHub Actions that fail PRs on contract regressions and run smokes after deploy.
- Optional Pact Broker to distribute and version contracts across teams.
- Pact that actually prevents regressions
TMF620 consumer test: list Product Offerings
// consumer/tmf620.catalog.consumer.spec.js
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import fetch from 'node-fetch';
const { like, term } = MatchersV3;
const provider = new PactV3({
consumer: 'CatalogUI',
provider: 'TMF620-Catalog',
});
describe('TMF620 Catalog - list productOfferings', () => {
it('returns minimal fields the UI depends on', async () => {
provider
.given('product offerings exist')
.uponReceiving('GET /productOffering?lifecycleStatus=Launched')
.withRequest({
method: 'GET',
path: '/tmf-api/productCatalogManagement/v4/productOffering',
query: { lifecycleStatus: 'Launched' }
})
.willRespondWith({
status: 200,
headers: { 'content-type': 'application/json' },
body: like([{
id: like('123'),
name: like('5G 50GB Plan'),
lifecycleStatus: term({ generate: 'Launched', matcher: '^(Launched|Active)$' }),
price: like(20),
currency: term({ generate: 'AUD', matcher: '^[A-Z]{3}$' })
}])
});
return provider.executeTest(async (mock) => {
const r = await fetch(
`${mock.url}/tmf-api/productCatalogManagement/v4/productOffering?lifecycleStatus=Launched`
);
const json = await r.json();
// assert only what the UI actually uses
expect(Array.isArray(json)).toBe(true);
expect(json[0].name).toBeTruthy();
expect(json[0].lifecycleStatus).toMatch(/^(Launched|Active)$/);
});
});
});
Value: you encode only what the UI relies on. Providers are free to change everything else.
TMF622 consumer test: create Product Order
// consumer/tmf622.order.consumer.spec.js
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import fetch from 'node-fetch';
const { like, term } = MatchersV3;
const provider = new PactV3({
consumer: 'CheckoutService',
provider: 'TMF622-Ordering',
});
describe('TMF622 - create productOrder', () => {
it('creates an order with minimal required fields', async () => {
const requestBody = {
orderItem: [{
action: "add",
productOffering: { id: "123" },
quantity: 1
}]
};
provider
.given('catalog offering 123 is orderable')
.uponReceiving('POST /productOrder')
.withRequest({
method: 'POST',
path: '/tmf-api/productOrderingManagement/v4/productOrder',
headers: { 'content-type': 'application/json' },
body: requestBody
})
.willRespondWith({
status: 201,
headers: { 'content-type': 'application/json' },
body: {
id: like('po-001'),
state: term({ generate: 'acknowledged', matcher: '^(acknowledged|inProgress)$' }),
orderItem: like([{
id: like('1'),
action: 'add',
productOffering: { id: like('123') },
quantity: like(1)
}])
}
});
return provider.executeTest(async (mock) => {
const r = await fetch(`${mock.url}/tmf-api/productOrderingManagement/v4/productOrder`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(requestBody)
});
expect(r.status).toBe(201);
const json = await r.json();
expect(json.state).toMatch(/^(acknowledged|inProgress)$/);
expect(json.orderItem?.[0]?.productOffering?.id).toBe('123');
});
});
});
Value: you cover the write path. Providers can change non-essential fields, but not the structure your checkout relies on.
Provider verification and a tiny mock
// provider/pact.verify.js
import { Verifier } from '@pact-foundation/pact';
const pactUrls = [
process.env.PACT_TMF620 || './pacts/catalogui-tmf620-catalog.json',
process.env.PACT_TMF622 || './pacts/checkoutservice-tmf622-ordering.json'
];
const opts = {
providerBaseUrl: process.env.PROVIDER_URL || 'http://localhost:3000',
pactUrls,
publishVerificationResult: false
};
new Verifier(opts)
.verifyProvider()
.then(() => console.log('✅ Provider verified against consumer expectations'))
.catch((e) => { console.error('❌ Verification failed', e); process.exit(1); });
// provider/mock.js
import express from "express";
const app = express();
app.use(express.json());
// TMF620
app.get("/tmf-api/productCatalogManagement/v4/productOffering", (req, res) => {
if (req.query.lifecycleStatus !== "Launched") {
return res.status(400).json({ error: "bad query" });
}
res.json([{
id: "123",
name: "5G 50GB Plan",
lifecycleStatus: "Launched",
price: 20,
currency: "AUD"
}]);
});
// TMF622
app.post("/tmf-api/productOrderingManagement/v4/productOrder", (req, res) => {
const body = req.body || {};
if (!Array.isArray(body.orderItem) || !body.orderItem[0]?.productOffering?.id) {
return res.status(422).json({ error: "invalid order" });
}
res.status(201).json({
id: "po-001",
state: "acknowledged",
orderItem: [{
id: "1",
action: "add",
productOffering: { id: String(body.orderItem[0].productOffering.id) },
quantity: Number(body.orderItem[0].quantity || 1)
}]
});
});
app.listen(3000, () => console.log("Mock provider on :3000"));
Show a failing case so teams learn to debug fast
Break the mock intentionally. For example, remove currency from the TMF620 response or change state to "created" in TMF622. Provider verification will fail with output like:
❌ Verification failed
- Expected body to have property: currency
- Expected "state" to match /^(acknowledged|inProgress)$/ but was "created"
Takeaway: this is exactly the signal you want in CI before users see a break.
Postman smoke tests and monitors
Contracts protect client expectations; smoke tests protect availability, auth, and latency.
Postman request examples use {{baseUrl}} so you can switch environments.
GET Launched offerings
GET {{baseUrl}}/tmf-api/productCatalogManagement/v4/productOffering?lifecycleStatus=Launched
Tests
pm.test("200 OK", () => pm.response.to.have.status(200));
pm.test("Launched only", () => {
const body = pm.response.json();
pm.expect(body.every(x => x.lifecycleStatus === "Launched")).to.be.true;
});
pm.test("p95 under 500ms", () => {
pm.expect(pm.response.responseTime).to.be.below(500);
});
POST product order
POST {{baseUrl}}/tmf-api/productOrderingManagement/v4/productOrder
Content-Type: application/json
{
"orderItem": [{
"action": "add",
"productOffering": { "id": "123" },
"quantity": 1
}]
}
Tests
pm.test("201 Created", () => pm.response.to.have.status(201));
pm.test("State acknowledged/inProgress", () => {
const j = pm.response.json();
pm.expect(j.state).to.match(/^(acknowledged|inProgress)$/);
});
Monitor operations that won’t page you at 2am
- Create two environments: staging and prod with different baseUrl values.
- Route alerts: staging → Slack channel with severity “warn”; prod → PagerDuty or email with severity “page”.
- Start with the three most customer-visible flows only. Add more later.
Golden payloads and a backward-compat policy
Pin a golden response per endpoint to a tag.
Example:
// /docs/golden/GET-productOffering-Launched.json
[
{
"id": "123",
"name": "5G 50GB Plan",
"lifecycleStatus": "Launched",
"price": 20,
"currency": "AUD"
}
]
Backward-compat policy
- Additive changes only to existing endpoints.
- Any breaking change uses a new versioned path
/v2/...or clearly prefixed fieldsxV2_*. - Dual-write and dual-read for 120 days, then deprecate with docs and monitor alerts.
Compatibility is a process, not a promise.
GitHub Actions you can copy
# .github/workflows/api-quality.yml
name: API Quality
on:
pull_request:
push:
branches: [ main ]
env:
BASE_URL_STAGING: https://staging.example.com
BASE_URL_PROD: https://api.example.com
jobs:
pact-consumer:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
working-directory: consumer
- run: npm test -- --runInBand
working-directory: consumer
- name: Upload pact artifact
uses: actions/upload-artifact@v4
with:
name: pact
path: consumer/pacts/*.json
pact-provider-verify:
runs-on: ubuntu-latest
needs: pact-consumer
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- name: Download pact
uses: actions/download-artifact@v4
with:
name: pact
path: provider/pacts
- run: npm ci
working-directory: provider
- name: Start provider mock
run: npm run start:mock &
working-directory: provider
- name: Verify against pact
run: node pact.verify.js
working-directory: provider
postman-smoke-staging:
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Postman collection with Newman (staging)
uses: matt-ball/newman-action@v1
with:
collection: postman/collections/tmf-smoke.json
environment: postman/envs/staging.json
reporters: cli,junit
postman-smoke-prod:
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Postman collection with Newman (prod)
uses: matt-ball/newman-action@v1
with:
collection: postman/collections/tmf-smoke.json
environment: postman/envs/prod.json
reporters: cli,junit
Security hygiene
- Store secrets such as Postman API keys or Pact Broker tokens in GitHub Encrypted Secrets.
- Use least-privilege tokens and avoid printing secrets in logs.
- Do not run prod monitors on pull requests.
Optional: share contracts via a Pact Broker
If you have multiple teams and pipelines, a Pact Broker makes contract distribution and versioning easier.
- Publish consumer pacts on PR merge; tag by env or version.
- Providers pull and verify the latest contracts for their tagged stream.
- Gate deploys on verification status.
Minimal publishing step:
pact-broker publish consumer/pacts \
--broker-base-url=$PACT_BROKER_URL \
--broker-token=$PACT_BROKER_TOKEN \
--consumer-app-version=$GITHUB_SHA \
--tag staging
Copy-paste quickstart structure
/contracts
/consumer # Pact consumer tests (TMF620, TMF622)
/provider # Pact verification + mock
/postman
/collections
/envs # staging.json, prod.json with baseUrl
/docs/golden/GET-productOffering-Launched.json
.github/workflows/api-quality.yml
Start with TMF620 “list productOfferings” and TMF622 “create productOrder”. Add endpoints as you touch them.
SLIs you can measure in a week
- Contract verification pass rate per service (target ≥ 99 percent).
- Time to detect a breaking change via CI (target under 10 minutes).
- Smoke MTTR (time from monitor failure to green).
- Change failure rate on API deploys (drop by 30–50 percent after rollout).
- Contract age without failure (days since last failing contract).
These are the signals that outages are going down.
Common pitfalls and how to avoid them
- Flaky fields such as timestamps and IDs. Use Pact matchers like like and term.
- Over-asserting consumers. Assert only what you use; keep contracts small and specific.
- “Optional” fields that became required. Capture them in golden payloads and add a consumer contract.
- Noisy monitors. Limit to your top three customer-visible flows; align thresholds with SLOs.
- Contract sprawl. Keep one contract per consumer use-case per endpoint to avoid brittle mega-contracts.
Where this fits your stack (and how we use it)
If you run a TMF-aligned BSS, contract tests sit between product catalog, ordering, and channels. That’s how we harden Veris Cloud BSS integrations at Cloudnet.ai: schema and contracts on the producer side, smokes and monitors on the consumer side.
For teams operating both BSS and private 5G, CloudRAN.AI follows the same API quality bar.
Closing thought
Contract tests aren’t paperwork. They’re how independent teams ship faster without collateral damage. Wire Pact into TMF620 and TMF622, keep Postman monitors watching real endpoints, and enforce a simple compatibility policy. You’ll see fewer Friday-night incidents, fewer “what changed?” threads, and calmer releases.
This content originally appeared on DEV Community and was authored by Yacine Si Tayeb
Yacine Si Tayeb | Sciencx (2025-11-25T22:07:50+00:00) Contract-Testing TM Forum Open APIs with Pact + Postman: Stop Breaking Your BSS. Retrieved from https://www.scien.cx/2025/11/25/contract-testing-tm-forum-open-apis-with-pact-postman-stop-breaking-your-bss/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.