This question was asked in Meta’s frontend interview where they asked to write a function to implement Test() and expect() as in Jest.
Input:
test('learnersbucket is a great platform', () => {
expect('system-design').toBe('system-design'); // true
expect('system-design').not.toBe('machine-coding'); // true
});
Output:
"Pass learnersbucket is a great platform"
Let us try to implement this function.
We can notice that there are two different methods that we have to create here.
test(title, callbackFn)expect(value)Test is the simple method to create, as it will take the title and the callback as arguments and invoke the callback, if callback does not throw error (which means all the expect method has passed) then it will print pass otherwise it will print failed.
// Test function to describe the tests
// the callback function accepts the expect functions
const test = async (title, callback) => {
try {
await callback();
console.log(`Pass ${title}`);
} catch (error) {
console.error(`Fail ${title}`);
console.error(error);
}
};
We have made this an async function as you can use it to test promises later.
If you read the Jest documentation, you will notice that there are many methods available, for the simplicity, we will just create the base structure and implement couple of the methods. After which you can add as many methods as you want.
Expect function returns a method or a property not which has all the expect methods available to it.
The not value is used for the denial check, which the normal method can be used assertion checks.
From this we understand that the expect function should accept an value as argument then return all the methods and the not property with all the methods.
// The assertion function
const expect = function(actual){
// This holds the matcherFns for expected and not expected values
const expectation = {
not: {}
};
// iterate over all the matcher methods and create an expected and not expected version of each
for(let key in matchers){
const matcherFn = matchers[key];
expectation[key] = helperMatcher(actual, matcherFn, false);
expectation.not[key] = helperMatcher(actual, matcherFn, true);
}
return expectation;
}
Here we created an object in the expect function that will be returned. In this object we will have the assertion and the denial methods. This list of methods will be created separately, where you can add as many methods as you want and can also provide functionality to the user to extend and create their own matcher methods.
// List of matcher methods
// Each hold logic of their own, the arguments to each function remain constant
// You can define the arguments in the order you like
const matchers = {
toBe: function(expected, actual, matcherProperties){
const { isNot } = matcherProperties;
if(isNot){
if(expected === actual){
throw error('Should not match');
return;
}
}else{
if(expected !== actual){
throw error('Should match');
}
}
},
toBeUndefined: function(expected, actual, matcherProperties){
const { isNot } = matcherProperties;
if(isNot){
if(actual === undefined){
throw error('Should not match');
return;
}
}else{
if(actual !== undefined){
throw error('Should match');
}
}
}
}
Here we have only two methods toBe checks if the actual and expected value are same or different depending upon the check type. toBeUndefined checks if the value is undefined or not.
While assigning the method to the expect function’s object, we will make use of a helper function, that will take the actual value and the matcher function and extra properties like if we have to do the denial or assertion check and then return a closure that will accept the expected value and pass it to the matcher function.
// A helper function that accepts the actual value, the matcherFn and the negation value
// and forms a closure and invokes the matcher function when actual value is received
const helperMatcher = (actual, matcherFn, isNot = false) => {
return function(expected){
matcherFn(expected, actual, { isNot });
}
}