জেসন স্কিমা কন্ট্রাক্ট টেস্টিং: ভ্যালিডেশন নিয়মগুলো CI-তে ঢেলে দিন
সংক্ষেপে: স্কিমা লেখা শেষ হওয়াই শেষ নয় — সেখান থেকেই অবনতি শুরু হয়। একটা টাইপো নীরবে ভ্যালিডেশন শিথিল করে, একটা রিফ্যাক্টরিং সব ফিক্সচার ভেঙে দেয়। এই নির্দেশিকায় একটি বাস্তবসম্মত কন্ট্রাক্ট টেস্টিং কাঠামো ব্যাখ্যা করা হয়েছে: পজিটিভ, নেগেটিভ ও স্ন্যাপশট ফিক্সচার, স্কিমা নিজের লিন্ট, একবার কম্পাইল করে পুনর্ব্যবহার এবং চারটি CI গেট যা কন্ট্রাক্ট ভাঙাকে লাল বিল্ডে পরিণত করে।

উপসংহার: স্কিমা একটি কোড, আর কোডের টেস্ট দরকার। পজিটিভ ফিক্সচার প্রমাণ করে সে যাকে পাস করানো উচিত তা পাস করায়; নেগেটিভ ফিক্সচার প্রমাণ করে যাকে আটকানো উচিত তা আটকায়; স্ন্যাপশট অনিচ্ছাকৃত পরিবর্তন পাহারা দেয়। শেষে ajv compile আর --ci স্ন্যাপশট আপনার পাইপলাইনে রাতের প্রহরী।
ভ্যালিডেশন নিয়ম ভাঙার মুহূর্তে কিছুই এরর ছোঁড়ে না: গ্রহণ চওড়া হলে কেউ টের পায় না, বাতিল কঠোর হলে পুরো দল তা বইতে হয়। স্কিমা কোনো কনফিগ ফাইল নয় — এটি এমন কোড যা কেউ টেস্ট করে না।
১. স্কিমা ভাঙল — কে প্রথম জানবে
একটি সত্যিকারের ফাঁদ দিয়ে শুরু করি। ২০২০-১২ ড্রাফট অনুযায়ী স্কিমা ঘোষণা করে Ajv-র ডিফল্ট প্যাকেজ দিয়ে কম্পাইল করলে সঙ্গে সঙ্গে এক্সসেপশন ছোটে:
Error: no schema with key or ref "https://json-schema.org/draft/2020-12/schema"
Ajv 8-এর ডিফল্ট এন্ট্রি শুধু draft-07 চেনে; ২০২০-১২-এর জন্য ajv/dist/2020 থেকে ইমপোর্ট করতে হয়। আরেক ফাঁদ: strict মোডে format: "email" লিখলেন কিন্তু ajv-formats ইনস্টল করলেন না — কম্পাইলেও ব্যর্থ হবে:
Error: unknown format "email" ignored in schema at path "#/properties/email"
এই এররগুলো প্রথম কম্পাইলেই লোকালি দেখা গেলে ভালো। সমস্যা: অনেক প্রজেক্ট কম্পাইল রিকোয়েস্ট আসা পর্যন্ত পিছিয়ে রাখে, আর ভাঙা স্কিমা প্রথম শিকারের অপেক্ষায় থাকে। আরও লুকানো বিপদ অর্ধভাঙা স্কিমা: required ভুলে অ্যারের বদলে স্ট্রিং হলে কম্পাইলে schema is invalid: data/required must be array ছোটে — কিন্তু এই ধাপ CI-তে কখনো চলেনি হলে সে প্রোডাকশনে ঘুমিয়ে থাকে।
তাই প্রথম গেট ডেটা ভ্যালিডেট করে না — স্কিমাকেই ভ্যালিডেট করে। একটি কমান্ড:
node scripts/lint-schemas.mjs
// scripts/lint-schemas.mjs
import { readFileSync, readdirSync } from 'node:fs';
import Ajv2020 from 'ajv/dist/2020.js';
import addFormats from 'ajv-formats';
const ajv = new Ajv2020({ allErrors: true, strict: true });
addFormats(ajv);
for (const file of readdirSync('schemas')) {
const schema = JSON.parse(readFileSync(`schemas/${file}`, 'utf8'));
ajv.compile(schema);
console.log(`ok ${file}`);
}
কেন npx ajv compile নয়: ajv v8 প্যাকেজে CLI নেই, তাই npx ajv could not determine executable to run দিয়ে ব্যর্থ হয়; CLI আলাদা ajv-cli প্যাকেজে (আমাদের পরিমাপে 5.0.0)। কিন্তু ajv-cli 5 কেবল draft-07 ও draft-2019-09 সমর্থন করে — প্রথম অধ্যায়ের 2020-12 স্কিমার দিকে তাক করলে একই no schema with key or ref "https://json-schema.org/draft/2020-12/schema" ছোঁড়ে। তাই 2020-12 প্রোজেক্টে লিন্ট হবে নিবন্ধের একই Ajv2020 এন্ট্রি দিয়ে; npx ajv-cli compile -s '**/*.schema.json' --strict কেবল draft-07 প্রোজেক্টের জন্য।
লিন্টারের মতো চালান: টাইপো, অপ্রচলিত কীওয়ার্ড, নিবন্ধিত নয় এমন ফরম্যাট PR ধাপেই ধরা পড়বে। এটাই স্কিমা টেস্টিং আর সাধারণ কোড টেস্টিংয়ের প্রথম মিল: প্রথমে নিশ্চিত করুন কম্পাইল হয়।
২. ত্রয়ী: পজিটিভ, নেগেটিভ, স্ন্যাপশট
স্কিমা টেস্টিংয়ের কঙ্কাল তিনটি অংশের।
পজিটিভ ফিক্সচার: বাস্তব রেসপন্স থেকে সংগ্রহ করা, যা পাস করা উচিত। মনে করে লিখবেন না — staging থেকে আনুন; হাতে লেখা পজিটিভ শুধু happy path ঢেকে রাখে, null, খালি স্ট্রিং আর শুধু-required সীমা ভুলে যায়।
নেগেটিভ ফিক্সচার: স্কিমার প্রতিটি কনস্ট্রেইন্টের জন্য অন্তত একটি লঙ্ঘনকারী ইনপুট। minimum: 0 হলে -1 (বাতিল) ও 0 (পাস) জোড়া; enum হলে তালিকার বাইরের মান; additionalProperties: false হলে অতিরিক্ত ফিল্ড। নেগেটিভ পজিটিভের চেয়ে বেশি জরুরি: স্কিমা নীরবে খারাপ ডেটা পাস করালে তা সাইলেন্ট বাগ, ডেটাবেস পর্যন্ত দূষিত করে।
ফাইলের নামকে অ্যাসার্শন বানানো নামকরণ রীতি ওপেন সোর্স থেকে কপি করা যায়:
schemas/order.schema.json
fixtures/order/valid-minimal.json # পাস করতেই হবে
fixtures/order/valid-full.json # পাস করতেই হবে
fixtures/order/invalid-type.json # বাতিল হতেই হবে
fixtures/order/invalid-extra-field.json # বাতিল হতেই হবে
Node-এর বিল্ট-ইন node:test-ই যথেষ্ট:
import { test } from 'node:test';
import { readFileSync, readdirSync } from 'node:fs';
import assert from 'node:assert/strict';
import Ajv2020 from 'ajv/dist/2020.js';
import addFormats from 'ajv-formats';
const ajv = new Ajv2020({ allErrors: true });
addFormats(ajv);
const validate = ajv.compile(
JSON.parse(readFileSync('schemas/order.schema.json', 'utf8'))
);
for (const f of readdirSync('fixtures/order')) {
const data = JSON.parse(readFileSync(`fixtures/order/${f}`, 'utf8'));
if (f.startsWith('valid-')) {
test(`${f} পাস করা উচিত`, () => { assert.ok(validate(data), JSON.stringify(validate.errors)); });
} else {
test(`${f} বাতিল হওয়া উচিত`, () => { assert.ok(!validate(data)); });
}
}
স্ন্যাপশট: validate.errors সিরিয়ালাইজ করে ফাইলে রেখে তুলনা করুন। কেউ অনিচ্ছাকৃতভাবে কনস্ট্রেইন্ট বদলালে — স্ন্যাপশট সঙ্গে সঙ্গে লাল। Jest/Vitest-এ এক লাইন: expect(errors).toMatchSnapshot(); CI-তে সবসময় --ci দিন: নতুন স্ন্যাপশট নীরবে তৈরি হওয়ার বদলে ফেল করবে, ড্রিফট চুপচাপ গৃহীত হবে না।
৩. একবারেই সব এরর জমা করুন
নেগেটিভ টেস্টে প্রায়ই ভুল হওয়া কনফিগ: ডিফল্টে ভ্যালিডেটর প্রথম এররেই থামে। ডেটা তিন জায়গায় ভাঙা, আপনি দেখছেন একটা, ঠিক করে আবার চালান — এভাবে চলতে থাকে। allErrors: true (Ajv) বা iter_errors (Python) দিলে একবারেই সব দেখা যায়:
from jsonschema import Draft202012Validator
validator = Draft202012Validator(schema)
errors = sorted(validator.iter_errors(bad), key=lambda e: list(e.absolute_path))
for e in errors:
print(list(e.absolute_path), e.validator, e.message)
Python-এর একটি প্রতি-স্বজ্ঞ তথ্যও আছে: validate() কোন এরর জানাবে তা "প্রথমটা" নয়, best_match যেটা বাছে। আমাদের পরিমাপে তিনটি লঙ্ঘনের মধ্যে (id-র ভুল টাইপ, তালিকার বাইরে role, অতিরিক্ত extra ফিল্ড) শুধু additionalProperties জানানো হয়েছিল — id-র টাইপ এরর লুকানো রইল। CI-তে সম্পূর্ণ দৃশ্যের জন্য সবসময় iter_errors ব্যবহার করুন।
আরও সহজ ফাঁদ: format ডিফল্টে অ্যাসার্শন নয়। ২০২০-১২ ড্রাফটে format অ্যানোটেশন হিসেবে চিহ্নিত: Python-এর jsonschema format_checker পাস না করলে তা ভ্যালিডেটই করে না — আমাদের টেস্টে "not-an-email" মুক্তভাবে পাস করেছে। Ajv-তে ajv-formats ইনস্টল ও রেজিস্টার করতে হয়। ফরম্যাট গুরুত্বপূর্ণ হলে (ইমেইল, তারিখ, uuid) এমন একটি নেগেটিভ ফিক্সচার থাকতেই হবে যা প্রমাণ করে আপনার ভ্যালিডেটর কনফিগ সত্যিই তা যাচাই করছে।
৪. একবার কম্পাইল করুন — কম্পাইলারকে একবারব্যবহারযোগ্য জিনিস ভাববেন না
লোকাল পরিমাপ (Node v22.13.1, Ajv 8.17, একই ত্রিস্তরীয় নেস্টেড স্কিমা, একই লঙ্ঘনকারী ডেটা):
| পন্থা | ১০,০০০ ভ্যালিডেশনে সময় |
|---|---|
প্রতিবার new Ajv() করে compile |
42233 ms |
মডিউল-লেভেল ইনস্ট্যান্স + একবার compile |
6.9 ms |
৬০০০ গুণ পার্থক্য। রিকোয়েস্ট হ্যান্ডলারের ভেতরে ভ্যালিডেটর রেখে প্রতি কলে নতুন Ajv বানানো সবচেয়ে প্রচলিত পারফরম্যান্স দুর্ঘটনা। একটি প্রচলিত ভুল ধারণাও নথিভুক্ত করি: "ajv.validate(schema, data) প্রতিবার নতুন করে কম্পাইল করে"। পরিমাপে: ১০০,০০০ কলে 8.2 ms — একবার কম্পাইল করা ফাংশনের 11.2 ms-এর সঙ্গে তুলনীয়, কারণ Ajv-র ভেতরে স্কিমা ক্যাশ আছে। আসল রোগ ক্যাশ মিস নয়, ইনস্ট্যান্স পুনর্ব্যবহার না করা। Python-এও তাই: পুনর্ব্যবহৃত Draft202012Validator প্রতিবার validate() কলের চেয়ে প্রায় ৫০০০ গুণ দ্রুত (২০০০ ইটারেশন: 0.3 ms বনাম 1566 ms), কারণ পরেরটি প্রতি কলে স্কিমা নিজেই পরীক্ষা করে।
৫. গেটগুলো CI-তে ঢেলে দিন: চার দরজা
সস্তা থেকে দামি ক্রমে: সামনেরগুলো পড়লে পিছনেরগুলো চালানোর দরকার নেই:
| গেট | কমান্ড রূপ | কী ধরে | খরচ |
|---|---|---|---|
| স্কিমা লিন্ট | node scripts/lint-schemas.mjs |
সিনট্যাক্স এরর, অপ্রচলিত কীওয়ার্ড | মিলিসেকেন্ড |
| ফিক্সচার ভ্যালিডেশন | সেকশন ২-এর টেস্ট স্যুট | ফিক্সচার-স্কিমা অসঙ্গতি, ভুলে বদলানো কনস্ট্রেইন্ট | মিলিসেকেন্ড |
| ভাঙা পরিবর্তনের diff | json-schema-diff-validator main-এর সঙ্গে |
required মুছে ফেলা, enum সংকুচিত করা, pattern কঠোর করা | সেকেন্ড |
| লাইভ রেসপন্স যাচাই | staging-এ smoke টেস্ট, .toMatchSchema() অ্যাসার্শন |
সত্যিকারের এন্ডপয়েন্টের ড্রিফট | সেকেন্ড |
GitHub Actions-এর ন্যূনতম কনফিগ:
- run: npm ci
- run: npm run lint:schemas # Ajv2020 lint
- run: npm run test:schema # ফিক্সচার ত্রয়ী
- run: npx jest --ci --testPathPattern=schema # স্ন্যাপশট ড্রিফট রক্ষী
ভাঙা পরিবর্তনের গেট আলাদা জোর দাবি করে: json-schema-diff-validator জাতীয় টুল "required ফিল্ড মুছে গেছে" আর "enum থেকে মান হারিয়েছে" রিভিউতেই লাল করে দেয় — সাধারণ API-diff যে দুটো পরিবর্তনকে যত্ন করে "নন-ব্রেকিং" বলে চালিয়ে দেয়। সহনশীল কনজিউমারের জন্য রেসপন্স ফিল্ড মোছা ক্ষতিকর নয়, কিন্তু exhaustive switch-ওয়ালা কনজিউমারের জন্য প্রোডাকশন ক্র্যাশ।
৬. শুধু বুলিয়ান নয়, এরর অবজেক্ট অ্যাসার্ট করুন
শুধু validate(data) === false যাচাই করা নেগেটিভ টেস্ট যথেষ্ট নয়। একই স্কিমার জন্য নিশ্চিত হোন সে সঠিক কারণে বাতিল করছে: অসম্পর্কিত কনস্ট্রেইন্ট কাকতালীয়ভাবে পাস করে থাকতে পারে। Ajv-র এরর অবজেক্টের একটি বিখ্যাত অসামঞ্জস্য: required ব্যর্থ হলে instancePath প্যারেন্ট অবজেক্টকে দেখায়, আর অনুপস্থিত ফিল্ডের নাম লুকিয়ে থাকে params.missingProperty-তে। id অনুপস্থিত অবস্থায় আমাদের পরিমাপ:
{ "instancePath": "", "schemaPath": "#/required",
"keyword": "required", "params": { "missingProperty": "id" } }
অ্যাসার্শন লিখুন "keyword=required ও missingProperty=id সহ একটি এরর বিদ্যমান", অস্পষ্ট errors.length > 0 নয়। এরর ফিল্ডের অর্থের পূর্ণ ব্যাখ্যা: JSON Schema এরর বোঝা যায় না: Ajv এরর লোকেট ও মেসেজ গোছানো।
৭. ঘন ঘন ফাঁদ — দ্রুত তালিকা
| ফাঁদ | পরিণতি | প্রতিকার |
|---|---|---|
| শুধু ২০০ happy path টেস্ট | এরর এনভেলপের ড্রিফট কেউ না দেখা | এরর রেসপন্সের জন্যও স্কিমা ও ফিক্সচার |
| জেনারেট করা স্কিমা রিভিউ ছাড়া | এক নমুনা থেকে অনুমিত required বাস্তব ডেটা বাতিল করে; দল ভ্যালিডেশন বন্ধ করে দেয় | জেনারেশনের পর required/enum মানুষের রিভিউ |
| সব ভার্সনে এক গ্লোবাল স্কিমা | v1/v2 পার্থক্য মুছে যায় | স্কিমা এন্ডপয়েন্টের সঙ্গে ভার্সন করুন |
| ব্যর্থতায় পুরো payload লগে | PII ও সিক্রেট CI লগে লিক | শুধু JSON Pointer পথ ও ছাঁটা অংশ লিখুন |
| জমে থাকা enum | পণ্যের নামবদল (canceled→cancelled) ভ্যালিডেশন ভাঙে | enum পরিবর্তন কম্প্যাটিবিলিটি রিভিউতে |
| ভ্যালিডেটর ভার্সন পিন করা হয়নি | মেশিনভেদে ভিন্ন আচরণ | CI-তে ভ্যালিডেটর ভার্সন পিন করুন |
| অথরাইজেশন টেস্টের বিকল্প ভাবা | 403 এরর স্কিমায় মেলে, তবু রোল লজিক ভুল | স্কিমা স্ট্রাকচার দেখে; অথরাইজেশন আলাদা টেস্ট |
| format-এর অর্থ ভুলে যাওয়া | আবর্জনা ঠিকানা মুক্তভাবে পাস | ajv-formats ইনস্টল / format_checker পাস, নেগেটিভ যোগ |
৮. প্রয়োগের চেকলিস্ট
- স্কিমা ফাইল ভার্সন কন্ট্রোলে,
node scripts/lint-schemas.mjsলিন্টে; - প্রতি স্কিমায় অন্তত ১টি পজিটিভ ফিক্সচার ও প্রতি কনস্ট্রেইন্টে ১টি নেগেটিভ; ফাইলের নামই অ্যাসার্শন;
- এরর অবজেক্ট স্ন্যাপশটে; CI-তে
--ciদিন যাতে স্ন্যাপশট নিজে থেকে ড্রিফট না করে; - ভ্যালিডেটর ইনস্ট্যান্স মডিউল-লেভেলে পুনর্ব্যবহার — হ্যান্ডলারের ভেতরে new নয়;
- ভাঙা পরিবর্তনের diff PR-এ ঝুলান; required/enum পরিবর্তনে বাধ্যতামূলক রিভিউ;
- প্রথম PR-ই প্রমাণ করে গেট পড়তে পারে: একটা required মুছুন, লাল দেখুন, ফেরত আনুন।
ভ্যালিডেশনের কড়াকড়ির প্রশ্ন (কখন additionalProperties বন্ধ করবেন) আলাদা বিষয় — দেখুন additionalProperties খোলা রাখলে স্কিমা ভ্যালিডেশন নিরর্থক। এই নিবন্ধ লেখা নিয়মগুলোকে ধারাবাহিকভাবে বিশ্বাসযোগ্য রাখা নিয়ে। ভিত্তি চাইলে JSON Schema বেসিক থেকে ইঞ্জিনিয়ারিং থেকে শুরু করুন।
FAQ
স্কিমা ভ্যালিডেশন আর Pact-স্টাইল কন্ট্রাক্ট টেস্টিংয়ের সম্পর্ক কী?
কাজের বিভাজন। স্কিমা ভ্যালিডেশন উত্তর দেয় "এই ডকুমেন্ট কাঠামোগতভাবে ঠিক কি না" — একমুখী অ্যাসার্শন। কনজিউমার-চালিত কন্ট্রাক্ট টেস্টিং (Pact) উত্তর দেয় "প্রোভাইডার কি এখনো কনজিউমার X-এর সুনির্দিষ্ট প্রয়োজন মেটায়" এবং দ্বিমুখী যাচাই চায়। ছোট দল স্কিমা গেট দিয়ে শুরু করুক, সার্ভিস সংখ্যা বাড়লে Pact নেবে।
কতগুলো নেগেটিভ টেস্ট যথেষ্ট?
প্রতি কনস্ট্রেইন্টে একটি নেগেটিভই নিম্নসীমা: স্কিমায় যত কনস্ট্রেইন্ট (required, type, minimum, enum, pattern, additionalProperties…), তত নেগেটিভ ফিক্সচার, সঙ্গে সীমামানের বাতিল/পাস জোড়া। নেগেটিভ কনস্ট্রেইন্টের সংখ্যার চেয়ে কম হলে কোনো না কোনো কনস্ট্রেইন্ট অরক্ষিত।
কোন স্কিমা পরিবর্তন "ব্রেকিং"?
কনজিউমারের দৃষ্টিতে বিচার করুন: রেসপন্স ফিল্ড মোছা, enum মোছা বা সংকুচিত করা, pattern কঠোর করা, রিকোয়েস্টে নতুন required ফিল্ড — সবই ব্রেকিং; রেসপন্সে ঐচ্ছিক ফিল্ড যোগ সাধারণত নিরাপদ — যতক্ষণ কনজিউমারের ডিকোডার অজানা কী সহ্য করে। json-schema-diff-validator দিয়ে PR ধাপে তুলনা স্বয়ংক্রিয় করুন, ধূসর এলাকা মানুষ সিদ্ধান্ত নেবে।
টেস্ট কি আসল API-তে মারবে নাকি mock-এ?
দুটোই, তবে স্তরে: ফিক্সচার টেস্ট নেটওয়ার্ক স্পর্শ করে না, মিলিসেকেন্ডে চলে, প্রতি PR-এ চলে; লাইভ যাচাই staging-এ মারে, সেকেন্ড নেয়, smoke পাইপলাইনে থাকে, ক্রেডেনশিয়াল না থাকলে skipif দিয়ে বাদ পড়ে। CI-তে শুধু আসল API চালানো দল নেটওয়ার্ক কাঁপুনি ও রেট লিমিটে ক্লান্ত হয়ে শেষে টেস্ট বন্ধ করে দেয়।