പരമ്പരാഗത ഉദാഹരണങ്ങളെ അടിസ്ഥാനമാക്കിയുള്ള ടെസ്റ്റുകൾക്ക് അപ്പുറത്തേക്ക് പോകുക. JavaScript-ൽ property-based testing വേഗത്തിൽ നടപ്പിലാക്കുന്നതിലൂടെ കുറഞ്ഞ കോഡ് ഉപയോഗിച്ച് കൂടുതൽ ബഗുകൾ കണ്ടെത്താൻ ഈ ഗൈഡ് സഹായിക്കുന്നു.
ഉദാഹരണങ്ങൾക്കപ്പുറം: JavaScript-ൽ പ്രോപ്പർട്ടി അടിസ്ഥാനമാക്കിയുള്ള ടെസ്റ്റിംഗിലേക്ക് ഒരു ആഴത്തിലുള്ള പഠനം
സോഫ്റ്റ്വെയർ ഡെവലപ്പർമാർ എന്ന നിലയിൽ, ടെസ്റ്റുകൾ എഴുതാനായി നമ്മൾ ധാരാളം സമയം ചെലവഴിക്കുന്നു. ഞങ്ങളുടെ ആപ്ലിക്കേഷനുകൾ കാര്യക്ഷമവും വിശ്വസനീയവുമാണെന്ന് ഉറപ്പാക്കാൻ യൂണിറ്റ് ടെസ്റ്റുകൾ, ഇന്റഗ്രേഷൻ ടെസ്റ്റുകൾ, എൻഡ്-ടു-എൻഡ് ടെസ്റ്റുകൾ എന്നിവ ശ്രദ്ധാപൂർവ്വം തയ്യാറാക്കുന്നു. ഇതിനായുള്ള പ്രധാനരീതി example-based testing ആണ്. ഒരു പ്രത്യേക ഇൻപുട്ടിനെക്കുറിച്ച് ചിന്തിക്കുകയും ഒരു പ്രത്യേക ഔട്ട്പുട്ട് ഉറപ്പിക്കുകയും ചെയ്യുന്നു. `[1, 2, 3]` എന്ന ഇൻപുട്ട് `6` എന്ന ഔട്ട്പുട്ട് നൽകണം. `"hello"` എന്ന ഇൻപുട്ട് `"HELLO"` ആകണം. എന്നാൽ ഈ സമീപനത്തിന് ഒരു നിശബ്ദമായ ദൗർബല്യം ഉണ്ട്: നമ്മുടെ ഭാവന.
ഒരു ശൂന്യമായ array ഉപയോഗിച്ച് ടെസ്റ്റ് ചെയ്യാൻ മറന്നുപോയാൽ എന്ത് സംഭവിക്കും? ഒരു നെഗറ്റീവ് സംഖ്യ? യൂണികോഡ് പ്രതീകങ്ങൾ അടങ്ങിയ ഒരു സ്ട്രിംഗ്? ആഴത്തിൽ നെസ്റ്റ് ചെയ്ത ഒബ്ജക്റ്റ്? ഒഴിവാക്കപ്പെടുന്ന ഓരോ എഡ്ജ് കേസും സംഭവിക്കാവുന്ന ഒരു ബഗാണ്. ഇവിടെയാണ് Property-Based Testing (PBT) രംഗപ്രവേശം ചെയ്യുന്നത്. കൂടുതൽ ആത്മവിശ്വാസവും കരുത്തുറ്റതുമായ സോഫ്റ്റ്വെയർ നിർമ്മിക്കാൻ സഹായിക്കുന്ന ഒരു ശക്തമായ മാറ്റം ഇത് നൽകുന്നു.
JavaScript-ൽ പ്രോപ്പർട്ടി അടിസ്ഥാനമാക്കിയുള്ള ടെസ്റ്റിംഗിന്റെ ലോകത്തിലൂടെ ഈ സമഗ്രമായ ഗൈഡ് നിങ്ങളെ നയിക്കും. ഇത് എന്താണെന്നും എന്തുകൊണ്ട് ഇത് വളരെ ഫലപ്രദമാണെന്നും `fast-check` എന്ന ലൈബ്രറി ഉപയോഗിച്ച് നിങ്ങളുടെ പ്രോജക്റ്റുകളിൽ ഇത് എങ്ങനെ നടപ്പിലാക്കാമെന്നും നമ്മുക്ക് പരിശോധിക്കാം.
പരമ്പരാഗത എക്സാമ്പിൾ അടിസ്ഥാനമാക്കിയുള്ള ടെസ്റ്റിംഗിന്റെ പരിമിതികൾ
ഒരു array-യിലെ സംഖ്യകളെ അടുക്കുന്ന ഒരു ലളിതമായ ഫംഗ്ഷൻ നമുക്ക് പരിഗണിക്കാം. Jest അല്ലെങ്കിൽ Vitest പോലുള്ള ഒരു പ്രമുഖ ചട്ടക്കൂട് ഉപയോഗിച്ച്, നമ്മുടെ ടെസ്റ്റ് ഇങ്ങനെയായിരിക്കാം:
// A simple (and slightly naive) sort function
function sortNumbers(arr) {
return [...arr].sort((a, b) => a - b);
}
// A typical example-based test
test('sortNumbers should correctly sort a simple array', () => {
const inputArray = [3, 1, 4, 1, 5, 9];
const expectedArray = [1, 1, 3, 4, 5, 9];
expect(sortNumbers(inputArray)).toEqual(expectedArray);
});
ഈ ടെസ്റ്റ് പാസ്സായി. നമുക്ക് കുറച്ച് `it` അല്ലെങ്കിൽ `test` ബ്ലോക്കുകൾ കൂടി ചേർക്കാം:
- മുമ്പേ അടുക്കിയ ഒരു array.
- നെഗറ്റീവ് സംഖ്യകളുള്ള ഒരു array.
- ഒരു പൂജ്യമുള്ള array.
- ശൂന്യമായ array.
- ഒരേ സംഖ്യകൾ അടങ്ങിയ array (ഇത് നമ്മൾ ഇതിനകം ഉൾക്കൊള്ളിച്ചു).
നമുക്ക് നല്ലതായി തോന്നുന്നു. നമ്മൾ അടിസ്ഥാനകാര്യങ്ങൾ ഉൾക്കൊള്ളിച്ചു. എന്നാൽ നമ്മുക്ക് എന്താണ് നഷ്ടമായത്? `[-0, 0]` നെക്കുറിച്ചോ? `[Infinity, -Infinity]` നെക്കുറിച്ചോ? പ്രകടനത്തിന്റെ പരിധികൾ അല്ലെങ്കിൽ JavaScript എഞ്ചിൻ ഒപ്റ്റിമൈസേഷനുകൾ ബാധിച്ചേക്കാവുന്ന വലിയ array-കളെക്കുറിച്ചോ? അടിസ്ഥാനപരമായ പ്രശ്നം നമ്മൾ സ്വയം ഡാറ്റ തിരഞ്ഞെടുക്കുന്നു എന്നതാണ്. നമ്മളുടെ ടെസ്റ്റുകൾ നമ്മുക്ക് കണ്ടെത്താൻ കഴിയുന്ന ഉദാഹരണങ്ങൾ പോലെ നല്ലതാണ്. ഡാറ്റകൾക്ക് രൂപം നൽകാൻ കഴിയുന്ന എല്ലാ വിചിത്രവും അത്ഭുതകരവുമായ വഴികൾ സങ്കൽപ്പിക്കാൻ മനുഷ്യർ മോശമായി അറിയപ്പെടുന്നു.
Example-based testing നിങ്ങളുടെ കോഡ് കുറച്ച് തിരഞ്ഞെടുത്ത സാഹചര്യങ്ങളിൽ പ്രവർത്തിക്കുമെന്ന് സാധൂകരിക്കുന്നു. Property-based testing നിങ്ങളുടെ കോഡ് ഇൻപുട്ടുകളുടെ മുഴുവൻ ക്ലാസ്സുകൾക്കും വേണ്ടി പ്രവർത്തിക്കുമെന്ന് സാധൂകരിക്കുന്നു.
എന്താണ് പ്രോപ്പർട്ടി അടിസ്ഥാനമാക്കിയുള്ള ടെസ്റ്റിംഗ്? ഒരു പുതിയ ചിന്താഗതി
Property-based testing രീതി മാറ്റുന്നു. ഒരു പ്രത്യേക ഇൻപുട്ട് ഒരു പ്രത്യേക ഔട്ട്പുട്ട് നൽകുന്നു എന്ന് ഉറപ്പിക്കുന്നതിന് പകരം, സാധുവായ ഏത് ഇൻപുട്ടിനും ശരിയായിരിക്കേണ്ട നിങ്ങളുടെ കോഡിന്റെ ഒരു പൊതു property നിങ്ങൾ നിർവചിക്കുന്നു. നിങ്ങളുടെ property തെറ്റാണെന്ന് തെളിയിക്കാൻ ടെസ്റ്റിംഗ് ചട്ടക്കൂട് നൂറുകണക്കിന് അല്ലെങ്കിൽ ആയിരക്കണക്കിന് റാൻഡം ഇൻപുട്ടുകൾ ഉണ്ടാക്കുന്നു.
ഒരു "property" എന്നത് ഒരു മാറ്റമില്ലാത്തതാണ്—നിങ്ങളുടെ ഫംഗ്ഷന്റെ സ്വഭാവത്തെക്കുറിച്ചുള്ള ഉയർന്നതലത്തിലുള്ള നിയമം. ഞങ്ങളുടെ `sortNumbers` ഫംഗ്ഷനായി, ചില properties ഇതായിരിക്കാം:
- Idempotence: അടുക്കിയ ഒരു array-യെ വീണ്ടും അടുക്കുന്നത് അതിനെ മാറ്റരുത്. `sortNumbers(sortNumbers(arr))` എന്നത് `sortNumbers(arr)` ന് തുല്യമായിരിക്കണം.
- Length Invariance: അടുക്കിയ array-യുടെ നീളം ആദ്യത്തെ array-യുടെ നീളത്തിന് തുല്യമായിരിക്കണം.
- Content Invariance: അടുക്കിയ array-യിൽ ആദ്യത്തെ array-യിലെ അതേ ഘടകങ്ങൾ ഉണ്ടായിരിക്കണം, ക്രമം മാത്രം മാറ്റിയിരിക്കും.
- Order: അടുക്കിയ array-യിലെ അടുത്തടുത്തുള്ള ഏതെങ്കിലും രണ്ട് ഘടകങ്ങൾക്ക്, `sorted[i] <= sorted[i+1]`.
ഈ സമീപനം വ്യക്തിഗത ഉദാഹരണങ്ങളെക്കുറിച്ച് ചിന്തിക്കുന്നതിൽ നിന്ന് നിങ്ങളുടെ കോഡിന്റെ അടിസ്ഥാനപരമായ കാര്യങ്ങളെക്കുറിച്ച് ചിന്തിക്കുന്നതിലേക്ക് നിങ്ങളെ മാറ്റുന്നു. മികച്ചതും പ്രവചനാതീതവുമായ API-കൾ രൂപകൽപ്പന ചെയ്യുന്നതിന് ഈ മാറ്റം വളരെ വിലപ്പെട്ടതാണ്.
PBT-യുടെ പ്രധാന ഘടകങ്ങൾ
ഒരു property-based testing ചട്ടക്കൂടിൽ സാധാരണയായി രണ്ട് പ്രധാന ഘടകങ്ങളുണ്ട്:
- ജനറേറ്ററുകൾ (അല്ലെങ്കിൽ ആർബിട്രറികൾ): നിർദ്ദിഷ്ട തരങ്ങൾക്കനുസരിച്ച് (integers, strings, arrays of objects, തുടങ്ങിയവ) വൈവിധ്യമാർന്ന റാൻഡം ഡാറ്റ നിർമ്മിക്കാൻ ഇവ സഹായിക്കുന്നു. ശൂന്യമായ strings, `NaN`, `Infinity` എന്നിവയും അതിലേറെയും പോലുള്ള ബുദ്ധിമുട്ടുള്ള എഡ്ജ് കേസുകൾ മാത്രമല്ല, "സന്തോഷകരമായ പാത" ഡാറ്റയും സൃഷ്ടിക്കാൻ ഇവ വളരെ മികച്ചതാണ്.
- കുറയ്ക്കൽ: ഇതാണ് പ്രധാനപ്പെട്ട ഘടകം. നിങ്ങളുടെ property തെറ്റാണെന്ന് ഫ്രെയിംവർക്ക് കണ്ടെത്തിയാൽ (അതായത്, ഒരു ടെസ്റ്റ് പരാജയത്തിന് കാരണമാകുന്നു), അത് വലിയ റാൻഡം ഇൻപുട്ട് റിപ്പോർട്ട് ചെയ്യില്ല. പകരം, പരാജയത്തിന് കാരണമാകുന്ന ഏറ്റവും ചെറിയതും ലളിതവുമായ ഇൻപുട്ട് കണ്ടെത്താൻ ഇത് ശ്രമിക്കുന്നു. ഇത് ഡീബഗ്ഗിംഗ് എളുപ്പമാക്കുന്നു.
തുടങ്ങാം: `fast-check` ഉപയോഗിച്ച് PBT നടപ്പിലാക്കുക
JavaScript എക്കോസിസ്റ്റത്തിൽ നിരവധി PBT ലൈബ്രറികൾ ഉണ്ടെങ്കിലും, `fast-check` എന്നത് മെച്ചപ്പെട്ടതും ശക്തവും നന്നായി പരിപാലിക്കപ്പെടുന്നതുമായ ഒന്നാണ്. ഇത് Jest, Vitest, Mocha, Jasmine പോലുള്ള ജനപ്രിയ ടെസ്റ്റിംഗ് ചട്ടക്കൂടുകളുമായി എളുപ്പത്തിൽ സംയോജിപ്പിക്കുന്നു.
ഇൻസ്റ്റാളേഷനും സജ്ജീകരണവും
ആദ്യം, നിങ്ങളുടെ പ്രോജക്റ്റിന്റെ ഡെവലപ്മെന്റ് ഡിപൻഡൻസികളിലേക്ക് `fast-check` ചേർക്കുക. നിങ്ങൾ Jest പോലുള്ള ഒരു ടെസ്റ്റ് റണ്ണറാണ് ഉപയോഗിക്കുന്നതെന്ന് കരുതുക.
npm install --save-dev fast-check jest
# or
yarn add --dev fast-check jest
# or
pnpm add -D fast-check jest
നിങ്ങളുടെ ആദ്യത്തെ പ്രോപ്പർട്ടി അടിസ്ഥാനമാക്കിയുള്ള ടെസ്റ്റ്
`fast-check` ഉപയോഗിച്ച് നമ്മുടെ `sortNumbers` ടെസ്റ്റ് വീണ്ടും എഴുതാം. നമ്മൾ നേരത്തെ നിർവചിച്ച "order" property പരിശോധിക്കും: ഓരോ ഘടകവും അതിനെ പിന്തുടരുന്നതിനേക്കാൾ ചെറുതോ തുല്യമോ ആയിരിക്കണം.
import * as fc from 'fast-check';
// The same function from before
function sortNumbers(arr) {
return [...arr].sort((a, b) => a - b);
}
test('the output of sortNumbers should be a sorted array', () => {
// 1. Describe the property
fc.assert(
// 2. Define the arbitraries (input generators)
fc.property(fc.array(fc.integer()), (data) => {
// `data` is a randomly generated array of integers
const sorted = sortNumbers(data);
// 3. Define the predicate (the property to check)
for (let i = 0; i < sorted.length - 1; ++i) {
if (sorted[i] > sorted[i + 1]) {
return false; // The property is falsified
}
}
return true; // The property holds for this input
})
);
});
test('sorting should not change the array length', () => {
fc.assert(
fc.property(fc.array(fc.float()), (data) => {
const sorted = sortNumbers(data);
return sorted.length === data.length;
})
);
});
ഇതിനെക്കുറിച്ച് കൂടുതൽ മനസ്സിലാക്കാം:
- `fc.assert()`: ഇതാണ് റണ്ണർ. ഇത് നിങ്ങളുടെ property പരിശോധന പലതവണ പ്രവർത്തിപ്പിക്കും (സ്ഥിരമായി 100 തവണ).
- `fc.property()`: ഇത് property നിർവചിക്കുന്നു. ഇതിന് ഒന്നോ അതിലധികമോ arbitraries ആർഗ്യുമെന്റുകളായി എടുക്കുന്നു, തുടർന്ന് ഒരു predicate ഫംഗ്ഷനും.
- `fc.array(fc.integer())`: ഇതാണ് നമ്മുടെ arbitrary. ഇത് integer-കളുടെ ഒരു array (`fc.array`) ഉണ്ടാക്കാൻ `fast-check`-നോട് പറയുന്നു (`fc.integer()`). `fast-check` വ്യത്യസ്ത integer values (positive, negative, zero, തുടങ്ങിയവ) ഉപയോഗിച്ച്, വ്യത്യസ്ത നീളത്തിലുള്ള arrays സ്വയം ഉണ്ടാക്കുന്നു.
- Predicate: `(data) => { ... }`എന്ന anonymous ഫംഗ്ഷനാണ് നമ്മുടെ ലോജിക്. ഇത് റാൻഡമായി ജനറേറ്റ് ചെയ്ത ഡാറ്റ സ്വീകരിക്കുകയും property ശരിയാണെങ്കിൽ `true` അല്ലെങ്കിൽ തെറ്റാണെങ്കിൽ `false` എന്ന് നൽകണം. `fast-check`, Jest-ന്റെ `expect` assertions-മായി നന്നായി സംയോജിപ്പിച്ച്, പരാജയത്തിൽ ഒരു എറർ ഉണ്ടാക്കുന്ന predicate ഫംഗ്ഷനുകളെ പിന്തുണയ്ക്കുന്നു.
ഇപ്പോൾ, ഒരു തിരഞ്ഞെടുത്ത array ഉപയോഗിച്ച് ഒരു ടെസ്റ്റിന് പകരം, ഓരോ തവണയും നമ്മുടെ സ്യൂട്ട് പ്രവർത്തിപ്പിക്കുമ്പോൾ 100 വ്യത്യസ്തവും സ്വയം ഉണ്ടാക്കിയതുമായ arrays-കൾക്കെതിരെ നമ്മുടെ സോർട്ടിംഗ് ലോജിക് പരിശോധിക്കുന്ന ഒരു ടെസ്റ്റ് ഉണ്ട്. കുറച്ച് കോഡുകൾ ഉപയോഗിച്ച് നമ്മുടെ ടെസ്റ്റ് കവറേജ് നമ്മൾ വർദ്ധിപ്പിച്ചു.
Arbitraries കണ്ടെത്തുക: ശരിയായ ഡാറ്റ ഉണ്ടാക്കുക
വൈവിധ്യവും വെല്ലുവിളികളും നിറഞ്ഞ ഡാറ്റ ഉണ്ടാക്കാനുള്ള കഴിവിലാണ് PBT-യുടെ ശക്തി സ്ഥിതി ചെയ്യുന്നത്. നിങ്ങൾക്ക് സങ്കൽപ്പിക്കാൻ കഴിയുന്ന ഏത് ഡാറ്റാ ഘടനയും ഉൾക്കൊള്ളാൻ `fast-check` arbitraries-കളുടെ ഒരു വലിയ ശേഖരം നൽകുന്നു.
അടിസ്ഥാനപരമായ Arbitraries
ഇവയാണ് നിങ്ങളുടെ ഡാറ്റാ ജനറേഷന്റെ അടിസ്ഥാന ഘടകങ്ങൾ.
- `fc.integer()`, `fc.float()`, `fc.bigInt()`: സംഖ്യകൾക്ക്. ഇവയെ നിയന്ത്രിക്കാൻ കഴിയും, ഉദാഹരണത്തിന്, `fc.integer({ min: 0, max: 100 })`.
- `fc.string()`, `fc.asciiString()`, `fc.unicodeString()`: വ്യത്യസ്ത പ്രതീക സെറ്റുകളുടെ strings-കൾക്ക്.
- `fc.boolean()`: `true` അല്ലെങ്കിൽ `false`-ന്.
- `fc.constant(value)`: എപ്പോഴും ഒരേ മൂല്യം നൽകുന്നു. `fc.oneof`-മായി ചേർക്കാൻ ഉപയോഗപ്രദമാണ്.
- `fc.constantFrom(val1, val2, ...)`: നൽകിയിട്ടുള്ള കോൺസ്റ്റന്റ് മൂല്യങ്ങളിൽ ഒന്ന് നൽകുന്നു.
സങ്കീർണ്ണവും കോമ്പോസ് ചെയ്തതുമായ Arbitraries
സങ്കീർണ്ണമായ ഡാറ്റാ ഘടനകൾ ഉണ്ടാക്കാൻ നിങ്ങൾക്ക് അടിസ്ഥാനപരമായ arbitraries-കൾ കൂട്ടിച്ചേർക്കാം.
- `fc.array(arbitrary, constraints)`: നൽകിയിട്ടുള്ള arbitrary ഉപയോഗിച്ച് ഉണ്ടാക്കിയ ഘടകങ്ങളുടെ ഒരു array ഉണ്ടാക്കുന്നു. നിങ്ങൾക്ക് `minLength`, `maxLength` എന്നിവ നിയന്ത്രിക്കാനാകും.
- `fc.tuple(arb1, arb2, ...)`: ഓരോ ഘടകത്തിനും ഒരു പ്രത്യേക തരം ഉള്ള ഒരു നിശ്ചിത-നീളമുള്ള array ഉണ്ടാക്കുന്നു.
- `fc.object(shape)`: നിർവചിക്കപ്പെട്ട ഘടനയുള്ള ഒബ്ജക്റ്റുകൾ ഉണ്ടാക്കുന്നു. ഉദാഹരണം: `fc.object({ id: fc.uuidV(4), name: fc.string() })`.
- `fc.oneof(arb1, arb2, ...)`: നൽകിയിട്ടുള്ള arbitraries-കളിൽ നിന്ന് ഒരു മൂല്യം ഉണ്ടാക്കുന്നു. ഒന്നിലധികം ഡാറ്റ തരങ്ങൾ കൈകാര്യം ചെയ്യുന്ന ഫംഗ്ഷനുകൾ പരിശോധിക്കുന്നതിന് ഇത് മികച്ചതാണ് (ഉദാഹരണത്തിന്, `string | number`).
- `fc.record({ key: arb, value: arb })`: arbitraries-കളിൽ നിന്ന് ഉണ്ടാക്കിയ കീകൾ, മൂല്യങ്ങൾ എന്നിവ ഉപയോഗിച്ച് ഡിക്ഷണറികളായി അല്ലെങ്കിൽ മാപ്പുകളായി ഉപയോഗിക്കാൻ ഒബ്ജക്റ്റുകൾ ഉണ്ടാക്കുന്നു.
`map`, `chain` എന്നിവ ഉപയോഗിച്ച് കസ്റ്റം Arbitraries ഉണ്ടാക്കുക
ചില സമയങ്ങളിൽ ഒരു സാധാരണ രൂപത്തിന് ചേരാത്ത ഡാറ്റ നിങ്ങൾക്ക് ആവശ്യമായി വരും. നിലവിലുള്ളവയെ മാറ്റിക്കൊണ്ട് നിങ്ങളുടെ സ്വന്തം arbitraries ഉണ്ടാക്കാൻ `fast-check` നിങ്ങളെ അനുവദിക്കുന്നു.
`.map()` ഉപയോഗിച്ച്
`.map()` രീതി ഒരു arbitrary-യുടെ ഔട്ട്പുട്ടിനെ മറ്റൊന്നിലേക്ക് മാറ്റുന്നു. ഉദാഹരണത്തിന്, ശൂന്യമല്ലാത്ത strings ഉണ്ടാക്കുന്ന ഒരു arbitrary ഉണ്ടാക്കാം.
const nonEmptyStringArb = fc.string({ minLength: 1 });
// Or, by transforming an array of characters
const nonAStringArb = fc.array(fc.char().filter(c => c !== 'a'))
.map(chars => chars.join(''));
`.chain()` ഉപയോഗിച്ച്
`.chain()` രീതി കൂടുതൽ ശക്തമാണ്. ഒരു arbitrary-യുടെ ജനറേറ്റ് ചെയ്ത മൂല്യത്തെ അടിസ്ഥാനമാക്കി ഒരു പുതിയ arbitrary ഉണ്ടാക്കാൻ ഇത് നിങ്ങളെ അനുവദിക്കുന്നു. ബന്ധപ്പെട്ട ഡാറ്റ ഉണ്ടാക്കുന്നതിന് ഇത് അത്യാവശ്യമാണ്.
നിങ്ങൾ ഒരു array-യും അതേ array-ക്കുള്ള സാധുവായ ഒരു ഇൻഡെക്സും ഉണ്ടാക്കേണ്ടി വരുമെന്ന് സങ്കൽപ്പിക്കുക. ഇൻഡെക്സ് പരിധിക്ക് പുറത്തായിരിക്കാൻ സാധ്യതയുള്ളതിനാൽ ഇത് രണ്ട് arbitraries ഉപയോഗിച്ച് ചെയ്യാൻ കഴിയില്ല. `.chain()` ഇത് കൃത്യമായി പരിഹരിക്കുന്നു.
// Generate an array and a valid index into it
const arrayAndValidIndexArb = fc.array(fc.anything()).chain(arr => {
// Based on the generated array `arr`, create a new arbitrary for the index
const indexArb = fc.integer({ min: 0, max: arr.length - 1 });
// Return a tuple of the array and the generated index
return fc.tuple(fc.constant(arr), indexArb);
});
// Usage in a test
test('slicing at a valid index should work', () => {
fc.assert(
fc.property(arrayAndValidIndexArb, ([arr, index]) => {
// Both `arr` and `index` are guaranteed to be compatible
const sliced = arr.slice(0, index);
expect(sliced.length).toBe(index);
})
);
});
കുറക്കുന്നതിന്റെ ശക്തി: ഡീബഗ്ഗിംഗ് എളുപ്പമാക്കുന്നു
property-based testing-ന്റെ ഏറ്റവും ആകർഷകമായ സവിശേഷത കുറയ്ക്കലാണ്. ഇത് എങ്ങനെ പ്രവർത്തിക്കുമെന്ന് കാണാൻ, മനഃപൂർവം ബഗുകളുള്ള ഒരു ഫംഗ്ഷൻ ഉണ്ടാക്കാം.
// This function fails if the input array contains the number 42
function sumWithoutBug(arr) {
if (arr.includes(42)) {
throw new Error('This number is not allowed!');
}
return arr.reduce((acc, val) => acc + val, 0);
}
test('sumWithoutBug should sum numbers', () => {
fc.assert(
fc.property(fc.array(fc.integer()), (data) => {
sumWithoutBug(data);
})
);
});
നിങ്ങൾ ഈ ടെസ്റ്റ് പ്രവർത്തിപ്പിക്കുമ്പോൾ, `fast-check` ഒരു പരാജയപ്പെടുന്ന കേസ് കണ്ടെത്തും. എന്നാൽ ഇത് കണ്ടെത്തിയ ആദ്യത്തെ റാൻഡം array റിപ്പോർട്ട് ചെയ്യില്ല, അത് `[-1024, 500, 42, 987, -2000]` പോലെയായിരിക്കാം. അങ്ങനെയുള്ള ഒരു പരാജയ റിപ്പോർട്ട് അത്ര സഹായകരമല്ല. പ്രശ്നമുണ്ടാക്കുന്ന `42` കണ്ടെത്താൻ നിങ്ങൾ സ്വയം പരിശോധിക്കേണ്ടി വരും.
പകരം, `fast-check`-ന്റെ shrinker പ്രവർത്തിക്കും. ഇത് പരാജയം കാണുകയും ഇൻപുട്ട് ലളിതമാക്കാൻ തുടങ്ങുകയും ചെയ്യും:
- എനിക്ക് ഒരു ഘടകം നീക്കം ചെയ്യാൻ കഴിയുമോ? `[500, 42, 987, -2000]` ശ്രമിക്കുക. ഇപ്പോളും പരാജയപ്പെടുന്നു. നല്ലത്.
- എനിക്ക് മറ്റൊന്ന് നീക്കം ചെയ്യാൻ കഴിയുമോ? `[42, 987, -2000]` ശ്രമിക്കുക. ഇപ്പോളും പരാജയപ്പെടുന്നു.
- ...അങ്ങനെ, ടെസ്റ്റ് പാസ്സാക്കാതെ കൂടുതൽ ഘടകങ്ങൾ നീക്കം ചെയ്യാൻ കഴിയാത്തതുവരെ തുടരും.
- ഇത് സംഖ്യകൾ ചെറുതാക്കാനും ശ്രമിക്കും. `42` എന്നത് `0` ആകാൻ കഴിയുമോ? ഇല്ല, ടെസ്റ്റ് പാസ്സാകുന്നു. അത് `41` ആകാൻ കഴിയുമോ? ടെസ്റ്റ് പാസ്സാകുന്നു. ഇത് കുറയ്ക്കുന്നു.
അവസാനത്തെ എറർ റിപ്പോർട്ട് ഏകദേശം ഇങ്ങനെയായിരിക്കും:
Error: Property failed after 15 tests
{ seed: 12345678, path: "14", endOnFailure: true }
Counterexample: [[42]]
Shrunk 5 time(s)
Got error: This number is not allowed!
പരാജയത്തിന് കാരണമായ കൃത്യമായ, ഏറ്റവും കുറഞ്ഞ ഇൻപുട്ട് ഇത് നിങ്ങളോട് പറയുന്നു: `[42]` എന്ന സംഖ്യ മാത്രം അടങ്ങിയ ഒരു array. ഇത് ബഗിന്റെ ഉറവിടത്തിലേക്ക് നിങ്ങളെ ഉടൻ തന്നെ എത്തിക്കുന്നു, ഡീബഗ്ഗിംഗിൽ നിങ്ങളുടെ സമയം ലാഭിക്കുന്നു.
പ്രാക്ടിക്കൽ PBT തന്ത്രങ്ങളും യഥാർത്ഥ ലോക ഉദാഹരണങ്ങളും
PBT എന്നത് ഗണിതശാസ്ത്രപരമായ ഫംഗ്ഷനുകൾക്ക് വേണ്ടി മാത്രമല്ല. ഇത് സോഫ്റ്റ്വെയർ ഡെവലപ്മെന്റിന്റെ പല മേഖലകളിലും ഉപയോഗിക്കാൻ കഴിയുന്ന ഒന്നാണ്.
Property: വിപരീത ഫംഗ്ഷനുകൾ
നിങ്ങൾക്ക് ഡാറ്റ എൻകോഡ് ചെയ്യുന്ന ഒരു ഫംഗ്ഷനും അത് ഡീകോഡ് ചെയ്യുന്ന മറ്റൊന്നുണ്ടെങ്കിൽ, അവ പരസ്പരം വിപരീതമാണ്. എൻകോഡ് ചെയ്ത ഒരു മൂല്യം ഡീകോഡ് ചെയ്യുന്നത് എപ്പോഴും ആദ്യത്തെ മൂല്യം നൽകണമെന്നുള്ളത് പരിശോധിക്കേണ്ട ഒരു മികച്ച property ആണ്.
// `encode` and `decode` could be for base64, URI components, or custom serialization
function encode(obj) { return JSON.stringify(obj); }
function decode(str) { return JSON.parse(str); }
test('decode(encode(x)) should be equal to x', () => {
// `fc.jsonValue()` generates any valid JSON value: strings, numbers, objects, arrays
fc.assert(
fc.property(fc.jsonValue(), (originalValue) => {
const encoded = encode(originalValue);
const decoded = decode(encoded);
expect(decoded).toEqual(originalValue);
})
);
});
Property: Idempotence
ഒരു പ്രവർത്തനം ഒന്നിലധികം തവണ പ്രയോഗിക്കുന്നത് ഒരു തവണ പ്രയോഗിക്കുന്നതിന് തുല്യമായ ഫലമുണ്ടാക്കുന്നുവെങ്കിൽ, അത് idempotent ആണ്. `f(f(x)) === f(x)`. ഡാറ്റ ക്ലീനിംഗ് ഫംഗ്ഷനുകൾ അല്ലെങ്കിൽ REST API-യിലെ `DELETE` എൻഡ്പോയിന്റുകൾ പോലുള്ള കാര്യങ്ങൾക്ക് ഇത് നിർണായകമാണ്.
// A function that removes leading/trailing whitespace and collapses multiple spaces
function normalizeWhitespace(text) {
return text.trim().replace(/\s+/g, ' ');
}
test('normalizeWhitespace should be idempotent', () => {
fc.assert(
fc.property(fc.string(), (originalString) => {
const once = normalizeWhitespace(originalString);
const twice = normalizeWhitespace(once);
expect(twice).toBe(once);
})
);
});
Property: സ്റ്റേറ്റ്ഫുൾ (മോഡൽ അടിസ്ഥാനമാക്കിയുള്ള) ടെസ്റ്റിംഗ്
ഇതൊരു വിപുലമായതും എന്നാൽ വളരെ ശക്തവുമായ സാങ്കേതികതയാണ്. ഇത് ഒരു UI ഘടകം, ഒരു ഷോപ്പിംഗ് കാർട്ട് അല്ലെങ്കിൽ ഒരു സ്റ്റേറ്റ് മെഷീൻ പോലുള്ള ഇന്റേണൽ സ്റ്റേറ്റുകളുള്ള സിസ്റ്റങ്ങളെ പരിശോധിക്കുന്നതിന് ഉപയോഗിക്കുന്നു. നിങ്ങളുടെ സിസ്റ്റത്തിന്റെ ഒരു സോഫ്റ്റ്വെയർ മോഡലും നിങ്ങളുടെ മോഡലിനും യഥാർത്ഥ നടപ്പാക്കലിനുമെതിരെ പ്രവർത്തിപ്പിക്കാൻ കഴിയുന്ന ഒരു കൂട്ടം കമാൻഡുകളും ഉണ്ടാക്കുക എന്നതാണ് ആശയം. മോഡലിന്റെ സ്റ്റേറ്റും യഥാർത്ഥ സിസ്റ്റത്തിന്റെ സ്റ്റേറ്റും എപ്പോഴും പൊരുത്തപ്പെടണം എന്നതാണ് property.
ഇതിനായി `fast-check` `fc.commands` നൽകുന്നു. ഒരു ലളിതമായ കൗണ്ടർ മോഡൽ ചെയ്യാം:
// The real implementation
class Counter {
constructor() { this.count = 0; }
increment() { this.count++; }
decrement() { this.count--; }
get() { return this.count; }
}
// The commands for fast-check
const incrementCmd = fc.command(
// check: a function to check if the command can be run on the model
(model) => true,
// run: a function to execute the command on both model and real system
(model, real) => {
model.count++;
real.increment();
expect(real.get()).toBe(model.count);
}
);
const decrementCmd = fc.command(
(model) => true,
(model, real) => {
model.count--;
real.decrement();
expect(real.get()).toBe(model.count);
}
);
test('Counter should behave according to the model', () => {
fc.assert(
fc.property(fc.commands([incrementCmd, decrementCmd]), (cmds) => {
const model = { count: 0 };
const real = new Counter();
fc.modelRun(() => ({ model, real }), cmds);
})
);
});
ഈ ടെസ്റ്റിൽ, `fast-check` `increment`, `decrement` കമാൻഡുകളുടെ ഒരു റാൻഡം സീക്വൻസ് ഉണ്ടാക്കുകയും അവ നമ്മുടെ ലളിതമായ ഒബ്ജക്റ്റ് മോഡലിനും യഥാർത്ഥ `Counter` ക്ലാസിനുമെതിരെ പ്രവർത്തിപ്പിക്കുകയും അവ ഒരിക്കലും വ്യതിചലിക്കുന്നില്ലെന്ന് ഉറപ്പാക്കുകയും ചെയ്യും. എക്സാമ്പിൾ അടിസ്ഥാനമാക്കിയുള്ള ടെസ്റ്റിംഗ് ഉപയോഗിച്ച് കണ്ടെത്താൻ കഴിയാത്ത സങ്കീർണ്ണമായ സ്റ്റേറ്റ്ഫുൾ ലോജിക്കിലെ ചെറിയ ബഗുകൾ ഇത് കണ്ടെത്താൻ സഹായിക്കും.
Property-Based Testing എപ്പോൾ ഉപയോഗിക്കരുത്
PBT നിങ്ങളുടെ ടെസ്റ്റിംഗ് ടൂൾകിറ്റിലേക്കുള്ള ശക്തമായ കൂട്ടിച്ചേർക്കലാണ്, എന്നാൽ ഇത് മറ്റ് എല്ലാ തരത്തിലുള്ള ടെസ്റ്റിംഗിനും പകരമല്ല. ഇത് ഒരു മാന്ത്രികവടി അല്ല.
എക്സാമ്പിൾ അടിസ്ഥാനമാക്കിയുള്ള ടെസ്റ്റിംഗ് എപ്പോഴും നല്ലത്:
- പ്രധാനപ്പെട്ട, അറിയപ്പെടുന്ന ബിസിനസ് നിയമങ്ങൾ പരിശോധിക്കുമ്പോൾ. ഒരു നികുതി കണക്കുകൂട്ടൽ ഒരു പ്രത്യേക ഇൻപുട്ടിന് കൃത്യമായി `$10.53` നൽകണമെങ്കിൽ, ഒരു ലളിതമായ എക്സാമ്പിൾ അടിസ്ഥാനമാക്കിയുള്ള ടെസ്റ്റ് കൂടുതൽ വ്യക്തവും നേരിട്ടുള്ളതുമാണ്. ഇത് അറിയപ്പെടുന്ന ആവശ്യകതകൾക്കായുള്ള ഒരു റിഗ്രഷൻ ടെസ്റ്റാണ്.
- "property" എന്നത് "X എന്ന ഇൻപുട്ട് Y എന്ന ഔട്ട്പുട്ട് നൽകുന്നു" എന്നത് മാത്രമാണ്. ഫംഗ്ഷന്റെ സ്വഭാവത്തെക്കുറിച്ച് ഉയർന്ന തലത്തിലുള്ളതോ പൊതുവൽക്കരിക്കാവുന്നതോ ആയ നിയമമില്ലെങ്കിൽ, property-based test നിർബന്ധിക്കുന്നത് അതിൻ്റെ വിലയേക്കാൾ സങ്കീർണ്ണമാക്കാം.
- കാഴ്ചയിൽ ശരിയാണോ എന്ന് അറിയാൻ യൂസർ ഇന്റർഫേസുകൾ പരിശോധിക്കുമ്പോൾ. PBT ഉപയോഗിച്ച് ഒരു UI ഘടകത്തിന്റെ സ്റ്റേറ്റ് ലോജിക് പരിശോധിക്കാൻ കഴിയുമെങ്കിലും, ഒരു പ്രത്യേക വിഷ്വൽ ലേഔട്ട് അല്ലെങ്കിൽ സ്റ്റൈൽ പരിശോധിക്കുന്നത് സ്നാപ്പ്ഷോട്ട് ടെസ്റ്റിംഗ് അല്ലെങ്കിൽ വിഷ്വൽ റിഗ്രഷൻ ടൂളുകൾ ഉപയോഗിച്ച് ചെയ്യുന്നതാണ് നല്ലത്.
ഏറ്റവും ഫലപ്രദമായ തന്ത്രം ഹൈബ്രിഡ് സമീപനമാണ്. നിങ്ങളുടെ അൽഗോരിതങ്ങൾ, ഡാറ്റാ മാറ്റങ്ങൾ, സ്റ്റേറ്റ്ഫുൾ ലോജിക് എന്നിവ ഒരുപാട് സാധ്യതകൾക്കെതിരെ പ്രവർത്തിപ്പിക്കാൻ property-based ടെസ്റ്റുകൾ ഉപയോഗിക്കുക. നിർദ്ദിഷ്ടവും നിർണായകവുമായ ബിസിനസ് ആവശ്യകതകൾ ഉറപ്പിക്കാനും അറിയപ്പെടുന്ന ബഗുകളിൽ റിഗ്രഷനുകൾ തടയാനും പരമ്പരാഗത എക്സാമ്പിൾ അടിസ്ഥാനമാക്കിയുള്ള ടെസ്റ്റുകൾ ഉപയോഗിക്കുക.
ഉപസംഹാരം: ഉദാഹരണങ്ങളിൽ മാത്രമല്ല, പ്രോപ്പർട്ടികളിൽ ചിന്തിക്കുക
Property-based testing ശരിയെക്കുറിച്ച് നമ്മൾ ചിന്തിക്കുന്ന രീതിയിൽ ഒരു വലിയ മാറ്റം വരുത്തുന്നു. വ്യക്തിഗത ഉദാഹരണങ്ങളിൽ നിന്ന് മാറിനിൽക്കാനും നമ്മുടെ കോഡ് ഉയർത്തിപ്പിടിക്കേണ്ട അടിസ്ഥാന തത്വങ്ങളും കരാറുകളും പരിഗണിക്കാനും ഇത് നമ്മെ പ്രോത്സാഹിപ്പിക്കുന്നു. അങ്ങനെ ചെയ്യുന്നതിലൂടെ, നമുക്ക്:
- ടെസ്റ്റുകൾ എഴുതാൻ നമ്മൾ ഒരിക്കലും ചിന്തിക്കാത്ത അതിശയിപ്പിക്കുന്ന എഡ്ജ് കേസുകൾ കണ്ടെത്താനാകും.
- നമ്മുടെ കോഡിന്റെ കരുത്തുറ്റതിൽ കൂടുതൽ ആത്മവിശ്വാസം നേടാനാകും.
- കുറച്ച് ഇൻപുട്ടുകളിൽ ഔട്ട്പുട്ട് ചെയ്യുന്നതിന് പകരം നമ്മുടെ സിസ്റ്റത്തിന്റെ സ്വഭാവം വ്യക്തമാക്കുന്ന കൂടുതൽ എക്സ്പ്രസ്സീവ് ടെസ്റ്റുകൾ എഴുതാനാകും.
- കുറയ്ക്കുന്നതിന്റെ ശക്തിക്ക് നന്ദി, ഡീബഗ് സമയം ഗണ്യമായി കുറയ്ക്കാൻ കഴിയും.
Property-based testing സ്വീകരിക്കുന്നത് ആദ്യം പരിചയമില്ലാത്തതായി തോന്നിയേക്കാം, പക്ഷേ ഇതിൽ നിക്ഷേപം നടത്തുന്നത് വളരെ നല്ലതാണ്. ചെറുതായി തുടങ്ങുക. നിങ്ങളുടെ കോഡ്ബേസിലെ ഒരു പ്യുവർ ഫംഗ്ഷൻ തിരഞ്ഞെടുക്കുക—ഡാറ്റാ മാറ്റം അല്ലെങ്കിൽ സങ്കീർണ്ണമായ കണക്കുകൂട്ടലുകൾ കൈകാര്യം ചെയ്യുന്ന ഒന്ന്—അതിനായി ഒരു property നിർവചിക്കാൻ ശ്രമിക്കുക. നിങ്ങളുടെ അടുത്ത പ്രോജക്റ്റിലേക്ക് ഒരു property-based ടെസ്റ്റ് ചേർക്കുക. ഇത് ആദ്യത്തെ കാര്യമായ ബഗ് കണ്ടെത്തുന്നതിന് നിങ്ങൾ സാക്ഷ്യം വഹിക്കുമ്പോൾ, മികച്ചതും വിശ്വസനീയവുമായ സോഫ്റ്റ്വെയർ നിർമ്മിക്കാനുള്ള അതിന്റെ ശക്തിയിൽ നിങ്ങൾക്ക് ബോധ്യമുണ്ടാകും.
കൂടുതൽ വിവരങ്ങൾ
- fast-check ഔദ്യോഗിക ഡോക്യുമെന്റേഷൻ
- പ്രോപ്പർട്ടി അടിസ്ഥാനമാക്കിയുള്ള ടെസ്റ്റിംഗ് മനസ്സിലാക്കുക സ്കോട്ട് വ്ലാഷിൻ എഴുതിയത് (ഒരു ക്ലാസിക്, ഭാഷാ-അജ്ഞേയ ആമുഖം)